【C++ 性能提升技巧】C++ 的引用、值类型、构造函数、移动语义与 noexcept 特性,可扩容的容器

文章目录

推荐一个零声教育学习教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK等技术内容,点击立即学习: https://github.com/0voice 链接

前言

只要稍加利用便能提升 C++ 中拥有随机访问和自动扩容功能的 STL 容器的运行性能。

值类别有三类:左值、右值、将亡值

  • 左值是有变量名身份的对象(字面值存储到栈或者堆上)
  • 右值是只有字面值没有变量储存其字面数据
  • 将亡值与左值没有本质区别,只是具备触发移动语义的条件(搭配赋值运算符)

左值引用:同一份资源数据有两个名字,这两个名字对期资源数据有同样的修改权。

我们在写一个函数的时候,可以传入左值避免了复制,起到的效果跟 C 语言的函数传入指针是一样的。

cpp 复制代码
std::string A;
std::string& B = A;
修改 B 可等价于修改 A

右值引用:与左值引用并无区别,右值引用实际上是带着 "将要在原对象取消" 标签的左值引用。

cpp 复制代码
std::move(A)

右值引用之所以跟左值引用分开来,完全是因为 C++ 里面的移动语义构造函数的使用而开路,拷贝函数是不够用的,某种情况下是很容易导致对象内部的指针变量因为其拷贝源对象的释放而被提前释放,进而导致了指针悬空。比如说 STL 容器库里面的 erase 方法就是删除容器里面某个对象,这是有可能导致段错误的。

故而,需要常备移动构造函数。左值与将亡值的区分就是有必要的。移动构造函数,使用的正是 "将亡值" 而非 "右值",目的是 "引用原对象并且使得其独占该资源"。

深拷贝与浅拷贝构造函数

拷贝构造函数不止有深拷贝,还有浅拷贝,全看程序员自己的实现方式。为了提升拷贝构造函数的性能,拷贝构造函数的传入参数都是传入 "左值引用" 的。传入左值引用,并不意味着就是 "浅拷贝",完全就是看程序员的设计水平。

浅拷贝构造函数是很危险的,因为浅拷贝构造函数是共享了资源数据,只要某一个对象被删,那其他进行了浅拷贝构造函数的对象因为某个原因错误释放了该共享资源,那么一脉复制的对象都无法正常使用。故而安全的做法是,深度拷贝,但是深度拷贝的性能是很低的。

操作 新对象? 内存复制? 所有权转移? 典型用途
引用传递 ❌ 否 ❌ 否 ❌ 否 避免拷贝,修改原对象
浅拷贝 ✅ 是 ❌ 否 ❌ 否 通常错误,除非引用计数
深拷贝 ✅ 是 ✅ 是 ❌ 否 需要独立副本时
移动构造 ✅ 是 ❌ 否 ✅ 是 高效转移资源

noexcept 与移动构造函数

什么是移动构造函数?

我的理解:其函数体内所有的赋值操作,都得使用 std::move(对象) 来进行赋值,必要的时候,还需要让源对象置为空,比如指针置为 nullptr

正如前面所说,std::move(对象) 本质上还是一个左值,只是它带有 "将亡值" 的标签,可是一旦他与赋值运算符关联起来,那情况就不简单了。自定义对象的移动赋值运算符,不一定具有移动复制语义(取决于程序员),但是官方标准库的类型的移动赋值运算符一定具有移动赋值语言,即 "浅拷贝 + 源对象不确定状态"。

cpp 复制代码
B = std::move(A);  触发移动语义

移动之后,源对象尽可能置空。源对象在发生了移动赋值之后,很可能不可以再用了。故而,移动构造极其适合 STL 的容器扩容。

概念 正确理解 常见误解
移动赋值 转移所有权,源对象状态未指定但有效 源对象被清空
浅拷贝 共享资源,两个对象都有效 与移动相同
std::move 转为右值引用,提示"可被移动" 执行移动操作
置空源对象 实现者的选择,非语言要求 语言强制要求

在 C++ 规范里面,移动构造函数、移动赋值运算符、析构函数、swap 函数(包括自定义的)都建议使用 noexcept 关键字。noexcept 会让异常逃逸(因为程序员承诺了该方法、函数不会出错),进而让整个程序 coredump。但是,如果我们确定了程序不会出错呢?用 noexcept 会提升程序运行的速度,它的速度提升来源于 "移动构造函数的浅拷贝 + 隔绝源对象的类型安全"。

对于 STL 容器,比如 vector<T>,如果类型 T 的移动构造函数是带有 noexcept 的。那么 vector 容器在动态数组扩容的过程中,就会优先选择使用 "移动构造函数" 而非拷贝构造函数(通常是深拷贝,浅拷贝构造是不安全的),从而加快了程序运行。标准库中几乎所有的移动构造函数、运算符都是使用了 noexcept,移动构造函数出了名是快的(程序员得要亲自去做移动构造编程,才会体现出它的作用)。

综上,noexcept 是一种超级激进的编译策略。

C++ STL 中可自动扩容且支持随机访问的容器(noexcept 可大展身手)

有两个候选者。

第一个是 vector 容器

