如何在C++中将一个向量添加到另一个向量:push_back隐藏了什么

面向初学者的在线Python训练器

轻松学习Python,无需理论负担。在浏览器中直接解决带自动检查的实践任务,无需安装任何东西。

开始课程

引言:文档中未提及的细节



设想一下:您正在编写一个高负载服务器或数据处理算法。您有两个std::vector<int>,需要将它们合并为一个。看起来,这有什么难的呢?直接添加就行了。但C++不会容忍浅尝辄止的方法。一个错误的方式——就会导致多余的拷贝、性能下降,甚至未定义行为。



Stack Overflow上关于“Appending a vector to a vector”的问题获得了数千次浏览,这并非偶然。即使是经验丰富的开发人员有时也会混淆push_back和insert,或者忘记移动语义。在本文中,我们将深入编译器底层,了解将一个向量添加到另一个向量的实际工作原理。您将了解为什么std::copy可能比insert更快,以及移动语义何时能拯救您的项目。



最显而易见(也最危险)的方式:在循环中使用push_back



初学者经常这样写:



std::vector<int> dest = {1, 2, 3};std::vector<int> src = {4, 5, 6};

for (size_t i = 0; i < src.size(); ++i) { dest.push_back(src[i]);}


这里发生了什么?在每次迭代中,push_back都会检查是否分配了足够的内存。如果没有——就会发生重新分配:旧数组被复制到新的内存区域,旧数组被删除。对于三个元素,可能会发生2-3次重新分配。对于小型向量来说,这并不明显,但对于数千个元素来说,这就是一场灾难。



问题:O(n)次检查以及潜在的整个数组复制。在C++17及更高版本中,编译器可能会进行优化,但依赖于此是有风险的。



push_back底层发生了什么?

  • 检查size() < capacity()。
  • 如果否——调用reallocate(new_capacity)。
  • 复制(或移动)现有元素。
  • 删除旧缓冲区。
  • 复制新元素。


结论:永远不要使用循环来添加整个向量,除非您有意识地只处理少量元素。



正确的方法:std::vector::insert



标准库提供了一个优雅的解决方案:



std::vector<int> dest = {1, 2, 3};std::vector<int> src = {4, 5, 6};

dest.insert(dest.end(), src.begin(), src.end());


一行代码!但这个调用背后隐藏着什么?insert执行以下操作:

  • 计算所需的内存量:dest.size() + src.size()。
  • 如果当前容量不足——精确分配所需内存(或预留更多,取决于实现)。
  • 移动现有元素(如果不是在末尾插入)。
  • 从src复制(或移动)新元素。


重要提示:insert执行一次重新分配(如果空间足够,则一次也没有)。这已经比循环好,但仍然存在从src复制元素的操作。



insert的性能

如果插入后不再需要src,您可以使用std::make_move_iterator移动其元素:



dest.insert(dest.end(),            std::make_move_iterator(src.begin()),            std::make_move_iterator(src.end()));


这将复制转换为移动。对于std::vector<int>来说没有区别(int是平凡类型),但对于std::vector<std::string>或复杂对象,移动操作的开销相比复制微乎其微。



替代方案:使用back_inserter的std::copy



一些开发人员更喜欢STL算法:





                
                

博客

书籍推荐