小米澎湃OS - C++开发一面
1、对c++封装,多态的感悟,面向对象语言是什么意思?
- 封装:封装是指将数据和操作数据的方法(即函数)封装在一起,形成一个类(Class)。类对外部隐藏了其内部实现细节,只提供了公共的接口(即公共的成员函数),其他对象必须通过这些公共接口来访问数据和操作数据的方法。封装可以有效地保护数据,防止外部直接访问和修改,提高了代码的安全性和稳定性。
- 继承:继承是指一个类(子类)可以继承另一个类(父类)的属性和方法,从而可以重用父类的代码。通过继承,子类可以扩展或修改父类的行为,同时可以保留父类的特性。继承可以减少重复代码,提高代码的可维护性和可扩展性。
- 多态:多态是指同样的操作作用于不同的对象上时,可以产生不同的行为。多态分为编译时多态(静态多态)和运行时多态(动态多态)两种。编译时多态是通过函数重载和运算符重载来实现的,根据函数参数的不同选择不同的函数实现;运行时多态是通过虚函数和继承来实现的,通过指针或引用调用虚函数时,根据指针或引用所指向的对象的实际类型来确定调用哪个函数实现。多态性使得程序可以更加灵活和可扩展,提高了代码的可读性和可维护性。
面向对象语言是指支持封装、继承和多态这三个特性的编程语言。面向对象语言的设计思想是将现实世界中的事物抽象成对象,对象间通过消息传递来进行通信和交互,从而更好地模拟现实世界的复杂性。通过面向对象编程,可以更加方便地组织和管理代码,提高代码的重用性和可维护性,使程序更加易读和易懂。
2、智能指针?
std::shared_ptr:- 原理:
std::shared_ptr是基于引用计数的智能指针,用于管理动态分配的对象。它维护一个引用计数,当计数为零时,释放对象的内存。 - 使用场景:适用于多个智能指针需要共享同一块内存的情况。例如,在多个对象之间共享某个资源或数据。
C++std::shared_ptr<int> sharedInt = std::make_shared<int>(42); std::shared_ptr<int> anotherSharedInt = sharedInt; // 共享同一块内存- 原理:
std::unique_ptr:- 原理:
std::unique_ptr是独占式智能指针,意味着它独占拥有所管理的对象,当其生命周期结束时,对象会被自动销毁。 - 使用场景:适用于不需要多个指针共享同一块内存的情况,即单一所有权。通常用于资源管理,例如动态分配的对象或文件句柄。
C++std::unique_ptr<int> uniqueInt = std::make_unique<int>(42); // uniqueInt 的所有权是唯一的- 原理:
std::weak_ptr:- 原理:
std::weak_ptr是一种弱引用指针,它不增加引用计数。它通常用于协助std::shared_ptr,以避免循环引用问题。 - 使用场景:适用于协助解决
std::shared_ptr的循环引用问题,其中多个shared_ptr互相引用,导致内存泄漏。
C++std::shared_ptr<int> sharedInt = std::make_shared<int>(42); std::weak_ptr<int> weakInt = sharedInt;- 原理:
std::auto_ptr(已废弃):- 原理:
std::auto_ptr是C++98标准引入的智能指针,用于独占地管理对象。但由于其存在潜在的问题,已在C++11中被废弃。 - 使用场景:在C++98标准中,可用于独占性地管理动态分配的对象。不推荐在现代C++中使用。
C++std::auto_ptr<int> autoInt(new int(42)); // 已废弃- 原理:
3、qt是怎么分层的?
- 硬件抽象层:这一层负责与底层硬件进行交互,提供了对不同平台硬件的统一访问接口,使得上层应用可以不关心具体的硬件细节。Qt通过模块化设计,提供了对不同硬件平台的支持,包括图形系统、输入输出、网络和操作系统接口等。
- 基础模块:这一层包含了Qt框架的核心功能模块,例如事件处理、信号与槽机制、对象模型、数据结构、线程等。基础模块提供了Qt框架的基本特性,为上层模块提供了基础支持。
- 图形引擎:Qt提供了强大的图形引擎,包括对2D和3D图形的支持,以及丰富的绘图功能。图形引擎使得开发者可以轻松地创建各种精美的用户界面和图形应用。
- 工具集:Qt提供了一套完善的开发工具集,包括Qt Creator集成开发环境(IDE)、调试器、国际化工具、自动化测试工具等。这些工具能够帮助开发者提高开发效率,简化开发流程。
- 应用程序框架:这一层是构建实际应用程序的基础,包括Qt Widgets模块(用于构建传统的桌面应用程序)、Qt Quick模块(用于构建现代的移动和嵌入式应用程序)、Qt Multimedia模块(用于处理多媒体内容)、Qt Network模块(用于网络通信)、Qt SQL模块(用于数据库访问)等。应用程序框架提供了丰富的功能和组件,方便开发者快速开发出各种类型的应用程序。
4、链表和队列的特性,相同点和不同点,平时有没有用到过栈的场景?
- 相同点:
- 都是线性数据结构,元素之间有顺序关系。
- 都可以用于存储一系列数据,并支持在数据集合中添加、删除、访问元素等操作。
- 都可以用于实现先进先出(FIFO)的数据处理模式。
- 不同点:
- 链表是一种动态数据结构,它的元素在内存中不一定是连续存储的,每个元素都包含一个指向下一个元素的指针;而队列是一种基于数组或链表实现的数据结构,它的元素是按顺序存储的。
- 链表支持在任意位置插入或删除元素,时间复杂度为O(1);队列只支持在队尾插入元素和在队头删除元素,时间复杂度也为O(1)。
- 链表可以是单向的(每个节点只有一个指针指向下一个节点)、双向的(每个节点有两个指针,分别指向前一个节点和后一个节点);队列一般是单向的。
5、TCP,UDP区别?
- 连接性:
- TCP是面向连接的协议,通信双方在传输数据前需要先建立连接,然后再传输数据,传输完成后再释放连接。这种连接方式保证了数据的可靠性和顺序性,但会增加一定的延迟和开销。
- UDP是无连接的协议,通信双方在传输数据时不需要建立连接,直接发送数据报,也不保证数据的可靠性和顺序性,因此传输速度更快,但数据可能丢失或乱序。
- 可靠性:
- TCP提供可靠的数据传输,通过序号、确认和重传机制来保证数据的可靠性,确保数据能够正确地到达目的地。
- UDP不提供数据的可靠性保证,发送端发送数据后不会收到确认,也不会进行重传,因此数据有可能丢失或重复。
- 数据量:
- TCP适用于需要可靠传输的场景,对数据完整性要求高,适合传输大量数据。
- UDP适用于实时性要求高、数据量较小的场景,如音视频传输、在线游戏等。
- 头部开销:
- TCP的头部较大,包含了序号、确认号、窗口大小等信息,头部开销较大。
- UDP的头部较小,只包含了源端口、目的端口、长度和校验和等基本信息,头部开销较小。
- 适用场景:
- TCP适用于需要可靠传输、数据完整性要求高的场景,如文件传输、网页访问等。
- UDP适用于实时性要求高、数据量较小、允许丢失部分数据的场景,如视频直播、在线游戏等。
6、c++全局变量,内存管理?
C++中,全局变量是在程序的全局作用域中定义的变量,它们在程序启动时被创建,在程序结束时被销毁。
- 内存分配:全局变量的内存分配由编译器在编译阶段完成,分配在静态存储区。静态存储区的内存在程序运行期间一直存在,直到程序结束才会被释放。全局变量的内存分配是在程序加载时完成的,不会在运行时动态分配内存。
- 内存释放:全局变量的内存由系统在程序结束时自动释放,不需要程序员手动释放。因为全局变量的生命周期与程序的生命周期相同,所以当程序结束时,系统会自动回收全局变量所占用的内存。
全局变量的内存管理相对简单,不需要程序员显式地分配和释放内存,但需要注意的是:
- 全局变量的生命周期较长,容易导致内存泄漏问题,因此在设计全局变量时应慎重考虑其生命周期和影响范围。
- 全局变量的使用应尽量避免过度依赖,以免影响程序的可维护性和扩展性。
- 如果全局变量需要在多个源文件中共享,可以将其声明为extern,然后在一个源文件中定义,其他源文件中通过extern关键字声明即可访问。
7、函数的局部变量存放在哪?
函数的局部变量存放在栈内存中。栈内存是一种后进先出(LIFO)的数据结构,用于存储函数的局部变量、函数的参数以及函数调用的返回地址。当函数被调用时,会在栈上分配一段内存用于存储局部变量。当函数执行完毕后,这段内存会被自动释放,从而保证了局部变量的生命周期与函数调用的一致性。
具体点来说,函数的局部变量存放在栈帧(stack frame)中。栈帧是存放在栈内存中的一块数据区域,用于存储函数的局部变量、函数的参数、函数调用的返回地址以及一些辅助信息。每次函数调用时,都会创建一个新的栈帧,函数执行完毕后,该栈帧会被销毁,同时释放栈上的内存空间。
栈内存的优点是管理方便、速度快,但缺点是空间有限,大小受限于系统栈的大小。因此,在编写程序时,应该避免在栈上分配过大的局部变量,以防止栈溢出的问题。
8、一维数组和二维数组的区别?
- 一维数组:最简单的数组形式,所有元素都存储在一条线上。在内存中,一维数组的元素是连续存储的,可以通过数组名和下标来访问数组中的元素。
- 二维数组:一种表格状的数据结构,包含行和列。在内存中,二维数组的元素也是连续存储的,但存储方式是按行存储的,即每一行的元素是连续存储的,不同行之间的元素可能不连续。可以通过两个下标(行索引和列索引)来访问二维数组中的元素。
9、递归是什么,怎么形成一个递归?
递归是指一个函数在执行过程中调用了自身的情况。递归函数通常包含两个部分:基本情况(base case)和递归情况(recursive case)。
- 基本情况:基本情况是递归函数中的终止条件,当满足这个条件时,递归函数不再调用自身,直接返回结果。没有基本情况的递归函数会陷入无限循环。
- 递归情况:递归情况是递归函数中的循环条件,满足这个条件时,递归函数会调用自身,但问题规模会减小,逐步向基本情况靠近。递归函数每次调用自身都会带来一个更简单的问题,直到达到基本情况。
形成一个递归的关键是将原问题分解为更简单的子问题,然后利用递归函数解决这些子问题,最终得到原问题的解。递归函数的执行过程可以看作是一种"分而治之"的思想,通过不断地分解问题,最终达到解决问题的目的。
例如,计算斐波那契数列的第n项可以使用递归实现:
int fibonacci(int n) {
if (n <= 1) { // 基本情况
return n;
} else { // 递归情况
return fibonacci(n - 1) + fibonacci(n - 2);
}
}10、手撕反转链表
思路
这里给大家讲最经典的双指针法。
- 如果链表为空或者只有一个节点,直接返回头结点head。
- 初始化 pre 为 nullptr,cur 为头结点 head,node 为 cur 的下一个节点。
- 在循环中,不断更新 pre、cur 和 node 的值,使得 cur 的 next 指向 pre,然后将 pre、cur 和 node 分别向后移动一位。
- 当 cur 移动到链表末尾时,pre 就是反转后的新头结点。
参考代码
C++
#include <iostream>
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(nullptr) {}
};
class Solution {
public:
ListNode* reverseList(ListNode* head) {
if (head == nullptr || head->next == nullptr) {
return head; // 如果链表为空或者只有一个节点,直接返回头结点
}
ListNode* pre = nullptr; // 初始化 pre 为 nullptr
ListNode* cur = head; // 初始化 cur 为头结点
ListNode* node = nullptr; // 初始化 node 为 nullptr
while (cur != nullptr) {
node = cur->next; // 保存当前节点的下一个节点
cur->next = pre; // 当前节点的 next 指向 pre,完成反转
pre = cur; // 更新 pre
cur = node; // 更新 cur
}
return pre; // pre 就是反转后的新头结点
}
};
int main() {
ListNode* head = new ListNode(1);
head->next = new ListNode(2);
head->next->next = new ListNode(3);
Solution solution;
ListNode* newHead = solution.reverseList(head);
while (newHead != nullptr) {
std::cout << newHead->val << " ";
newHead = newHead->next;
}
return 0;
}