C++ 面试题01
1. C 和 C++ 有什么区别?
C 是面向过程的语言,主要就是函数和结构体;C++ 在 C 基础上加了面向对象、模板、异常、STL 这些。说白了 C++ 更“重”,但能力也更多,能做抽象和复用。C++ 基本兼容 C,不过像类型转换、const 这些地方 C++ 会更严格。
2. 指针和引用有什么区别?
指针是一个变量,它自己占内存,存的是别人的地址;引用是别人的别名,不占额外空间。引用必须初始化,之后不能再改指向;指针可以空,也可以指向不同的对象。用起来引用更安全一点,指针更灵活。
3. const 有什么用?
const 就是“只读”的意思。修饰变量表示不能改;修饰函数参数表示函数里不改这个参数;修饰成员函数表示这个函数不能修改成员变量。它主要帮编译器检查错误,也方便别人读代码。
4. static 的作用是什么?
static 在不同地方意思不一样。局部静态变量只初始化一次,生命周期到程序结束;全局静态变量或函数限制只能在本文件用;类里的静态成员属于整个类,所有对象共享,不依赖具体对象。
5. volatile 是什么?
volatile 告诉编译器:这个变量可能随时被外部改变,不要把它优化成寄存器或直接缓存。比如硬件寄存器、信号处理里的标志位会用。注意它不保证原子性,也不等于线程安全。
6. extern “C” 是干嘛的?
C++ 有函数名修饰,比如参数类型不同,编译出来的符号名会不一样。extern “C” 就是告诉编译器这段代码按 C 的规则生成符号,方便 C 和 C++ 互相调用。一般用在跨语言接口或者动态库导出。
7. 宏定义和内联函数有什么区别?
宏是预处理器直接文本替换,没有类型检查,容易出优先级问题;内联函数是编译期展开,有类型检查,也更好调试。所以能用内联函数、const 常量,就尽量少用宏。
8. sizeof 和 strlen 有什么区别?
sizeof 是运算符,编译期就能算出来变量或类型占多少字节;strlen 是函数,运行时才去数字符串到 ‘\0’ 为止的长度。char buf[10] 里存 “abc”,sizeof(buf) 是 10,strlen(buf) 是 3。
9. i++ 和 ++i 有什么区别?
i++ 会返回旧值,一般要临时变量存一下;++i 返回新值,通常是引用。对于 int 这种内置类型编译器会优化,但如果是类对象,i++ 可能多一次拷贝,所以写循环尽量用 ++i。
10. 结构体和联合体有什么区别?
结构体每个成员都有自己独立的内存;联合体所有成员共享同一块内存,同一时间只能存一个成员。所以联合体省内存,但用的时候要小心覆盖问题。
11. 位域是什么?
位域就是让一个结构体里的成员可以占指定位数,比如一个 int 只用 3 个 bit,用来节省空间。不过它的布局和可移植性比较差,一般用在协议头、寄存器映射这种场景。
12. 什么是大小端?
大端是高字节放低地址,小端是低字节放低地址。比如 0x1234,小端在内存里就是 34 12。网络字节序一般是大端,x86 一般小端,所以网络编程经常要转换字节序。
13. 函数指针和回调函数是什么?
函数指针就是指向函数的指针,能像普通变量一样传参、调用。回调函数就是把一个函数指针传给另一个函数,让它在合适的时候调回来,比如事件通知、排序比较函数。
14. C++ 四种类型转换是什么?
static_cast 做编译期明确转换,像 int 转 double;dynamic_cast 做多态下的安全向下转换,会运行时检查;const_cast 去掉或加上 const;reinterpret_cast 重新解释底层 bit,非常不安全,一般只在底层用。
15. nullptr 和 NULL 有什么区别?
NULL 通常就是 0,在重载的时候可能会被当成 int;nullptr 有自己的类型 nullptr_t,只能转成指针,所以更安全。C++11 以后都推荐用 nullptr。
16. 栈和堆有什么区别?
栈由系统自动分配和释放,速度快,但空间小,一般存局部变量;堆由程序员手动申请和释放,空间大,但容易造成内存泄漏和碎片。栈地址一般从高向低增长,堆从低向高增长。
17. malloc/free 和 new/delete 有什么区别?
malloc/free 是函数,只负责分配和释放内存;new/delete 是运算符,除了内存还会调用构造和析构函数。new 失败会抛异常,malloc 失败返回 NULL。C++ 里尽量用 new/delete。
18. new 底层做了什么?
new 先调用 operator new 分配一块原始内存,然后在这块内存上调用构造函数构造对象。如果构造失败,会释放已分配的内存并抛异常。operator new 也可以重载来做内存池之类的事情。
19. 内存对齐是什么?
CPU 访问对齐的数据更快,所以编译器会在成员之间加一些填充,让每个成员的地址是它自身大小的整数倍。这会导致结构体大小比成员大小之和更大。可以用 #pragma pack 修改,但可能影响性能。
20. 什么是内存泄漏?怎么检测?
内存泄漏就是堆上申请的内存没释放,程序的可用内存越来越少。检测可以用 valgrind、AddressSanitizer 这类工具,或者自己写内存池统计。C++ 里尽量用智能指针避免泄漏。
21. 野指针和悬空指针有什么区别?
野指针是没有初始化的指针,指向随机地址;悬空指针是原来指向的内存被释放了,但指针还保存着那个地址。两个都很危险,访问都可能崩溃,用之前要确保指针有效。
22. 智能指针有哪几种?分别怎么用?
unique_ptr 独占所有权,不能拷贝只能移动;shared_ptr 多个指针共享一个对象,靠引用计数管理,计数归零就释放;weak_ptr 是弱引用,不增加引用计数,用来观察 shared_ptr,主要解决循环引用问题。
23. shared_ptr 循环引用怎么解决?
两个对象互相持有对方的 shared_ptr,引用计数永远不会变成 0,就泄漏了。解决办法是其中一方改成 weak_ptr,因为 weak_ptr 不增加计数,打破循环。
24. RAII 是什么?
RAII 就是资源获取即初始化。把资源的获取和释放绑定到对象的生命周期:构造时获取,析构时释放。比如智能指针、文件流、互斥锁都用这个思想,能有效避免忘记释放。
25. 深拷贝和浅拷贝有什么区别?
浅拷贝只拷贝指针,不拷贝指针指向的内容,多个对象指向同一块内存,容易 double free;深拷贝会新开辟内存,把内容也复制一份,两个对象互不影响。带动态内存的类通常要自己写深拷贝。
26. 构造函数可以是虚函数吗?析构函数为什么建议虚?
构造函数不能是虚函数,因为构造的时候对象还没完全建立,虚表指针还没准备好。析构函数建议虚,是因为用基类指针 delete 派生类对象时,如果析构不是虚,只会调基类析构,派生类资源没释放,行为未定义。
27. 虚函数机制是怎么实现的?
编译器会给有虚函数的类生成一个虚表,里面存各个虚函数的地址;每个对象里有一个虚表指针,指向这个虚表。调用虚函数时,先通过虚表指针找到虚表,再找函数地址,所以是运行时多态。
28. 纯虚函数和抽象类是什么?
纯虚函数就是只声明不实现的虚函数,写法是后面加 = 0。有纯虚函数的类叫抽象类,不能实例化,只能被继承,派生类必须实现这些纯虚函数,否则也变成抽象类。一般用来定义接口。
29. 多重继承和虚继承是怎么回事?
多重继承就是一个类继承多个父类。如果多个父类有共同基类,会出现菱形继承,导致最底层类里有多个基类子对象。虚继承就是让这些子对象只保留一份,解决二义性。
30. 怎么实现一个不能被继承的类?
C++11 直接用 final 关键字最简单。老办法是把构造函数私有,然后提供一个静态工厂方法返回对象,但这样类也没法直接栈上创建,使用上会受限。
31. 重载、重写、隐藏有什么区别?
重载是同一个作用域里,函数名相同但参数不同;重写是派生类覆盖基类的虚函数,参数和返回类型要一致;隐藏是派生类定义了同名函数,不管参数一不一样,只要不是虚函数重写,就把基类函数隐藏了。
32. 拷贝构造函数什么时候调用?
用一个已有对象初始化新对象时会调用,比如 Foo b = a; 另外按值传参、按值返回对象的时候也可能调用。不过返回值可能会有编译器优化,比如 RVO 直接构造到目标位置。
33. 默认会生成哪些成员函数?
C++11 以前默认生成:默认构造、拷贝构造、拷贝赋值、析构函数。C++11 以后还会默认生成移动构造和移动赋值,但如果你显式声明了拷贝构造、拷贝赋值或析构函数,移动版本就不会自动生成。
34. 移动构造和移动赋值是什么?
移动构造就是从一个右值对象“偷”资源,把指针直接转移过来,源对象置空,避免深拷贝。移动赋值类似,只是先释放自己的资源再偷别人的。一般配合 std::move 使用。
35. 单例模式怎么保证线程安全?
最常用的是局部静态变量写法:函数里写 static Singleton inst; 然后返回。C++11 保证局部静态变量初始化是线程安全的。也可以用双重检查锁加上 volatile/atomic,不过更麻烦。
36. vector 扩容机制是怎样的?
当 size 超过 capacity 时,vector 会重新分配一块更大的内存,通常是原来的 1.5 倍或 2 倍,然后把旧元素拷贝或移动过去,释放旧内存。扩容会导致所有指向旧内存的迭代器失效。
37. vector 和 list 有什么区别?
vector 是连续内存,随机访问快,尾部增删快,但中间插入删除要移动元素,慢;list 是双向链表,插入删除快,但随机访问要遍历,而且每个节点有额外指针开销。内存上 vector 更紧凑。
38. map 和 unordered_map 底层是什么?怎么选?
map 底层是红黑树,元素有序,查找、插入都是 O(log n);unordered_map 底层是哈希表,平均 O(1),但元素无序,哈希冲突严重时可能退化。需要顺序用 map,追求速度用 unordered_map。
39. 迭代器失效有哪些常见情况?
vector 插入元素时如果发生扩容,所有迭代器都失效;vector 删除元素后,被删除位置及之后的迭代器通常失效;list 插入不影响其他迭代器,但删除节点后该节点迭代器失效。一般 erase 会返回下一个有效迭代器。
40. STL sort 底层是什么?稳定吗?
sort 通常用内省排序,就是快速排序为主,递归太深转堆排序,小数据用插入排序。它不稳定,如果需要稳定排序用 stable_sort。
41. 模板特化是什么?
模板特化就是针对某个具体类型提供特殊的实现。比如模板函数对所有类型都做一件事,但对 const char* 做另一件事。有全特化和偏特化,常用来优化或处理特殊类型。
42. emplace_back 和 push_back 有什么区别?
push_back 是先构造一个临时对象,再拷贝或移动到容器里;emplace_back 是直接在容器尾部原地构造,少一次临时对象和拷贝/移动,效率更高,尤其是对象比较大的时候。
43. std::function 和 lambda 是什么关系?
std::function 是一个通用可调用对象包装器,可以存函数指针、函数对象、lambda。lambda 是一个匿名函数对象,有捕获列表,用起来更简洁。lambda 可以转成 std::function,但有轻微开销。
44. 移动语义和完美转发是什么?
移动语义就是让资源所有权可以转移,避免不必要拷贝,核心是右值引用和 std::move。完美转发是模板里用 T&& 万能引用加 std::forward,保持参数原来的左右值属性,避免传参时丢失右值。
45. 进程和线程有什么区别?
进程是资源分配的基本单位,有独立的地址空间;线程是 CPU 调度的基本单位,同一个进程里的线程共享地址空间、文件描述符等。线程切换开销小,但一个线程崩溃可能影响整个进程。
46. 死锁怎么产生?怎么避免?
死锁就是多个线程互相等对方持有的锁。比如 A 等 B 的锁,B 也等 A 的锁。避免方法:固定加锁顺序、使用 try_lock 不阻塞等待、设置锁超时、减小锁粒度,或者用 std::lock 一次锁多个。
47. 编译链接过程是怎样的?
一般分四步:预处理处理宏和头文件;编译把源码变成汇编;汇编把汇编变成目标文件;链接把多个目标文件和库合并成可执行文件。链接阶段会做符号解析和重定位。
48. 静态库和动态库有什么区别?
静态库在链接时会被拷贝到可执行文件里,体积大,更新库要重新编译;动态库运行时才加载,多个程序可以共享一份,更新方便,但部署时要保证库存在,可能有版本兼容问题。
49. 程序崩了怎么排查 core dump?
先用 ulimit -c unlimited 打开 core 生成,然后用 gdb 程序名 core 文件。进去后 bt 看调用栈,看崩溃在哪个函数,再打印相关变量。如果代码有调试符号信息,能直接看到文件名和行号。
50. 手写 strcpy 要注意什么?
主要注意:返回值返回目标指针,方便链式调用;要保存目标首地址;要拷贝 ‘\0’;最好对空指针做检查;目标缓冲区要足够大;如果内存重叠,strcpy 不保证正确,应该用 memmove。实现就是 while (*dst++ = *src++); 然后返回原来的 dst。