C++零基础到工程实战(4.3.6):vector中push_back和emplace_back性能分析
目录
一、前言
二、本节代码示例
三、push_back 和 emplace_back 都是做什么的
3.1 push_back:把一个已有元素放到vector尾部
3.2 emplace_back:直接在vector尾部构造元素
四、push_back 与 emplace_back 的插入过程分析
4.1 vs.push_back(string("test")); 的执行逻辑
(1)先构造一个临时 string 对象
(2)vector 尾部准备一个位置
(3)把这个临时 string 放到容器里
4.2 vs.push_back("test"); 的执行逻辑
4.3 vs.emplace_back("test"); 的执行逻辑
(1)vector 先在尾部准备好一块空间
(2)把 "test" 作为参数,直接传给 string 的构造函数
(3)在这块尾部空间中直接构造出这个 string 对象
4.4 vs.emplace_back(string("test")); 的执行逻辑
4.5 vs.emplace_back(); 是什么意思
五、性能分析:emplace_back为什么通常更高效
5.1 中间临时对象更少
5.2 对复杂对象优势更明显
5.3 但不是所有场景都差很多
六、结合性能因素:扩容
七、本节重点总结
7.1 push_back 的特点
7.2 emplace_back 的特点
7.3 最容易误解的地方
八、小结
一、前言
在前面的内容中,我们已经学习了vector的常见操作,也知道vector作为动态数组容器,在工程开发中使用非常频繁。
当我们往vector尾部插入元素时,最常用的有两个函数:
- push_back()
- emplace_back()
很多初学者第一次看到这两个函数时,会觉得它们功能几乎一样:
- 都能在尾部插入元素
- 都能让
vector增加新内容 - 看起来经常可以互相替代
于是就会产生一个问题:
既然都能插入元素,那为什么 C++ 还要设计两个接口?它们到底有什么区别?
这背后其实涉及到一个很重要的性能问题:
push_back往往是先有对象,再放进容器emplace_back往往是直接在容器内部构造对象
如果元素类型比较简单,这种差别可能不明显;
但如果元素类型比较复杂,比如string、自定义类对象、大对象等,这种差别就可能带来一定的性能影响。
本节我们就结合string和代码示例,详细分析:
push_back的插入过程emplace_back的插入过程- 哪些场景下
emplace_back更高效- 为什么有时两者差别不大
- 为什么
emplace_back(string("test"))不一定比push_back(string("test"))更占优势
二、本节代码示例
#include <iostream> #include <vector> #include <string> using namespace std; int main() { // vector插入元素的效率 { vector<string> vs; // push_back:先构造对象,再放入容器 vs.push_back(string("test")); // emplace_back:这里虽然用了emplace_back, // 但string("test")这个临时对象依然已经先构造出来了 vs.emplace_back(string("test")); // push_back:隐式把"test"转换成string,再放入容器 vs.push_back("test"); // emplace_back:直接把"test"作为参数, // 在vector尾部原地构造string对象 vs.emplace_back("test"); // 默认构造一个空字符串 vs.emplace_back(); for (auto& s : vs) { cout << "[" << s << "]" << endl; } } return 0; }三、push_back 和 emplace_back 都是做什么的
3.1 push_back:把一个已有元素放到vector尾部
push_back的字面意思就是:
把一个元素推到末尾。
例如:
vector<string> vs; vs.push_back("test");这句代码的效果是:
在vs的尾部增加一个字符串元素"test"。
但它的思路通常是:
先准备好一个对象,再把这个对象放入容器。
也就是说,push_back更像是在说:
“我这里已经有一个东西了,请你把它塞进 vector 末尾。”
3.2 emplace_back:直接在vector尾部构造元素
emplace_back的字面意思可以理解为:
在末尾原地构造。
例如:·
vs.emplace_back("test");它的思路通常是:
不用先在外面准备一个完整对象,而是直接把构造参数交给容器,让容器在自己的内部空间里把对象构造出来。
所以emplace_back更像是在说:
“你不要等我先把对象造好,你直接在容器内部帮我把它创建出来。”
这也就是它被认为“更高效”的核心原因。
四、push_back 与 emplace_back 的插入过程分析
4.1vs.push_back(string("test"));的执行逻辑
(1)先构造一个临时string对象
代码:
string("test")这里先根据"test"创建了一个临时的string对象。
也就是说,在调用push_back之前,对象其实已经先存在了。
(2)vector尾部准备一个位置
vector会检查自己当前是否还有容量。
- 如果容量够,就直接使用尾部空位
- 如果容量不够,就会先扩容,再准备新位置
(3)把这个临时string放到容器里
早期 C++ 中,这里往往是拷贝进去。
现代 C++ 中,如果传入的是右值临时对象,通常会优先使用移动语义,也就是把临时对象“移动”进容器。
先在外部构造一个临时对象,再把这个对象移动到容器内部。
4.2vs.push_back("test");的执行逻辑
代码:
vs.push_back("test");这里看起来没有写string("test"),但并不意味着没有构造string对象。
因为vs的类型是:
vector<string>所以push_back最终要插入的是string类型元素。
而"test"本身是字符串字面量,本质上更接近:
const char*因此,push_back("test")背后的逻辑通常可以理解为:
(1)先根据
"test"隐式构造一个临时string(2)再把这个临时
string放入vector尾部(3)现代 C++ 中通常是移动进去
所以:
vs.push_back("test");虽然写法更短,但它本质上还是“先构造一个对象,再放进去”。
4.3vs.emplace_back("test");的执行逻辑
代码:
vs.emplace_back("test");这句才是emplace_back最典型、最有代表性的用法。
它的执行过程通常可以理解为:
(1)vector先在尾部准备好一块空间
(2)把"test"作为参数,直接传给string的构造函数
(3)在这块尾部空间中直接构造出这个string对象
也就是说:
没有先在外部单独生成一个临时string,而是直接在容器内部构造。
这就是所谓的:
原地构造、就地构造。
所以从机制上说:
vs.emplace_back("test");通常比:
vs.push_back("test");更贴近“少一次中间对象转换”的思路。
4.4vs.emplace_back(string("test"));的执行逻辑
代码:
vs.emplace_back(string("test"));这一句非常值得专门讲,因为很多人会误以为:
只要写了emplace_back,就一定比push_back更高效。
其实不一定。
因为这里你已经先写了:
string("test")这说明:
临时string对象已经在外部先构造出来了。
也就是说,这一句已经不是最纯粹的“直接在容器内部构造对象”了。
它更像是:
(1)先在外部生成一个临时
string(2)再把这个临时
string作为参数传给emplace_back(3)容器再利用这个参数在内部构造元素
所以从效果上看:
vs.emplace_back(string("test"));和:
vs.push_back(string("test"));差别就没有你想象中那么大。
真正能体现emplace_back优势的,通常不是这种写法,而是:
vs.emplace_back("test");因为这里直接把构造参数交给了容器。
4.5vs.emplace_back();是什么意思
代码:
vs.emplace_back();这句表示:
在vector末尾直接构造一个默认的string对象。
对于string来说,默认构造出来的是一个空字符串。
所以这句代码的含义就是:
在容器尾部新增一个空字符串元素。
这也是emplace_back很灵活的地方:
它不是只能传某个完整对象,而是可以直接传构造参数,甚至可以什么都不传,让它调用默认构造函数。
五、性能分析:emplace_back为什么通常更高效
5.1 中间临时对象更少
如果使用:
vs.emplace_back("test");容器可以直接在自己的尾部空间构造string。
相比之下:
vs.push_back("test");通常要先把"test"转成临时string,再放入容器。
所以在很多场景下,emplace_back可以减少一次临时对象构造与转移过程。
5.2 对复杂对象优势更明显
如果元素类型只是int这种简单类型,那么:
vector<int> v; v.push_back(10); v.emplace_back(10);两者差别通常很小,几乎没什么可感知的性能差异。
但如果元素类型是:
string- 大型对象
- 包含资源管理的对象
- 自定义复杂类
那么少一次临时对象构造、少一次移动或拷贝,就更有意义。
所以emplace_back的优势,在复杂对象场景下更容易体现出来。
5.3 但不是所有场景都差很多
这里也要讲得客观一些。
虽然大家常说:
emplace_back比push_back更高效
但在实际代码里,不一定每次都能感知到明显差距。因为现代 C++ 已经有:
- 移动语义
- 编译器优化
- 小对象优化(例如部分
string实现)
所以很多时候:
push_back(string("test"))和
emplace_back(string("test"))性能差距可能并不大。
真正差异更明显的,是:
push_back("test")和
emplace_back("test")因为后者更接近“直接原地构造”。
六、结合性能因素:扩容
前面我们分析的是“单次插入时对象构造方式的差异”,但在实际开发中,影响vector插入性能的,往往还有一个更大的因素:
扩容。
例如:
vector<string> vs; for (int i = 0; i < 10000; i++) { vs.emplace_back("test"); }如果vs一开始没有预留足够容量,那么中间会多次扩容。每次扩容都可能涉及:
- 申请新内存
- 搬移已有元素
- 释放旧内存
这个开销,往往比你单次push_back和emplace_back的微小差距更大。
所以从工程角度说,真正想提升插入性能时,不能只盯着push_back和emplace_back的区别,还要记得:
vs.reserve(预计数量);提前预留空间。
例如:
vector<string> vs; vs.reserve(10000); for (int i = 0; i < 10000; i++) { vs.emplace_back("test"); }这样通常会更高效。
七、本节重点总结
7.1 push_back 的特点
(1)插入一个已有对象
(2)通常是先构造对象,再放入容器
(3)现代 C++ 中对右值通常优先移动而不是拷贝
7.2 emplace_back 的特点
(1)在容器尾部直接构造对象
(2)通过传构造参数来创建元素
(3)对复杂对象通常更高效
(4)最能体现优势的写法是直接传构造参数,例如emplace_back("test")
7.3 最容易误解的地方
(1)不是所有emplace_back都一定明显更快
(2)emplace_back(string("test"))依然已经先创建了临时对象
(3)真正影响整体性能的还有vector扩容
(4)工程里经常配合reserve()一起优化插入效率
八、小结
本节我们学习了vector中两个非常常见的尾部插入函数:
push_back() emplace_back()它们看起来都能“往尾部加元素”,但背后的设计思路并不一样:
push_back更偏向把现成对象放进去emplace_back更偏向直接在容器内部构造对象
因此,从机制上说,emplace_back往往更贴近高效设计,尤其是在元素类型较复杂时更有意义。
不过真正写代码时也要记住:
不要机械地认为emplace_back一定绝对更快,而要看你是不是直接传了构造参数,以及容器是否发生扩容。
