Rust在2025年:实践中的新特性

面向初学者的在线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 算法:





                
                

博客

书籍推荐