C++ emplace_back比push_back快在哪里
push_back 和 emplace_back 的核心区别在于:是否在内存中“搬运”对象。理解这一点,就能明白为什么 emplace_back 更快。
理解核心区别:搬家 vs 现场组装
要把一件家具放进房间,有两种方法:
- push_back(搬家模式):先在走廊里把家具组装好,然后把它搬运进房间,最后把走廊里的那个拆掉或扔掉。
- emplace_back(现场模式):直接把木板和钉子拿进房间,在房间里的目标位置直接把家具组装好。
在 C++ 中,对象就是家具,内存就是房间。push_back 需要先构造一个临时对象,再移动或拷贝进容器;而 emplace_back 直接在容器的内存空间中构造对象,省去了中间的“搬运”过程。
观察代码运行过程
阅读下面的代码,我们定义一个简单的结构体 Widget,并在构造函数中打印日志,以便追踪对象的创建过程。
#include <iostream>
#include <vector>
#include <string>
struct Widget {
std::string name;
int value;
// 构造函数
Widget(std::string n, int v) : name(n), value(v) {
std::cout << "构造函数被调用: " << name << std::endl;
}
// 拷贝构造函数
Widget(const Widget& other) : name(other.name), value(other.value) {
std::cout << "拷贝构造函数被调用: " << name << std::endl;
}
// 移动构造函数
Widget(Widget&& other) noexcept : name(std::move(other.name)), value(other.value) {
std::cout << "移动构造函数被调用: " << name << std::endl;
}
};
int main() {
std::vector<Widget> vec;
std::cout << "--- 使用 push_back ---" << std::endl;
vec.push_back(Widget("PushBackObj", 1)); // 传入临时对象
std::cout << "\n--- 使用 emplace_back ---" << std::endl;
vec.emplace_back("EmplaceBackObj", 2); // 直接传入构造参数
return 0;
}
分析这段代码的输出结果(忽略编译器优化的情况):
- push_back:
- 先调用一次构造函数(创建临时对象
Widget("PushBackObj", 1))。 - 再调用一次移动构造函数(将临时对象移动到 vector 内存中)。
- 最后析构临时对象。
- 先调用一次构造函数(创建临时对象
- emplace_back:
- 仅调用一次构造函数(直接在 vector 内存中创建对象)。
显然,emplace_back 少走了一次“移动”和“析构”的流程。
查看执行流程对比
下面的流程图清晰地展示了两者在内存操作上的步骤差异。
生成临时对象] P2 --> P3[调用移动/拷贝构造函数
对象入容器] P3 --> P4[析构临时对象] end subgraph E["emplace_back 执行流程"] E1[传入构造参数] --> E2[在容器内存内
直接调用构造函数] end P3 -.-> E2
从图中可以看出,emplace_back 跳过了生成临时对象和处理临时对象的繁琐步骤。
掌握性能损耗公式
当对象很复杂(例如包含 std::string、std::vector 等动态资源)时,移动构造虽然比拷贝构造快,但依然需要消耗时间。
对于 push_back,其总开销 $C_{push}$ 大约为:
$$ C_{push} = C_{construct} + C_{move} + C_{destruct} $$
而对于 emplace_back,其总开销 $C_{emplace}$ 仅为:
$$ C_{emplace} = C_{construct} $$
结论:$C_{move}$(移动开销)和 $C_{destruct}$(析构开销)就是 emplace_back 节省下来的成本。对于简单类型(如 int),两者差异微乎其微;但对于复杂对象,节省的成本非常可观。
遵循使用建议
在实际开发中,请遵循以下原则以确保代码既高效又安全:
-
优先使用
emplace_back。
在添加新元素时,默认使用emplace_back,除非有特殊理由。 -
直接传入构造函数的参数。
不要在emplace_back里创建对象,要像下面这样写:// 正确做法:直接传参数 vec.emplace_back("ObjectName", 100); // 错误做法:传了一个临时对象(这样就退化成了 push_back) vec.emplace_back(Widget("ObjectName", 100)); -
注意参数类型匹配。
如果构造函数是explicit(显式)的,或者参数类型需要隐式转换,emplace_back会直接进行转换并在原地构造,依然比先构造再移动要快。
对比特性差异
下表总结了两者在主要特性上的区别:
| 特性 | push_back | emplace_back |
|---|---|---|
| 传入参数 | 已存在的对象 | 构造函数所需的参数 |
| 临时对象 | 需要生成 | 不需要生成 |
| 底层操作 | 拷贝或移动构造 | 原地构造(直接初始化) |
| 适用场景 | 已有对象需要插入 | 需要新建对象并插入 |
| 代码可读性 | 较高(意图明显) | 中等(需识别参数) |

暂无评论,快来抢沙发吧!