前言

C++11 是 C++ 语言发展史上里程碑式的版本,作为继 C++98 之后第二个主要版本,它终结了长达 8 年的版本空窗期,彻底重构了 C++ 的编程范式,也为后续 C++14/17/20/23 的迭代奠定了核心基础。

很多开发者对 C++11 的认知停留在autolambda这些表层语法上,但实际上,C++11 的核心价值在于解决了 C++98 长期存在的性能痛点、语法不统一、泛型能力不足等问题,真正让 C++ 迈入了 “现代 C++” 的时代。

本文将从原理出发,结合实战代码,系统拆解 C++11 的核心特性,帮你彻底吃透这些改变 C++ 编程方式的新能力。

一、统一初始化:告别混乱的初始化方式

1.1 C++98 的初始化困境

在 C++98 中,初始化规则极其混乱,不同类型的初始化方式完全不统一:

  • 普通数组、POD 结构体可以使用花括号{}进行聚合初始化
  • 内置类型、自定义类类型只能用()=初始化
  • STL 容器想要批量初始化,只能先创建空对象,再循环插入数据
#include <iostream>
#include <vector>
using namespace std;

// POD结构体
struct Point {
    int x;
    int y;
};

class Date {
public:
    Date(int year, int month, int day) 
        : _year(year), _month(month), _day(day) {}
private:
    int _year;
    int _month;
    int _day;
};

int main() {
    // C++98 支持的初始化方式
    int arr1[] = {1, 2, 3, 4, 5}; // 数组聚合初始化
    int arr2[5] = {0}; // 数组零初始化
    Point p = {1, 2}; // 结构体聚合初始化

    // C++98 不支持的方式(编译报错)
    // int a {10}; // 内置类型不能用{}
    // Date d {2025, 5, 1}; // 自定义类不能用{}
    // vector<int> v {1,2,3,4,5}; // 容器不能直接用{}批量初始化

    // C++98 容器只能这样初始化
    vector<int> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);

    return 0;
}

1.2 C++11 统一列表初始化

C++11 引入了列表初始化(Uniform Initialization),核心目标是用一套花括号{}规则,统一所有类型的初始化方式,无论是内置类型、自定义类、数组还是 STL 容器,都可以使用{}完成初始化。

核心规则:

  1. 所有类型都可以使用{}进行初始化,可省略赋值符=
  2. 列表初始化会进行严格的类型检查,禁止隐式窄化转换
  3. 对于自定义类型,底层会先通过{}内的参数构造临时对象,编译器会通过 RVO 优化为直接构造,无额外拷贝开销
#include <iostream>
#include <vector>
#include <map>
using namespace std;

struct Point {
    int x;
    int y;
};

class Date {
public:
    Date(int year = 1, int month = 1, int day = 1) 
        : _year(year), _month(month), _day(day) {
        cout << "Date构造函数调用" << endl;
    }
    Date(const Date& d) 
        : _year(d._year), _month(d._month), _day(d._day) {
        cout << "Date拷贝构造函数调用" << endl;
    }
private:
    int _year;
    int _month;
    int _day;
};

int main() {
    // 1. 内置类型初始化
    int a1 = {10}; // 带=
    int a2 {20};    // 省略=,C++11核心特性
    double d {3.14};

    // 禁止窄化转换(编译报错)
    // int num {3.14}; //  double -> int 窄化转换,编译不通过

    // 2. 结构体/数组初始化
    Point p1 = {1, 2};
    Point p2 {3, 4}; // 省略=
    int arr[] {1,2,3,4,5};

    // 3. 自定义类类型初始化
    Date d1 = {2025, 5, 1}; // 构造+拷贝构造,编译器优化为直接构造
    Date d2 {2025, 5, 2};    // 直接构造,无拷贝
    const Date& d3 = {2025, 5, 3}; // 引用绑定临时对象,延长生命周期

    // 4. STL容器初始化(核心便利场景)
    vector<int> v1 {1,2,3,4,5}; // 直接批量初始化
    map<string, int> dict {
        {"苹果", 5},
        {"香蕉", 3},
        {"橙子", 4}
    }; // 键值对容器直接初始化

    // 5. 容器插入时的便捷用法
    vector<Date> vd;
    vd.push_back({2025, 5, 1}); // 无需显式构造匿名对象,{}直接传参

    return 0;
}

运行代码可以发现,d1d2都只调用了构造函数,没有调用拷贝构造,这是编译器的返回值优化(RVO),把临时对象的构造和目标对象的构造合二为一了。

1.3 底层支撑:std::initializer_list

STL 容器能直接用{1,2,3,4,5}初始化,背后的核心就是 C++11 新增的std::initializer_list

核心原理

  • 当编译器遇到{x1,x2,x3...}这样的花括号列表时,会自动构造一个std::initializer_list<T>类型的临时对象,其中 T 是列表中元素的类型。
  • std::initializer_list本质是一个轻量级的代理对象,内部只包含两个指针,分别指向常量数组的首元素和尾后元素,数组本身存储在常量区 / 栈上,拷贝开销极低。
  • STL 容器都新增了接收std::initializer_list的构造函数和赋值运算符重载,因此支持花括号列表初始化。

