C++零基础到工程实战(4.3.6):vector中push_back和emplace_back性能分析

张开发
2026/6/12 5:00:36 15 分钟阅读
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 对象2vector 尾部准备一个位置3把这个临时 string 放到容器里4.2 vs.push_back(test); 的执行逻辑4.3 vs.emplace_back(test); 的执行逻辑1vector 先在尾部准备好一块空间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插入元素的效率 { vectorstring 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的字面意思就是把一个元素推到末尾。例如vectorstring 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之前对象其实已经先存在了。2vector尾部准备一个位置vector会检查自己当前是否还有容量。如果容量够就直接使用尾部空位如果容量不够就会先扩容再准备新位置3把这个临时string放到容器里早期 C 中这里往往是拷贝进去。现代 C 中如果传入的是右值临时对象通常会优先使用移动语义也就是把临时对象“移动”进容器。先在外部构造一个临时对象再把这个对象移动到容器内部。4.2vs.push_back(test);的执行逻辑代码vs.push_back(test);这里看起来没有写string(test)但并不意味着没有构造string对象。因为vs的类型是vectorstring所以push_back最终要插入的是string类型元素。而test本身是字符串字面量本质上更接近const char*因此push_back(test)背后的逻辑通常可以理解为1先根据test隐式构造一个临时string2再把这个临时string放入vector尾部3现代 C 中通常是移动进去所以vs.push_back(test);虽然写法更短但它本质上还是“先构造一个对象再放进去”。4.3vs.emplace_back(test);的执行逻辑代码vs.emplace_back(test);这句才是emplace_back最典型、最有代表性的用法。它的执行过程通常可以理解为1vector先在尾部准备好一块空间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先在外部生成一个临时string2再把这个临时string作为参数传给emplace_back3容器再利用这个参数在内部构造元素所以从效果上看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这种简单类型那么vectorint 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插入性能的往往还有一个更大的因素扩容。例如vectorstring vs; for (int i 0; i 10000; i) { vs.emplace_back(test); }如果vs一开始没有预留足够容量那么中间会多次扩容。每次扩容都可能涉及申请新内存搬移已有元素释放旧内存这个开销往往比你单次push_back和emplace_back的微小差距更大。所以从工程角度说真正想提升插入性能时不能只盯着push_back和emplace_back的区别还要记得vs.reserve(预计数量);提前预留空间。例如vectorstring 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都一定明显更快2emplace_back(string(test))依然已经先创建了临时对象3真正影响整体性能的还有vector扩容4工程里经常配合reserve()一起优化插入效率八、小结本节我们学习了vector中两个非常常见的尾部插入函数push_back() emplace_back()它们看起来都能“往尾部加元素”但背后的设计思路并不一样push_back更偏向把现成对象放进去emplace_back更偏向直接在容器内部构造对象因此从机制上说emplace_back往往更贴近高效设计尤其是在元素类型较复杂时更有意义。不过真正写代码时也要记住不要机械地认为emplace_back一定绝对更快而要看你是不是直接传了构造参数以及容器是否发生扩容。

更多文章