Qt C++/Go/Python 面试题(持续更新)
目录
1、封装、继承、多态是什么?
2、final标识符的作用是什么?
3、介绍一下虚函数
4、介绍一下智能指针
5、介绍一下左值、右值、左值引用、右值引用
6、指针和引用有什么区别?
7、define和const的区别是什么?
8、C++程序的内存划分
1. 代码区(Text Segment)
2. 全局/静态数据区(Data Segment)
3. 堆区(Heap)
4. 栈区(Stack)
5. 常量区(Read-Only Data)
6. 内存映射区(Memory Mapping Segment)
9、class与struct的区别
10、内存对齐是什么?
11、进程之间的通信方式有哪些?
12、介绍一下 I/O 多路复用
13、多线程为什么会发生死锁?
14、面向过程和面向对象
15、++i和i++哪个效率更高
16、介绍一下vector、list的底层实现原理和优缺点
17、空对象指针为什么能调用函数?
18、介绍一下 push_back 和 emplace_back
19、空类中有什么函数?
20、explicit 什么作用?
21、成员变量初始化的顺序是什么?
22、malloc和new的区别是什么?
23、迭代器和指针
24、线程有哪些状态
1、封装、继承、多态是什么?
封装:将具体实现过程和数据封装成一个类,只能通过接口进行访问,降低耦合性。
继承:子类继承父类的特征和行为,复用了基类的全体数据和成员函数,基类私有成员可被继承,但是无法被访问,其中构造函数、析构函数、友元函数、静态数据成员、静态成员函数都不能被继承。
多态:静态多态通过函数重载实现,可以实现同一个函数名根据传入参数不同而具有不同实现,编译期间就已经确定;动态多态:通过虚函数表实现,在派生类中重写基类函数,实现同一个函数名不同实现,运行时根据虚函数表确定调用哪个函数。
2、final标识符的作用是什么?
放在类的后面表示该类无法被继承,也就是阻止了从类的继承,放在虚函数后面该虚函数无法被重写。
3、介绍一下虚函数
虚函数是通过虚函数表实现的,含有虚函数的类在构造函数中会初始化虚函数表指针,它存储在类内存中,指向虚函数表,虚函数表存储指向每个虚函数的指针,多个类对象共用一张虚函数表。虚函数和普通函数都存储在代码段。
构造函数不能为虚函数,因为虚函数表指针需要在构造函数中初始化。析构函数最好为虚函数,这样当一个指向派生类的基类指针被释放时,可以先调用派生类析构函数,再调用基类析构函数,否则只会调用指向的类的析构函数。
类中static函数不能声明为虚函数,因为类中的 static 函数是所有类实例化对象所共有的,没有 this 指针,此时无法访问到虚函数表指针。
4、介绍一下智能指针
智能指针可以自动管理内存,通过调用对象的析构函数来实现。
share_ptr:共享同一个资源,每增加一个智能指针,引用计数加1,当引用计数为0时释源。
unique_ptr:对其持有的堆内存具有唯一拥有权,也就是说引用计数永远是 1,禁止复制语义。
weak_ptr:不占用引用计数,用于解决 share_ptr 循环引用问题。比如有两个类,它们各有一个指向对方的 share_ptr,然后相互初始化,此时就会造成引用循环。将其中的一个 share_ptr 换成 weak_ptr,就可以解决。
实际开发中,有时候需要在类中返回包裹当前对象(this)的一个 std::shared_ptr
对象给外部使用,C++ 新标准也为我们考虑到了这一点,有如此需求的类只要继承自 std::enable_shared_from_this
模板对象即可。用法如下:
#include <iostream>
#include <memory>
class A : public std::enable_shared_from_this<A>
{
public:A(){std::cout << "A constructor" << std::endl;}
~A(){std::cout << "A destructor" << std::endl;}
std::shared_ptr<A> getSelf(){return shared_from_this();}
};
int main()
{std::shared_ptr<A> sp1(new A());
std::shared_ptr<A> sp2 = sp1->getSelf();
std::cout << "use count: " << sp1.use_count() << std::endl;
return 0;
}
5、介绍一下左值、右值、左值引用、右值引用
左值:有地址的值,如下列代码中的变量a。
右值:临时值,如下列代码中的10。
int a = 10;
左值引用:就是普通的引用。
右值引用:一种引用类型,用&&
表示,它专门用于绑定到临时对象(右值)上,主要用于实现完美转发和移动语义。
完美转发就是在传递参数时维持参数的类型(左值、右值)不变
移动语义就是允许资源的所有权转移而非复制。
std::move 的功能是将一个左值引用强制转化为右值引用,继而可以通过右值引用使用该值,以用于移动语义,从实现原理上讲基本等同一个强制类型转换
6、指针和引用有什么区别?
指针:指针是变量,内存中存储的是目标数据的地址。
引用:是变量的别名,不额外占用内存,必须初始化且只能初始化一次。
7、define、const、inline 的区别是什么?
define 是在预处理时进行简单的文本替换,不会进行类型安全检查,define 定义的宏常量,在程序中使用多少次就会进行多少次替换,内存中有多个备份,占用的是代码段的内存。
const 则是在编译时确定其值,会进行类型安全检查。
inline 是建议编译器在函数调用处将函数展开,适用于比较短、功能单一的函数,节省函数调用的开销。如果函数体过长,频繁使用内联函数会导致代码编译膨胀问题
在头文件中类内声明并定义的函数默认是 inline,类内声明类外初始化的必须加 inline,以解决多重定义错误(否则多个cpp文件包含了这个h文件,链接时会出现多重定义错误)。
在头文件中类内声明,cpp文件中定义的函数不需要加 inline,此时不会发生多重定义错误。
8、C++程序的内存划分
1. 代码区(Text Segment)
-
存储内容:程序的机器指令(可执行代码)
-
特点:
-
只读内存区域
-
在程序启动时加载
-
可能被多个进程共享(对于相同的可执行文件)
-
2. 全局/静态数据区(Data Segment)
分为两部分:
-
已初始化数据段(.data)
-
存储已初始化的全局变量和静态变量
-
包括静态局部变量
-
-
未初始化数据段(.bss)
-
存储未初始化的全局变量和静态变量
-
程序加载时由操作系统初始化为零
-
3. 堆区(Heap)
-
存储内容:动态分配的内存(通过
new
/malloc
分配) -
特点:
-
手动管理(需要
delete
/free
释放) -
内存分配方向从低地址向高地址增长
-
分配速度较慢(需要系统调用)
-
大小受系统虚拟内存限制
-
4. 栈区(Stack)
-
存储内容:
-
局部变量
-
函数参数
-
函数调用信息(返回地址、寄存器状态等)
-
-
特点:
-
自动管理(函数结束时自动释放)
-
内存分配方向从高地址向低地址增长
-
分配速度快(只需移动栈指针)
-
大小有限(通常几MB,可配置)
-
5. 常量区(Read-Only Data)
-
存储内容:
-
字符串字面量
-
编译期确定的常量表达式(如
constexpr
)
-
-
特点:
-
只读内存区域
-
尝试修改会导致段错误
-
6. 内存映射区(Memory Mapping Segment)
-
存储内容:
-
动态链接库
-
内存映射文件
-
共享内存区域
-
-
特点:
-
由操作系统管理
-
可用于进程间通信
-
9、class与struct的区别
class 默认是 private 继承,struct 默认是 public 继承。
10、内存对齐是什么?
CPU通常以固定大小的块(如4字节、8字节)从内存中读取数据。如果数据未对齐,可能需要多次内存访问才能读取完整数据,降低效率。
11、进程之间的通信方式有哪些?
管道:管道本质上是一个内核中的一个缓存,当进程创建管道后会返回两个文件描述符,一个写入端一个输出端。缺点:半双工通信,一个管道只能一个进程写,一个进程读。不适合进程间频繁的交换数据
消息队列:可以边发边收,但是每个消息体都有最大长度限制,队列所包含的消息体的总数量也有上限并且在通信过程中存在用户态和内核态之间的数据拷贝问题
共享内存
套接字:例如QLocalSocket,可以创建一个类,继承于QLocalServer,接受QLocalServer::newConnection信号,在槽函数中获取客户端套接字,并调用其函数进行读取/写入。
12、介绍一下 I/O 多路复用
这三个都是I/O多路复用技术,用于同时监控多个文件描述符(fd)的状态变化(可读、可写、异常等),是高性能网络编程的核心机制。
select:每次调用select都需要把fd集合从用户区拷贝到内核区,而select系统调用后有需要把fd集合从内核区拷贝到用户区,这个系统开销在fd数量很多的时候会很大,并且存在最大fd数量限制,一般为1024
poll:和select基本相同,但采用了链表存储,没有最大fd数量限制
epoll:Linux特有的高效I/O多路复用机制,采用事件驱动方式,在调用epoll_ctl系统调用时拷贝一次要监听的文件描述符数据结构到内核区,在调用epoll_wait的时候不需要再把所有的要监听的文件描述符重复拷贝进内核区,支持水平触发和边缘触发
13、多线程为什么会发生死锁?
当各个线程想获取的资源,但是得不到满足,同时自身也不释放已有资源,就会产生死锁。
四个必要条件:互斥条件、请求和保持条件、不可剥夺条件、环路等待条件,破坏其一就可解除死锁。
14、面向过程和面向对象
面向过程:分析出解决问题的步骤,然后将这些步骤一步一步的实现,使用的时候一个一个调用就好。代码效率更高但是代码复用率低,不易维护。
面向对象:就是将问题分解为各个对象,每个对象描述某个事物在整个解决问题的步骤中的行为,相比面向过程,代码更易维护和复用。但是代码效率相对较低。
15、++i和i++哪个效率更高
++i 是左值,++i
直接修改 i
并返回 i
本身,因此它是可修改的具名对象
i++ 是右值,i++
返回的是递增前的临时副本,该副本是临时对象,过程中会触发拷贝构造
16、介绍一下vector、list的底层实现原理和优缺点
vector 是由一块连续的内存空间组成,因此可使用下标随机访问,尾插尾删效率高,当容器大小超过容量时,会自动申请一块更大的内存(原有内存的1至2倍),并且把所有数据拷贝到新内存中。
list 是由双链表实现的,不支持下标随机访问,按需申请内存,不会造成内存空间浪费。在任意位置的插入删除下效率高。
17、空对象指针为什么能调用函数?
空对象指针可以调用成员函数,前提是成员函数中不涉及到成员变量。
18、介绍一下 push_back 和 emplace_back
push_back 接收左值时调用拷贝构造函数,接受右值时调用移动构造函数
emplace_back 则是在容器内部直接构造新对象,避免了复制操作
19、空类中有什么函数?
默认构造函数、默认拷贝构造函数、默认析构函数、默认赋值运算符
空类的大小为1B
20、explicit 什么作用?
只能用于修饰只有一个参数的类构造函数(有一个例外就是,当除了第一个参数以外的其他参数都有默认值的时候此关键字依然有效),它的作用是表明该构造函数是显示的,而非隐式的。作用是防止类构造函数的隐式自动转换,比如构造函数中接收 float 类型,结果传了 int 类型,此时不进行类型转换。
跟它对应的另一个关键字是implicit,意思是隐藏的,类构造函数默认情况下声明为implicit。
21、成员变量初始化的顺序是什么?
与构造函数中初始化成员列表的顺序无关,只与类中定义成员变量的顺序有关。
类中const成员常量必须在构造函数初始化列表中初始化。类中static成员变量,只能在类内定义、类外初始化。
22、malloc和new的区别是什么?
New/delete会调用构造析构函数,malloc/free不会,所以他们无法满足动态对象的要求。
23、迭代器和指针
迭代器返回的是对象引用,而指针是一个变量。
24、线程有哪些状态
创建,就绪,运行,阻塞,结束。
阻塞态/创建态 + 资源 = 就绪态
就绪态 + CPU调度 = 运行态
运行态 - 资源 = 阻塞态
阻塞态不可直接到达运行态,需要先转为就绪态。
25、介绍一下 map 和 unordered_map
-
map
:底层使用 红黑树(Red-Black Tree) 实现。红黑树是一种自平衡二叉搜索树,元素是按照键的升序排列的。插入元素时,会自动根据键的大小进行排序。查找、插入和删除操作的时间复杂度是 O(log n)。红黑树的节点存储需要额外的指针来维护树的结构,因此内存开销相对较大。 -
unordered_map
:底层使用 哈希表(Hash Table) 实现。哈希表通过哈希函数将键映射到桶中,从而实现快速查找。元素的顺序是 不确定的,因为哈希表中的元素是根据哈希值存储的,而哈希值与元素的插入顺序没有关系。平均情况下,查找、插入和删除操作的时间复杂度是 O(1)。哈希表需要额外的内存来存储哈希桶和处理哈希冲突,但是内存开销小于 map。
26、lamda表达式捕获列表捕获的方式有哪些?
按值捕获:符号=,不能修改参数
引用捕获:符号&,可以修改传入的外部参数
默认的引用捕获可能会导致悬挂引用,引用捕获会导致闭包包含一个局部变量的引用或者形参的引用,如果一个由lambda创建的闭包的生命周期超过了局部变量或者形参的生命期,那么闭包的引用将会空悬。解决方法是对个别参数使用值捕获
27、哈希碰撞的处理方法
开放定址法:当遇到哈希冲突时,去寻找一个新的空闲的哈希地址。
再哈希法:同时构造多个哈希函数,等发生哈希冲突时就使用其他哈希函数知道不发生冲突为止,虽然不易发生聚集,但是增加了计算时间
链地址法:将所有的哈希地址相同的记录都链接在同一链表中
28、动态链接和静态链接
动态链接:不会将代码直接复制到自己程序中,只会留下调用接口,程序运行时根据代码中的路径将动态库加载到内存中,所有程序只会共享这一份动态库,因此动态库也被称为共享库。
静态链接:通俗来说就是把静态库打包到可执行程序中,如果有个多程序,每个程序链接静态库后,都会包含一份独立的代码,当程序运行起来时,所有这些重复的代码都需要占用独立的存储空间,显然很浪费计算机资源。