下面我们模拟实现 vector 的initializer_list构造函数,彻底理解其底层逻辑:

#include <iostream>
#include <vector>
using namespace std;

// 模拟实现vector的initializer_list构造函数
template<class T>
class MyVector {
public:
    typedef T* iterator;

    MyVector() : _start(nullptr), _finish(nullptr), _endofstorage(nullptr) {}

    // 接收initializer_list的构造函数
    MyVector(initializer_list<T> il) 
        : _start(nullptr), _finish(nullptr), _endofstorage(nullptr) {
        // 遍历initializer_list,逐个插入元素
        for (const auto& e : il) {
            push_back(e);
        }
        cout << "MyVector initializer_list构造函数调用" << endl;
    }

    // 接收initializer_list的赋值运算符重载
    MyVector& operator=(initializer_list<T> il) {
        clear();
        for (const auto& e : il) {
            push_back(e);
        }
        return *this;
    }

    ~MyVector() {
        delete[] _start;
        _start = _finish = _endofstorage = nullptr;
    }

    void push_back(const T& val) {
        if (_finish == _endofstorage) {
            size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
            reserve(newcapacity);
        }
        *_finish = val;
        _finish++;
    }

    void reserve(size_t n) {
        if (n > capacity()) {
            T* tmp = new T[n];
            size_t oldsize = size();
            for (size_t i = 0; i < oldsize; i++) {
                tmp[i] = _start[i];
            }
            delete[] _start;
            _start = tmp;
            _finish = _start + oldsize;
            _endofstorage = _start + n;
        }
    }

    void clear() {
        _finish = _start;
    }

    size_t size() const { return _finish - _start; }
    size_t capacity() const { return _endofstorage - _start; }

    iterator begin() { return _start; }
    iterator end() { return _finish; }

private:
    T* _start;
    T* _finish;
    T* _endofstorage;
};

int main() {
    // 1. initializer_list基本用法
    initializer_list<int> il = {1,2,3,4,5};
    cout << "il的元素个数:" << il.size() << endl;
    // 迭代器遍历
    for (auto it = il.begin(); it != il.end(); it++) {
        cout << *it << " ";
    }
    cout << endl;

    // 2. 自定义vector使用initializer_list初始化
    MyVector<int> mv1 = {1,2,3,4,5};
    cout << "mv1的元素:";
    for (auto e : mv1) {
        cout << e << " ";
    }
    cout << endl;

    // 3. initializer_list赋值
    mv1 = {10,20,30,40,50};
    cout << "赋值后mv1的元素:";
    for (auto e : mv1) {
        cout << e << " ";
    }
    cout << endl;

    return 0;
}

二、C++11 的灵魂:右值引用与移动语义

这是 C++11 最核心的特性,没有之一,它从根本上解决了 C++ 长期以来的深拷贝性能损耗问题。

2.1 C++98 的性能痛点:无谓的深拷贝

在 C++98 中,对于带有堆内存资源的类(比如 string、vector),传值返回、值传递参数时,会触发拷贝构造函数,执行深拷贝。而很多时候,拷贝的源对象是临时对象,拷贝完成后就会被销毁,这就导致了 “申请堆空间 -> 拷贝数据 -> 释放源对象堆空间” 的无谓开销,尤其是当对象存储大量数据时,性能损耗极其严重。

#include <iostream>
#include <cstring>
using namespace std;

// C++98风格的string类,只有深拷贝构造
class MyString {
public:
    MyString(const char* str = "") {
        _size = strlen(str);
        _capacity = _size;
        _str = new char[_capacity + 1];
        strcpy(_str, str);
        cout << "MyString:构造函数" << endl;
    }

    // 深拷贝构造函数
    MyString(const MyString& s) {
        cout << "MyString:深拷贝构造函数" << endl;
        _size = s._size;
        _capacity = s._capacity;
        _str = new char[_capacity + 1];
        strcpy(_str, s._str);
    }

    // 深拷贝赋值运算符
    MyString& operator=(const MyString& s) {
        cout << "MyString:深拷贝赋值运算符" << endl;
        if (this != &s) {
            delete[] _str;
            _size = s._size;
            _capacity = s._capacity;
            _str = new char[_capacity + 1];
            strcpy(_str, s._str);
        }
        return *this;
    }

    ~MyString() {
        cout << "MyString:析构函数" << endl;
        delete[] _str;
        _str = nullptr;
    }

    // 字符串拼接
    MyString operator+(const MyString& s) const {
        MyString tmp;
        tmp._size = _size + s._size;
        tmp._capacity = tmp._size;
        tmp._str = new char[tmp._capacity + 1];
        strcpy(tmp._str, _str);
        strcat(tmp._str, s._str);
        return tmp;
    }

