聊到C/C++程序设计面试题目整理这件事,我先说个观察:真正能拿到好offer的人,靠的不是题量,而是对基础和底层的理解深度。C/C++的面试题高度集中,翻来覆去就是指针、内存、面向对象、编译链接、STL那么几大块,但每年还是能刷掉一大批人。原因不在于没练过,而在于没弄懂背后的"为什么"——面试官问vector扩容,想听的绝不只是"2倍",而是你是否理解均摊复杂度;问虚函数,也不只是要你背出虚表,而是想确认你真正写过、调试过、踩过坑。
这篇文章就是一份能直接照着复习的C/C++程序设计面试题目整理,围绕面试官最喜欢用的核心考点,把考察思路、高频问题、手写代码的常见坑和实战经验一次讲透。适合正在准备校招、社招,或者打算系统过一遍C/C++基础知识的开发者。你会看到很多题目是"老生常谈",但我会把每个结论背后的推理链一起补上,这才是面试准备真正值钱的部分。
1. 面试准备的整体思路:先搞懂考官究竟想看什么
1.1 面试题目背后想考察的三层能力
C/C++的面试和Java、Python有个显著区别:它更偏向"你究竟怎么使用内存、怎么理解程序结构"。我面过不少开发者,也被面过很多次,总结下来,面试官出题大多围绕三层能力展开。
第一层是基础记忆。比如const和宏的区别、static在C和C++里的不同作用、指针和引用的区别。这一层是门槛,背得出不一定进,背不出大概率凉。但光背结论是不够的,因为面试官很擅长换一个代码场景,让你判断某个细微写法的输出。
第二层是原理理解。问题通常是"vector扩容时发生了什么""为什么析构函数要virtual""shared_ptr引用计数是怎么维护的"。这一层考察的是你是否读过源码、是否理解过底层运作。只要能把原理讲清楚,哪怕细节有遗漏,面试官一般也会放行。
第三层是工程直觉。比如多线程下如何安全使用shared_ptr、两个对象互相引用会不会泄漏、迭代器在什么情况下失效。这类问题没有标准说教答案,考察的是你有没有真实项目经验,有没有被线上问题毒打过。
所以"C/C++程序设计面试题目整理"不能只整理题目和结论,要把结论背后的推理链一起备好。我在后面每个板块都会尽量把这条链讲完整。你准备的时候也别满足于"这题我会",多问自己一层"为什么",效果会完全不一样。
1.2 一套兼顾效率和深度的复习打法
考虑到面试准备通常时间紧张,一周到一个月不等,我在实际准备中比较推荐"主线优先、成体系走一遍"的打法。
主线基本就是:指针和内存、类和对象、编译链接、STL和C++11新特性。这个顺序其实和程序的构建运行顺序一致:先有源码、再有对象、最后跑库。按这条线走,哪怕题目换了包装,你也能快速归位到对应知识点。
其次是"画内存图"。我见过太多人看懂了代码但画不出内存布局,结果一遇到改错题或手写题就露馅。自己准备时,只要碰到指针题就画一遍栈、堆、常量区和全局区的关系,看起来慢,实际收益极高。
再一个技巧是"自己给自己出题"。把每个知识点反着问:知道虚表存在,就问"如果虚表指针放在对象第一位,继承时怎么覆盖";知道move语义,就问"不用move会多几次拷贝"。这种自问自答能把知识点连成网,面试时被追问也不虚。
还有一个很多人忽略的点:手写代码和口头讲原理要分开练。有的人讲原理头头是道,一让手写链表翻转就卡壳。C/C++面试的手写环节频率非常高,务必在纸上或白板上多练几次,别只在编辑器里写。
2. C语言核心考点:先把指针、内存、修饰符这三座山翻过去
2.1 指针和引用的区别,以及数组和指针的血缘关系
先说最基础的:指针和引用的区别。常规答案是——指针是一个变量,保存的是地址,可以为空,可以重新指向别的对象;引用是已存在对象的别名,定义时必须初始化,且不能更换指向;sizeof一个指针在64位平台下是8字节,而sizeof引用返回的是被引对象的大小;有指针数组,但没有引用数组。
面试里还有一种更狠的考法:给出一大段代码,让你说输出或报错。这种题其实就是在测试你对"数组名退化""指针运算""取地址运算符"的敏感度。最典型的几个例子摘出来:
- int a[10]定义后,sizeof(a)是40,因为整个数组的大小是40字节;而int* p = a之后,sizeof(p)是8。原因是数组名在大多数表达式中会退化为指向首元素的指针,但作为sizeof的参数时保留数组类型。
- &a的类型是int(*)[10],数值虽然和a相同,但步长是整个数组的40字节。所以a+1跨4字节,&a+1跨40字节。这种题坑过无数人。
数组和指针的纠缠,本质上可以用一句"数组名带有长度信息,指针只保存一个地址"来破。但面试时最好进一步说明:数组名作为sizeof的参数、被&取地址、作为定义时的对象,都不退化;作为右值参与运算时才退化为指针。
函数指针也是一个高频点。比如要你写出函数指针类型:int (*fp)(int, double) = 某个函数;。为什么面试官盯上这个?因为函数指针在回调函数、表驱动编程、底层框架里极其常见,直接考察代码阅读能力。建议把"返回函数指针的函数"也看一眼,比如int (*get_fp(void))(int),剥洋葱式拆开就好,先看返回类型,再看参数列表。
2.2 内存布局:栈、堆、全局区、常量区,谁分配谁释放
C语言面试如果只考一道题,大概率是内存相关。我建议先把一张内存布局图刻在脑子里:代码段、数据段(已初始化全局/静态变量)、BSS段(未初始化全局/静态变量)、堆、栈、常量区。然后对应回答三个高频问题:
- 局部变量放在哪?栈。生命周期和所在函数绑定。
- 全局变量和static局部变量放在哪?数据段或BSS段。程序启动就分配,程序结束才释放。
- malloc出来的内存放哪?堆。需要free手动释放,否则内存泄漏。
这里有一个常被问的细节:malloc/realloc/calloc的区别。realloc可能移动内存,导致原来的指针失效;calloc会清零内存,malloc不保证清零。另一个容易忽略的点:malloc失败返回NULL,所以"一定要检查返回值"是基本素养。但实践中很多面试官会追问"malloc失败后应该怎么办",答案要根据场景说:嵌入式环境可能要保留现场,服务端可能直接失败退出,也可以考虑重试或缩小申请,但绝不能假装它不会失败。
手写字符串函数是经典复试环节。以strcpy为例,基础写法谁都会,但面试官要看的其实是边界处理和安全意识:
#include <stdio.h> char* my_strcpy(char* dest, const char* src) { if (dest == NULL || src == NULL) { return NULL; } if (dest == src) { return dest; } char* ret = dest; while (*src != '\0') { *dest++ = *src++; } *dest = '\0'; return ret; }这里有几个容易翻车的地方。第一,返回类型写成void会扣分,标准库返回char*是为了支持链式调用。第二,源地址和目的地址重叠时,从前往后拷贝可能把源数据覆盖掉,虽然严格来说重叠行为是未定义的,但很多面试官会追问;如果目标地址在源内容会变化的范围内,可以改用从后往前拷贝的思路。第三,忘记补结尾的'\0',这在边界条件下会导致缓冲区越界。
顺带说一句,很多面试官还爱让写字符串反转、atoi、二分查找。这些题都不难,但都是"看似简单、细节多"的典型。比如atoi要处理空指针、正负号、溢出、前导空格,能把这些边界都覆盖掉,代码虽然不长,却能体现工程意识。我建议准备阶段把这些函数每个都亲手写一遍,写到不用想为止。
2.3 const和static:最容易被组合出花样的修饰符
先说const。问"const intp"、"int * const p"、"int constp"的区别,是出镜率极高的基础题。前两个是"指针指向常量"和"指针本身是常量"的区别,第三个其实和第一个含义一样,只是写法别扭。读法有个口诀:从右往左读,遇到const就表示它左边的那个东西不可变。int * const p,从右往左:p先被const修饰,说明p本身不可改,而p可以改;const intp则是*p不可改,p本身可改。
const在C和C++里的语义差异也值得准备。C里const全局变量的值通常放在只读段,但不一定是标准保证;C++里const全局变量默认有内部链接,除非显式加extern。面试时能讲到这里,基本可以证明你确实在项目里用过,而不是背答案。
static在C语言里有三个主要作用:修饰局部变量,延长生命周期到程序结束,但作用域仍在函数内;修饰全局变量,限制链接属性,只在当前编译单元可见;修饰函数,同样限制外部链接。在C++里,static还可以修饰类成员变量和成员函数。
面试里最常见的坑是:"static局部变量是否线程安全?"答案是不一定。C++11引入了magic static,即局部static变量的初始化是线程安全的,但后续访问和修改并不是。这个点能体现你对语言标准的熟悉程度,也能引出多线程相关的追问。
3. C++面向对象与核心机制:从封装到多态,原理和代码一起上
3.1 虚函数和虚表:多态原理面试必答
C++面试里,虚函数相关的题几乎必出。第一个问题往往就是"为什么析构函数要声明为virtual"。因为当基类指针指向派生类对象时,如果析构不是虚函数,delete这个基类指针只会调用基类析构,派生类成员资源就泄漏了。
反过来,构造函数为什么不能是虚函数?因为构造对象时虚表指针vptr的初始化是分阶段的,构造完基类部分、再构造派生类部分的过程中,无法确定最终类型;而且虚函数调用依赖对象已经存在的vptr,没有对象就无法引用虚表地址。这个"为什么"比背答案重要太多。
原理层面要能画出来:每个包含虚函数的类都有一个虚表vtable,里面存放虚函数地址;对象内存布局中有一个隐藏的vptr指向这张表;继承时如果派生类没有覆盖虚函数,表里存的就是基类版本;覆盖之后,派生类版本替换对应表项。多态调用发生在运行时,通过vptr找到虚表,再索引到对应函数。
一个常见的衍生题:基类和派生类的虚表是否一样?不一样,派生类会生成自己的完整虚表,多继承下还可能有多个vptr。还有一个偏题:"虚函数可以inline吗?"答案是可以声明,但实际是否内联由编译器决定,因为运行时多态要通过虚表间接调用,通常无法内联。
再一个重点:纯虚函数和抽象类。含纯虚函数的类不能实例化,派生类必须实现所有纯虚函数后才能实例化。可以顺便提一句"接口类"的设计习惯:只有纯虚函数、没有数据成员,用于依赖倒置和插件架构。面试聊项目时,如果能自然提到自己用接口类解耦过模块,会很加分。
3.2 深拷贝、浅拷贝、拷贝构造与赋值运算符
只要C++里涉及自定义类,就一定会考到深拷贝和浅拷贝。浅拷贝是逐字节复制,如果类里有裸指针,两个对象会指向同一块内存,析构时出现double free。深拷贝则是重新分配内存并把目标数据复制过去。经典手写题是实现一个String类,需要包括构造、析构、拷贝构造、拷贝赋值、移动构造。一个值得写的版本大致是:
class String { public: String(const char* s = "") : m_size(strlen(s)), m_data(new char[m_size + 1]) { strcpy(m_data, s); } String(const String& other) : m_size(other.m_size), m_data(new char[m_size + 1]) { strcpy(m_data, other.m_data); } String(String&& other) noexcept : m_size(other.m_size), m_data(other.m_data) { other.m_data = nullptr; other.m_size = 0; } String& operator=(String other) { String tmp(other); swap(m_data, tmp.m_data); swap(m_size, tmp.m_size); return *this; } ~String() { delete[] m_data; } private: size_t m_size; char* m_data; };注意这里赋值用了copy-and-swap技巧,好处是异常安全。面试官如果性格较真的话,会追问:为什么赋值参数用传值而不是传const引用?因为传值时已经构造了一个临时对象,后面swap不抛异常,整个函数天然具备强异常安全保证。这个技巧如果能主动讲出来,会明显加分。
拷贝构造和赋值运算符的区别也要能脱口而出:拷贝构造是创建一个新对象;赋值运算符是在对象已存在的前提下,把另一个对象的状态赋过来。所以实现赋值运算符一定要处理自我赋值,否则先释放再拷贝时把自己清空了。标准写法里处理自我赋值可以用"先判断if (this != &other)",也可以依赖copy-and-swap自动规避。
3.3 重载、重写、隐藏:三个概念别糊成一团
面试常问"重载和重写的区别",但经常还藏着"隐藏"。重载发生在同一作用域内,函数名相同、参数列表不同,返回值不参与区分。重写发生在继承体系中,需要基类函数是虚函数,且函数签名完全一致。隐藏则是派生类中出现了与基类同名的函数,不管参数是否一致,都会导致基类版本被遮蔽。
隐藏最容易被考到:基类有void f(),派生类有void f(int),在派生类对象上调用obj.f(),会报错。很多人以为是重载,其实不匹配。因为名字查找先于参数匹配:在派生类作用域内先找到同名的f,就不再往基类找了,所以基类的f被隐藏。要访问基类版本,需要显式用Base::f(obj)。
这三个概念背后是C++名字查找规则和虚函数机制的差异。讲清楚区别,比光记结论更有说服力。我在准备面试时还给自己总结过一句话:重载是横向的,重写是纵向的,隐藏是不讲道理的遮蔽。
4. 内存管理与C++11智能指针:现代C++面试必考的RAII实践
4.1 智能指针三兄弟:shared_ptr、unique_ptr、weak_ptr怎么选
现代C++面试几乎不碰裸new/delete,问得更多的是智能指针。首先要能说出RAII:资源获取即初始化,资源在构造时获得,在析构时释放,保证异常安全。然后比较三种指针:
- unique_ptr独占所有权,不需要引用计数,开销几乎为零,是默认选择。不能直接拷贝,但可以move转移所有权。
- shared_ptr共享所有权,通过引用计数管理。引用计数本身线程安全,但被管理的对象不是自动线程安全的。需要注意控制块的存在,make_shared可以一次分配对象和控制块,减少一次内存分配。
- weak_ptr是弱引用,不增加引用计数,用来打破shared_ptr循环引用。要使用对象时先lock提升为shared_ptr。
高频题是:两个shared_ptr互相引用,会导致内存泄漏吗?会。因为引用计数永远无法归零,对象永远不会被析构。解法是其中一个方向改用weak_ptr。这个例子在项目里很常见:父子结构中父节点持有子节点的shared_ptr,子节点持有父节点的shared_ptr,结果整棵树的所有节点都不会释放。改成父节点用shared_ptr指向子节点,子节点用weak_ptr指回父节点即可。
还有一个延伸题:shared_ptr线程安全吗?要分两层回答:引用计数的加减是原子操作,所以计数本身安全;但指向对象的多线程读写,需要外部加锁或由使用者保证没有数据竞争。如果只回答"shared_ptr是线程安全的",基本等于没学过。
4.2 移动语义和完美转发:为什么C++11要引入右值引用
移动语义是为了避免无意义的深拷贝。传统C++里,函数返回一个大的vector或其他临时对象,中间过程可能多次深拷贝。右值引用允许我们把临时对象的资源"偷"过来,而不是重新分配内存复制一遍。
关键点要理解左值和右值:一个简单的判断标准是"能不能取地址"。能取地址的是左值,不能取地址的(比如字面量、临时对象)是右值。std::move本身并不移动任何东西,它只是把对象强制转换为右值引用类型,告诉编译器"你可以把资源转移出去"。所以move之后,原来的对象一般不要再使用,除非重新赋值。
完美转发是另一个老大难。template void wrapper(T&& arg)这里的T&&是转发引用,可以绑定左值和右值。如果要保持实参的左值/右值属性传给下一个函数,需要std::forward。这个点笔试出现概率极高,面试中更多是问理解,能说出"forward用于保持参数类别"就算过关。建议自己写一个小栗子验证一下,比如传左值进去,最终函数能接受到一个左值;传右值进去,最终函数接受到一个右值,这样才能真正理解std::forward的作用。
4.3 lambda:写起来简单,问起来全是细节
lambda也常见,因为现代C++工程里到处是std::sort、remove_if、for_each搭配lambda的写法。被问最多的是捕获列表:
- [=]按值捕获所有局部变量,[&]按引用捕获所有局部变量。
- 按值捕获的变量在lambda生成时是一个成员副本,默认operator()是const的,除非lambda标记mutable。
- 按引用捕获要注意生命周期,如果lambda逃逸出当前作用域(比如存到容器里异步执行),捕获的引用可能悬空。
有一个追问题:捕获this指针的lambda,捕获的对象生命周期怎么保证?捕获this一般用于成员函数内部的lambda,如果lambda在对象销毁后被调用,this就悬空了。解决方法是考虑捕获对象的副本值,或者在调用前确认对象仍存在。这些细节能答上来,面试官基本就不会再为难了。
我在实际项目中还遇到过一个坑:lambda按值捕获了一个大对象,本以为只是复制了引用,结果每次调用都深拷贝,性能下降明显。后来改成按引用捕获才解决。所以面试里如果被问"什么时候该用按值、什么时候该用按引用",可以结合性能场景回答,效果比背规则好得多。
5. STL底层原理与高频问答:别再只记接口
5.1 vector扩容机制:均摊复杂度为什么是O(1)
vector是STL的开门题。底层是连续内存,size()表示实际元素个数,capacity()表示已分配空间能容纳的元素个数。当push_back时size超过capacity,需要重新申请更大的内存,把旧元素迁移过去,再释放旧内存。常见扩容因子是2倍,也有实现采用1.5倍。
均摊复杂度是O(1),原理是扩容呈几何增长。虽然单次扩容是O(n),但扩容次数是O(log n),平均到每个push_back上就是常数时间。
选择2倍还是1.5倍有讲究。2倍重分配次数少,但每次浪费的空间可能较大;1.5倍能更好复用已释放的内存块,因为堆分配器在回收旧块之后更容易找到合适大小的空闲块。能讨论到这个层面,就不是背答案了。
衍生题:为什么不要在vector头部插入或删除?因为需要移动所有后续元素,复杂度O(n)。resize和reserve也经常被混在一起考:resize改变size,可能构造或析构元素;reserve只改变capacity,不改变size。这两个词区分不了,面试会很减分。
5.2 map和unordered_map:红黑树和哈希表的选择题
map基于红黑树,key有序,支持按顺序遍历,查找、插入、删除平均O(logn)。unordered_map底层是哈希表,查找、插入、删除平均O(1),但最坏情况O(n),元素顺序不确定。
面试常见追问是:什么情况必须用map?如果需要对元素排序、求前驱后继、范围查找,map有优势。如果只是按键查值且对顺序无要求,unordered_map一般更快,但内存开销是哈希桶。
关于红黑树,要能说出性质:节点分红黑,最长路径不超过最短路径的两倍,所以树高大约是2*log(n+1),保证了任一路径最长是O(logn)。如果面试官追问"为什么不直接用平衡二叉树AVL",答案方向是红黑树插入删除的旋转次数更少,AVL查询略快但平衡维护更严格,工程实现里红黑树更常见。不需要面面俱到,但有这个对比意识就够。
5.3 迭代器失效:这是必须亲手踩过的坑
迭代器失效是STL的经典杀招。vector的insert或erase会导致所有指向该位置之后的迭代器、指针、引用全部失效,因为元素搬移了;扩容后所有迭代器都失效。deque类似,但可能在两端操作时部分失效。list的erase只使指向被删除节点的迭代器失效,其他迭代器保持有效。map/set的erase也只影响被删除的迭代器,其他迭代器仍可用。
所以写删除循环时,最常见的错误是:
// 错误写法:erase后it已失效 for (auto it = vec.begin(); it != vec.end(); ++it) { if (*it == 某个值) { vec.erase(it); } }vector的正确写法是配合返回值:
for (auto it = vec.begin(); it != vec.end();) { if (*it == 某个值) { it = vec.erase(it); } else { ++it; } }map的erase场景则可以沿用"先记录下一个迭代器再删"的写法,因为map的erase不使其他迭代器失效。这个对比是面试官埋好的伏笔,能分清楚说明你确实写过STL代码,而不是只看过教程。
6. 编译、链接与工程实践:面试中容易被忽略的硬核项
6.1 从源码到可执行文件:四个阶段分别做了什么
C/C++程序的构建过程是面试里容易翻车的知识盲区。一句话版本是:预处理、编译、汇编、链接。
- 预处理:处理#include、宏替换、条件编译,生成.i文件。
- 编译:把预处理后的源码翻译成汇编语言,同时做语法和语义分析。
- 汇编:把汇编翻译成机器码,生成可重定位目标文件.o。
- 链接:把多个.o文件、静态库、动态库合并,解析符号引用,生成可执行文件。
常被追问的是:链接时"未定义符号"是怎么来的?通常是声明了但没有实现,或者忘记链接对应库。比如用了pthread_create却忘了加-lpthread。能结合一个实际的编译错误来回答,会显得很有实战经验。
还有一个经典坑:静态链接库、动态链接库和可执行文件对符号的处理不同。静态库在链接时会把用到的目标文件直接合入可执行文件,动态库则在运行时加载。因此动态库更新后无需重新编译程序,但需要确保运行时能找到对应的库,否则就报"cannot find shared library"。在交流项目经验时,提一句"我之前遇到过动态库版本不兼容的问题,最后用检查依赖和版本列表解决的",面试官就会认可你的工程能力。
6.2 static、extern、头文件防卫:工程规范类必知
在多个文件中共享全局变量,需要extern声明:在一个.c中定义int g_count;,在另一个.c中用extern int g_count;声明。但更好的实践是尽量不要用非const的全局变量,或者用函数、类、单例来封装。面试时如果被问"为什么不推荐全局变量",可以从耦合、线程安全、初始化顺序三个角度回答。
头文件防止重复包含有两种典型写法:头文件卫士(#ifndef XX_H)和#pragma once。面试问两者的差别,需要回答宏卫士更通用、跨编译器可靠,但需要手写宏名且可能存在宏名冲突;#pragma once写法简洁、编译稍快,但不是所有编译器都支持(实际上现代主流编译器都支持)。工程上的建议是项目统一一种风格就够,不要混用。
C++里还有常问的:inline函数在头文件里定义,为什么不会导致链接期多重定义?因为inline函数允许在多个翻译单元中重复定义,编译器会保证最终只留一份。这跟普通函数定义在头文件中被多个.cpp包含后产生的重定义错误形成对比。
6.3 一个完整的手写题:实现一个简化版shared_ptr
如果把"编译链接+内存管理+模板"串起来,有一个很好的手写题:实现一个简化的shared_ptr。它考察模板类、析构函数、引用计数、拷贝构造和赋值。一个Mini版本:
template <typename T> class shared_ptr { public: explicit shared_ptr(T* ptr = nullptr) : m_ptr(ptr), m_count(new size_t(1)) {} shared_ptr(const shared_ptr& other) : m_ptr(other.m_ptr), m_count(other.m_count) { ++(*m_count); } shared_ptr& operator=(const shared_ptr& other) { if (this != &other) { release(); m_ptr = other.m_ptr; m_count = other.m_count; ++(*m_count); } return *this; } ~shared_ptr() { release(); } T& operator*() const { return *m_ptr; } T* operator->() const { return m_ptr; } private: void release() { if (--(*m_count) == 0) { delete m_count; delete m_ptr; } } T* m_ptr; size_t* m_count; };这个简化版没有考虑make_shared、控制块、线程安全和数组删除,但用于手写展示已经足够。写完可以补一句:真实实现还要考虑T为数组、弱引用、线程安全控制块,这说明你了解工程版的复杂度。这种手写题往往出现在面试后半段,写得好能拉回前面很多失分。
7. 高频问题速查与避坑心得:把"临场答不好"变成"踩过坑所以会"
7.1 一张高频题速查表
整理了一张个人认为最值得考前过一遍的清单,每一条都标了回答要点。不是让你硬背,而是作为自测目录:
| 问题 | 回答要点 |
|---|---|
| 指针和引用区别 | 定义、必须初始化、是否可换绑、占用内存、数组作为参数退化 |
| const修饰不同位置 | 指针常量 vs 常量指针;C++中const全局默认内部链接 |
| static的作用 | 局部变量生命周期、函数/全局变量链接属性、类静态成员 |
| malloc/free和new/delete区别 | 是否调用构造/析构、类型安全、失败行为;函数与运算符 |
| 构造函数为什么不能虚 | vptr尚未建立;没有对象可索引虚表 |
| 析构函数为什么需要虚 | 基类指针delete派生类时避免只析构基类部分 |
| 深拷贝/浅拷贝 | 裸指针成员、double free、深拷贝的代码差异 |
| vector扩容 | capacity增长、几何倍数、均摊O(1);reserve与resize |
| map vs unordered_map | 红黑树/哈希表、有序性、时间复杂度、内存模型 |
| 迭代器失效 | vector、deque、list、map/set各自的行为 |
| shared_ptr循环引用 | 父子互指泄漏;weak_ptr破环 |
| 智能指针选择 | unique_ptr默认、shared_ptr共享、weak_ptr弱引用 |
| 编译过程 | 预处理/编译/汇编/链接;未定义符号原因 |
| 重载/重写/隐藏 | 作用域、虚函数、函数签名、名字查找 |
| 左值右值、move/forward | 取地址判断、move不搬运、forward保型 |
| lambda捕获 | 按值/引用、this、mutable、生命周期悬空 |
这张表我称为"考前半小时速翻表",只写最核心的锚点,细节还是要看前面各节的展开。
7.2 几个我踩过的坑和临场技巧
再讲几个真实体会。
第一,千万不要把"理解"和"能背"混为一谈。我见过有人能把虚函数原理背得一字不差,但手写一个带虚函数的类却忘记给析构加virtual,或者定义虚函数时语法写错。面试官一看代码就知道你到底有没有真正写过C++类。所以准备阶段要动手写,不要只读。
第二,答原理题时学会"分层回答"。比如问vector扩容,先一句话给结论,再展开分配策略和均摊复杂度,最后补一句"实际项目里如果频繁扩容,尽量先用reserve"。这种从结论到细节再到工程的路径,逻辑清晰,面试官容易给高分。关键是答题节奏要稳,先讲结论,别一上来就缠进细节。
第三,手写代码前先花十秒设计一下。很多人被"手写strcpy"这种题吓得直接动笔,结果越写越乱。正确的做法是先想清楚:目标是什么、要处理哪些边界、返回类型选什么。甚至可以说一句"我先说思路,然后写代码",面试官很吃这一套,因为真实开发里也是先明确设计再写代码。
第四,准备一两个"我对C++的反常识认知"小话题。比如"智能指针的引用计数开销比想象中大""map的插入不一定比unordered_map慢"。面试官聊到项目时,如果你能抛出这样的观点,会让整个面试往深度方向走。这是加分项,但前提是你真的亲手验证过。
第五,别忽视手写时的命名和风格。面试官看白板代码时,变量名混乱、不缩进、没有头文件包含意识,都会悄悄扣印象分。哪怕只写几行,也要当成正式代码来写:头文件、命名、空格、注释都到位。这一点经常被准备面试的人忽略,却往往直接影响最终评价。
这两年我陆陆续续参与的面试和被面试都不少,最大的体会是:C/C++面试题整理得再全,也只是把路铺出来,真正走过去的还是你自己动手写的每一行代码。与其考前狂刷一百道题,不如静下心把文中这几个核心原理亲手验证一遍。哪怕只是把String类、shared_ptr、strcpy这三段代码自己写一次,再画一遍内存图,效果都会比看十篇总结来得扎实。希望这份整理能帮你少走一些弯路,面试场上少一点紧张,多一些笃定。