适用岗位:C++开发工程师(后端/嵌入式/客户端等)
复习重点:C++底层原理、现代C++特性、STL源码机制、内存管理、多线程与网络、常见手撕算法

选自:C++八股文2026版》 & 学员投稿

第一部分:C++基础语法与关键字(高频必问)

1. 指针和引用的区别

  • 定义与绑定:指针是一个变量,存储地址,可重新赋值指向其他地址,可为nullptr;引用是变量的别名,定义时必须初始化,且终身绑定同一个对象。

  • 内存占用:指针在32/64位系统下占4/8字节,有独立存储空间;引用在语义上不占用额外存储(底层通常也是地址,但视为原变量的直接别名)。

  • 操作安全性:指针存在空指针、野指针风险,支持指针算术运算;引用更安全,使用起来像原变量本身。

  • 函数传参:指针传参可通过解引用修改原值,也可修改指针指向;引用传参直接映射原变量,效率高且语法简洁。

2. static关键字的作用(多场景)

  • 局部变量:生命周期延长至程序结束,但作用域不变,函数退出后变量值保留。

  • 全局变量/函数:作用域限定在当前编译单元(.cpp文件),防止多文件链接时符号冲突(内部链接属性)。

  • 类成员变量:属于类本身,所有对象共享一份,必须在类外单独初始化(C++17后可用inline在类内初始化)。

  • 类成员函数:不依赖于具体对象,只能访问静态成员变量或其他静态函数,不能调用非静态成员。

3. const关键字的完整用法(新增重点)

  • 指针与引用

    • const int* p(指向常量的指针):不能通过p修改所指对象的值,但p本身可以指向其他地址。

    • int* const p(常量指针):p本身不能指向其他地址,但可以通过p修改所指对象的值。

    • const int* const p:两者都不能修改。

  • 成员函数void func() const 表示该函数不会修改对象的非mutable成员变量,可被常量对象调用。const和非const版本可以重载(如operator[])。

  • mutable成员变量:在const成员函数中仍可被修改,常用于锁、缓存计数器等场景。

4. structclass的区别

  • 默认访问权限struct默认为publicclass默认为private

  • 设计意图struct多用于纯数据聚合(兼容C语言),class用于完整的面向对象封装(继承、多态、RAII等)。

  • 模板类型参数(重要修正)classtypename在模板类型参数中完全等价;struct不能直接用于模板类型参数的声明template <struct T>是非标准写法)。

5. malloc/freenew/delete的区别

  • 语言特性malloc/free是C库函数,new/delete是C++运算符(可重载)。

  • 内存大小计算malloc需手动传入字节数,new由编译器自动计算。

  • 构造函数/析构函数malloc只分配原始内存,不调用构造函数;new分配后调用构造函数。free只释放内存,不调用析构函数;delete先析构再释放。

  • 数组操作delete[]会依次调用数组中每个元素的析构函数;delete只调用第一个,对数组使用delete会导致未定义行为(内存泄漏或崩溃)。

第二部分:面向对象与多态(深挖底层)

6. 多态的实现原理(虚函数表机制)

  • 分类:编译时多态(重载、模板)和运行时多态(虚函数、继承)。

  • 核心数据结构:每个包含虚函数的类都有一个虚函数表(vtable),存储该类所有虚函数的实际地址。

  • 对象布局:每个对象内部有一个虚表指针(vptr),指向所属类的虚表。对象构造时vptr被初始化。

  • 调用过程:通过基类指针/引用调用虚函数时,程序运行时根据对象的vptr找到虚表,再定位到正确的函数地址(动态绑定)。

7. 析构函数为什么必须是虚函数(重要)

  • 如果基类析构函数不是虚函数,用基类指针delete派生类对象时,只会调用基类析构函数,派生类中的资源(如堆内存、文件句柄)无法释放,造成资源泄漏。

  • 将基类析构函数声明为virtual后,会先调用派生类析构函数,再调用基类析构函数,确保完整的对象清理。