    const char* c_str() const { return _str; }

private:
    char* _str;
    size_t _size;
    size_t _capacity;
};

// 返回局部MyString对象
MyString GetString() {
    MyString s("hello ");
    return s; // 返回局部对象,会生成临时对象
}

int main() {
    MyString s1 = GetString(); // 用返回的临时对象拷贝构造s1
    MyString s2 = s1 + "world"; // 拼接返回临时对象,拷贝构造s2
    cout << "s1: " << s1.c_str() << endl;
    cout << "s2: " << s2.c_str() << endl;
    return 0;
}

在关闭编译器优化的情况下(g++ 编译时加-fno-elide-constructors),这段代码会发生多次深拷贝,每一次都要申请堆内存、拷贝数据。而这些被拷贝的源对象,都是马上要被销毁的临时对象,我们完全可以直接 “偷” 走它的资源,而不是重新拷贝 —— 这就是移动语义的核心思想。

2.2 左值与右值:核心区别是能否取地址

要理解右值引用,首先要搞清楚左值和右值的本质定义。

左值(lvalue):可以取地址、有持久生命周期的表达式。简单来说,能通过&运算符拿到地址的,就是左值。左值可以出现在赋值号的左边,也可以出现在右边。

  • 典型例子:变量名、解引用的指针、const 修饰的常量、数组元素、函数返回的左值引用

右值(rvalue):无法取地址、生命周期短暂的临时表达式。不能通过&运算符拿到地址,只能出现在赋值号的右边,不能出现在左边。

  • 典型例子:字面量常量、表达式计算结果、函数返回的传值对象、lambda 表达式

C++11 把右值进一步分为两类:

  • 纯右值(prvalue):字面量、表达式求值产生的临时对象,对应 C++98 中的右值概念
  • 将亡值(xvalue):即将被销毁、资源可以被转移的对象,比如std::move(左值)的返回值、函数返回的右值引用
#include <iostream>
#include <string>
using namespace std;

int main() {
    // 左值:可以取地址
    int a = 10;
    int* p = &a;
    const int b = 20;
    const int* pb = &b; // const左值也能取地址,也是左值
    string s("hello");
    char* pc = &s[0];

    // 以下都是左值,可以取地址
    cout << &a << endl;
    cout << &b << endl;
    cout << &s << endl;
    cout << p << endl;

    // 右值:无法取地址,编译报错
    // cout << &10 << endl; // 字面量10是右值
    // cout << &(a + 1) << endl; // 表达式结果是右值
    // cout << &(s + " world") << endl; // 临时string对象是右值

    return 0;
}

2.3 左值引用与右值引用

C++98 中的引用,在 C++11 中被称为左值引用,用Type&表示,只能给左值取别名;C++11 新增的右值引用,用Type&&表示,只能给右值取别名。

核心规则:

  1. 左值引用不能直接引用右值,但const 左值引用可以引用右值(C++98 就支持)
  2. 右值引用不能直接引用左值,但可以通过std::move(左值)将左值转为右值,从而绑定
  3. 右值引用绑定到右值后,会延长该右值的生命周期,和右值引用变量的生命周期一致
  4. 右值引用变量本身是左值(因为可以取地址),这是完美转发的核心前提
#include <iostream>
#include <string>
using namespace std;

int main() {
    int a = 10;
    double x = 1.1, y = 2.2;

    // 1. 左值引用:给左值取别名
    int& r1 = a;
    // int& r2 = 10; // 错误:左值引用不能直接引用右值
    const int& r3 = 10; // 正确:const左值引用可以引用右值

    // 2. 右值引用:给右值取别名
    int&& rr1 = 10; // 字面量右值
    double&& rr2 = x + y; // 表达式结果右值
    string&& rr3 = string("hello world"); // 临时对象右值
    // int&& rr4 = a; // 错误:右值引用不能直接引用左值

    // 3. std::move:将左值转为右值,让右值引用可以绑定
    int&& rr5 = move(a);
    cout << "move前a = " << a << ", rr5 = " << rr5 << endl;
    rr5 = 100;
    cout << "move后a = " << a << ", rr5 = " << rr5 << endl;
    // 注意:move只是类型转换,本身不移动资源,真正的资源移动是移动构造/赋值做的

    // 4. 生命周期延长
    // 临时对象string("test")的生命周期被延长,和rr4一致
    string&& rr4 = string("test") + " demo";
    rr4 += "!"; // 右值引用是非const的,可以修改引用的对象
    cout << rr4 << endl;

    const string& r4 = string("test") + " demo"; // const左值引用也能延长生命周期,但不能修改
    // r4 += "!"; // 错误:const引用不能修改

    // 5. 关键:右值引用变量本身是左值
    cout << &rr1 << endl; // 可以取地址,说明rr1是左值
    int& r6 = rr1; // 正确:左值引用可以绑定rr1(左值)
    // int&& rr6 = rr1; // 错误:右值引用不能绑定左值rr1
    int&& rr6 = move(rr1); // 正确:move转为右值

    return 0;
}

