C++ 基础 常考面试题总结
1. C和C++的区别?
- C是面向过程的语言,核心是函数和过程;C++是多范式语言,支持面向过程、面向对象、泛型编程。
- C++在C基础上扩展了类、继承、多态、模板、STL等特性,C没有这些面向对象和泛型的能力。
- C++支持函数重载、运算符重载、异常处理,C不支持。
- C++的类型检查更严格,C相对更灵活但也更容易出错。
- C++引入了引用、命名空间、智能指针等现代特性,提升了代码安全性和可维护性。
2. const与#define的区别?
- 类型安全:const有明确类型,编译器会做类型检查;#define是简单文本替换,无类型,容易出错。
- 作用域:const遵循C++作用域规则,可限制在局部/类内;#define是全局宏,作用域从定义点到文件结束(或#undef)。
- 调试友好:const变量会进入符号表,调试时可见;#define宏在预处理阶段展开,调试时看不到原始宏名。
- 内存占用:const会分配内存(除非被优化掉);#define不分配内存,只是文本替换。
3. 悬空指针与野指针的区别?
- 悬空指针:指向的内存已经被释放(如delete后未置空),但指针本身非空,访问会导致未定义行为。
- 野指针:未初始化的指针,值是随机垃圾值,指向不可预料的内存地址,访问极其危险。
- 共同点:两者都指向无效内存,访问都会导致程序崩溃或数据破坏。
4. struct与class的区别?
- 默认访问权限:struct默认是public,class默认是private。
- 默认继承权限:struct默认是public继承,class默认是private继承。
- 语义习惯:struct多用于数据聚合(POD类型),class多用于封装和行为定义。
- 语法上,两者几乎等价,可互换使用,只是默认权限不同。
5. sizeof和strlen的区别?
- sizeof是运算符,编译时计算,返回类型/对象占用的内存字节数,包括字符串末尾的'\0'。
- strlen是库函数,运行时计算,返回字符串的实际长度(不包括'\0'),遇到'\0'停止。
- 对数组:sizeof(数组名)返回整个数组大小;strlen(数组名)只计算到第一个'\0'。
- 对指针:sizeof(指针)返回指针本身大小(4/8字节);strlen(指针)计算指向的字符串长度。
6. 32位、64位系统中,各种常用内置数据类型占用的字节数?
7. 深拷贝与浅拷贝的区别?
- 浅拷贝:仅拷贝指针本身,不拷贝指向的内存,多个对象共享同一块内存,释放时会导致double free。
- 深拷贝:拷贝指针指向的内存内容,每个对象拥有独立的内存,互不影响,避免内存泄漏和重复释放。
- 默认拷贝构造/赋值运算符是浅拷贝;当类中有堆内存成员时,必须手动实现深拷贝。
8. 派生类中构造函数、析构函数调用顺序?
- 构造顺序:先调用基类构造函数 → 再调用成员对象构造函数 → 最后调用派生类构造函数。
- 析构顺序:先调用派生类析构函数 → 再调用成员对象析构函数 → 最后调用基类析构函数。
- 多继承时,基类构造顺序按声明顺序,析构顺序相反。
9. C++类中数据成员初始化顺序?
- 初始化顺序与成员在类中的声明顺序一致,与初始化列表中的顺序无关。
- 先初始化基类成员,再初始化本类成员,按声明顺序依次初始化。
- 若初始化列表中顺序与声明顺序不同,仍按声明顺序初始化,可能导致未定义行为。
10. 结构体内存对齐问题?结构体/类大小的计算?
- 内存对齐是为了提升CPU访问效率,编译器会自动填充字节,使成员地址对齐到指定边界(如4/8字节)。
- 对齐规则:每个成员的起始地址是自身大小的整数倍;结构体总大小是最大成员大小的整数倍;嵌套结构体按其最大成员大小对齐。
- 例:struct { char a; int b; },在32位系统中大小为8(char占1,填充3字节,int占4,共8)。
11. static_cast、dynamic_cast、const_cast、reinterpret_cast的区别?
- static_cast:用于基本类型转换、父子类指针/引用转换(无运行时检查),编译期完成。
- dynamic_cast:用于多态类型的父子类指针/引用转换,运行时检查类型,失败返回空指针/抛出异常。
- const_cast:用于移除或添加const/volatile修饰,不能改变类型本身。
- reinterpret_cast:用于任意指针/整数之间的强制转换,直接重解释内存,极其危险,仅用于底层操作。
12. 智能指针是什么?
- 智能指针是封装了原始指针的类模板,通过RAII机制自动管理堆内存,避免内存泄漏。
- 核心类型:unique_ptr:独占所有权,不可拷贝,可移动,离开作用域自动释放。shared_ptr:共享所有权,通过引用计数管理,计数为0时释放,线程安全。weak_ptr:辅助shared_ptr,不增加引用计数,解决循环引用问题。
13. 计算类大小例子
- 空类:大小为1字节(占位,保证不同对象地址不同)。
- 仅含非虚成员函数:大小为0(函数不占对象内存)。
- 含虚函数:大小为指针大小(4/8字节,指向虚函数表)。
- 例:
14. 大端与小端的概念?各自的优势是什么?
- 大端:高位字节存放在低地址,低位字节存放在高地址(符合人类阅读习惯)。
- 小端:低位字节存放在低地址,高位字节存放在高地址(CPU处理更高效)。
- 优势:大端:网络传输常用,数据表示直观,便于调试。小端:CPU在处理多字节数据时,可直接从低地址开始读取,无需字节序转换。
15. C++中*和&同时使用是什么意思?
- int* &p:表示指向int的指针的引用,即指针的别名,可修改指针本身的值。
- int &*p:非法语法,不存在指向引用的指针(引用本身不是对象,无地址)。
- 常见场景:函数参数中传递指针的引用,用于修改指针本身(如动态分配内存)。
16. C++ vector与list区别?
17. 引用和指针之间的区别?
- 定义方式:引用必须在定义时初始化,且一旦绑定到某个对象后就不能再改变;指针可以在定义后重新指向其他对象。
- 空值情况:引用不能为“空”,必须绑定到一个有效的对象;指针可以为nullptr,表示不指向任何对象。
- 内存占用:引用本身不占用额外内存(本质是别名);指针占用内存(32位4字节,64位8字节),存储指向对象的地址。
- 使用方式:引用直接使用对象名访问,无需解引用;指针需要通过*解引用访问对象。
- sizeof:sizeof(引用)返回被引用对象的大小;sizeof(指针)返回指针本身的大小。
18. 栈和堆中的内存分配有何区别?
19. 存在哪些类型的智能指针?
- unique_ptr:独占所有权,不可拷贝,可移动,离开作用域自动释放内存。
- shared_ptr:共享所有权,通过引用计数管理,线程安全,计数为0时释放。
- weak_ptr:辅助shared_ptr,不增加引用计数,可解决循环引用问题,需通过lock()转为shared_ptr使用。
- auto_ptr:C++98引入,C++11后被弃用,独占所有权但拷贝语义不安全。
20. unique_ptr是如何实现的?我们如何强制在unique_ptr中仅存在一个对象所有者?
- 实现核心:封装原始指针,在析构函数中调用delete释放内存;禁用拷贝构造和拷贝赋值运算符,只保留移动构造和移动赋值。
- 强制唯一所有者:显式删除拷贝构造函数和拷贝赋值运算符:unique_ptr(const unique_ptr&) = delete;和unique_ptr& operator=(const unique_ptr&) = delete;。提供移动语义:通过std::move转移所有权,确保同一时间只有一个unique_ptr指向对象。禁止拷贝,只允许移动,从语法层面保证所有权唯一。
21. 插入排序和选择排序
- 插入排序:核心:将未排序元素逐个插入到已排序序列的合适位置。时间复杂度:最好O(n)(已排序),最坏O(n²),平均O(n²)。空间复杂度:O(1),稳定排序。
- 选择排序:核心:每次从未排序部分选择最小/最大元素,放到已排序部分末尾。时间复杂度:最好/最坏/平均均为O(n²)。空间复杂度:O(1),不稳定排序(交换可能破坏相等元素的相对顺序)。
22. 关于静态内存分配和动态内存分配的区别及过程
- 静态内存分配:时机:编译期/链接期确定,程序启动时分配。位置:全局/静态变量存放在数据段/只读数据段。生命周期:整个程序运行期间有效,程序结束时释放。特点:大小固定,分配/释放由编译器自动管理。
- 动态内存分配:时机:运行时根据需求分配。位置:堆区。生命周期:从new/malloc到delete/free,或进程退出。特点:大小可变,需手动管理,易产生内存泄漏/碎片。
23. C++从代码到可执行二进制文件的过程
- 预处理:处理#include、#define等宏,生成.i文件。
- 编译:将预处理后的代码翻译成汇编代码,生成.s文件。
- 汇编:将汇编代码翻译成机器码,生成.o目标文件。
- 链接:将多个目标文件和库文件链接,解析符号引用,生成可执行文件。
24. 宏定义求两个元素的最小值
#define MIN(a, b) ((a) < (b) ? (a) : (b))
- 注意:宏是文本替换,需用括号包裹参数和整体,避免优先级问题;若参数是表达式(如MIN(i++, j++)),会导致多次求值。
25. 分别设置和清除一个整数的第三位?
- 假设从0开始计数,第三位是1 << 3(即8)。
- 设置第三位为1:num |= (1 << 3)。
- 清除第三位为0:num &= ~(1 << 3)。
- 检查第三位:(num & (1 << 3)) != 0。
26. 什么是多态?多态有什么用途?
- 多态定义:同一接口,不同实现;通过基类指针/引用调用时,根据实际对象类型执行对应子类的方法。
- 实现条件:基类有虚函数,子类重写虚函数,通过基类指针/引用调用。
- 用途:接口抽象:统一对外接口,隐藏具体实现。代码复用:通过继承复用基类代码,通过多态扩展子类行为。可扩展性:新增子类无需修改原有代码,符合开闭原则。
27. new和malloc的区别?
28. C++的内存分区?
- 栈区(Stack):局部变量、函数参数、返回地址,自动分配释放。
- 堆区(Heap):new/malloc分配的内存,手动管理。
- 全局/静态存储区:全局变量、静态变量,程序运行期间有效。
- 常量存储区:字符串常量、const全局变量,只读。
- 代码区(Text):可执行代码、只读常量,共享且只读。
29. vector、map、multimap、unordered_map
30. unordered_multimap的底层数据结构,以及几种map容器如何选择?
- unordered_multimap底层:哈希表(数组+链表/红黑树),通过哈希函数映射键到桶,解决冲突用链地址法。
- 容器选择:需要有序、键唯一:选map。需要有序、键可重复:选multimap。追求平均O(1)查找、无序、键唯一:选unordered_map。追求平均O(1)查找、无序、键可重复:选unordered_multimap。
31. 内存泄漏怎么产生的?如何避免?
- 产生原因:堆内存分配后,指针丢失(如new后未delete、指针重新赋值、异常导致delete未执行),导致内存无法回收。
- 避免方法:优先使用智能指针(unique_ptr/shared_ptr),自动管理内存。遵循RAII原则,资源获取即初始化。避免裸指针,使用容器管理动态分配的对象。工具检测:Valgrind、AddressSanitizer等。
32. 说几个C++11的新特性?
- 智能指针:unique_ptr、shared_ptr、weak_ptr,替代auto_ptr。
- 右值引用与移动语义:减少拷贝开销,支持移动构造/移动赋值。
- Lambda表达式:匿名函数,方便编写回调和算法。
- auto与decltype:自动类型推导,简化代码。
- 范围for循环:for (auto &x : container),遍历容器更简洁。
- nullptr:替代NULL,避免空指针歧义。
- ** constexpr**:编译期常量表达式。
- 委托构造函数:构造函数可调用其他构造函数。
- STL扩展:unordered_map、forward_list、array等
33. 虚函数表(vtable)和虚表指针(vptr)是什么?如何工作?
- 核心定义虚表(vtable):每个含虚函数的类都有一个独立的只读存储区表,存储该类的虚函数地址,由编译器在编译期生成。虚表指针(vptr):每个含虚函数的对象都会在内存头部占用4/8字节空间,指向所属类的虚表,由构造函数在对象创建时自动初始化。
- 工作流程基类定义虚函数,编译器为基类生成虚表,存入基类虚函数地址;子类重写虚函数,编译器为子类生成新虚表,覆盖重写函数的地址,继承未重写的虚函数地址;对象创建时,构造函数给vptr赋值,指向自身类的虚表;基类指针/引用调用虚函数时,通过vptr找到虚表,再根据函数偏移量调用实际对象的虚函数(动态绑定)。
- 关键特性:虚表属于类,vptr属于对象;多继承时对象会有多个vptr,分别指向对应基类的虚表。
34. 为什么构造函数不能是虚函数?析构函数为什么建议设为虚函数?
- 构造函数不能是虚函数虚函数依赖vptr调用,而vptr在构造函数执行时才初始化,构造函数执行前vptr不存在,无法完成动态绑定;构造函数的作用是创建对象,需明确具体类的构造逻辑,虚构造无实际意义。
- 析构函数建议设为虚函数避免基类指针指向子类对象时,仅调用基类析构函数导致子类资源泄漏;设为虚函数后,会通过vptr触发动态绑定,先执行子类析构,再执行基类析构。
35. 什么是右值引用?移动语义的作用是什么?
- 右值引用定义:用&&标识,专门绑定到临时对象(右值,如返回值、字面量),无法绑定到左值(有名字的对象)。
- 移动语义核心:通过移动构造/移动赋值,直接“窃取”临时对象的资源(如堆内存指针),而非拷贝,大幅减少内存拷贝开销。
- 典型场景:STL容器扩容、函数返回大对象、智能指针所有权转移。
// 移动构造示例核心逻辑
class MyString {
public:
MyString(MyString&& other) noexcept { // 右值引用参数
this->str = other.str;
other.str = nullptr; // 源对象置空,避免重复释放
}
};
36. 什么是完美转发?std::forward的作用?
- 完美转发定义:在函数模板中,将传入的参数原封不动地转发给内部调用的函数,保留参数的左值/右值属性。
- std::forward作用:实现完美转发的核心函数,根据参数的实际类型,将其转换为左值引用或右值引用。
- 适用场景:泛型函数、工厂函数、智能指针构造等,避免重复编写左值/右值重载函数。
template <typename T>
void wrapper(T&& arg) {
func(std::forward<T>(arg)); // 完美转发,保留arg的左/右值属性
}
37. C++中的拷贝省略是什么?有哪些场景?
- 核心定义:编译器的优化手段,直接跳过临时对象的拷贝/移动构造过程,减少内存开销,即使拷贝构造是私有的也能生效。
- 常见场景返回值优化(RVO):函数返回局部对象时,直接在调用方的内存空间构造对象;具名返回值优化(NRVO):函数返回具名局部对象时的优化;临时对象传参:将临时对象直接作为函数参数,跳过拷贝。
- 注意:C++17后,部分场景的拷贝省略成为强制行为,编译器必须执行。
38. 什么是RAII?在C++中有哪些应用?
- 核心定义:资源获取即初始化(Resource Acquisition Is Initialization),将资源的生命周期与对象的生命周期绑定。
- 核心逻辑:构造函数:获取资源(如内存、文件句柄、锁);析构函数:释放资源(自动执行,无需手动管理)。
- 典型应用智能指针(unique_ptr/shared_ptr):管理堆内存;锁守卫(std::lock_guard/std::unique_lock):管理互斥锁;文件流(std::fstream):管理文件句柄。
39. 什么是仿函数(函数对象)?与普通函数的区别?
- 核心定义:重载了()运算符的类/结构体实例,可像普通函数一样调用,也被称为函数对象。
- 与普通函数的区别状态保持:仿函数可通过成员变量存储状态,普通函数无法做到;类型信息:仿函数有具体类型,可作为模板参数(如STL算法的比较规则);性能:仿函数可被编译器内联优化,效率高于普通函数指针。
struct Add {
int operator()(int a, int b) const { return a + b; }
};
Add add;
int res = add(1, 2); // 调用仿函数
40. 模板特化和模板偏特化的区别?
- 模板特化:对模板的所有模板参数进行具体指定,分为全特化(类/函数)和局部特化(仅类)。全特化示例:template<> class MyTemplate<int> { ... };
- 模板偏特化:仅对模板的部分参数进行指定,或对参数的类型范围进行限制,仅支持类模板。偏特化示例:template<typename T> class MyTemplate<T*> { ... };(针对指针类型的偏特化)
- 核心区别:特化是“完全定制”,偏特化是“部分定制”;函数模板不支持偏特化,只能通过重载实现。
41. 什么是SFINAE?有什么作用?
- 核心定义:替换失败不是错误(Substitution Failure Is Not An Error),是C++模板的核心规则。
- 工作逻辑:编译器在模板参数推导时,若某一模板的替换失败,不会直接报错,而是跳过该模板,尝试其他匹配的模板。
- 典型应用类型萃取(判断类型是否有某成员、是否为指针等);函数模板重载的优先级控制;C++20前的概念(Concepts)替代方案。
42. C++中的异常处理机制是什么?throw、try-catch、finally的注意点?
- 核心机制:通过throw抛出异常,try包裹可能抛出异常的代码,catch捕获并处理异常,实现错误的集中处理。
- 关键规则异常抛出后,会沿着调用栈向上回溯,匹配第一个类型兼容的catch;异常抛出时,会自动调用栈上对象的析构函数(栈展开),保证资源释放;C++无原生finally,可通过RAII(如智能指针、锁守卫)实现“无论是否抛异常都执行的逻辑”。
- 注意点:构造函数抛异常会导致对象构造失败,析构函数禁止抛异常(会导致栈展开时的未定义行为)。
43. 什么是类型萃取?C++中有哪些常用的类型萃取工具?
- 核心定义:通过模板元编程,在编译期获取类型的属性(如是否为指针、是否为类、是否可拷贝),并返回编译期常量或类型。
- 常用工具(<type_traits>头文件)std::is_pointer<T>:判断是否为指针类型;std::is_class<T>:判断是否为类类型;std::remove_reference<T>:移除类型的引用属性;std::enable_if:结合SFINAE,控制模板的实例化。
- 应用场景:模板函数的条件编译、泛型算法的优化、智能指针的类型判断。
44. 并发编程中,互斥锁和自旋锁的区别?适用场景?
- 互斥锁(std::mutex)核心逻辑:获取锁失败时,线程进入阻塞状态,释放CPU资源;优点:CPU开销低,适合临界区执行时间长的场景;缺点:线程切换有开销。
- 自旋锁(std::atomic_flag实现)核心逻辑:获取锁失败时,线程循环忙等,不释放CPU资源;优点:无线程切换开销,适合临界区执行时间极短的场景;缺点:CPU开销高,易导致忙等竞争。
- 核心区别:获取锁失败时的线程状态(阻塞vs忙等),决定了适用场景的不同。
45. std::atomic的作用?与互斥锁的区别?
- 核心作用:实现原子操作,保证对共享变量的读-改-写操作在CPU层面不可分割,无需手动加锁,解决多线程下的竞态条件。
- 与互斥锁的区别粒度:atomic针对单个变量的原子操作,粒度细;互斥锁针对临界区(多个操作),粒度粗;性能:atomic基于CPU指令(如CAS),无线程切换开销,性能远高于互斥锁;适用场景:atomic适用于单个共享变量的计数、标志位;互斥锁适用于多个操作组成的临界区。
- 示例:std::atomic<int> count = 0; count++;(原子自增,无需加锁)。
46. 什么是内存屏障?在C++并发中起什么作用?
- 核心定义:CPU指令或编译器指令,用于禁止内存重排序,并强制刷新缓存,保证多线程间的内存可见性。
- 核心问题:编译器优化和CPU乱序执行会导致多线程下,一个线程对变量的修改,另一个线程无法及时看到(可见性问题)。
- C++中的实现std::memory_order:原子操作的内存序参数(如memory_order_seq_cst、memory_order_acquire/release);隐式内存屏障:互斥锁的加锁/解锁操作会自动插入内存屏障。
- 作用:保证原子操作的有序性和可见性,避免多线程下的重排序问题。
47. C++中的拷贝构造函数、赋值运算符重载、析构函数的“三法则”是什么?
- 核心定义:若类需要手动实现析构函数、拷贝构造函数、赋值运算符重载中的任意一个,通常需要实现另外两个,这一规则被称为“三法则”。
- 核心原因:需要手动实现的场景多为类管理堆内存(如char*),仅实现其中一个会导致内存泄漏、double free等问题。仅写析构函数:拷贝构造/赋值会做浅拷贝,导致多个对象共享内存,析构时重复释放;仅写拷贝构造:赋值运算符仍做浅拷贝,同样会导致内存问题。
- C++11扩展:加入移动构造和移动赋值,形成“五法则”。
C++面试总结 文章被收录于专栏
本专栏系统梳理C++面试高频考点,从基础语法、内存管理、STL与设计模式,到操作系统与项目实战,结合真实面试题深度解析,帮助开发者高效查漏补缺,提升技术理解与面试通过率,打造扎实的C++工程能力。

查看24道真题和解析