16. emplace_back 和 push_back 的区别是什么?为什么 emplace_back 更高效?

这个问题,几乎每次 C++ 面试都会出现。我当年面试时也被问过,当时答得磕磕绊绊。后来在项目里真正用上了,才彻底搞明白。

说白了,push_backemplace_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 vs emplace_back 流程对比 push_back("hello") 步骤1:构造临时 string("hello") 步骤2:移动构造到 vector 内部 步骤3:析构临时对象(额外开销) emplace_back("hello") 直接构造:在 vector 内存中构造 string 完成!无临时对象,无额外开销 vs

总结一下:

  • push_back:接受对象,可能多一次临时对象的构造和析构。
  • emplace_back:接受构造参数,原地构造,省掉中间步骤。
  • 性能差距在重对象上尤其明显。轻量类型(如 int)差别不大。
  • 注意 explicit 构造函数和左值/右值的问题。

避坑指南:

我曾经在代码审查时看到有人把所有 push_back 都改成 emplace_back,觉得这样更「现代」。结果引入了一个 bug——emplace_backexplicit 构造函数的处理方式和 push_back 不同,导致某些类型插不进去。

我的建议是:按需使用,不要无脑替换。该用 push_back 的地方就用 push_back,该用 emplace_back 的地方就用 emplace_back。两者各有各的适用场景。

专注资料整理