简介:文档没告诉你的那些事
想象一下你在编写高负载服务器或数据处理算法。手头有两个 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 算法:
博客
书籍推荐
推荐阅读