引言:文档中未提及的细节
设想一下:您正在编写一个高负载服务器或数据处理算法。您有两个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算法:
博客
书籍推荐
推荐阅读