基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

std::vector 中,push_back 和 emplace_back 的区别

知识点图片

在 C++ 的 std::vector 中,push_backemplace_back 都是用于在容器末尾添加元素的函数,但它们的底层机制性能表现有显著的区别。

简单来说:push_back 是“先创建对象,再拷贝/移动到容器中”,而 emplace_back 是“直接在容器的内存中原地构造对象”。

以下是详细的对比和解析:

1. 核心工作原理

push_back

  • 它接收一个已经构造好的对象(左值或右值)。
  • 如果是左值,它会调用类的拷贝构造函数,将对象复制到 vector 的内存中。
  • 如果是右值(例如临时对象),它会调用类的移动构造函数,将对象移动到 vector 的内存中。

emplace_back (C++11 引入)

  • 它接收的是构造对象所需的参数(通过可变参数模板和完美转发实现)。
  • 它直接在 vector 预先分配的内存上,调用类的构造函数来实例化对象。
  • 完全省去了临时对象的创建、拷贝或移动的过程。

2. 代码示例与性能对比

为了直观看出区别,我们定义一个包含打印信息的类:

cpp
#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;
}

输出结果:

plaintext
--- 测试 push_back ---
调用了构造函数      <-- 构造临时对象
调用了移动构造函数  <-- 将临时对象移动到 vector 中
调用了析构函数      <-- 销毁临时对象

--- 测试 emplace_back ---
调用了构造函数      <-- 直接在 vector 中原地构造,没有临时对象!

结论:在传递多参数构造复杂对象时,emplace_back 性能更高,因为它少了一次移动(或拷贝)操作和一次析构操作。


3. 需要注意的陷阱 (Gotchas)

虽然 emplace_back 看起来更好,但它并不是完美的,有时会因为“过于聪明”而导致意想不到的 bug。

emplace_back 会绕过 explicit 构造函数的限制,因为它是在容器内部直接调用的构造函数。

看下面这个例子:

cpp
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. 总结与最佳实践:我该用哪个?

  1. 内置类型(int, double, 指针等):
    两者没有任何性能差异。写 vec.push_back(5)vec.emplace_back(5) 编译出的汇编代码是一样的。通常习惯写 push_back

  2. 已经存在的对象(左值):
    如果你已经有了一个对象 obj,准备放入容器,两者没区别,都会调用拷贝构造。
    vec.push_back(obj)vec.emplace_back(obj) 效果一样,建议用 push_back,语义更清晰。

  3. 通过参数临时构建对象:
    如果你正在传递参数来创建一个新对象放入容器(如上面的 MyClass 例子),强烈建议使用 emplace_back,因为它能原地构造,避免产生临时对象,性能更好。
    写成:vec.emplace_back(arg1, arg2);

  4. 注意 C++17 的一个变化:
    在 C++17 之前,这两个函数都返回 void。从 C++17 开始,emplace_back 返回插入元素的引用T&),这让你可以在插入后直接修改它:

    cpp
    auto& ref = vec.emplace_back(1, "test");
    // 可以直接使用或修改 ref

    (注:C++17 中的 push_back 依然返回 void,不过 C++20 中引入了 push_back 的返回值提案,但在许多编译器中 emplace_back 返回引用更普及)。

00:00
00:00