引言:为什么这很重要?
\n\n你是否曾想过,为什么C++中有两种方法可以将元素添加到向量的末尾?既然有经过验证的push_back,为什么还要多此一举?答案在于现代C++的性能和灵活性。
在Stack Overflow上,这个问题获得了数千次浏览和数百条评论。这并非偶然:理解push_back和emplace_back之间的区别不仅仅是学术知识,而是一项可以将代码速度提升2-3倍的实用技能。让我们一劳永逸地弄清楚。
基本区别:文档怎么说?
\n\n我们从简单的开始。两种方法都将元素添加到容器(向量、列表、队列)的末尾。但它们的实现方式不同:
\n\n- \n
push_back— 接受一个现成的对象(按值、引用或右值引用),并将其拷贝/移动到容器中。 \n emplace_back— 接受对象的构造函数参数,并直接在容器内部创建对象(原地构造)。 \n
听起来很抽象?让我们看看代码。
\n\n实际示例:创建一个类
\n\n假设我们有一个简单的类Person:
#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\nint 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对象(第一次调用构造函数)。然后该对象被移动到向量中(移动构造函数)。两次调用——双倍工作量。
方案2:使用现成对象的push_back
\n\nint 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\nint 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对于简单类型(int、double、指针),差异不明显——编译器会进行优化。但对于复杂对象,差异可能非常显著:
\n\n- \n
- 具有重型拷贝构造函数的对象(例如
std::string、容器