【C++】vector的push_back和emplace_back
在 C++ 中,push_back
和 emplace_back
都是 std::vector
提供的成员函数,用于向向量末尾添加元素,但它们在用法和实现上有一些关键区别。以下是详细说明:
1. 基本功能
push_back
:将一个已经构造好的对象(或值的副本)添加到向量的末尾。emplace_back
:直接在向量的末尾构造一个对象,避免额外的拷贝或移动操作。
2. 参数传递
push_back
:接受一个完整的对象(可以是右值或左值)。如果传入的是左值,会发生拷贝;如果传入的是右值,会发生移动。emplace_back
:接受对象的构造函数参数,直接将这些参数转发给对象的构造函数,在容器内就地构造对象。
3. 性能差异
push_back
:因为需要传入一个构造好的对象,可能会涉及额外的拷贝或移动开销。emplace_back
:通过就地构造,避免了不必要的拷贝或移动,通常更高效。
4. 代码示例
#include <vector>
#include <string>
#include <iostream>
struct Person {
std::string name;
int age;
Person(const std::string& n, int a) : name(n), age(a) {
std::cout << "Constructor called for " << name << "\n";
}
Person(const Person& other) : name(other.name), age(other.age) {
std::cout << "Copy constructor called for " << name << "\n";
}
};
int main() {
std::vector<Person> vec;
// 使用 push_back
Person p1("Alice", 25);
vec.push_back(p1); // 拷贝构造
std::cout << "----\n";
// 使用 push_back 配合 std::move
vec.push_back(std::move(p1)); // 移动构造
std::cout << "----\n";
// 使用 emplace_back
vec.emplace_back("Bob", 30); // 直接构造,无拷贝或移动
return 0;
}
输出:
Constructor called for Alice
Copy constructor called for Alice
----
Copy constructor called for Alice // 注意:这里假设移动被优化为拷贝
----
Constructor called for Bob
push_back(p1)
:触发了一次拷贝构造,因为p1
是左值。push_back(std::move(p1))
:通过std::move
触发移动构造(如果类支持移动语义)。emplace_back("Bob", 30)
:直接在向量内存中构造Person
,只调用一次构造函数。
5. 适用场景
push_back
:适合已有对象需要添加到容器的情况,或者你明确想传递一个完整的对象。emplace_back
:适合需要高效构造对象的情况,尤其是当对象构造成本较高或希望避免拷贝时。
6. 注意事项
emplace_back
不一定总是更快:如果传入的参数本身需要构造临时对象(例如emplace_back(std::string("test"), 42)
),编译器可能仍会生成临时对象,此时性能可能与push_back
相近。push_back
更直观:代码可读性上,push_back
更常用且易懂。
假设emplace_back的是一个对象,会如何, 先说答案,会触发拷贝构造函数或移动构造函数!
如果你将一个已经构造好的对象传入 emplace_back
,它的行为会与预期有所不同,因为 emplace_back
的设计初衷是接受构造函数的参数并在容器内就地构造对象,而不是直接处理完整的对象。让我们具体分析这种情况:
传入对象情况说明
当你尝试将一个完整的对象传入 emplace_back
时,C++ 编译器会将其视为一个参数,并尝试用这个对象调用目标类型的构造函数。这可能会导致以下几种情况:
- 隐式转换或构造:如果目标类型可以用传入的对象构造(比如通过拷贝构造函数或移动构造函数),
emplace_back
会在容器内调用相应的构造函数。 - 编译错误:如果目标类型的构造函数无法接受这个对象,代码将无法编译。
示例代码
#include <vector>
#include <string>
#include <iostream>
struct Person {
std::string name;
int age;
Person(const std::string& n, int a) : name(n), age(a) {
std::cout << "Constructor called for " << name << "\n";
}
Person(const Person& other) : name(other.name), age(other.age) {
std::cout << "Copy constructor called for " << name << "\n";
}
};
int main() {
std::vector<Person> vec;
// 创建一个对象
Person p1("Alice", 25);
// 使用 emplace_back 传入对象
vec.emplace_back(p1); // 注意这里传入的是完整的对象
return 0;
}
输出:
Constructor called for Alice
Copy constructor called for Alice
分析
- 在
vec.emplace_back(p1)
中,p1
是一个已经构造好的Person
对象。 emplace_back
会将p1
作为参数传递给Person
的拷贝构造函数,在向量内部构造一个新对象。- 结果是:仍然会触发一次拷贝构造,而不是直接使用
p1
。
这与 push_back(p1)
的行为类似,因为两者最终都会调用拷贝构造函数。区别在于:
push_back(p1)
是明确地将p1
拷贝(或移动)到容器中。emplace_back(p1)
是将p1
作为参数,间接触发拷贝构造。
如果传入右值对象
如果传入的是右值(例如通过 std::move
),emplace_back
会调用移动构造函数:
vec.emplace_back(std::move(p1)); // 触发移动构造
输出(假设有移动构造函数):
Constructor called for Alice
Move constructor called for Alice
与预期设计的差异
emplace_back
的优势在于直接使用构造函数参数(例如 emplace_back("Alice", 25)
),从而避免额外的拷贝或移动。
总结
- 效率:
emplace_back
通常更高效,因为它避免了拷贝或移动。但是如果是添加的对象,还是会触发拷贝或者移动(右值) - 灵活性:
emplace_back
直接用构造函数参数,push_back
需要现成的对象。 - 选择建议:优先考虑
emplace_back
,除非你已经有构造好的对象需要传入。