💬 :如果你在阅读过程中有任何疑问或想要进一步探讨的内容,欢迎在评论区畅所欲言!我们一起学习、共同成长~!
👍 :如果你觉得这篇文章还不错,不妨顺手点个赞、加入收藏,并分享给更多的朋友噢~!
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容器 vector<int> v{ 1,2,3,4 }; return 0; }面试高频问答
问:()初始化和{}初始化的核心区别?
答:
1. {}不允许窄化隐式转换,更安全;
2. {}支持所有类型统一初始化;
3. {}无初始化歧义(最经典:vector<int> v(10)是10个元素,vector<int> v{10}是单个元素10)。
问:为什么推荐统一使用{}初始化?
答:禁止窄化隐式转换,类型安全;支持所有类型,语法统一。
1.2 std::initializer_list 底层原理【必考核心】
核心原理:std::initializer_list是 C++11 提供的轻量级只读视图,底层仅记录一组连续同类型数据的首地址与元素个数,不会拷贝存储列表元素。编译器遇到{...}列表时,自动生成临时std::initializer_list<T>对象。
- STL 容器批量
{}初始化 / 赋值:必然依赖std::initializer_list,标准容器全部实现了接收std::initializer_list<T>的构造函数与赋值运算符重载; - 自定义类批量
{}初始化分情况:
① 类未定义接收std::initializer_list的构造,{...}匹配普通多参构造函数,不依赖std::initializer_list;
② 类显式定义接收std::initializer_list的构造函数,列表初始化会优先匹配该构造。
核心特点【高频易错】:
- 只读:
initializer_list内的元素不允许修改; - 低开销代理:仅存储首地址与长度,不持有实体数据,几乎无内存开销;
- 全部 STL 标准容器均提供
std::initializer_list<T>版本构造函数;
代码示例:
#include <vector> #include <map> #include <string> using namespace std; int main() { // 1. vector<int> 列表初始化 // {1,2,3,4} 编译器自动推导为临时 initializer_list<int> // 调用 vector 提供的 initializer_list<int> 构造函数完成批量初始化 vector<int> v = { 1,2,3,4 }; // 2. 特殊:map存储 pair<K,V> // {"sort", "排序"} 构造 pair<string, string> // 整体被转为 initializer_list<pair<string,string>> map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} }; // 3. 容器赋值运算符重载 // 右侧 {10,20,30} 生成临时 initializer_list<int> // 调用 vector operator=(initializer_list<int>) 重载,覆盖原数据 v = { 10,20,30 }; return 0; }面试高频问答
问:initializer_list初始化容器的效率高吗?
答:极高,底层无多次插入,一次性遍历初始化,减少扩容开销。
问:自定义类如何支持{}初始化?
答:重载接收initializer_list的构造函数即可。
2. 变量类型推导(面试高频对比auto/decltype/nullptr)
C++98 中,复杂类型(如map<string,string>::iterator)需要完整写出,代码冗余且易出错,C++11 新增 auto / decltype 自动推导类型,另增nullptr修复NULL的二义性。
2.1 auto:自动推导类型【必考核心】
核心规则(必背):
auto必须初始化,无初始化无法确定类型,编译报错
auto不保留const、引用属性(需手动加const/&)
高频坑点:
auto arr = {1,2,3}推导为initializer_list<int>,而非数组
// auto a; // 未初始化,编译器无法推导类型,报错 const int a = 200; auto b = a; // int auto& c = a; // const int& const auto& d = a; // const int&,可读性更强,推荐 map<string, int> mp = { {"apple", 1}, {"banana", 2} }; // 传统冗长写法 map<string, 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推导 template<class 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空指针【必考核心】
核心痛点(面试必背):C++98的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 左值右值区分
- 左值:有持久内存、可取地址、可赋值(变量、解引用指针)。
- 右值:临时值、无持久内存、不可取地址、不可赋值(字面量、表达式结果、函数返回的局部对象),如字面量
10(&10编译报错)、x+y(表达式结果,不可赋值)、to_string(123)(函数返回的局部对象,生命周期仅当前语句)。
3.2 引用绑定规则
| 引用类型 | 绑定对象 | 核心用途 |
|---|---|---|
左值引用(T&) | 仅左值(不能绑定临时右值,极易编译报错) | 普通函数传参,避免拷贝 |
const T& | 左值、右值均可 | 万能引用,C++98 接收临时对象唯一方案 |
右值引用(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。
- 参数列表:无参数写()。
- mutable:lambda 默认是 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() { vector<Student> 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 + bind)
5.1 function
C++ 中 “可调用对象”(如普通函数、函数对象、Lambda、静态成员函数)类型不同,导致模板会实例化多份(如useF(f)、useF(Lambda)是两个不同模板实例)。
- 核心作用:
function是C++11 提供的类模板——将不同类型的可调用对象封装为同一类型,让模板只实例化一次;并简化接口设计。 - 包含头文件
<functional> - 模板参数格式:
function<返回值类型(参数类型1, 参数类型2, ...)> // 括号内为可调用对象的参数列表(无参时写 function<返回值类型()>)如,
function<int(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() { // 包装 function<int(int, int)> func1 = Add; function<int(int, int)> func2 = AddFunc(); function<int(int, int)> func3 = [](int a, int b) // 3、Lambda 表达式 { return a * b; }; function<int(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、普通函数 | function<int(int, int)> func1 = Add;(直接赋值函数名) | 函数名本质是函数指针,可直接作为function初始化值,无需加&(加&也可,如&Add)。 | 无论包装哪种可调用对象,都像普通函数一样通过“function对象名(参数)”调用 |
| 2、函数对象(仿函数) | function<int(int, int)> func2 = AddFunc();(赋值实例化对象) | 函数对象(仿函数)本质是对象不是函数,必须先通过AddFunc()创建临时对象(或AddFunc obj; func2 = obj;),不能直接赋值类名AddFunc。 | |
| 3、Lambda 表达式 | function<int(int, int)> func3 = [](int a, int b){ return a*b; };(直接赋值 Lambda) | Lambda 是匿名函数,定义时直接写在赋值右侧即可,捕捉列表根据需求填写(此处无外部变量,填[])。 | |
| 4、类静态成员函数 | function<int(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() { // (1)placeholders::_1 = 新函数add10第1个实参5 // (2)10和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); // (1)placeholders::_2 = 新函数subReverse第2个实参10;placeholders::_1 = 3 // (2)placeholders::_2和placeholders::_1分别传给原函数...... cout << subReverse(3, 10) << endl; return 0; }类非静态成员函数的特殊要求:
① 函数地址写法:&类名::成员函数名(不可省略&,与普通函数不同);
② 必须传this指针:非静态成员函数隐含第一个参数this(指向类对象),因此bind时需在 “原函数地址” 后,第一个参数位置传入类对象的地址(如&s,表示绑定到s这个对象)。
| 关键点 | 掌握优先级 | 核心目标(赶时间时优先确保) |
|---|---|---|
bind本质与作用 | ★★★ | 能说清 “绑定参数、调整顺序” |
| 固定参数绑定(含占位符) | ★★★★ | 能独立写出固定参数的代码 |
| 类非静态成员函数绑定规则 | ★★★★ | 记准 “& 类名::函数 + 对象地址” |
| 参数顺序调整 | ★★★ | 能分析占位符与原函数参数对应关系 |
auto接收返回值 | ★★ | 写代码时记得用auto |
6. C++11 线程库(多线程面试基础)
C++11 首次引入标准线程库(#include <thread>),彻底解决了传统多线程编程中 Windows和 Linux接口不统一的问题,实现了跨平台兼容。这部分是面试中 “多线程编程” 的基础考点,核心围绕线程创建、线程同步展开。
6.1 线程创建的 3 种方式(必掌握)
线程是程序执行的最小单位,C++11 通过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 << "子线程3:Lambda 表达式" << endl; }); t1.join(); // 主线程阻塞,直到 t1 子线程执行完毕 t2.join(); // 同理,等待 t2 结束 t3.join(); // 等待 t3 结束 cout << "主线程结束" << endl; // 所有子线程结束后,主线程才执行此句 return 0; }注意:多线程输出可能乱序(输出语句执行顺序不定,重新运行代码后输出结果可能不同),因为 cout 是共享资源。
输出结果如下:
子线程2:函数对象子线程1:参数 a = 子线程3:Lambda 表达式 10 主线程结束子线程3:Lambda 表达式 子线程1:参数 a = 子线程2:函数对象 10 主线程结束| 关键点 | 掌握程度要求 |
|---|---|
| 3 种创建方式的语法 | 能独立写出每种方式的代码:
|
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 sum:sum++ 会有数据竞争,最终结果 < 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_guard<mutex> 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_guard<mutex> lock(互斥锁对象),无需手动调用lock()/unlock()。 |
| RAII 机制的优势 | 记住:自动上锁 / 解锁,即使代码中出现return或异常,仍能保证锁释放,避免死锁。 |
| 临界区的划分 | 理解:临界区应 “尽可能小”(只包含需要保护的代码),过大会降低多线程效率。 |
| 与原子操作的区别 | 能区分:原子操作适用于单个变量的简单操作;lock_guard适用于保护一段代码。 |