2.4 移动构造与移动赋值:资源窃取,告别深拷贝

有了右值引用,我们就可以实现移动构造函数移动赋值运算符重载,它们的核心是:接收右值引用参数,直接 “窃取” 源对象的资源,而不是重新申请内存拷贝数据,源对象最后被置为空,不会释放被窃取的资源。

移动构造的核心规则:

  • 第一个参数必须是当前类类型的右值引用(Type&&),其他参数必须有默认值
  • 不申请新的堆内存,只转移源对象的资源指针
  • 源对象的指针要置空,避免析构时释放被转移的资源
  • 对于内置类型成员,直接按字节拷贝即可

现在给之前的 MyString 类加上移动构造和移动赋值,对比性能差异:

#include <iostream>
#include <cstring>
using namespace std;

class MyString {
public:
    MyString(const char* str = "") {
        _size = strlen(str);
        _capacity = _size;
        _str = new char[_capacity + 1];
        strcpy(_str, str);
        cout << "MyString:构造函数" << endl;
    }

    // 深拷贝构造函数
    MyString(const MyString& s) {
        cout << "MyString:深拷贝构造函数" << endl;
        _size = s._size;
        _capacity = s._capacity;
        _str = new char[_capacity + 1];
        strcpy(_str, s._str);
    }

    // 移动构造函数
    MyString(MyString&& s) noexcept {
        cout << "MyString:移动构造函数" << endl;
        // 直接窃取源对象的资源,无需深拷贝
        _str = s._str;
        _size = s._size;
        _capacity = s._capacity;

        // 源对象置空,避免析构时释放资源
        s._str = nullptr;
        s._size = 0;
        s._capacity = 0;
    }

    // 深拷贝赋值运算符
    MyString& operator=(const MyString& s) {
        cout << "MyString:深拷贝赋值运算符" << endl;
        if (this != &s) {
            delete[] _str;
            _size = s._size;
            _capacity = s._capacity;
            _str = new char[_capacity + 1];
            strcpy(_str, s._str);
        }
        return *this;
    }

    // 移动赋值运算符
    MyString& operator=(MyString&& s) noexcept {
        cout << "MyString:移动赋值运算符" << endl;
        if (this != &s) {
            // 先释放当前对象的资源
            delete[] _str;

            // 窃取源对象资源
            _str = s._str;
            _size = s._size;
            _capacity = s._capacity;

            // 源对象置空
            s._str = nullptr;
            s._size = 0;
            s._capacity = 0;
        }
        return *this;
    }

    ~MyString() {
        cout << "MyString:析构函数" << endl;
        delete[] _str; // 源对象_str为nullptr,delete空指针无风险
        _str = nullptr;
    }

    MyString operator+(const MyString& s) const {
        MyString tmp;
        tmp._size = _size + s._size;
        tmp._capacity = tmp._size;
        tmp._str = new char[tmp._capacity + 1];
        strcpy(tmp._str, _str);
        strcat(tmp._str, s._str);
        return tmp;
    }

    const char* c_str() const { return _str; }

private:
    char* _str;
    size_t _size;
    size_t _capacity;
};

MyString GetString() {
    MyString s("hello ");
    return s;
}

int main() {
    cout << "===== 场景1:用临时对象构造新对象 =====" << endl;
    MyString s1 = GetString(); // 临时对象是右值,调用移动构造
    cout << "s1: " << s1.c_str() << endl;

    cout << "\n===== 场景2:用临时对象赋值 =====" << endl;
    MyString s2;
    s2 = GetString(); // 临时对象是右值,调用移动赋值
    cout << "s2: " << s2.c_str() << endl;

    cout << "\n===== 场景3:move左值,触发移动 =====" << endl;
    MyString s3("world");
    MyString s4 = move(s3); // s3被转为右值,调用移动构造
    cout << "s4: " << s4.c_str() << endl;
    // 注意:s3的资源已经被转移,此时s3是无效状态,不能再使用

    cout << "\n===== 场景4:字符串拼接 =====" << endl;
    MyString s5 = s1 + s4; // 拼接返回临时对象,调用移动构造
    cout << "s5: " << s5.c_str() << endl;

    cout << "\n===== 主函数结束,对象析构 =====" << endl;
    return 0;
}

在关闭编译器优化的情况下,这段代码中原本的深拷贝都被替换成了移动构造 / 赋值。移动操作没有申请堆内存,只是转移了指针,开销和拷贝内置类型差不多,性能提升极其显著。

2.5 引用折叠与万能引用

在模板编程中,我们会遇到这样的情况:模板参数是T&&,但它既能接收左值,也能接收右值,这就是万能引用(Universal Reference),而支撑它的底层规则就是引用折叠

引用折叠规则:C++ 不允许直接定义 “引用的引用”,但在模板推导、typedef/using 中会间接产生,此时编译器会按照以下规则折叠:

  • 右值引用的右值引用(T&& &&),折叠为右值引用T&&
  • 其他所有组合(T& &T& &&T&& &),全部折叠为左值引用T&

