1. C++基础与语言特性
1.1 构造函数是否能声明为虚函数?为什么?什么情况下为错误?
回答:
构造函数不能声明为虚函数。构造函数用于初始化对象,而虚函数机制依赖于对象的虚函数表(vtable)。在构造对象时,vtable尚未完全建立,因此无法调用虚函数。如果构造函数可以声明为虚函数,那么在基类构造函数中调用虚函数可能会导致调用派生类未初始化的虚函数,从而导致未定义行为。
1.2 类中static函数是否能声明为虚函数?
回答:
类中的static
函数不能声明为虚函数。静态函数属于类本身而不是某个对象,因此它们不依赖于具体的对象实例,自然也就没有虚函数机制。
1.3 哪些函数不能被声明为虚函数?
回答:
以下函数不能声明为虚函数:
- 构造函数
- 静态成员函数
- 内联函数(虽然内联函数可以是虚函数,但其内联效果可能会因虚函数的性质而丧失)
1.4 explicit用在哪里?有什么作用?
回答:
explicit
关键字用于构造函数,防止构造函数在单参数的情况下被隐式调用。它的作用是避免隐式转换可能导致的编译器错误或意外行为。通常用于防止单参数构造函数被错误地用于隐式类型转换。
2. 内存管理与指针
2.1 指针占用的大小是多少?
回答:
指针的大小取决于编译器和处理器的架构。在32位系统上,指针大小为4字节(32位);在64位系统上,指针大小为8字节(64位)。
2.2 野指针和内存泄漏是什么?如何避免?
回答:
野指针是指向已释放或未初始化内存的指针,使用它可能导致未定义行为。内存泄漏是指分配的内存没有被正确释放,从而造成内存的浪费。
避免方法:
- 初始化指针,在使用完后将其置为
nullptr
。 - 使用智能指针(如
std::unique_ptr
或std::shared_ptr
)来自动管理内存。 - 避免手动管理内存,尽量使用STL容器。
2.3 malloc和new的区别是什么?
回答:
malloc
是C语言的内存分配函数,仅分配内存,不会调用构造函数。new
是C++中的操作符,分配内存并调用构造函数。- 使用
malloc
分配的内存需要使用free
释放,而使用new
分配的内存需要使用delete
释放。
2.4 C++中空类的大小是多少?
回答:
在C++中,空类的大小为1字节。C++标准要求空类也应占用一定的内存空间,以确保两个不同对象的地址不同。
2.5 空类中有什么函数?
回答:
空类中有以下默认生成的函数:
- 默认构造函数
- 默认析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数(C++11及以后)
- 移动赋值运算符(C++11及以后)
2.6 成员变量初始化的顺序是什么?
回答:
成员变量的初始化顺序按照它们在类中声明的顺序进行,而不是按照初始化列表中的顺序。这点很重要,特别是在成员变量之间存在依赖关系时。
3. STL与容器
3.1 什么是STL?
回答:
STL(Standard Template Library,标准模板库)是C++标准库的一部分,包含了一组通用算法、容器、迭代器和函数对象,帮助开发者更方便地进行数据操作和管理。
3.2 解释说明一下map和unordered_map
回答:
map
是基于红黑树实现的有序关联容器,支持按键值顺序遍历,查找、插入、删除操作的时间复杂度为O(log n)。unordered_map
是基于哈希表实现的无序关联容器,查找、插入、删除操作的平均时间复杂度为O(1),但最坏情况下可能退化为O(n)。
3.3 哈希碰撞的处理方法
回答:
哈希碰撞是指两个不同的键值经过哈希函数计算后得到相同的哈希值。处理方法包括:
- 开放地址法:寻找下一个空闲位置存放冲突的键值。
- 链地址法:将冲突的键值存在同一个链表中。
- 再哈希法:使用第二个哈希函数重新计算冲突的键值的存放位置。
3.4 unordered_map的扩容过程
回答:
unordered_map
在元素数量超过当前容量时会自动扩容,通常通过增加桶的数量来实现。扩容后,所有元素将重新计算哈希值并移动到新的桶中。这是一个相对昂贵的操作。
3.5 vector中的push_back()和emplace_back()的区别、以及使用场景
回答:
push_back()
将一个元素的副本添加到vector
的末尾,需要调用拷贝构造函数或移动构造函数;emplace_back()
在vector
的末尾直接构造元素,避免了不必要的拷贝或移动操作,适用于需要直接传递构造参数的情况。
3.6 vector扩容,resize和reserve的区别
回答:
resize
用于改变vector
的大小,如果新大小大于当前大小,会默认初始化新元素。reserve
仅预留存储空间,改变capacity
但不改变size
,适用于提前分配空间以减少多次分配的开销。
3.7 vector扩容为了避免重复扩容做了哪些机制?
回答:
vector
在需要扩容时会按照一定的比例(通常是2倍)增加容量,以减少频繁分配内存的开销。这种几何增长策略使得扩容操作的摊销成本较低。
3.8 vector如何判断应该扩容?(size和capacity)
回答:
vector
的扩容是在size
达到capacity
时触发的。当添加新元素时,如果size
等于capacity
,则vector
会自动扩容。
3.9 对比迭代器和指针的区别
回答:
- 迭代器是STL容器用于遍历元素的抽象工具,可以是智能的(如
random access iterator
),而指针是一个直接指向内存地址的简单工具。 - 迭代器可以具备更高级的功能(如范围检查、容器一致性),而指针更轻量级、没有额外的开销。
4. 现代C++特性
4.1 push_back()左值和右值的区别是什么?
回答:
push_back()
左值会调用拷贝构造函数,而右值会调用移动构造函数,从而避免不必要的深拷贝,提高性能。
4.2 move底层是怎么实现的?
回答:
std::move
是一个类型转换,它将其参数强制转换为右值引用(T&&
),从而允许移动语义的实现。move
本质上不会移动数据,而是允许调用者触发移动构造函数或移动赋值操作符。
4.3 完美转发的原理是什么?
回答:
完美转发使用模板和std::forward
实现,将参数原封不动地传递给另一个函数。std::forward
会根据传递的值类别(左值或右值)进行正确的转发,确保参数在传递时保持原有的类型特性。
4.4 移动构造和拷贝构造的区别是什么?
回答:
移动构造函数通过“窃取”资源来构造新对象,而拷贝构造函数则是通过深拷贝复制资源。移动构造通常比拷贝构造更高效,因为它避免了不必要的资源分
配和释放。
4.5 lamda表达式捕获列表捕获的方式有哪些?如果是引用捕获要注意什么?
回答:
捕获方式包括值捕获、引用捕获、隐式捕获(值或引用)、按值捕获(移动语义)。使用引用捕获时,需要注意生命周期问题,确保被捕获的变量在Lambda表达式执行时仍然有效。
5. 多线程与并发
5.1 线程有哪些状态,线程锁有哪些?
回答:
线程的状态包括:新建(new)、就绪(ready)、运行(running)、等待(waiting)、终止(terminated)。常用的线程锁包括:互斥锁(std::mutex
)、读写锁、递归锁、定时锁。
5.2 多线程会发生什么问题?线程同步有哪些手段?
回答:
多线程常见问题包括竞争条件、死锁、资源饥饿。同步手段包括:互斥锁、条件变量、原子操作、信号量。
5.3 如何实现线程安全,除了加锁还有没有其他的方式?
回答:
实现线程安全的方式包括:
- 加锁(如
mutex
) - 使用无锁编程技术(如使用
std::atomic
类型) - 使用线程本地存储(
thread_local
关键字) - 不可变对象设计(线程间不共享可变状态)
6. 高级C++与系统编程
6.1 动态链接和静态链接的区别,动态链接的原理是什么?
回答:
-
静态链接:在编译时将所有依赖的库链接到可执行文件中,生成的文件较大,但运行时不依赖外部库。
-
动态链接:可执行文件在运行时动态加载依赖的库,生成的文件较小,但需要依赖外部的共享库(如
.dll
或.so
)。动态链接原理:动态链接库在运行时由操作系统加载到内存中,动态链接器负责将库中的符号解析到程序的地址空间中。
6.2 讲讲你理解的虚基类
回答:
虚基类用于解决多重继承中的“菱形继承”问题。通过使用虚继承,基类中的数据成员在派生类中只会存在一份,从而避免了重复继承带来的数据冗余和二义性问题。
6.3 C++哪些运算符不能被重载?
回答:
以下运算符不能被重载:
::
(作用域解析运算符).
(成员访问运算符).*
(成员指针访问运算符)sizeof
(大小运算符)?:
(条件运算符)typeid
(类型识别运算符)alignof
(对齐运算符)
6.4 C++中怎么编译C语言代码?
回答:
在C++代码中编译C语言代码需要使用extern "C"
来指示编译器使用C语言的链接规范,以避免C++名称修饰(name mangling)导致的链接问题。例如:
extern "C" {
#include "c_code.h"
}
7. 高级内存管理与优化
7.1 weak_ptr是怎么实现的?
回答:
std::weak_ptr
是一种不控制资源生命周期的智能指针。它通过内部引用计数器(与std::shared_ptr
共享)实现。weak_ptr
不会增加资源的引用计数,但可以检查资源是否仍然有效,并通过lock()
函数获取一个shared_ptr
以安全地访问资源。
7.2 虚函数的底层原理是什么?
回答:
虚函数通过虚函数表(vtable)实现。当类中有虚函数时,编译器会为每个类生成一张虚函数表,表中存放指向虚函数的指针。对象实例有一个隐藏的指向vtable的指针,调用虚函数时通过vtable动态解析出对应的函数地址,从而实现多态。
7.3 一个函数f(int a,int b),其中a和b的地址关系是什么?
回答:
在函数参数传递中,a
和b
通常会依次压入栈中。因此,在大多数系统中,后传递的参数b
的地址会比前一个参数a
的地址低。不过这也取决于系统的栈增长方向(通常是从高地址向低地址增长)。
7.4 如何保证类的对象只能被开辟在堆上?(将构造函数声明为私有、单例)
回答:
为了保证类的对象只能被创建在堆上,可以:
- 将构造函数设为
private
或protected
,并提供一个静态成员函数(如工厂方法)来在堆上创建对象,并返回指向该对象的指针。 - 通过删除栈上分配的运算符(如重载
operator new
和operator delete
)。
7.5 动态链接和静态链接的区别,动态链接的原理是什么?
回答:
见6.1。