一、C++11 为什么重要?
C++11 是 C++ 发展中的一个重要版本。它新增了很多现代 C++ 开发中常用的特性,例如:
auto
lambda 表达式
智能指针
右值引用和移动语义
线程库
nullptr
范围 for 循环
override
这些特性让 C++ 的代码写法更简洁,也提升了资源管理和并发编程的安全性。
面试中,最常被问到的通常是:
auto 如何推导类型?
lambda 如何捕获外部变量?
unique_ptr、shared_ptr、weak_ptr 有什么区别?
什么是左值和右值?
std::move 到底做了什么?
std::thread 如何创建线程?
join 和 detach 有什么区别?
mutex 和 lock_guard 的作用是什么?
二、auto:自动类型推导
auto 的作用是让编译器根据右侧表达式自动推导变量类型。
1. auto 的基本使用
#include <iostream>
#include <string>
using namespace std;
int main() {
// 编译器推导 x 的类型为 int
auto x = 10;
// 编译器推导 name 的类型为 string
auto name = string("Tom");
// 编译器推导 pi 的类型为 double
auto pi = 3.14;
cout << x << endl;
cout << name << endl;
cout << pi << endl;
return 0;
}
上面的代码本质上相当于:
int x = 10;
string name = "Tom";
double pi = 3.14;
但是使用 auto 后,不需要手动写出较长的类型名称。
2. auto 在 STL 遍历中的使用
在 STL 容器中,迭代器类型往往很长,使用 auto 会更方便。
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
map<string, int> scores;
scores["Tom"] = 90;
scores["Jack"] = 85;
// auto 自动推导 it 的类型
for (auto it = scores.begin(); it != scores.end(); ++it) {
cout << it->first << " : " << it->second << endl;
}
return 0;
}
如果不用 auto,代码会变成:
map<string, int>::iterator it = scores.begin();
相比之下,auto 更简洁,也不容易把复杂类型写错。
3. auto 和引用
使用 auto 时要注意:auto 默认会进行类型推导,有时会产生拷贝。
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {1, 2, 3};
// item 是 nums 中每个元素的副本
for (auto item : nums) {
item = 100;
}
// nums 中的元素没有被修改
for (auto item : nums) {
cout << item << " ";
}
return 0;
}
输出结果仍然是:
1 2 3
因为第一个循环中的 item 是元素副本,修改副本不会影响原来的 nums。
如果希望直接修改容器中的元素,需要使用引用:
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {1, 2, 3};
// auto& 表示 item 是元素的引用
for (auto& item : nums) {
item = 100;
}
for (auto item : nums) {
cout << item << " ";
}
return 0;
}
输出结果:
100 100 100
如果只读取数据、不修改数据,推荐使用:
const auto& item
例如:
for (const auto& item : nums) {
cout << item << " ";
}
这样既避免拷贝,也能防止误修改数据。
4. auto 面试总结
面试时可以这样回答:
auto 是 C++11 的自动类型推导关键字,编译器会根据初始化表达式自动推导变量类型。它常用于 STL 迭代器、复杂模板类型和范围 for 循环,可以减少冗长代码。需要注意的是,auto 默认可能产生拷贝;如果希望直接操作原对象,应该使用 auto&;如果只读且不希望拷贝,可以使用 const auto&。
三、lambda 表达式
lambda 表达式可以理解为"匿名函数"。
以前如果需要传入一个临时的小函数,往往需要单独定义函数对象或者普通函数。C++11 引入 lambda 后,可以直接在需要的位置写出函数逻辑。
lambda 的基本格式是:
[捕获列表](参数列表) -> 返回值类型 {
函数体
};
其中返回值类型很多情况下可以省略,让编译器自动推导。
1. lambda 的基本使用
#include <iostream>
using namespace std;
int main() {
// 定义一个 lambda,接收两个 int 参数并返回它们的和
auto add = [](int a, int b) {
return a + b;
};
cout << add(3, 5) << endl;
return 0;
}
这里的:
[](int a, int b) {
return a + b;
}
就是一个匿名函数。
2. lambda 配合 sort 排序
lambda 最常见的使用场景之一,是给 sort() 自定义排序规则。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
vector<int> nums = {5, 2, 8, 1, 3};
// 使用 lambda 指定降序排序规则
sort(nums.begin(), nums.end(),
[](int a, int b) {
return a > b;
});
for (int num : nums) {
cout << num << " ";
}
return 0;
}
输出结果:
8 5 3 2 1
在 lambda 中:
return a > b;
表示如果 a 应该排在 b 前面,就返回 true。
3. lambda 捕获外部变量
lambda 默认不能直接使用外部局部变量。如果想使用,需要通过捕获列表捕获。
#include <iostream>
using namespace std;
int main() {
int x = 10;
// [x] 表示按值捕获 x
auto printX = [x]() {
cout << "x = " << x << endl;
};
printX();
return 0;
}
常见捕获方式如下:
| 捕获方式 | 含义 |
|---|---|
[] |
不捕获任何外部变量 |
[x] |
按值捕获 x |
[&x] |
按引用捕获 x |
[=] |
按值捕获所有外部变量 |
[&] |
按引用捕获所有外部变量 |
[this] |
捕获当前对象的 this 指针 |
4. 按值捕获和按引用捕获
#include <iostream>
using namespace std;
int main() {
int x = 10;
// 按值捕获:lambda 内部保存 x 的副本
auto func1 = [x]() {
cout << "func1 中的 x = " << x << endl;
};
// 按引用捕获:lambda 内部操作外部的 x
auto func2 = [&x]() {
x = 100;
};
func1();
func2();
cout << "外部 x = " << x << endl;
return 0;
}
这里:
[x]:lambda 获得 x 的副本。
[&x]:lambda 直接引用外部的 x。
按引用捕获时要注意对象生命周期问题。不要让 lambda 保存一个已经销毁的局部变量引用,否则会出现悬空引用问题。
5. lambda 面试总结
面试时可以这样回答:
lambda 表达式是 C++11 引入的匿名函数,可以直接在使用位置定义一段临时逻辑。它常用于 STL 算法、自定义排序、回调函数和异步任务。lambda 可以通过捕获列表使用外部变量,按值捕获会保存副本,按引用捕获可以直接修改外部变量,但要注意引用对象的生命周期。
四、智能指针
C++11 引入智能指针的主要目的,是自动管理动态内存,减少手动 new/delete 导致的内存泄漏问题。
常见智能指针包括:
unique_ptr
shared_ptr
weak_ptr
使用智能指针需要包含:
#include <memory>
1. unique_ptr:独占所有权
unique_ptr 表示一块资源只能由一个智能指针拥有。
#include <iostream>
#include <memory>
using namespace std;
int main() {
// C++11 中可以直接用 new 初始化 unique_ptr
unique_ptr<int> p1(new int(10));
cout << "*p1 = " << *p1 << endl;
// unique_ptr 离开作用域时会自动释放内存
return 0;
}
unique_ptr 不允许拷贝,因为同一块资源不能由两个 unique_ptr 同时管理。
unique_ptr<int> p1(new int(10));
// 错误:unique_ptr 不支持拷贝
// unique_ptr<int> p2 = p1;
但是可以通过 std::move 转移所有权。
#include <iostream>
#include <memory>
using namespace std;
int main() {
unique_ptr<int> p1(new int(10));
// 将资源的所有权从 p1 转移到 p2
unique_ptr<int> p2 = move(p1);
// p1 不再拥有资源
if (p1 == nullptr) {
cout << "p1 已经不再管理资源" << endl;
}
cout << "*p2 = " << *p2 << endl;
return 0;
}
注意:make_unique 是 C++14 引入的,不是 C++11 的标准内容。
2. shared_ptr:共享所有权
shared_ptr 允许多个智能指针共同管理同一块资源。
#include <iostream>
#include <memory>
using namespace std;
int main() {
// make_shared 是 C++11 提供的创建方式
shared_ptr<int> p1 = make_shared<int>(10);
cout << "p1 的引用计数:" << p1.use_count() << endl;
{
// p2 和 p1 共同管理同一个 int 对象
shared_ptr<int> p2 = p1;
cout << "p1 的引用计数:" << p1.use_count() << endl;
cout << "p2 的引用计数:" << p2.use_count() << endl;
}
// p2 离开作用域后,引用计数减少
cout << "p1 的引用计数:" << p1.use_count() << endl;
return 0;
}
shared_ptr 的核心是引用计数。
当最后一个 shared_ptr 被销毁时,引用计数变为 0,对象才会被释放。
3. weak_ptr:弱引用
weak_ptr 不拥有对象,也不会增加 shared_ptr 的引用计数。
它通常用于解决 shared_ptr 的循环引用问题。
#include <iostream>
#include <memory>
using namespace std;
int main() {
shared_ptr<int> sp = make_shared<int>(10);
// weak_ptr 弱引用 shared_ptr 管理的对象
weak_ptr<int> wp = sp;
// weak_ptr 不能直接使用,需要通过 lock 获取 shared_ptr
shared_ptr<int> temp = wp.lock();
if (temp) {
cout << "*temp = " << *temp << endl;
} else {
cout << "对象已经被释放" << endl;
}
return 0;
}
4. 智能指针面试总结
面试时可以这样回答:
C++11 的智能指针通过 RAII 自动管理动态内存。unique_ptr 表示独占所有权,不能拷贝,只能移动;shared_ptr 表示共享所有权,通过引用计数控制对象生命周期;weak_ptr 是弱引用,不增加引用计数,主要用于解决 shared_ptr 的循环引用问题。实际开发中,如果资源不需要共享,优先使用 unique_ptr;只有确实存在共享所有权时,才考虑 shared_ptr。
五、左值、右值和右值引用
右值引用是 C++11 中非常重要的内容,它和移动语义、std::move、智能指针转移所有权都有关系。
1. 什么是左值和右值?
可以先简单理解:
左值:有名字、能取地址、可以长期存在的对象。
右值:临时对象、字面量、表达式计算结果,通常不能直接取地址。
例如:
int a = 10;
其中:
a 是左值。
10 是右值。
再例如:
int result = a + 10;
表达式:
a + 10
计算出来的是一个临时结果,通常可以看作右值。
2. 左值引用和右值引用
普通引用是左值引用:
int a = 10;
// 左值引用只能绑定左值
int& ref1 = a;
右值引用使用两个 &&:
// 右值引用可以绑定右值
int&& ref2 = 10;
示例代码:
#include <iostream>
using namespace std;
int main() {
int a = 10;
// 左值引用绑定左值
int& leftRef = a;
// 右值引用绑定右值
int&& rightRef = 20;
cout << leftRef << endl;
cout << rightRef << endl;
return 0;
}
3. 为什么要有右值引用?
右值引用最重要的作用是支持移动语义,减少不必要的对象拷贝。
例如一个对象内部有很大的数据,如果每次传递都复制一份,开销会很大。
移动语义的思路是:
不再复制原来的资源,
而是把原对象持有的资源直接转移给新对象。
4. std::move 的作用
std::move 的作用不是"移动数据"。
它本质上是把一个左值转换成可以被当作右值处理的表达式,从而让编译器有机会调用移动构造函数或移动赋值函数。
示例:
#include <iostream>
#include <vector>
#include <utility>
using namespace std;
int main() {
vector<int> nums1 = {1, 2, 3, 4, 5};
// 使用 std::move 将 nums1 的资源转移给 nums2
vector<int> nums2 = move(nums1);
cout << "nums2 的元素:";
for (int num : nums2) {
cout << num << " ";
}
cout << endl;
// 被移动后的 nums1 仍然是有效对象
// 但内部内容通常不应该再依赖
cout << "nums1 的大小:" << nums1.size() << endl;
return 0;
}
这里的:
vector<int> nums2 = move(nums1);
通常不会把每个元素都复制一遍,而是把 nums1 内部管理的数据资源转移给 nums2。
需要注意:
std::move 本身不负责移动资源。
它只是把对象转换为右值形式。
真正是否移动,取决于对象是否提供移动构造函数或移动赋值函数。
5. 右值引用和 unique_ptr
右值引用的一个实际应用就是 unique_ptr 的所有权转移。
#include <iostream>
#include <memory>
using namespace std;
int main() {
unique_ptr<int> p1(new int(100));
// unique_ptr 不允许拷贝
// 只能通过 move 转移所有权
unique_ptr<int> p2 = move(p1);
cout << "*p2 = " << *p2 << endl;
return 0;
}
这就是"移动而不是拷贝"的典型场景。
6. 右值引用面试总结
面试时可以这样回答:
左值通常是有名字、可以取地址、可以重复使用的对象;右值通常是临时对象或表达式结果。右值引用使用 && 表示,可以绑定右值。右值引用的主要作用是实现移动语义,减少大对象复制带来的开销。std::move 本身并不移动数据,而是把左值转换为右值形式,使对象可以调用移动构造或移动赋值逻辑。
六、C++11 线程库
C++11 提供了标准线程库,使 C++ 可以直接使用 std::thread、std::mutex、std::lock_guard、std::condition_variable 等工具进行多线程编程。
常用头文件包括:
#include <thread>
#include <mutex>
#include <condition_variable>
七、std::thread:创建线程
1. 基本线程创建
#include <iostream>
#include <thread>
using namespace std;
// 子线程要执行的任务函数
void printMessage() {
cout << "这是子线程执行的内容" << endl;
}
int main() {
// 创建一个线程,执行 printMessage 函数
thread worker(printMessage);
// 等待子线程执行结束
worker.join();
cout << "主线程执行结束" << endl;
return 0;
}
这里:
thread worker(printMessage);
表示创建一个子线程,并让它执行 printMessage()。
worker.join();
表示主线程等待子线程执行结束。
2. 为什么要调用 join?
如果一个 std::thread 对象销毁时仍然处于可连接状态,也就是没有调用 join() 或 detach(),程序会调用 std::terminate(),导致程序异常结束。
因此,线程创建后通常需要:
要么调用 join,等待线程结束。
要么调用 detach,让线程独立运行。
3. join 和 detach 的区别
join:主线程等待子线程执行结束。
detach:子线程独立运行,主线程不再等待它。
示例:
#include <iostream>
#include <thread>
using namespace std;
void task() {
cout << "子线程正在执行任务" << endl;
}
int main() {
thread worker(task);
// 主线程等待子线程结束
worker.join();
cout << "主线程继续执行" << endl;
return 0;
}
join() 比较常用,也更容易保证线程结束后资源安全释放。
detach() 使用时要特别注意:如果子线程仍然访问已经销毁的对象或局部变量,就可能出现错误。
八、mutex 和 lock_guard
多个线程同时访问同一份数据时,可能发生数据竞争。
例如两个线程同时对同一个变量执行加 1 操作,最终结果可能不正确。
1. 不加锁的问题
#include <iostream>
#include <thread>
using namespace std;
int countValue = 0;
void add() {
for (int i = 0; i < 100000; i++) {
// 多个线程同时执行时,可能发生数据竞争
countValue++;
}
}
int main() {
thread t1(add);
thread t2(add);
t1.join();
t2.join();
cout << "countValue = " << countValue << endl;
return 0;
}
理论上结果应该是:
200000
但在多线程环境中,实际结果可能小于 200000,因为 countValue++ 并不是一个不可分割的操作。
2. 使用 mutex 加锁
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int countValue = 0;
// 定义互斥锁
mutex mtx;
void add() {
for (int i = 0; i < 100000; i++) {
// 手动加锁
mtx.lock();
countValue++;
// 手动解锁
mtx.unlock();
}
}
int main() {
thread t1(add);
thread t2(add);
t1.join();
t2.join();
cout << "countValue = " << countValue << endl;
return 0;
}
这种写法可以保证同一时间只有一个线程修改 countValue。
但是手动 lock() 和 unlock() 容易出错。例如函数中间提前 return 或抛出异常,可能导致 unlock() 没有执行。
3. 使用 lock_guard 自动管理锁
lock_guard 使用 RAII 思想管理互斥锁。
创建 lock_guard 时自动加锁,离开作用域时自动解锁。
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int countValue = 0;
mutex mtx;
void add() {
for (int i = 0; i < 100000; i++) {
// 创建 lock_guard 时自动加锁
lock_guard<mutex> lock(mtx);
// 当前代码块中只有一个线程可以执行
countValue++;
// 当前循环结束时 lock 自动析构并解锁
}
}
int main() {
thread t1(add);
thread t2(add);
t1.join();
t2.join();
cout << "countValue = " << countValue << endl;
return 0;
}
这就是 RAII 在多线程中的典型应用。
九、condition_variable:线程等待与唤醒
条件变量通常用于"生产者---消费者模型"。
例如:当任务队列为空时,消费者线程不应该一直循环检查,而应该进入等待状态;等生产者放入任务后,再通知消费者继续执行。
#include <iostream>
#include <queue>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
queue<int> taskQueue;
mutex mtx;
condition_variable cv;
// 消费者线程:从队列中取任务
void consumer() {
unique_lock<mutex> lock(mtx);
// 如果队列为空,就等待
// 第二个参数是等待条件
cv.wait(lock, []() {
return !taskQueue.empty();
});
int task = taskQueue.front();
taskQueue.pop();
cout << "消费者处理任务:" << task << endl;
}
// 生产者线程:向队列中加入任务
void producer() {
{
lock_guard<mutex> lock(mtx);
taskQueue.push(100);
cout << "生产者加入任务:100" << endl;
}
// 通知等待中的消费者线程
cv.notify_one();
}
int main() {
thread t1(consumer);
thread t2(producer);
t1.join();
t2.join();
return 0;
}
在这段代码中:
消费者发现任务队列为空,就调用 wait 进入等待。
生产者加入任务后,调用 notify_one 唤醒消费者。
十、线程部分面试总结
面试时可以这样回答:
C++11 提供了标准线程库,可以使用 std::thread 创建线程。线程创建后通常需要调用 join() 或 detach(),否则线程对象析构时可能导致程序异常结束。多个线程访问共享资源时,需要使用 mutex 保证线程安全。相比手动 lock/unlock,更推荐使用 lock_guard 或 unique_lock,因为它们基于 RAII,能够在离开作用域时自动释放锁。condition_variable 用于线程间等待和通知,常见于生产者消费者模型和线程池任务队列。
十一、面试高频问题整理
1. auto 有什么作用?
auto 用于自动类型推导,编译器会根据初始化表达式推导变量类型。它适合复杂类型、STL 迭代器和范围 for 循环。使用 auto 时需要注意是否产生拷贝,需要修改原对象时使用 auto&,只读且避免拷贝时使用 const auto&。
2. lambda 表达式是什么?
lambda 表达式是匿名函数,可以直接在需要的位置定义临时逻辑。它常用于 STL 算法、自定义排序、回调函数和异步任务。lambda 可以通过捕获列表使用外部变量,按值捕获保存副本,按引用捕获可以直接操作外部变量。
3. unique_ptr 和 shared_ptr 有什么区别?
unique_ptr 表示独占所有权,同一资源只能由一个 unique_ptr 管理,不能拷贝,只能移动。
shared_ptr 表示共享所有权,多个 shared_ptr 可以共同管理同一个对象,使用引用计数控制生命周期。
一般情况下,如果资源不需要共享,优先使用 unique_ptr;只有确实需要多个对象共同管理资源时,再使用 shared_ptr。
4. 什么是右值引用?
右值引用使用 && 表示,可以绑定右值。它的主要作用是支持移动语义,减少临时对象或大对象拷贝带来的开销。右值引用常和移动构造函数、移动赋值函数以及 std::move 一起使用。
5. std::move 到底做了什么?
std::move 本身不移动数据,它只是把一个左值转换成右值形式,使对象有机会调用移动构造函数或移动赋值函数。真正是否发生资源移动,取决于对象本身是否实现了移动操作。
6. join 和 detach 有什么区别?
join() 表示主线程等待子线程执行结束,适合需要确保子线程完成任务后再继续执行的场景。
detach() 表示子线程独立运行,主线程不再等待。detach 使用时要注意对象生命周期,避免子线程访问已经销毁的数据。
7. lock_guard 和 mutex 有什么关系?
mutex 是互斥锁,用于保护共享资源。
lock_guard 是对 mutex 的自动管理工具。创建 lock_guard 时自动加锁,离开作用域时自动解锁,避免手动 lock/unlock 忘记解锁的问题。它体现了 RAII 思想。
8. 为什么多线程访问共享变量需要加锁?
多个线程同时读写共享变量时,可能发生数据竞争,导致结果不确定。加锁可以保证同一时间只有一个线程进入临界区,从而保证共享数据操作的正确性。
十二、总结
C++11 引入了许多现代 C++ 开发中常用的特性。
auto 可以自动推导类型,适合简化复杂类型和 STL 代码,但需要注意值拷贝和引用问题。
lambda 表达式可以定义匿名函数,常用于排序、回调和临时任务逻辑。使用外部变量时,需要理解按值捕获和按引用捕获的区别。
智能指针通过 RAII 自动管理内存。unique_ptr 表示独占所有权,shared_ptr 表示共享所有权,weak_ptr 用于弱引用和解决循环引用问题。
右值引用和 std::move 支持移动语义,可以减少不必要的对象复制。需要注意,std::move 只是类型转换工具,本身不负责真正移动资源。
C++11 线程库提供了 thread、mutex、lock_guard 和 condition_variable 等工具。多线程编程中,要特别关注线程结束、共享数据访问、锁管理和对象生命周期问题。
简单记忆:
auto:自动推导类型。
lambda:匿名函数。
unique_ptr:独占资源。
shared_ptr:共享资源。
weak_ptr:弱引用,不增加引用计数。
右值引用:支持移动语义。
std::move:把左值转换为右值形式。
thread:创建线程。
mutex:保护共享资源。
lock_guard:自动加锁和解锁。
condition_variable:线程等待和唤醒。
面试中回答 C++11 相关问题时,可以从"它解决了什么问题、基本原理是什么、适合什么场景、需要注意什么"这几个角度展开,这样回答会更完整。