万能引用:当模板参数 T 处于推导阶段时,T&&就是万能引用,它会根据传入的实参类型自动推导:

  • 如果传入的是左值,T 会被推导为Type&,结合引用折叠,最终参数类型是Type& && -> Type&(左值引用)
  • 如果传入的是右值,T 会被推导为Type,最终参数类型是Type&&(右值引用)
#include <iostream>
using namespace std;

// 万能引用模板
template <typename T>
void Func(T&& t) {
    cout << "万能引用调用" << endl;
}

// 普通右值引用,只能接收右值
void Test(int&& t) {
    cout << "普通右值引用调用" << endl;
}

int main() {
    int a = 10;
    const int b = 20;

    // 万能引用:既能接收左值,也能接收右值
    Func(a); // 传入左值,T推导为int&,参数类型int&
    Func(b); // 传入const左值,T推导为const int&,参数类型const int&
    Func(10); // 传入右值,T推导为int,参数类型int&&
    Func(move(a)); // 传入右值,T推导为int,参数类型int&&

    // 普通右值引用:只能接收右值
    Test(10); // 正确
    // Test(a); // 错误:不能接收左值

    return 0;
}

2.6 完美转发:保持值属性的传递

万能引用解决了参数类型的通用接收问题,但还有一个坑:右值引用变量本身是左值。这就导致,当我们把万能引用的参数传递给下一层函数时,它的右值属性会丢失,永远只会匹配左值版本的重载函数。

C++11 提供了std::forward<T>()函数来实现完美转发,它会根据模板参数 T 的类型,保持参数的原始值属性:

  • 如果 T 是左值引用类型,参数会被转为左值
  • 如果 T 是值类型,参数会被转为右值
#include <iostream>
#include <utility> // forward头文件
using namespace std;

void Fun(int& x) { cout << "左值引用重载" << endl; }
void Fun(const int& x) { cout << "const左值引用重载" << endl; }
void Fun(int&& x) { cout << "右值引用重载" << endl; }
void Fun(const int&& x) { cout << "const右值引用重载" << endl; }

template <typename T>
void Transmit(T&& t) {
    Fun(forward<T>(t)); // 完美转发,保持t的原始值属性
}

int main() {
    int a = 10;
    const int b = 20;

    Transmit(a); // 左值,调用左值版本
    Transmit(b); // const左值,调用const左值版本
    Transmit(10); // 右值,调用右值版本
    Transmit(move(a)); // 右值,调用右值版本

    return 0;
}

完美转发在 STL 的 emplace 系列接口、线程库、绑定器中都有大量应用,是泛型编程中不可或缺的能力。

三、可变参数模板:让模板支持任意个数参数

C++98 的模板只能固定参数个数和类型,C++11 引入的可变参数模板(Variadic Templates),让模板可以接收任意个数、任意类型的参数,是 C++11 泛型能力的重大升级,也是 STL 中 tuple、emplace 系列接口的底层支撑。

3.1 基本语法

可变参数模板的核心是参数包,分为两种:

  • 模板参数包:typename... Args / class... Args,表示零个或多个模板类型参数
  • 函数参数包:Args... args,表示零个或多个函数参数

sizeof...(args)运算符可以计算参数包中参数的个数。

#include <iostream>
using namespace std;

// 空参数的终止函数
void Print() {
    cout << endl;
}

// 可变参数模板:递归展开参数包
template <typename T, typename... Args>
void Print(T first, Args... args) {
    cout << first << " "; // 处理第一个参数
    Print(args...); // 递归处理剩下的参数包
}

// 计算参数包个数
template <typename... Args>
void CountArgs(Args... args) {
    cout << "参数包中参数个数:" << sizeof...(args) << endl;
}

int main() {
    Print(1);
    Print(1, 2.5, "hello", string("world"));
    Print(10, 20, 30, 40, 50);
    Print(); // 空参数包

    CountArgs(1, 2.5, "hello");
    CountArgs();
    CountArgs(1,2,3,4,5,6,7,8,9,10);

    return 0;
}

3.2 参数包的展开方式

可变参数模板的核心是参数包的展开,主要有两种常用方式:

  1. 递归展开:每次提取参数包的第一个参数处理,然后递归处理剩下的参数,直到参数包为空,匹配终止函数。
  2. 逗号表达式展开:利用逗号表达式的执行顺序,在初始化列表中展开参数包,无需递归。
#include <iostream>
using namespace std;

// 辅助函数,处理单个参数
template <typename T>
void HandleArg(const T& arg) {
    cout << arg << " ";
}

// 逗号表达式展开参数包
template <typename... Args>
void Print(Args... args) {
    // 初始化列表+逗号表达式展开
    initializer_list<int> { (HandleArg(args), 0)... };
    cout << endl;
}