8. 构造函数为什么不能是虚函数

  • 依赖vptr:虚函数调用依赖对象的vptr,而vptr是在构造函数初始化列表执行期间才被设置的。

  • 逻辑矛盾:构造顺序是从基类到派生类,如果构造函数是虚的,在基类构造时无法确定派生类的具体信息,导致无法完成多态构造。

  • 语义不符:构造函数的作用是“创建对象”,而虚函数的含义是“在运行时根据对象实际类型调用函数”,对象都还没创建完,虚调用没有意义。

9. 纯虚函数和虚函数的区别

  • 有无实现:虚函数可以有默认实现(可选重写);纯虚函数用=0声明,通常不提供实现(但C++允许给纯虚函数提供定义,极少使用)。

  • 对类的影响:包含纯虚函数的类成为抽象类,无法实例化。派生类必须重写所有纯虚函数,否则派生类也是抽象类。

  • 设计目的:虚函数用于提供默认行为;纯虚函数用于定义接口契约(类似Java的Interface)。

第三部分:内存管理(避坑指南 + 现代C++)

10. 堆和栈的区别

特性 栈(Stack) 堆(Heap)
管理方式 编译器自动分配/释放 程序员手动管理(new/delete
空间大小 较小(通常几MB,取决于系统/编译器) 较大(受系统虚拟内存限制)
访问速度 快(CPU直接支持) 慢(涉及内存分配器算法)
碎片问题 无碎片(后进先出) 容易产生外部碎片
分配方式 连续分配 链表/空闲链表管理,不保证连续

11. 内存对齐(字节填充)(新增高频考点)

  • 对齐规则:每个成员按其自身大小对齐(如int按4字节对齐,double按8字节对齐);结构体的整体对齐大小为其最大成员大小的整数倍。

  • 示例struct A { char c; int i; }; 在32位下sizeof(A)通常为8(c后面填充3个字节),而非5。

  • 为什么要对齐:CPU访问内存时按字长(4/8字节)读取,未对齐的数据需要两次读取并拼接,降低效率。

  • 手动控制#pragma pack(n) 可以指定对齐值(如网络协议数据包常用#pragma pack(1)紧凑排列)。

12. 智能指针(C++11)

  • unique_ptr:独占所有权,禁止拷贝(拷贝构造被delete),支持移动语义(std::move)。内部无引用计数,开销极小。

  • shared_ptr:共享所有权,基于引用计数(控制块)。计数归零时自动释放对象。存在循环引用风险。

  • weak_ptr:弱引用,不增加引用计数。循环引用打破机制详解:对象A持有B的shared_ptr,B持有A的weak_ptr。A析构时B的计数减1,B正常释放;B释放后,A中的weak_ptr自动置空,形成完整释放链。使用时需lock()提升为shared_ptr

  • 循环引用案例:两个类互相持有对方的shared_ptr,导致双方引用计数永远为1,无法释放。解决方案:将其中一方改为weak_ptr

13. 移动语义与完美转发(C++11)(新增现代C++核心)

  • 左值与右值:左值有持久地址(如变量名),右值无持久地址(如临时对象、字面量、std::move的结果)。

  • std::move的本质:无条件将左值强制转换为右值引用,使其可以匹配移动构造函数或移动赋值运算符,从而“窃取”资源(如堆内存),避免拷贝。

  • std::forward(完美转发):在泛型函数中,保持参数的原始引用类型(左值保持左值,右值保持右值),常用于工厂函数和容器emplace_back的实现。

  • 性能收益:移动语义使大对象(如vectorstring)传参和返回时开销极低,是C++11性能优化的关键。

14. 内存泄漏的定位与解决

  • 定义:动态分配的内存(堆)未被释放,且指针丢失,导致内存无法被回收。

  • 常见场景new/malloc后未对应delete/free;异常抛出时跳过释放代码;循环引用导致shared_ptr无法释放。

  • 解决手段

    1. 优先使用智能指针(RAII思想)。

    2. 使用工具检测:Valgrind(Linux)、AddressSanitizer(ASAN)、Visual Studio诊断工具。

    3. 代码审查确保new/delete配对,或使用std::unique_ptr<T[]>处理数组。

第四部分:STL容器与迭代器(高频源码级)

15. vectorlist的区别

  • 底层结构vector是动态数组(连续内存),list是双向链表(不连续节点)。

  • 随机访问vector支持O(1)随机访问,list仅支持顺序遍历O(n)

  • 插入/删除vector尾部操作O(1),中间操作需移动元素O(n)list任意位置插入/删除O(1)(前提是已知迭代器位置)。

  • 内存开销vector可能预留多余容量(capacity),list每个节点额外存储前后指针(8/16字节开销)。

16. map/setunordered_map/unordered_set的区别

  • 底层实现map/set为红黑树(平衡二叉搜索树),元素自动排序(升序);unordered_*为哈希表,元素无序。

  • 时间复杂度:红黑树增删查改均为O(log n);哈希表平均O(1),最坏O(n)(大量哈希冲突时)。

  • 适用场景:需要有序遍历、范围查找时用map;追求极致查找速度、不关心顺序时用unordered_map

  • 键的类型要求:红黑树要求键支持<运算符;哈希表要求键支持std::hash==运算符。

17. 迭代器失效问题及解决方案(修正版)

  • vectorpush_back仅在触发扩容(重新分配内存)时使所有迭代器失效;若不扩容,仅end()迭代器失效。inserterase会导致插入/删除位置及其之后的所有迭代器失效。

  • listinsert不影响已有迭代器;erase仅使被删迭代器失效,其余保持有效。

  • deque:头部/尾部插入可能使所有迭代器失效,但元素地址不变(视具体实现)。

  • 解决策略:利用erase/insert的返回值(返回新的有效迭代器);遍历删除时使用it = vec.erase(it)写法;避免在循环中做插入操作。

第五部分:操作系统与多线程(必考硬核)

18. 进程和线程的区别

  • 资源维度:进程是资源分配的最小单位(独立虚拟地址空间、文件描述符等);线程是CPU调度的最小单位,线程共享所属进程的资源。

  • 切换开销:进程切换需刷新TLB、切换页表,开销大;线程切换仅需切换寄存器上下文,开销小。

  • 通信方式:进程间通信(IPC)复杂(管道、共享内存、Socket等);线程间通信天然共享内存,但需加锁同步。

19. 死锁的四个必要条件与避免方法

  • 互斥(Mutual exclusion):资源不能被共享,一次只能被一个进程占用。

  • 持有并等待(Hold and wait):进程持有至少一个资源,并正在等待其他进程占有的资源。

  • 不可抢占(No preemption):已分配的资源不能被强行剥夺。

  • 循环等待(Circular wait):多个进程形成环路,每个进程等待下一个进程占有的资源。

  • 避免策略:破坏任一条件。常用方法——资源有序分配法(破坏循环等待)、设置超时机制(破坏等待条件)、使用银行家算法(预防死锁)。

20. 进程间通信(IPC)方式

  • 管道(Pipe/FIFO):半双工,用于父子进程或同主机进程。

  • 消息队列(Message Queue):存储在系统内核,容量有限,支持消息类型过滤。

  • 共享内存(Shared Memory):最快IPC,直接映射物理内存,需配合信号量或互斥锁同步。

  • 信号量(Semaphore):计数器,用于同步与互斥,本质上不是传输数据,而是协调访问。

  • Socket:最通用,支持跨主机TCP/UDP通信。

21. selectpollepoll的区别(IO多路复用)

  • select:fd数量限制为FD_SETSIZE(通常1024/2048);每次调用需将fd集合从用户态拷贝到内核态;采用轮询方式,效率随fd数量线性下降。

  • poll:使用链表存储fd,无数量限制;但仍需拷贝和全量轮询,效率与select类似。

  • epoll(Linux专属):基于事件驱动(红黑树+就绪链表)。epoll_wait只返回就绪的fd,无需轮询。支持边缘触发(ET)和水平触发(LT)。重要补充:ET模式需配合非阻塞fd使用,若一次read未读完数据,下次不再通知,可能漏读,因此必须循环读取直至EAGAIN。适合高并发、长连接场景。

第六部分:网络基础与系统交互

22. HTTP和HTTPS的区别

  • 传输层:HTTP默认80端口,HTTPS默认443端口。

  • 安全层:HTTP明文传输,无加密;HTTPS = HTTP + SSL/TLS加密层,提供数据机密性、完整性校验和服务器身份认证(数字证书)。

  • 性能:HTTPS初次握手(SSL/TLS握手)耗时较长,需加解密计算,略慢于HTTP,但现已被广泛采用(尤其是涉及隐私的场景)。

23. 访问www.baidu.com背后发生了什么(经典全链路)

  1. DNS解析:本地Hosts -> 递归/迭代DNS查询,将域名解析为IP地址。

  2. TCP三次握手:客户端与服务器建立TCP连接(SYN -> SYN+ACK -> ACK)。

  3. 发送HTTP请求:构建HTTP报文(GET /),通过TCP连接发送。

  4. 服务器处理:Nginx/Tomcat等后端服务处理请求,可能涉及数据库查询、缓存等。

  5. 服务器响应:返回HTTP状态码(200)、响应头及HTML资源。

  6. 浏览器渲染:解析HTML、CSS、JS,加载静态资源(图片、JS等)。

  7. TCP四次挥手:关闭连接(或保持长连接复用)。

第七部分:设计模式(新增常考模块)

24. 单例模式(线程安全实现)

  • 懒汉式(双重检查锁定 DCLP):在C++11之前存在内存屏障问题(指令重排可能导致获取未初始化对象),C++11之后可用std::atomicstd::memory_order正确实现,但复杂度高。

  • Meyers Singleton(最推荐):利用C++11标准的局部静态变量初始化线程安全特性。

    class Singleton {
    public:
        static Singleton& getInstance() {
            static Singleton instance; // C++11保证线程安全
            return instance;
        }
        Singleton(const Singleton&) = delete;
        Singleton& operator=(const Singleton&) = delete;
    private:
        Singleton() {}
    };
  • 饿汉式:程序启动时即创建实例,线程安全但可能造成启动延迟和资源浪费(即使未使用也会创建)。

第八部分:手撕代码(高频题型)

面试中通常会要求现场写代码,难度中等(LeetCode Hot 100级别),重点考察思路清晰度边界条件处理时间/空间复杂度优化

重要技巧:熟练使用STL算法(std::reversestd::sortstd::unique等)可减少手写冗余代码,体现对标准库的熟悉,但面试官可能追问其复杂度(如std::sortO(n log n))。

高频题目类型汇总

  • 字符串处理:翻转字符串(原地,注意std::reverse)、判断数字回文(不用转字符串)、最长无重复子串(滑动窗口)。

  • 数组操作:双指针(去重、两数之和)、滑动窗口(最大/最小子数组)、合并两个有序数组(从后往前填充)。

  • 链表:反转链表(迭代/递归)、判断环形链表(快慢指针)、合并两个有序链表(递归/迭代)。

  • 二叉树:后序遍历(递归+非递归(双栈法))、层序遍历(BFS队列)、求二叉树的最大深度、判断平衡二叉树(后序遍历剪枝)。

  • 动态规划:爬楼梯(斐波那契,注意long long防溢出)、最大子序和(Kadane算法)。

  • 数学逻辑:判断素数(优化到sqrt(n))、正整数各位相加直到个位数(数根,直接(n-1)%9+1即可)。

备考冲刺建议

  1. 深挖原理:不能停留于“会用”,要理解STL底层数据结构(如红黑树特点、vector扩容因子通常为2或1.5)、智能指针控制块结构(包含强引用计数、弱引用计数、删除器等)。

  2. 手写练习:务必在自己电脑上编译调试高频手撕题,注意处理空指针、负值等边界,关注编译器的警告信息。

  3. 项目结合:准备1-2个能体现C++11/14/17特性的项目,准备好“为什么用这个特性”以及“遇到了什么坑”的回答(例如为什么用shared_ptr而非裸指针,std::move在哪处优化了性能)。

  4. 反问准备:面试最后通常有反问环节,建议从“技术栈迭代周期”、“团队当前面临的技术挑战”等角度提问,展现积极思考。


祝你面试顺利!这份文档涵盖了基础、进阶和实战细节,反复温习一定能大幅提升通过率。💪

更多推荐