8、移动语义在标准库中的应用:std::vector 的移动插入、std::unique_ptr 的移动语义、std::thread 的移动
说实话,学移动语义光看语法挺枯燥的。但当你真正在标准库里用上它,那种「原来如此」的感觉就来了。这一章,我带你看看 C++ 标准库中三个最典型的移动语义应用场景:std::vector、std::unique_ptr 和 std::thread。
8.1 std::vector 的移动插入
先聊聊 std::vector。你想想看,往 vector 里插元素,最怕什么?
最怕拷贝大对象。尤其是那些持有动态内存的对象,比如 std::string、std::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_back 和 emplace_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 变成 nullptr,p2 接管了内存。这种设计非常清晰——所有权转移了,不会出现双重释放。
我曾经在写一个资源管理器时犯过错误:试图把 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::fstream、std::future 等类型时,也能举一反三。
好了,这一章就到这里。记住:移动语义不是花架子,它是 C++ 标准库高效运行的基石。下次你写代码时,多想想「这里能不能用移动代替拷贝」,你的程序会感谢你的。