int main() {
    Print(1, 2.5, "hello", string("world"));
    Print(10, 20, 30, 40, 50);
    return 0;
}

3.3 实战应用:emplace 系列接口

C++11 给 STL 容器新增了 emplace 系列接口,比如emplace_backemplace,它们的底层就是可变参数模板 + 完美转发,相比 push_back/insert,性能更优。

emplace_back 和 push_back 的核心区别

  • push_back 接收容器元素类型的对象,只能传入已经构造好的对象,会触发拷贝构造或移动构造
  • emplace_back 接收构造元素对象的参数包,直接在容器的内存空间上构造对象,无需拷贝 / 移动,零开销
#include <iostream>
#include <string>
#include <utility>
using namespace std;

// 模拟list的节点
template <typename T>
struct ListNode {
    T _data;
    ListNode* _next;
    ListNode* _prev;

    // 普通构造
    ListNode(const T& data) : _data(data), _next(nullptr), _prev(nullptr) {
        cout << "ListNode:拷贝构造" << endl;
    }

    ListNode(T&& data) : _data(move(data)), _next(nullptr), _prev(nullptr) {
        cout << "ListNode:移动构造" << endl;
    }

    // 可变参数模板构造:接收构造T的参数包,直接构造_data
    template <typename... Args>
    ListNode(Args&&... args) : _data(forward<Args>(args)...), _next(nullptr), _prev(nullptr) {
        cout << "ListNode:直接构造" << endl;
    }
};

// 模拟list类
template <typename T>
class MyList {
    typedef ListNode<T> Node;
public:
    MyList() {
        _head = new Node(T());
        _head->_next = _head;
        _head->_prev = _head;
    }

    // push_back:接收已构造的对象
    void push_back(const T& val) {
        Node* newnode = new Node(val);
        Node* tail = _head->_prev;
        tail->_next = newnode;
        newnode->_prev = tail;
        newnode->_next = _head;
        _head->_prev = newnode;
    }

    void push_back(T&& val) {
        Node* newnode = new Node(move(val));
        Node* tail = _head->_prev;
        tail->_next = newnode;
        newnode->_prev = tail;
        newnode->_next = _head;
        _head->_prev = newnode;
    }

    // emplace_back:可变参数模板+完美转发,接收构造T的参数
    template <typename... Args>
    void emplace_back(Args&&... args) {
        // 直接把参数包传给Node的构造函数,在节点内存上直接构造T对象
        Node* newnode = new Node(forward<Args>(args)...);
        Node* tail = _head->_prev;
        tail->_next = newnode;
        newnode->_prev = tail;
        newnode->_next = _head;
        _head->_prev = newnode;
    }

private:
    Node* _head;
};

// 测试用的自定义类
class Person {
public:
    Person(string name, int age) : _name(name), _age(age) {
        cout << "Person:构造函数" << endl;
    }

    Person(const Person& p) : _name(p._name), _age(p._age) {
        cout << "Person:拷贝构造函数" << endl;
    }

    Person(Person&& p) : _name(move(p._name)), _age(p._age) {
        cout << "Person:移动构造函数" << endl;
    }

private:
    string _name;
    int _age;
};

int main() {
    MyList<Person> lt;

    cout << "===== push_back 左值 =====" << endl;
    Person p1("张三", 20);
    lt.push_back(p1); // 拷贝构造

    cout << "\n===== push_back 右值 =====" << endl;
    lt.push_back(Person("李四", 21)); // 构造+移动构造

    cout << "\n===== emplace_back =====" << endl;
    lt.emplace_back("王五", 22); // 直接构造,无拷贝无移动

    return 0;
}

运行结果可以清晰看到,emplace_back 只触发一次 Person 构造,直接在节点内存上构造对象,零额外开销,这也是 C++11 推荐优先使用 emplace_back 代替 push_back 的原因。

四、类的新功能

C++11 对类的默认成员函数、控制能力做了很多增强,让类的设计更灵活、更安全。

4.1 默认移动构造与移动赋值

C++98 中,类有 6 个默认成员函数,C++11 新增了两个默认成员函数:移动构造函数移动赋值运算符重载

编译器生成默认移动构造 / 赋值的规则:

  • 如果你没有自己实现移动构造 / 赋值,并且没有实现析构函数、拷贝构造、拷贝赋值中的任意一个,编译器会自动生成默认的移动构造 / 赋值。
  • 默认生成的移动构造 / 赋值,对内置类型成员按字节拷贝,对自定义类型成员,会调用它的移动构造 / 赋值,如果没有则调用拷贝构造 / 赋值。
  • 如果你自己实现了移动构造 / 赋值,编译器不会再自动生成拷贝构造和拷贝赋值。

4.2 类内成员变量默认初始化

C++11 允许在类内声明成员变量时,直接给缺省值,这个缺省值会在初始化列表中使用,如果我们没有在初始化列表中显式初始化该成员,就会用这个缺省值初始化。

#include <iostream>
#include <string>
using namespace std;