cpp 复制代码
#include <vector>

// 自动扩容:当容量不足时自动重新分配内存
std::vector<int> vec;
vec.push_back(1);  // 可能触发扩容
vec.push_back(2);  // 可能触发扩容
vec.push_back(3);  // 可能触发扩容

// 随机访问:O(1) 时间复杂度
int x = vec[0];      // 下标访问
int y = vec.at(1);   // 带边界检查的访问

其图示如下

第二个是 deque 双端队列(更灵活一点)

cpp 复制代码
#include <deque>

// 自动扩容:分段连续存储,自动管理分段
std::deque<int> deq;
deq.push_back(1);    // 后端插入
deq.push_front(0);   // 前端插入

// 随机访问:O(1) 时间复杂度(但比 vector 稍慢)
int a = deq[0];
int b = deq.at(1);

其图示如下

其余,几乎跟 vector 一样

特性 std::vector std::deque
内存布局 单一连续数组 分段连续数组(多个固定大小块)
自动扩容 ✅ 重新分配整个数组 ✅ 添加新内存块
随机访问 ✅ O(1)(最快) ✅ O(1)(稍慢)
前端插入 ❌ O(n)(需要移动所有元素) ✅ O(1)
后端插入 ✅ 平摊 O(1) ✅ O(1)
中间插入 ❌ O(n) ❌ O(n)
迭代器失效 扩容时全部失效 只在操作位置失效
内存使用 更紧凑 有额外开销(块指针)
缓存友好 ✅ 非常好 ⚠️ 一般

noexcept 使用案例

运行时间测量

cpp 复制代码
class Timer {
private:
    std::chrono::high_resolution_clock::time_point start;
    std::string name;
    
public:
    Timer(const std::string& n = "") : name(n) {
        start = std::chrono::high_resolution_clock::now();
    }
    
    ~Timer() {
        auto end = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
        std::cout << std::setw(30) << name << ": " 
                  << std::setw(10) << duration.count() << " μs" << std::endl;
    }
    
    size_t elapsed() const {
        auto now = std::chrono::high_resolution_clock::now();
        return std::chrono::duration_cast<std::chrono::microseconds>(now - start).count();
    }
};

移动赋值、构造不带 noexcept 的版本

cpp 复制代码
// ==================== 测试类定义 ====================
class Widget {
private:
    int* data;
    size_t size;
    int id;  // 添加一个唯一ID用于哈希和比较
    
public:
    // 构造函数
    explicit Widget(size_t s = 100, int i = 0) : size(s), id(i) {
        data = new int[size];
        std::fill(data, data + size, 42);
    }
    
    // 拷贝构造函数
    Widget(const Widget& other) : size(other.size), id(other.id) {
        data = new int[size];
        std::copy(other.data, other.data + size, data);
    }
    
    // 拷贝赋值运算符
    Widget& operator=(const Widget& other) {
        if (this != &other) {
            delete[] data;
            size = other.size;
            id = other.id;
            data = new int[size];
            std::copy(other.data, other.data + size, data);
        }
        return *this;
    }
    
    // 版本1:没有 noexcept 的移动构造函数
    Widget(Widget&& other) /* 没有 noexcept */ 
        : data(other.data), size(other.size), id(other.id) {
        other.data = nullptr;
        other.size = 0;
        other.id = -1;
    }
    
    // 版本1:没有 noexcept 的移动赋值运算符
    Widget& operator=(Widget&& other) /* 没有 noexcept */ {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            id = other.id;
            other.data = nullptr;
            other.size = 0;
            other.id = -1;
        }
        return *this;
    }
    
    // 比较运算符(用于 set)
    bool operator<(const Widget& other) const {
        return id < other.id;
    }
    
    bool operator==(const Widget& other) const {
        return id == other.id;
    }
    
    ~Widget() {
        delete[] data;
    }
    
    int getID() const { return id; }
    size_t getSize() const { return size; }
};

移动赋值、构造带上 noexcept 的版本

cpp 复制代码
// ==================== noexcept 版本 ====================
class WidgetNoExcept {
private:
    int* data;
    size_t size;
    int id;
    
public:
    explicit WidgetNoExcept(size_t s = 100, int i = 0) : size(s), id(i) {
        data = new int[size];
        std::fill(data, data + size, 42);
    }
    
    WidgetNoExcept(const WidgetNoExcept& other) : size(other.size), id(other.id) {
        data = new int[size];
        std::copy(other.data, other.data + size, data);
    }
    
    WidgetNoExcept& operator=(const WidgetNoExcept& other) {
        if (this != &other) {
            delete[] data;
            size = other.size;
            id = other.id;
            data = new int[size];
            std::copy(other.data, other.data + size, data);
        }
        return *this;
    }
    
    // 关键区别:添加 noexcept
    WidgetNoExcept(WidgetNoExcept&& other) noexcept 
        : data(other.data), size(other.size), id(other.id) {
        other.data = nullptr;
        other.size = 0;
        other.id = -1;
    }
    
