【C++11 面试高频:auto、lambda、智能指针、右值引用和线程】

一、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::threadstd::mutexstd::lock_guardstd::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_guardunique_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 线程库提供了 threadmutexlock_guardcondition_variable 等工具。多线程编程中,要特别关注线程结束、共享数据访问、锁管理和对象生命周期问题。

简单记忆:

复制代码
auto:自动推导类型。
lambda:匿名函数。
unique_ptr:独占资源。
shared_ptr:共享资源。
weak_ptr:弱引用,不增加引用计数。
右值引用:支持移动语义。
std::move:把左值转换为右值形式。
thread:创建线程。
mutex:保护共享资源。
lock_guard:自动加锁和解锁。
condition_variable:线程等待和唤醒。

面试中回答 C++11 相关问题时,可以从"它解决了什么问题、基本原理是什么、适合什么场景、需要注意什么"这几个角度展开,这样回答会更完整。

0voice · GitHub