class Person {
public:
    Person() {} // 无参构造,没有在初始化列表初始化成员,会用缺省值

    Person(string name, int age) : _name(name), _age(age) {} // 显式初始化,不用缺省值

    void Print() {
        cout << "姓名:" << _name << ",年龄:" << _age << ",性别:" << _gender << endl;
    }

private:
    // 类内成员声明时给缺省值
    string _name = "未知";
    int _age = 0;
    string _gender = "男";
};

int main() {
    Person p1;
    p1.Print(); // 用缺省值

    Person p2("李四", 21);
    p2.Print(); // 显式初始化的值

    return 0;
}

4.3 default 与 delete:控制默认函数的生成

C++11 提供了defaultdelete关键字,让我们可以精准控制编译器是否生成默认成员函数。

  • default 关键字:显式要求编译器生成该函数的默认版本,即使我们自己实现了其他函数,导致编译器不会自动生成,也可以用 default 强制生成。
  • delete 关键字:显式禁止编译器生成该函数,比 C++98 的 “声明为 private 不实现” 更简洁、更安全。
#include <iostream>
using namespace std;

// 禁止拷贝的类
class NoCopy {
public:
    NoCopy() = default; // 强制生成默认构造
    ~NoCopy() = default; // 强制生成默认析构

    // 禁止拷贝构造和拷贝赋值
    NoCopy(const NoCopy&) = delete;
    NoCopy& operator=(const NoCopy&) = delete;

    // 强制生成默认移动构造和移动赋值
    NoCopy(NoCopy&&) = default;
    NoCopy& operator=(NoCopy&&) = default;
};

int main() {
    NoCopy nc1;
    // NoCopy nc2 = nc1; // 错误:拷贝构造被delete
    NoCopy nc3 = move(nc1); // 正确:移动构造被default生成
    return 0;
}

4.4 final 与 override:继承与多态的安全控制

  • final 关键字:修饰类时,该类不能被继承;修饰虚函数时,该虚函数不能在子类中被重写。
  • override 关键字:修饰子类的虚函数,显式告诉编译器这个函数是重写父类的虚函数,编译器会检查函数签名是否和父类一致,不一致会编译报错,避免手滑写错函数名 / 参数导致的隐藏 bug。

五、lambda 表达式:让 C++ 支持匿名函数

C++98 中,可调用对象只有函数指针和仿函数,函数指针语法繁琐,仿函数需要定义类,使用起来很麻烦。C++11 引入的lambda 表达式,让我们可以在代码中定义匿名函数,极大简化了代码,尤其是在算法、回调函数等场景。

5.1 lambda 的语法格式

lambda 表达式的完整格式:

[capture-list] (parameters) mutable -> return-type { function-body }

各部分说明:

  1. [capture-list] 捕捉列表:必须存在,不能省略,用于捕捉外层作用域的变量,供 lambda 函数体使用。
  2. (parameters) 参数列表:和普通函数的参数列表一致,无参数时可以省略()
  3. mutable:默认情况下,值捕捉的变量在 lambda 中是 const 的,加 mutable 可以取消 const 属性。
  4. -> return-type 返回值类型:无返回值或返回值类型明确时可以省略,编译器会自动推导。
  5. {function-body} 函数体:必须存在,不能为空,实现函数逻辑。

5.2 捕捉列表的用法

捕捉列表是 lambda 的核心,它决定了 lambda 可以访问哪些外层变量,主要分为:

  1. 值捕捉[var],拷贝外层变量 var 的值到 lambda 中。
  2. 引用捕捉[&var],给外层变量 var 取别名,修改会影响外部。
  3. 隐式值捕捉[=],编译器自动捕捉 lambda 中用到的所有外层变量,全部值捕捉。
  4. 隐式引用捕捉[&],编译器自动捕捉 lambda 中用到的所有外层变量,全部引用捕捉。
  5. 混合捕捉[=, &a, &b],默认值捕捉,a 和 b 引用捕捉;[&, a, b],默认引用捕捉,a 和 b 值捕捉。

5.3 lambda 的底层原理

lambda 的本质就是仿函数(函数对象)。编译器在编译时,会把 lambda 表达式转换成一个唯一命名的仿函数类,这个类重载了operator(),捕捉列表的变量会变成仿函数类的成员变量,lambda 的参数、返回值、函数体就是operator()的参数、返回值和实现。

5.4 实战场景:算法中的排序

lambda 最常用的场景之一,就是 STL 算法的自定义规则,相比仿函数,lambda 代码更简洁,可读性更高。

#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
using namespace std;

// 商品结构体
struct Goods {
    string _name;
    double _price;
    int _evaluate;

    Goods(string name, double price, int evaluate)
        : _name(name), _price(price), _evaluate(evaluate) {}
};

