push_back vs emplace_back:有什么区别以及新手该如何选择?

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

引言:为什么这很重要?

\n\n

你是否曾想过,为什么C++中有两种方法可以将元素添加到向量的末尾?既然有经过验证的push_back,为什么还要多此一举?答案在于现代C++的性能和灵活性。

\n\n

在Stack Overflow上,这个问题获得了数千次浏览和数百条评论。这并非偶然:理解push_back和emplace_back之间的区别不仅仅是学术知识,而是一项可以将代码速度提升2-3倍的实用技能。让我们一劳永逸地弄清楚。

\n\n

基本区别:文档怎么说?

\n\n

我们从简单的开始。两种方法都将元素添加到容器(向量、列表、队列)的末尾。但它们的实现方式不同:

\n\n
    \n
  • push_back — 接受一个现成的对象(按值、引用或右值引用),并将其拷贝/移动到容器中。
  • \n
  • emplace_back — 接受对象的构造函数参数,并直接在容器内部创建对象(原地构造)。
  • \n
\n\n

听起来很抽象?让我们看看代码。

\n\n

实际示例:创建一个类

\n\n

假设我们有一个简单的类Person:

\n\n
#include <iostream>\n#include <vector>\n#include <string>\n\nclass Person {\npublic:\n    std::string name;\n    int age;\n\n    // 构造函数\n    Person(const std::string& n, int a) \n        : name(n), age(a) {\n        std::cout << "构造函数被调用,对象为 " << name << std::endl;\n    }\n\n    // 拷贝构造函数\n    Person(const Person& other) \n        : name(other.name), age(other.age) {\n        std::cout << "拷贝构造函数被调用,对象为 " << name << std::endl;\n    }\n\n    // 移动构造函数\n    Person(Person&& other) noexcept\n        : name(std::move(other.name)), age(other.age) {\n        std::cout << "移动构造函数被调用,对象为 " << name << std::endl;\n    }\n};
\n\n

现在看看使用不同方法时会发生什么。

\n\n

方案1:使用临时对象的push_back

\n\n
int main() {\n    std::vector<Person> people;\n    people.push_back(Person("Alice", 25));\n    return 0;\n}
\n\n

输出:

\n
构造函数被调用,对象为 Alice\n移动构造函数被调用,对象为 Alice
\n\n

发生了什么?首先创建了一个临时Person对象(第一次调用构造函数)。然后该对象被移动到向量中(移动构造函数)。两次调用——双倍工作量。

\n\n

方案2:使用现成对象的push_back

\n\n
int main() {\n    std::vector<Person> people;\n    Person alice("Alice", 25);\n    people.push_back(alice);  // alice 是左值\n    return 0;\n}
\n\n

输出:

\n
构造函数被调用,对象为 Alice\n拷贝构造函数被调用,对象为 Alice
\n\n

这里我们看到的是拷贝,而不是移动。如果对象很大(例如包含动态数组),拷贝可能会很昂贵。

\n\n

方案3:emplace_back

\n\n
int main() {\n    std::vector<Person> people;\n    people.emplace_back("Bob", 30);  // 传递构造函数参数\n    return 0;\n}
\n\n

输出:

\n
构造函数被调用,对象为 Bob
\n\n

只调用了一次构造函数!Person对象直接在向量的内存中创建,没有中间拷贝或移动。

\n\n

性能:何时差异至关重要?

\n\n

对于简单类型(int、double、指针),差异不明显——编译器会进行优化。但对于复杂对象,差异可能非常显著:

\n\n
    \n
  • 具有重型拷贝构造函数的对象(例如std::string、容器

博客

书籍推荐