C++ vector 中 push back 与 emplace back 的区别:从原理到性能

C++ vector 中 push back 与 emplace back 的区别:从原理到性能
C vector 中 push_back 与 emplace_back 的区别从原理到性能一、引言两个相似但不同的添加方法std::vector提供了两种在末尾添加元素的方法push_back和emplace_back(C11 引入)。它们都能将新元素添加到容器末尾但在参数传递方式和对象构造时机上有本质区别。push_back传入一个已经构造好的对象(或可转换为元素类型的对象)将其拷贝或移动到容器中emplace_back接受构造函数的参数在容器内部原地直接构造对象避免了拷贝或移动的开销。这一差异在元素类型构造代价高昂(如std::string、自定义类)时能带来显著的性能提升。二、核心区别速览| 维度 | push_back | emplace_back ||------|-----------|--------------|| 引入版本 | C98 |C11|| 参数 |已构造的对象或可转换的类型 |构造函数的参数|| 构造次数 | 1 次构造 1 次拷贝/移动 |1 次原地构造|| 临时对象 | 可能产生 |不产生|| 适用场景 | 已有对象或简单类型 | 需要直接构造复杂对象 || 性能 | 可能多一次拷贝/移动 |更优避免临时对象 |三、工作原理对比3.1 push_back 的工作流程#include vector #include string #include iostream int main() { std::vectorstd::string vec; // push_back 的几种调用方式 std::string s hello; // 方式一传入左值 → 拷贝 vec.push_back(s); // 拷贝构造 // 方式二传入右值 → 移动 vec.push_back(std::move(s)); // 移动构造 // 方式三传入临时对象 → 移动 vec.push_back(std::string(world)); // 先构造临时对象再移动 // 方式四传入字符串字面量 → 构造临时对象再移动 vec.push_back(temporary); // 隐式构造 std::string 临时对象再移动 }push_back(hello) │ ▼ ① 隐式构造临时对象 std::string tmp(hello) ◄── 有临时对象额外开销 │ ▼ ② 在容器内分配空间 │ ▼ ③ 转移临时对象 ├─ 移动构造可用 → 移动到容器内 └─ 无移动构造 → 拷贝到容器内 │ ▼ ④ 析构临时对象 ◄── 临时对象被销毁3.2 emplace_back 的工作流程std::vectorstd::string vec; // emplace_back直接传入构造参数 vec.emplace_back(hello); // 在容器内原地构造 vec.emplace_back(10, x); // 调用 string(10, x) vec.emplace_back(world, 3); // 调用 string(world, 3) → wor移动构造可用无移动构造push_back hello1. 隐式构造临时对象2. 在容器内分配空间3. 转移临时对象4a. 移动到容器内4b. 拷贝到容器内5. 析构临时对象关键差异push_back(hello)经历“临时对象构造 → 移动 → 临时对象析构”三步emplace_back(hello)只有“原地构造”一步。四、性能对比与验证#include vector #include string #include chrono #include iostream struct Verbose { Verbose() { std::cout Default ctor std::endl; } Verbose(const char*) { std::cout C-string ctor std::endl; } Verbose(const Verbose) { std::cout Copy ctor std::endl; } Verbose(Verbose) noexcept { std::cout Move ctor std::endl; } ~Verbose() { } }; int main() { std::cout push_back std::endl; { std::vectorVerbose vec; vec.reserve(2); std::cout push_back(\hello\): std::endl; vec.push_back(hello); // 输出: C-string ctor → Move ctor // ↑ 临时对象 ↑ 移动到容器 } std::cout \n emplace_back std::endl; { std::vectorVerbose vec; vec.reserve(2); std::cout emplace_back(\hello\): std::endl; vec.emplace_back(hello); // 输出: C-string ctor // ↑ 直接在容器内构造无临时对象 } }性能基准测试#include benchmark/benchmark.h #include vector #include string static void BM_PushBack(benchmark::State state) { for (auto _ : state) { std::vectorstd::string vec; for (int i 0; i 10000; i) { vec.push_back(std::to_string(i)); // 临时对象 移动 } } } BENCHMARK(BM_PushBack); static void BM_EmplaceBack(benchmark::State state) { for (auto _ : state) { std::vectorstd::string vec; for (int i 0; i 10000; i) { vec.emplace_back(std::to_string(i)); // 仍然有临时对象 } } } BENCHMARK(BM_EmplaceBack); // 注此例中两者性能相近因为参数本身就是临时对象性能差异显著的场景// push_back构造临时对象 移动 vec.push_back(std::string(100, x)); // emplace_back直接原地构造省略临时对象 vec.emplace_back(100, x); // 性能优势明显五、emplace_back 的注意事项5.1 使用 initializer_list 的陷阱std::vectorstd::vectorint vec; // 错误emplace_back 无法推导 initializer_list // vec.emplace_back({1, 2, 3}); // 编译错误 // 正确显式传递 vec.emplace_back(std::initializer_listint{1, 2, 3}); // 或直接用 push_back vec.push_back({1, 2, 3}); // push_back 可以直接用大括号5.2 移动构造是 noexcept 时 push_back 也高效vec.push_back(std::move(existingObject)); // 移动构造开销小 vec.push_back(hello); // 有临时对象 vec.emplace_back(hello); // 无临时对象更优六、选择决策流程简单类型int,double...复杂类型string,自定义类已有对象vec.push_back(obj)移动已有对象vec.push_back(std::move(obj))构造参数已知vec.emplace_back(args...)向 vector 末尾添加元素元素类型?两者均可性能几乎无差异添加方式?使用 push_back左值拷贝右值移动使用 push_back调用移动构造使用 emplace_back原地构造最优性能注意: 避免 initializer_list 歧义避免引用悬空七、总结push_back和emplace_back的核心区别可以归纳为本质差异push_back接收已构造的对象通过拷贝或移动到容器emplace_back接收构造参数在容器内原地构造对象。前者产生临时对象后者不产生。性能影响当构造参数可以直接传递给构造函数时emplace_back省略了临时对象的构造和析构性能更优。对于已存在的对象push_back配合std::move同样高效。使用建议构造新对象→emplace_back如vec.emplace_back(hello)、vec.emplace_back(10, x)添加已有对象→push_back如vec.push_back(existingObj)或vec.push_back(std::move(obj))简单类型→ 两者均可无显著差异初始化列表→push_back更方便emplace_back需显式指定类型总结emplace_back不是push_back的完全替代两者各有最佳适用场景。在需要构造新对象时优先使用emplace_back在已有对象时使用push_back。理解它们的底层差异才能在合适的场景做出性能最优的选择。