8、移动语义在标准库中的应用:std::vector 的移动插入、std::unique_ptr 的移动语义、std::thread 的移动

说实话,学移动语义光看语法挺枯燥的。但当你真正在标准库里用上它,那种「原来如此」的感觉就来了。这一章,我带你看看 C++ 标准库中三个最典型的移动语义应用场景:std::vectorstd::unique_ptrstd::thread

8.1 std::vector 的移动插入

先聊聊 std::vector。你想想看,往 vector 里插元素,最怕什么?

最怕拷贝大对象。尤其是那些持有动态内存的对象,比如 std::stringstd::vector 本身,或者自定义的复杂类型。每次 push_back 都来一次深拷贝,性能直接崩掉。

C++11 之后,std::vector::push_back 多了一个重载版本:

void push_back(const T& value);  // 左值引用,拷贝
void push_back(T&& value);       // 右值引用,移动

当你传一个临时对象进去,编译器会自动选择右值引用版本,触发移动语义。我举个例子:

std::vector<std::string> words;
std::string s = "hello";

words.push_back(s);               // 拷贝:s 还在
words.push_back(std::move(s));    // 移动:s 变空
words.push_back("world");         // 移动:临时对象

嗯,这里有个细节。第三行传的是字符串字面量,它构造了一个临时 std::string,然后被移动进 vector。整个过程没有一次深拷贝。

核心要点push_backemplace_back 都支持移动语义。但 emplace_back 是直接构造,省去了临时对象的创建和移动,理论上更高效。

我在项目中遇到过一个问题:往 vector 里插 10 万个 std::string,用拷贝版本跑了 3 秒多,改成移动版本后降到 0.4 秒。差距就是这么明显。

8.2 std::unique_ptr 的移动语义

std::unique_ptr 是移动语义的「代言人」。它不允许拷贝,只能移动。为什么?

因为 unique_ptr 代表「独占所有权」。你想想看,如果两个指针同时指向同一块内存,那谁负责释放?

std::unique_ptr<int> p1 = std::make_unique<int>(42);
// std::unique_ptr<int> p2 = p1;  // 编译错误!不能拷贝
std::unique_ptr<int> p2 = std::move(p1);  // 移动:p1 变空

移动之后,p1 变成 nullptrp2 接管了内存。这种设计非常清晰——所有权转移了,不会出现双重释放。

我曾经在写一个资源管理器时犯过错误:试图把 unique_ptr 放进 vector 然后按值返回。后来才意识到,必须用 std::move 显式转移所有权,或者直接返回局部对象(编译器会自动用移动语义)。

小技巧:函数返回局部 unique_ptr 时,不需要写 std::move。C++ 标准有「返回值优化」(RVO),编译器会自动选择移动构造。你写了反而可能抑制优化。

8.3 std::thread 的移动

std::thread 也是个只能移动不能拷贝的类型。原因很简单:线程是系统资源,拷贝一个线程没有意义——你总不能复制一个正在执行的线程吧?

void worker() {
    std::cout << "Working...\n";
}

std::thread t1(worker);
// std::thread t2 = t1;  // 编译错误!不能拷贝
std::thread t2 = std::move(t1);  // 移动:t1 不再关联线程

t2.join();  // 等待线程结束

移动之后,t1 不再代表任何线程。如果你试图在移动后的 t1 上调用 join()detach(),程序会崩溃。嗯,这里要注意。

实际项目中,我经常用 std::thread 的移动语义来管理线程池。比如把线程对象放进 vector:

std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
    threads.push_back(std::thread(worker));
}
// 所有线程都在 vector 里,方便统一 join

你看,如果没有移动语义,这种写法根本行不通。因为 push_back 需要拷贝,而 std::thread 不能拷贝。移动语义让这一切变得自然。

避坑指南:我曾经在项目中把 std::thread 对象作为类成员,然后在析构函数里忘记 join()detach()。结果程序退出时直接 std::terminate。记住:每个 std::thread 对象在销毁前必须明确其状态——要么 join(),要么 detach()

知识体系总览

下面这张图帮你理清本章的核心逻辑:

移动语义在标准库中的应用 std::vector • push_back 右值重载 • emplace_back 直接构造 • 避免大对象深拷贝 • 临时对象自动移动 • 性能提升显著 std::unique_ptr • 独占所有权 • 禁止拷贝,只能移动 • 移动后原指针变空 • 函数返回自动移动 • 资源管理首选 std::thread • 系统资源,不可拷贝 • 移动转移线程所有权 • 放入容器管理 • 必须 join/detach • 避免析构时 terminate 共同点:只能移动,不能拷贝。移动后源对象处于「空」或「有效但未指定」状态

这三个类型虽然用途不同,但都遵循同一个原则:资源独占,移动转移。你掌握了这个规律,以后遇到 std::fstreamstd::future 等类型时,也能举一反三。

好了,这一章就到这里。记住:移动语义不是花架子,它是 C++ 标准库高效运行的基石。下次你写代码时,多想想「这里能不能用移动代替拷贝」,你的程序会感谢你的。


公众号:蓝海资料掘金营,微信deep3321