C++11 工程级应用 10:减少拷贝,让容器跑得更快

C++11 工程级应用 10:减少拷贝,让容器跑得更快

  • [Bilibili 同步视频](#Bilibili 同步视频)
  • [一、别再盲目用 push_back!emplace_back 帮你省下多余拷贝💡](#一、别再盲目用 push_back!emplace_back 帮你省下多余拷贝💡)
  • [二、告别红黑树排序开销:C++11 无序哈希容器🚀](#二、告别红黑树排序开销:C++11 无序哈希容器🚀)
    • [自定义 Key 使用 unordered_map 示例](#自定义 Key 使用 unordered_map 示例)
  • [三、小结:C++11 性能优化核心要点📌](#三、小结:C++11 性能优化核心要点📌)

导读:C++11 不仅仅是新增语法糖,更是一套实打实的性能工具箱。很多人写容器还在惯性使用push_backmap,殊不知换几个 API,就能大幅减少内存拷贝、移动开销,让程序效率再上一个台阶。今天我们就聊聊 C++11 中两个非常实用的性能优化点:emplace系列接口与无序哈希容器。

Bilibili 同步视频

C++11 工程级应用 10:减少拷贝,让容器跑得更快

一、别再盲目用 push_back!emplace_back 帮你省下多余拷贝💡

在 C++ 的容器开发中,对象拷贝、对象移动是隐形的性能杀手。很多时候我们向vector存放自定义对象,会写出这样的代码:

cpp 复制代码
v.push_back(Complicated(1, 2.0, "demo"));

这段代码看似简洁,背后却发生了不少看不见的操作:先在栈上构造临时对象,再把这个临时对象移动拷贝到 vector 的内存空间里。一来一回,产生额外的内存开销,如果对象内部持有字符串、堆内存,频繁调用时性能损耗会被持续放大。

C++11 带来了emplace_back,它的核心思想是就地构造对象 。不需要我们提前造出一个完整对象再传入容器,直接把构造对象所需要的参数传给emplace_back,容器会直接在它自己管理的内存空间内调用对象的构造函数,原地生成实例,省去临时对象的拷贝与移动。

💬小提示:除了emplace_back,标准库几乎所有可变容器都配套了同类接口:emplaceemplace_hintemplace_frontemplace_after。唯独std::array除外,因为它大小固定,本身就不支持插入元素。

最简单的使用示例

cpp 复制代码
#include <vector>
#include <iostream>

struct A
{
    int x;
    double y;
    A(int a, double b) : x(a), y(b) {}
};

int main()
{
    std::vector<A> v;
    // 直接传入构造函数参数,容器内部原地构造A对象
    v.emplace_back(1, 2.0);
    std::cout << v.size() << std::endl;
    return 0;
}

这里直接传入1,2.0两个构造参数,vector 的内存里直接诞生 A 实例。如果我们把结构体 A 的双参数构造函数注释掉,编译器会直接报错,提醒你没有匹配的构造重载。**emplace_back**依赖对象对应的构造函数,没有对应构造函数就无法使用

眼见为实:push_back vs emplace_back 对比实验

我们写一个带日志的复杂结构体,打印构造、移动的调用记录,直观看到两者之间的开销差距:

cpp 复制代码
#include <vector>
#include <map>
#include <string>
#include <iostream>

struct Complicated
{
    int year;
    double country;
    std::string name;

    Complicated(int a, double b, std::string c)
        : year(a), country(b), name(std::move(c))
    {
        std::cout << "✅ is constructed" << std::endl;
    }

    Complicated(const Complicated& other)
        : year(other.year), country(other.country), name(other.name)
    {
        std::cout << "📋 is copied" << std::endl;
    }

    Complicated(Complicated&& other) noexcept
        : year(other.year), country(other.country), name(std::move(other.name))
    {
        std::cout << "🚀 is moved" << std::endl;
    }
};

int main()
{
    std::map<int, Complicated> m;
    int anInt = 4;
    double aDouble = 5.0;
    std::string aString = "C++";

    std::cout << "------ map::insert ------" << std::endl;
    m.insert(std::make_pair(4, Complicated(anInt, aDouble, aString)));

    std::cout << "n------ map::emplace ------" << std::endl;
    m.emplace(4, Complicated(anInt, aDouble, aString));

    std::vector<Complicated> v;
    std::cout << "n------ vector::emplace_back ------" << std::endl;
    v.emplace_back(anInt, aDouble, aString);

    std::cout << "n------ vector::push_back ------" << std::endl;
    v.push_back(Complicated(anInt, aDouble, aString));

    return 0;
}

程序输出结果大致如下:

Plain 复制代码
------ map::insert ------
✅ is constructed
🚀 is moved
🚀 is moved

------ map::emplace ------
✅ is constructed
🚀 is moved

------ vector::emplace_back ------
✅ is constructed

------ vector::push_back ------
✅ is constructed
🚀 is moved
🚀 is moved

结果解读📝

  1. map::insert:先构造临时对象,发生两次移动操作

  2. map::emplace:减少一次移动,只剩一次移动;

  3. vector::emplace_back仅执行构造,没有任何拷贝、移动,对象直接诞生在 vector 的内存中;

  4. vector::push_back:临时对象构造完成后触发两次移动。

⚠️重要提醒:不要把emplace_back当成push_back的完全替代品!

当你的类型没有对应的构造函数重载时,emplace 系列接口无法使用,这时只能老老实实使用push_back传入已经构造完成的对象。

二、告别红黑树排序开销:C++11 无序哈希容器🚀

提到键值容器,很多同学第一反应就是std::mapstd::set。这一组有序容器底层基于红黑树实现,元素插入的时候会自动做排序维护。

排序是有代价的,每次插入、查找都要维护树结构,当数据量很大的时候,性能压力就上来了。

C++11 带来了一套无序容器:

  • unordered_map / unordered_multimap

  • unordered_set / unordered_multiset

底层实现是哈希表(Hash Table),不再维护元素的大小顺序,依靠哈希函数完成定位。平均情况下插入、查找可以达到 O (1) 时间复杂度,性能会比红黑树容器优秀很多。

容器 底层结构 特点
map/set 红黑树 元素有序;插入查询 O (logN)
unordered_map/unordered_set 哈希表 元素无序;平均 O (1) 读写,最坏 O (N)

注意坑❗:

基础内置类型(int、std::string 等)可以开箱即用。但如果使用自定义结构体作为 key,我们必须自己提供两件东西:

  1. 哈希函数:告诉容器如何把 key 转换成哈希值

  2. 相等比较函数:解决哈希冲突时判断两个 key 是否真正相等

自定义 Key 使用 unordered_map 示例

cpp 复制代码
#include <unordered_map>
#include <string>
#include <iostream>

struct Key {
    std::string first;
    std::string second;
};

// 自定义哈希函数
struct KeyHash
{
    std::size_t operator()(const Key& k) const
    {
        // 简单的哈希组合算法
        return std::hash<std::string>()(k.first)
               ^ (std::hash<std::string>()(k.second) << 1);
    }
};

// 自定义相等判断,用于哈希冲突比对
struct KeyEqual
{
    bool operator()(const Key& lhs, const Key& rhs) const
    {
        return lhs.first == rhs.first && lhs.second == rhs.second;
    }
};

int main()
{
    // 模板参数依次:key类型,value类型,哈希仿函数,相等比较仿函数
    std::unordered_map<Key, std::string, KeyHash, KeyEqual> myMap =
    {
        {{"John", "Doe"}, "example"},
        {{"Mary", "Sue"}, "another"}
    };

    for(auto& item : myMap)
    {
        std::cout << item.key.first << "," << item.key.second
                  << " -> " << item.second << std::endl;
    }
    return 0;
}

小 Tips:无序容器虽然快,但元素是乱序的。如果你业务逻辑必须要求元素按 key 有序排列,那就不能替换成 unordered 系列,还是要继续使用 map/set。

三、小结:C++11 性能优化核心要点📌

C++11 不只是语法革新,更是面向性能的一次升级,我们可以简单归纳几个重点:

  1. 右值引用:通过转移对象资源所有权,避免不必要内存拷贝;

  2. std::forward 完美转发:保留参数原始值类别,转发到正确重载函数;

  3. emplace 系列接口emplace_backemplace等原地构造对象,消灭大量临时对象拷贝移动,优先选用;

  4. unordered 无序容器:哈希表实现,平均读写性能优于红黑树容器;自定义 key 需要手动提供哈希函数和相等判断。

日常开发中养成好习惯:向容器放对象优先试一试emplace_back;不需要有序就优先选用哈希容器。积少成多,一点点减少拷贝开销,你的 C++ 程序性能就会悄悄上一个台阶。

拓展思考:你平时写代码的时候,什么场景下会坚持用 push_back 而不是 emplace_back?欢迎在评论区一起交流~

相关推荐
广州浮点FLOATLIC1 小时前
ANSYS许可证排队时怎样确认是模块还是并发数不足
运维·服务器·网络
玖玥拾1 小时前
Lua 基础语法(八) Unity Huatuo (HybridCLR)
开发语言·unity·游戏引擎·lua
小此方1 小时前
Linux网络(九):从 URL 到 HTTP 报文:彻底搞懂 HTTP 请求与响应,并手写一个简单的 HTTP 服务器
linux·服务器·网络
前端世界1 小时前
Linux服务器实战:日志越来越大怎么办?用Logrotate+DNS解决日志管理与内网访问问题
linux·服务器·chrome
Wang's Blog1 小时前
Java框架快速入门: Spring Security+OAuth2之环境配置与多环境部署
java·开发语言·spring
小此方1 小时前
Linux网络(十):HTTP请求是如何被服务器解析的?从请求报文、路径处理到响应状态码与异常处理
linux·服务器·网络
小灰灰搞电子1 小时前
Rust suppaftp 库详解:基于 FTP 客户端实战指南
开发语言·后端·rust
狂人开飞机1 小时前
10、相机系统
游戏引擎·godot
编码浪子1 小时前
Rust unsafe 与 FFI 互操作生产级实战:把危险关进笼子的四道闸门
开发语言·后端·rust