:C++ 11)
如果你在阅读过程中有任何疑问或想要进一步探讨的内容欢迎在评论区畅所欲言我们一起学习、共同成长~ 如果你觉得这篇文章还不错不妨顺手点个赞、加入收藏并分享给更多的朋友噢~1. 统一列表初始化面试常考用法扩展C 98 仅支持数组/结构体用 {} 初始化。C 11 中内置类型、new 创建的动态数组、自定义类、全部 STL 容器均可直接通过 {} 初始化核心考点是初始化统一特性、隐式转换、initializer_list底层。1.1 {} 初始化的核心扩展核心特性{}初始化可省略等号不允许窄化隐式转换“窄化”即大范围类型→小范围类型笔试高频坑点初始化允许窄化转换int a{1.5}; // 编译报错double转int窄化{}严格校验 int a 1.5; // 编译通过截断赋值为1传统初始化不校验必掌握代码#include vector using namespace std; class Date { public: Date(int y,int m,int d) :_y(y),_m(m),_d(d) {} private: int _y, _m, _d; }; int main() { // 1.内置类型 int x1{ 2 }; // int x2{ 2.5 }; // 报错禁止窄化隐式转换 // 2.动态数组 int* p new int[4] {0}; // 4个元素初始化为0 delete[] p; // 3.自定义类 Date d1{ 2024,5,20 }; Date d2 { 2025,6,21 }; // {}初始化可写/可不写 // 4.STL容器 vectorint v{ 1,2,3,4 }; return 0; }面试高频问答问()初始化和{}初始化的核心区别答1. {}不允许窄化隐式转换更安全2. {}支持所有类型统一初始化3. {}无初始化歧义最经典vectorint v(10)是10个元素vectorint v{10}是单个元素10。问为什么推荐统一使用{}初始化答禁止窄化隐式转换类型安全支持所有类型语法统一。1.2 std::initializer_list 底层原理【必考核心】核心原理std::initializer_list是 C11 提供的轻量级只读视图底层仅记录一组连续同类型数据的首地址与元素个数不会拷贝存储列表元素。编译器遇到{...}列表时自动生成临时std::initializer_listT对象。STL 容器批量{}初始化 / 赋值必然依赖std::initializer_list标准容器全部实现了接收std::initializer_listT的构造函数与赋值运算符重载自定义类批量{}初始化分情况① 类未定义接收std::initializer_list的构造{...}匹配普通多参构造函数不依赖std::initializer_list② 类显式定义接收std::initializer_list的构造函数列表初始化会优先匹配该构造。核心特点【高频易错】只读initializer_list内的元素不允许修改低开销代理仅存储首地址与长度不持有实体数据几乎无内存开销全部 STL 标准容器均提供std::initializer_listT版本构造函数代码示例#include vector #include map #include string using namespace std; int main() { // 1. vectorint 列表初始化 // {1,2,3,4} 编译器自动推导为临时 initializer_listint // 调用 vector 提供的 initializer_listint 构造函数完成批量初始化 vectorint v { 1,2,3,4 }; // 2. 特殊map存储 pairK,V // {sort, 排序} 构造 pairstring, string // 整体被转为 initializer_listpairstring,string mapstring, string dict { {sort, 排序}, {insert, 插入} }; // 3. 容器赋值运算符重载 // 右侧 {10,20,30} 生成临时 initializer_listint // 调用 vector operator(initializer_listint) 重载覆盖原数据 v { 10,20,30 }; return 0; }面试高频问答问initializer_list初始化容器的效率高吗答极高底层无多次插入一次性遍历初始化减少扩容开销。问自定义类如何支持{}初始化答重载接收initializer_list的构造函数即可。2. 变量类型推导面试高频对比auto/decltype/nullptrC98 中复杂类型如mapstring,string::iterator需要完整写出代码冗余且易出错C11 新增 auto / decltype 自动推导类型另增nullptr修复NULL的二义性。2.1 auto自动推导类型【必考核心】核心规则必背auto必须初始化无初始化无法确定类型编译报错auto不保留const、引用属性需手动加const/高频坑点auto arr {1,2,3}推导为initializer_listint而非数组// auto a; // 未初始化编译器无法推导类型报错 const int a 200; auto b a; // int auto c a; // const int const auto d a; // const int可读性更强推荐 mapstring, int mp { {apple, 1}, {banana, 2} }; // 传统冗长写法 mapstring, int::iterator it_old mp.begin(); // auto简化写法效果完全一致 auto it_new mp.begin();2.2 decltype根据表达式推导类型【高频拔高】核心作用弥补auto必须初始化的缺陷根据表达式推导类型不执行表达式多用于模板编程。推导规则笔试必考普通表达式推导表达式结果类型变量表达式推导变量原有类型保留const/引用模板编程场景#include typeinfo // typeid #include iostream using namespace std; // 模板函数不知道t1*t2的返回类型用decltype推导 templateclass T1, class T2 void PrintMulType(T1 t1, T2 t2) { decltype(t1 * t2) ret t1 * t2; cout t1 * t2 结果类型 typeid(ret).name() endl; cout t1 * t2 ret endl; } int main() { PrintMulType(3, 4.5); PrintMulType(5, 6); const int x 1; decltype(x) p x; cout x 类型 typeid(p).name() endl; return 0; }2.3 nullptr空指针【必考核心】核心痛点面试必背C98的NULL本质是宏定义0存在二义性既可以表示整型0也可以表示空指针重载场景优先表示整型0——nullptr是nullptr_t专属空指针类型仅匹配指针类型。重载匹配代码#include iostream using namespace std; void func(int x) { cout int endl; } void func(int* p) { cout int* endl; } int main() { func(NULL); // 优先匹配int重载而非指针重载 func(nullptr); // 只匹配指针重载 return 0; }面试追问nullptr可以隐式转换为任意指针类型吗可以转换为int吗答可以隐式转换为所有指针类型不可以转换为整型类型安全。3. 右值引用与移动语义面试重点3.1 左值右值区分左值有持久内存、可取地址、可赋值变量、解引用指针。右值临时值、无持久内存、不可取地址、不可赋值字面量、表达式结果、函数返回的局部对象如字面量1010编译报错、xy表达式结果不可赋值、to_string(123)函数返回的局部对象生命周期仅当前语句。3.2 引用绑定规则引用类型绑定对象核心用途左值引用T仅左值不能绑定临时右值极易编译报错普通函数传参避免拷贝const T左值、右值均可万能引用C98 接收临时对象唯一方案右值引用T纯右值、std::move(左值)转换得到的右值实现移动语义窃取资源减少拷贝参数不能加 const否则无法修改对象窃取资源#include iostream #include string using namespace std; void funcL(int x) { cout 普通左值引用 T endl; } void funcCL(const int x) { cout 常量左值引用 const T endl; } void funcR(string s) { cout 右值引用 T可窃取资源 endl; } string getTempStr() { return 临时字符串; // 函数返回的返回局部对象为右值 } int main() { // 1. T 普通左值引用 int a 10; funcL(a); // 左值 // funcL(20); // 笔试高频坑点T 不能绑定右值 // 2. const T 常量左值引用 funcCL(a); // 左值 funcCL(20); // 右值 // 3. T 右值引用 funcR(getTempStr()); // 绑定函数返回右值 string s test; // funcR(s); // 报错普通左值无法直接绑定T funcR(move(s)); // std::move将左值转为右值 // 若参数写 const string s无法修改对象窃取资源移动语义失效面试重点 return 0; }3.3 移动构造与移动赋值【手撕必考】3.3.1 为什么需要移动构造 / 移动赋值核心价值传统拷贝构造是深拷贝重新分配内存、拷贝数据开销极大移动语义直接窃取右值资源仅交换指针、大小、容量零内存分配极致优化临时对象拷贝开销。3.3.2 移动构造 / 移动赋值与拷贝构造的核心区别移动语义需要修改右值对象窃取资源其参数绝对不能加const否则无法修改对象窃取资源导致移动语义失效。3.3.3 代码自定义 string 为例#include cstring #include iostream using namespace std; class MyString { public: // 普通构造 MyString(const char* str ) :_size(strlen(str)), _capacity(_size) { _str new char[_capacity 1]; strcpy(_str, str); } // 拷贝构造 MyString(const MyString s) :_str(nullptr), _size(0), _capacity(0) { cout 拷贝构造深拷贝 endl; MyString tmp(s._str); // 为tmp自动调用new分配内存完成深拷贝 MySwap(tmp); // 交换初始资源(nullptr,0,0)和tmp资源 } // 移动构造右值无const MyString(MyString s) :_str(nullptr), _size(0), _capacity(0) { cout 移动构造 endl; MySwap(s); // 交换初始资源(nullptr,0,0)和右值s的资源未分配内存直接窃取 } // 移动赋值右值给已存在对象转移资源 MyString operator(MyString s) { cout 移动赋值 endl; if (this ! s) // 判断自赋值防止自己和自身交换导致资源丢失、双重析构 MySwap(s); // 交换当前对象旧资源与右值s的新资源 return *this; } ~MyString() { delete[] _str; _str nullptr; } // 移动语义核心仅交换堆地址、大小、容量不用重新分配内存、不用拷贝数据直接窃取资源 void MySwap(MyString s) { // :: 全局作用域解析符强制用标准库std::swap避免类内同名函数遮蔽 ::swap(_str, s._str); ::swap(_size, s._size); ::swap(_capacity, s._capacity); } private: char* _str; size_t _size; size_t _capacity; }; MyString getTemp() { MyString t(123); return t; // 返回局部对象为纯右值 } int main() { // 临时右值触发移动构造 MyString s1 getTemp(); // s2是已实例化对象右值赋值触发移动赋值 MyString s2; s2 getTemp(); // std::move()左值→右值触发移动构造 MyString s3 move(s1); return 0; }3.3.4 面试高频问答问move之后的原对象还能使用吗答处于有效但未定义状态资源已被转移禁止读写否则大概率崩溃。问移动语义为什么比深拷贝高效答无需重新分配堆内存、无需循环拷贝数据仅交换成员变量指针时间复杂度O(1)。问编译器什么时候会自动触发移动语义答用临时右值初始化对象 / 赋值对象自动优先匹配移动构造 / 移动赋值触发移动语义。4. Lambda 表达式【必考核心】面试高频4.1 Lambda 语法结构必背[捕捉列表](参数列表)mutable-返回值类型{函数体}捕捉列表指定外部变量如何传入 Lambda。参数列表无参数写()。mutablelambda 默认是 const 函数要修改捕捉变量必须加 mutable。返回值类型单return语句自动推导返回值类型多return必须显式指定。4.2 捕捉列表 4 大用法【刷题必考】前提Lambda 需要访问外部变量时捕捉方式含义坑点[var]值捕捉拷贝 var 副本到 Lambda 内部内部修改副本不影响外部修改时必须加 mutable[var]引用捕捉 var①内部修改同步改动外部②若外部变量销毁后 lambda 还在使用引用指向已销毁栈内存 → 野引用程序崩溃[]值捕捉所有用到的外部变量仅拷贝this指针不拷贝整个对象①内部修改同步改动外部修改时必须加 mutable②若类对象销毁后 lambda 还在使用this 变成野指针访问成员崩溃[]引用捕捉所有用到的外部变量慎用极易捕获局部变量若局部变量销毁后 lambda 还在使用野引用4.3 sort 自定义排序笔试算法高频考点多条件排序、阈值筛选、捕捉列表使用#include iostream #include algorithm // sort #include vector using namespace std; struct Student { string name; int age, score; }; int main() { vectorStudent v { {张三, 18, 90}, {李四, 17, 95},{王五, 18, 88}, {赵六, 19, 85} }; // []空捕捉这里 lambda 只使用传入参数不访问外部变量 sort(v.begin(), v.end(), [](const Student s1, const Student s2) { if (s1.score ! s2.score) return s1.score s2.score; // s1排s2前的条件。降序 if (s1.age ! s2.age) return s1.age s2.age; // 升序 return s1.name s2.name; // 按字典序升序 }); for (const auto stu : v) { cout stu.name stu.score stu.age endl; } return 0; }4.4 面试原理追问问Lambda底层是什么答编译器编译时将lambda转换为匿名仿函数函数对象捕捉变量作为类的成员变量功能与自定义仿函数完全一致仅语法更简洁。问sort比较函数返回true代表什么答代表第一个参数排在第二个参数前面严格弱序规则写反排序直接错误。5. 包装器function bind5.1 functionC 中 “可调用对象”如普通函数、函数对象、Lambda、静态成员函数类型不同导致模板会实例化多份如useF(f)、useF(Lambda)是两个不同模板实例。核心作用function是C11 提供的类模板——将不同类型的可调用对象封装为同一类型让模板只实例化一次并简化接口设计。包含头文件functional模板参数格式function返回值类型(参数类型1, 参数类型2, ...) // 括号内为可调用对象的参数列表无参时写 function返回值类型()如functionint(int, int) func1表示 “包装一个接收两个int参数、返回int的可调用对象”统一类型为int(int, int)。必掌握代码#include functional #include iostream using namespace std; // 1、普通函数 int Add(int a, int b) { return a b; } // 2、函数对象仿函数通过结构体重载operator()使其能像函数一样被调用 struct AddFunc { int operator()(int a, int b) { return a b; } }; // 4、类静态成员函数属于类而非对象可直接通过“类名::函数名”调用 class Math { public: static int Sub(int a, int b) { return a - b; } }; int main() { // 包装 functionint(int, int) func1 Add; functionint(int, int) func2 AddFunc(); functionint(int, int) func3 [](int a, int b) // 3、Lambda 表达式 { return a * b; }; functionint(int, int) func4 Math::Sub; // 调用方式统一 cout func1(1, 2) endl; cout func2(3, 4) endl; cout func3(5, 6) endl; cout func4(10, 5) endl; return 0; }可调用对象类型包装方法关键注意事项简化接口设计1、普通函数functionint(int, int) func1 Add;直接赋值函数名函数名本质是函数指针可直接作为function初始化值无需加加也可如Add。无论包装哪种可调用对象都像普通函数一样通过“function对象名(参数)”调用2、函数对象仿函数functionint(int, int) func2 AddFunc();赋值实例化对象函数对象仿函数本质是对象不是函数必须先通过AddFunc()创建临时对象或AddFunc obj; func2 obj;不能直接赋值类名AddFunc。3、Lambda 表达式functionint(int, int) func3 [](int a, int b){ return a*b; };直接赋值 LambdaLambda 是匿名函数定义时直接写在赋值右侧即可捕捉列表根据需求填写此处无外部变量填[]。4、类静态成员函数functionint(int, int) func4 Math::Sub;必须加和类名限定Math::Sub不能直接写Math::Sub静态成员函数地址需显式获取。5.2 bind核心作用1给可调用对象绑定固定参数将函数的部分参数固定生成 “参数更少的新函数”。bind(原函数, 绑定的固定参数1, 绑定的固定参数2, ..., placeholders::_n) // placeholders::_n是占位符等于新函数的第n个实参2调整参数顺序修改函数参数的传递顺序。包含头文件functional必掌握代码#include functional #include iostream using namespace std; int Plus(int a, int b) { return a b; } class Sub { public: int sub(int a, int b) { return a - b; } }; int main() { // 1placeholders::_1 新函数add10第1个实参5 // 210和placeholders::_1分别传给原函数Plus的形参 // 3执行原函数 auto add10 bind(Plus, 10, placeholders::_1); cout add10(5) endl; Sub s; // 类成员函数需实例化对象 auto subReverse bind(Sub::sub, s, placeholders::_2, placeholders::_1); // 1placeholders::_2 新函数subReverse第2个实参10placeholders::_1 3 // 2placeholders::_2和placeholders::_1分别传给原函数...... cout subReverse(3, 10) endl; return 0; }类非静态成员函数的特殊要求① 函数地址写法类名::成员函数名不可省略与普通函数不同② 必须传this指针非静态成员函数隐含第一个参数this指向类对象因此bind时需在 “原函数地址” 后第一个参数位置传入类对象的地址如s表示绑定到s这个对象。关键点掌握优先级核心目标赶时间时优先确保bind本质与作用★★★能说清 “绑定参数、调整顺序”固定参数绑定含占位符★★★★能独立写出固定参数的代码类非静态成员函数绑定规则★★★★记准 “ 类名函数 对象地址”参数顺序调整★★★能分析占位符与原函数参数对应关系auto接收返回值★★写代码时记得用auto6. C11 线程库多线程面试基础C11 首次引入标准线程库#include thread彻底解决了传统多线程编程中 Windows和 Linux接口不统一的问题实现了跨平台兼容。这部分是面试中 “多线程编程” 的基础考点核心围绕线程创建、线程同步展开。6.1 线程创建的 3 种方式必掌握线程是程序执行的最小单位C11 通过std::thread类创建线程核心逻辑用thread对象关联一个 “可调用对象”函数、函数对象、Lambda 等作为线程函数再通过join()让主线程等待子线程执行完毕。#include thread #include iostream using namespace std; // 1. 线程函数函数指针支持带参数参数类型需与线程构造时匹配 void ThreadFunc(int a) { cout 子线程1参数 a a endl; } // 2. 线程函数函数对象仿函数需重载 operator() // 可在构造函数对象时初始化成员变量携带状态 struct ThreadObj { void operator()() { cout 子线程2函数对象 endl; } }; int main() { // 方式1 thread t1(ThreadFunc, 10); // 子线程执行 ThreadFunc参数 10 传给 a // 方式2 ThreadObj obj; thread t2(obj); // 子线程执行 obj.operator()() // 方式3 thread t3([] { // Lambda 无参数无返回值 cout 子线程3Lambda 表达式 endl; }); t1.join(); // 主线程阻塞直到 t1 子线程执行完毕 t2.join(); // 同理等待 t2 结束 t3.join(); // 等待 t3 结束 cout 主线程结束 endl; // 所有子线程结束后主线程才执行此句 return 0; }注意多线程输出可能乱序输出语句执行顺序不定重新运行代码后输出结果可能不同因为 cout 是共享资源。输出结果如下 子线程2函数对象子线程1参数 a 子线程3Lambda 表达式 10 主线程结束子线程3Lambda 表达式 子线程1参数 a 子线程2函数对象 10 主线程结束关键点掌握程度要求3 种创建方式的语法能独立写出每种方式的代码用函数指针指针创建函数指针需正确传递参数语法thread 线程对象(函数指针, 函数参数列表)用函数对象创建先实例化函数对象再传给 thread用Lambda表达式创建最常用优势——Lambda 表达式无需单独定义函数/类直接在创建线程时写逻辑简洁。join()的作用必须记住让主线程等待子线程执行完毕不调用会导致程序崩溃子线程还在运行时主线程退出。线程函数的参数传递理解thread t(fn, arg1, arg2)中arg1, arg2会按值传递给线程函数若需传递引用需用std::ref(x)包装实参x。6.2 原子操作多线程同时操作共享变量时普通变量的操作如sum可能被中断导致数据错误称为 “数据竞争”。原子操作通过硬件支持保证操作 “不可分割”从根本上避免数据竞争效率高于锁机制。原理普通变量的问题sum本质是三步操作读sum的值 → 加 1 → 写回sum多线程下可能被打断。原子操作的解决思路std::atomic是模板类#include atomic封装基本类型如int、long其操作如由硬件保证 “不可中断”三步合为一步避免数据竞争。#include thread #include atomic #include iostream using namespace std; atomic_long sum{ 0 }; // 子线程函数 void AddSum(size_t num) { for (size_t i 0; i num; i) { sum; // 若用普通 long sumsum 会有数据竞争最终结果 2000000 } } int main() { thread t1(AddSum, 1000000); thread t2(AddSum, 1000000); t1.join(); t2.join(); cout sum sum endl; return 0; }关键点掌握程度要求数据竞争的原因理解多线程对普通共享变量的 “读 - 改 - 写” 操作非原子性可能被中断导致错误。atomic类型的声明能正确声明atomic_类型 变量名{初始值}如atomic_int a{0}、atomic_long b{10}。原子操作的适用场景记住仅适用于单个变量的简单操作如、--、复杂逻辑如先判断再修改仍需锁。与普通变量的区别熟练区分原子变量操作无需加锁效率高普通变量多线程操作必须同步锁或原子操作。6.3 线程同步lock_guard当需要保护 “一段代码”而非单个变量时需用互斥锁mutex#include mutex保证同一时间只有一个线程执行该代码段临界区。lock_guard是锁的 RAII 封装自动管理锁的生命周期避免手动操作锁导致的死锁。原理互斥锁mutex核心作用是 “互斥”调用lock()上锁后其他线程调用lock()会阻塞直到当前线程unlock()解锁。lock_guard的优势通过 RAII资源获取即初始化机制在构造函数中自动调用mutex.lock()析构函数中自动调用mutex.unlock()确保锁一定会释放即使代码中出现异常避免 “忘记解锁” 导致的死锁。#include thread #include mutex #include iostream using namespace std; mutex mtx; // 全局互斥锁所有线程共享保证临界区互斥 int number 0; // 共享变量 void ThreadProc() { for (int i 0; i 100; i) { // 创建 lock_guard 对象构造时自动调用 mtx.lock() 上锁 lock_guardmutex lock(mtx); // lock 是局部变量作用域在 for 循环内 // 临界区从 lock 构造到 lock 析构期间只有一个线程能执行 number; cout number: number endl; } // for 循环结束lock 析构自动调用 mtx.unlock() 解锁 } int main() { thread t1(ThreadProc); thread t2(ThreadProc); t1.join(); t2.join(); return 0; }关键点掌握程度要求mutex的作用理解保证临界区代码 “互斥执行”同一时间只有一个线程能执行。lock_guard的用法熟练使用lock_guardmutex lock(互斥锁对象)无需手动调用lock()/unlock()。RAII 机制的优势记住自动上锁 / 解锁即使代码中出现return或异常仍能保证锁释放避免死锁。临界区的划分理解临界区应 “尽可能小”只包含需要保护的代码过大会降低多线程效率。与原子操作的区别能区分原子操作适用于单个变量的简单操作lock_guard适用于保护一段代码。