std::vector 中,push_back 和 emplace_back 的区别
在 C++ 的 std::vector 中,push_back 和 emplace_back 都是用于在容器末尾添加元素的函数,但它们的底层机制和性能表现有显著的区别。
简单来说:push_back 是“先创建对象,再拷贝/移动到容器中”,而 emplace_back 是“直接在容器的内存中原地构造对象”。
以下是详细的对比和解析:
1. 核心工作原理
push_back
- 它接收一个已经构造好的对象(左值或右值)。
- 如果是左值,它会调用类的拷贝构造函数,将对象复制到 vector 的内存中。
- 如果是右值(例如临时对象),它会调用类的移动构造函数,将对象移动到 vector 的内存中。
emplace_back (C++11 引入)
- 它接收的是构造对象所需的参数(通过可变参数模板和完美转发实现)。
- 它直接在 vector 预先分配的内存上,调用类的构造函数来实例化对象。
- 完全省去了临时对象的创建、拷贝或移动的过程。
2. 代码示例与性能对比
为了直观看出区别,我们定义一个包含打印信息的类:
#include <iostream>
#include <vector>
#include <string>
class MyClass {
public:
// 普通构造函数
MyClass(int a, std::string b) {
std::cout << "调用了构造函数\n";
}
// 拷贝构造函数
MyClass(const MyClass& other) {
std::cout << "调用了拷贝构造函数\n";
}
// 移动构造函数
MyClass(MyClass&& other) noexcept {
std::cout << "调用了移动构造函数\n";
}
~MyClass() {
std::cout << "调用了析构函数\n";
}
};
int main() {
std::vector<MyClass> vec;
vec.reserve(10); // 预分配内存,避免扩容带来的额外拷贝/移动干扰
std::cout << "--- 测试 push_back ---\n";
// 需要先创建一个临时对象 MyClass(1, "test"),然后将其移动到 vec 中,最后销毁临时对象
vec.push_back(MyClass(1, "test"));
std::cout << "\n--- 测试 emplace_back ---\n";
// 直接传入参数,在 vec 的内存中原地构造对象
vec.emplace_back(2, "test");
return 0;
}
输出结果:
--- 测试 push_back ---
调用了构造函数 <-- 构造临时对象
调用了移动构造函数 <-- 将临时对象移动到 vector 中
调用了析构函数 <-- 销毁临时对象
--- 测试 emplace_back ---
调用了构造函数 <-- 直接在 vector 中原地构造,没有临时对象!
结论:在传递多参数构造复杂对象时,emplace_back 性能更高,因为它少了一次移动(或拷贝)操作和一次析构操作。
3. 需要注意的陷阱 (Gotchas)
虽然 emplace_back 看起来更好,但它并不是完美的,有时会因为“过于聪明”而导致意想不到的 bug。
emplace_back 会绕过 explicit 构造函数的限制,因为它是在容器内部直接调用的构造函数。
看下面这个例子:
std::vector<std::vector<int>> vec;
// 假设我们想向 vec 中添加元素:
// vec.push_back(10);
// 编译报错!因为 std::vector<int> 的构造函数是 explicit 的,
// 拒绝将 int 隐式转换为 std::vector<int>。这保护了我们不犯错。
vec.emplace_back(10);
// 编译通过!它直接调用了 explicit std::vector<int>(10) 构造函数。
// 结果是:向 vec 中插入了一个包含 10 个 0 的 vector。
// 这通常不是程序员的本意(通常是手误写错了)。
4. 总结与最佳实践:我该用哪个?
内置类型(int, double, 指针等):
两者没有任何性能差异。写vec.push_back(5)和vec.emplace_back(5)编译出的汇编代码是一样的。通常习惯写push_back。已经存在的对象(左值):
如果你已经有了一个对象obj,准备放入容器,两者没区别,都会调用拷贝构造。vec.push_back(obj)和vec.emplace_back(obj)效果一样,建议用push_back,语义更清晰。通过参数临时构建对象:
如果你正在传递参数来创建一个新对象放入容器(如上面的MyClass例子),强烈建议使用emplace_back,因为它能原地构造,避免产生临时对象,性能更好。
写成:vec.emplace_back(arg1, arg2);注意 C++17 的一个变化:
在 C++17 之前,这两个函数都返回void。从 C++17 开始,emplace_back返回插入元素的引用(T&),这让你可以在插入后直接修改它:cppauto& ref = vec.emplace_back(1, "test"); // 可以直接使用或修改 ref(注:C++17 中的
push_back依然返回void,不过 C++20 中引入了push_back的返回值提案,但在许多编译器中emplace_back返回引用更普及)。