int main() {
    vector<Goods> v = {
        {"苹果", 2.1, 5},
        {"香蕉", 3.0, 4},
        {"橙子", 2.2, 3},
        {"菠萝", 1.5, 4}
    };

    // 按价格降序
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
        return g1._price > g2._price;
    });
    cout << "按价格降序:" << endl;
    for (auto& g : v) {
        cout << g._name << " " << g._price << endl;
    }

    // 按评价降序
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
        return g1._evaluate > g2._evaluate;
    });
    cout << "\n按评价降序:" << endl;
    for (auto& g : v) {
        cout << g._name << " " << g._evaluate << endl;
    }

    return 0;
}

六、函数包装器与绑定器

C++11 提供了std::functionstd::bind两个工具,分别用于包装可调用对象、调整可调用对象的参数,解决了可调用对象类型不统一、参数不匹配的问题。

6.1 std::function:统一可调用对象类型

std::function是一个可调用对象的包装器,它可以把所有类型兼容的可调用对象(普通函数、函数指针、仿函数、lambda、类成员函数),包装成统一的std::function类型。

std::function是一个类模板,定义在<functional>头文件中,原型如下:

template <class Ret, class... Args>
class function<Ret(Args...)>;

其中Ret是返回值类型,Args...是函数的参数类型列表。

#include <iostream>
#include <functional>
#include <map>
#include <string>
using namespace std;

// 普通函数
int Add(int a, int b) {
    return a + b;
}

// 仿函数
struct Sub {
    int operator()(int a, int b) {
        return a - b;
    }
};

int main() {
    // 包装不同类型的可调用对象,统一为function<int(int, int)>类型
    map<string, function<int(int, int)>> calcMap = {
        {"+", Add},
        {"-", Sub()},
        {"*", [](int a, int b) { return a * b; }},
        {"/", [](int a, int b) { return a / b; }}
    };

    cout << "简易计算器:" << endl;
    cout << "10 + 5 = " << calcMap["+"](10, 5) << endl;
    cout << "10 - 5 = " << calcMap["-"](10, 5) << endl;
    cout << "10 * 5 = " << calcMap["*"](10, 5) << endl;
    cout << "10 / 5 = " << calcMap["/"](10, 5) << endl;

    return 0;
}

6.2 std::bind:调整可调用对象的参数

std::bind是一个函数适配器,可以把一个可调用对象包装成新的可调用对象,实现调整参数顺序绑死固定参数的功能。

#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;

// 原函数
int Sub(int a, int b) {
    return a - b;
}

int main() {
    // 1. 调整参数顺序
    auto sub1 = bind(Sub, _1, _2); // 原顺序:a=_1, b=_2
    cout << "10-5=" << sub1(10, 5) << endl;

    auto sub2 = bind(Sub, _2, _1); // 调换顺序:a=_2, b=_1
    cout << "10-5=" << sub2(10, 5) << endl; // 实际是5-10=-5

    // 2. 绑死固定参数,调整参数个数
    auto sub3 = bind(Sub, 100, _1); // 绑死a=100,只需要传b
    cout << "100-20=" << sub3(20) << endl;

    return 0;
}

七、STL 的其他变化

  1. 新增容器

    • unordered_map/unordered_set:基于哈希表的无序关联容器,增删查改平均时间复杂度 O (1)。
    • array:固定大小的数组,比原生数组更安全,支持 STL 接口。
    • forward_list:单向链表,比 list 更节省内存。
    • tuple:元组,支持任意个数、任意类型的元素组合。
  2. 容器新增接口

    • 所有容器都新增了initializer_list版本的构造和赋值,支持花括号初始化。
    • 所有容器都新增了移动构造和移动赋值,避免整个容器的深拷贝。
    • 序列式容器新增了 emplace 系列接口,性能更优。
  3. 范围 for 循环:C++11 新增了范围 for 循环,可以更简洁地遍历容器、数组等可迭代对象,底层就是迭代器实现的。

总结

C++11 不是对 C++98 的小修小补,而是一次彻底的革新,它从语法、性能、泛型能力、编程范式多个维度,重塑了 C++ 语言。

本文讲解的这些核心特性,各有其核心价值:

  • 列表初始化 + initializer_list:统一了初始化规则,让代码更简洁、更易读。
  • 右值引用 + 移动语义:从根本上解决了深拷贝的性能痛点,让 C++ 程序的性能有了质的提升。
  • 可变参数模板 + 完美转发:极大增强了 C++ 的泛型能力,是现代 STL 的底层基石。
  • lambda 表达式:让 C++ 支持匿名函数,简化了代码,让函数式编程成为可能。
  • function+bind:统一了可调用对象的类型,让回调函数、函数映射的实现更灵活。
  • 类的新功能:让类的设计更安全、更灵活,对继承和多态的控制更精准。

掌握这些 C++11 的核心特性,是从 “传统 C++” 迈向 “现代 C++” 的关键一步,也是后续学习 C++14/17/20 新特性的基础。在实际开发中,合理使用这些特性,不仅能大幅提升程序的运行效率,还能让代码更简洁、更易维护、更具现代 C++ 的风格。

更多推荐