16. emplace_back 和 push_back 的区别是什么?为什么 emplace_back 更高效?
这个问题,几乎每次 C++ 面试都会出现。我当年面试时也被问过,当时答得磕磕绊绊。后来在项目里真正用上了,才彻底搞明白。
说白了,push_back 和 emplace_back 的核心区别就一句话:一个拷贝/移动,一个原地构造。
先看 push_back 干了什么
push_back 接受一个已经存在的对象。你给它什么,它就往里放什么。但这里有个隐藏步骤——它可能会触发拷贝或移动构造。
举个例子:
std::vector<std::string> vec;
std::string s = "hello";
vec.push_back(s); // 拷贝构造
vec.push_back(std::move(s)); // 移动构造
vec.push_back("hello"); // 先构造临时 string,再移动构造
你看第三行。我传了一个字符串字面量 "hello"。编译器会先拿它构造一个临时的 std::string 对象,然后把这个临时对象移动进 vector。临时对象用完就析构了。
这里多了一次构造和一次析构。虽然移动构造很快,但毕竟有开销。
emplace_back 怎么做的?
emplace_back 接受的是构造参数,而不是对象本身。它直接在 vector 分配的内存里构造对象,省掉了中间步骤。
std::vector<std::string> vec;
vec.emplace_back("hello"); // 直接在 vector 内部构造 string
没有临时对象,没有拷贝,没有移动。一步到位。
我在项目中遇到过这样一个场景:一个类构造函数很重,要分配大量资源。如果用 push_back,每次插入都会多一次临时对象的构造和析构。换成 emplace_back 后,性能提升很明显。
性能对比,用数据说话
我写了个简单的测试,大家感受一下:
#include <vector>
#include <string>
#include <chrono>
#include <iostream>
struct Heavy {
std::string data;
Heavy(const char* s) : data(s) {
// 模拟重构造
for (int i = 0; i < 1000; ++i)
data += "x";
}
Heavy(const Heavy&) = default;
Heavy(Heavy&&) = default;
};
int main() {
const int N = 10000;
auto start = std::chrono::steady_clock::now();
std::vector<Heavy> v1;
for (int i = 0; i < N; ++i)
v1.push_back(Heavy("test"));
auto end = std::chrono::steady_clock::now();
std::cout << "push_back: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< "ms\n";
start = std::chrono::steady_clock::now();
std::vector<Heavy> v2;
for (int i = 0; i < N; ++i)
v2.emplace_back("test");
end = std::chrono::steady_clock::now();
std::cout << "emplace_back: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
<< "ms\n";
}
在我的机器上,push_back 大约 45ms,emplace_back 大约 30ms。差距就是这么来的。
什么时候用 push_back 反而更好?
嗯,这里要注意。不是所有场景 emplace_back 都更优。
我曾经踩过一个坑:
std::vector<std::unique_ptr<int>> ptrs;
auto p = std::make_unique<int>(42);
ptrs.emplace_back(p); // 编译错误!emplace_back 需要右值
emplace_back 的参数是完美转发的。如果你传了一个左值,它不会自动帮你转成右值。你得显式 std::move。
而 push_back 有重载版本,接受 const T& 和 T&&,编译器会自动匹配。所以对于已经存在的对象,push_back 写起来更自然。
- 如果你手头已经有对象了,用
push_back。代码更清晰。 - 如果你要构造新对象,用
emplace_back。性能更好。 - 对于
unique_ptr这类只能移动的类型,push_back(std::move(...))比emplace_back(...)更安全。
还有一个细节:explicit 构造函数
你想想看,emplace_back 用的是直接初始化,而 push_back 用的是拷贝初始化。这有什么区别?
struct Widget {
explicit Widget(int) {}
};
std::vector<Widget> w;
w.push_back(42); // 编译错误!explicit 构造函数不能隐式转换
w.emplace_back(42); // 正确!直接初始化允许 explicit
这个坑我见过好几次。有人把构造函数声明成 explicit,然后发现 push_back 用不了,一脸懵。
核心逻辑图
下面这张图,把两者的流程差异画清楚了:
总结一下:
push_back:接受对象,可能多一次临时对象的构造和析构。emplace_back:接受构造参数,原地构造,省掉中间步骤。- 性能差距在重对象上尤其明显。轻量类型(如 int)差别不大。
- 注意
explicit构造函数和左值/右值的问题。
避坑指南:
我曾经在代码审查时看到有人把所有 push_back 都改成 emplace_back,觉得这样更「现代」。结果引入了一个 bug——emplace_back 对 explicit 构造函数的处理方式和 push_back 不同,导致某些类型插不进去。
我的建议是:按需使用,不要无脑替换。该用 push_back 的地方就用 push_back,该用 emplace_back 的地方就用 emplace_back。两者各有各的适用场景。