    WidgetNoExcept& operator=(WidgetNoExcept&& other) noexcept {
        if (this != &other) {
            delete[] data;
            data = other.data;
            size = other.size;
            id = other.id;
            other.data = nullptr;
            other.size = 0;
            other.id = -1;
        }
        return *this;
    }
    
    // 比较运算符
    bool operator<(const WidgetNoExcept& other) const {
        return id < other.id;
    }
    
    bool operator==(const WidgetNoExcept& other) const {
        return id == other.id;
    }
    
    ~WidgetNoExcept() {
        delete[] data;
    }
    
    int getID() const { return id; }
    size_t getSize() const { return size; }
};

测试内容是不断的扩容,中间会经历不同的运行。使用前面定义的定时器(RAII 风格,退出作用域时完成计数)

cpp 复制代码
// 测试 vector 扩容时的性能差异
template<typename T>
void test_vector_expand(size_t element_count = 50000) {
    std::vector<T> vec;
    vec.reserve(10);  // 故意设置小容量,强制多次扩容
    
    Timer timer("vector emplace_back");
    static int id_counter = 0;
    for (size_t i = 0; i < element_count; ++i) {
        vec.emplace_back(100, id_counter++);  // 使用不同的ID
    }
}

// 测试 deque 操作
template<typename T>
void test_deque(size_t element_count = 25000) {
    std::deque<T> deq;
    
    Timer timer("deque push_back/push_front");
    static int id_counter = 0;
    for (size_t i = 0; i < element_count; ++i) {
        deq.push_back(T(100, id_counter++));
        deq.push_front(T(100, id_counter++));
    }
}

测试主函数

cpp 复制代码
// ==================== 主测试函数 ====================
void run_focused_tests() {
    const size_t TEST_SIZE = 20000;
    
    std::cout << "\n==================== noexcept 性能对比测试 ====================\n";
    std::cout << "测试目标:验证 noexcept 对 STL 容器性能的影响\n";
    std::cout << "测试规模:每个测试 " << TEST_SIZE << " 个元素\n\n";
    
    // 只测试最重要的几个场景
    std::cout << "1. vector 扩容测试(最受影响):\n";
    {
        Timer total("总时间 - vector 扩容");
        test_vector_expand<Widget>(TEST_SIZE);
        std::cout << "这是移动语义没有 noexcept 的版本" << std::endl;
        test_vector_expand<WidgetNoExcept>(TEST_SIZE);
        std::cout << "这是移动语义有 noexcept 的版本" << std::endl;
    }
    
    
    std::cout << "\n2. deque 操作测试:\n";
    {
        Timer total("总时间 - deque 操作");
        test_deque<Widget>(TEST_SIZE / 4);
        std::cout << "这是移动语义没有 noexcept 的版本" << std::endl;
        test_deque<WidgetNoExcept>(TEST_SIZE / 4);
        std::cout << "这是移动语义有 noexcept 的版本" << std::endl;
    }
}

主函数(程序入口)

cpp 复制代码
int main() {
    // 设置随机种子
    srand(static_cast<unsigned>(time(nullptr)));
    
    std::cout << "C++ noexcept 性能测试\n";
    std::cout << "========================================\n";
    
    run_focused_tests();

    return 0;
}

运行

bash 复制代码
qiming@k8s-master1:~/share/mycpp_work/c++20-trait/meta-progame$ g++ -std=c++20 -O2 noexceptTest.cpp -o test
qiming@k8s-master1:~/share/mycpp_work/c++20-trait/meta-progame$ ./test 
C++ noexcept 性能测试 
========================================

==================== noexcept 性能对比测试 ====================
测试目标:验证 noexcept 对 STL 容器性能的影响
测试规模:每个测试 20000 个元素

1. vector 扩容测试(最受影响):
           vector emplace_back:      11444 μs
                 这是移动语义没有 noexcept 的版本
           vector emplace_back:       6429 μs
                    这是移动语义有 noexcept 的版本
     总时间 - vector 扩容:      21673 μs

2. deque 操作测试:
    deque push_back/push_front:       3416 μs
                 这是移动语义没有 noexcept 的版本
    deque push_back/push_front:       1019 μs
                    这是移动语义有 noexcept 的版本
      总时间 - deque 操作:       7021 μs
相关推荐
故以往之不谏2 小时前
函数--值传递
开发语言·数据结构·c++·算法·学习方法
卢锡荣2 小时前
Type-c OTG数据与充电如何进行交互使用应用讲解
c语言·开发语言·计算机外设·电脑·音视频
A懿轩A2 小时前
【Java 基础编程】Java 变量与八大基本数据类型详解:从声明到类型转换,零基础也能看懂
java·开发语言·python
2301_811232982 小时前
低延迟系统C++优化
开发语言·c++·算法
我能坚持多久2 小时前
D20—C语言文件操作详解:从基础到高级应用
c语言·开发语言
txinyu的博客3 小时前
解析muduo源码之 ThreadLocal.h
c++
橘子师兄3 小时前
C++AI大模型接入SDK—ChatSDK封装
开发语言·c++·人工智能·后端
上天_去_做颗惺星 EVE_BLUE3 小时前
Docker高效使用指南:从基础到实战模板
开发语言·ubuntu·docker·容器·mac·虚拟环境