C++11 核心特性详解

前言

C++11 是 C++ 标准的一次重大更新,引入了大量影响深远的新特性。本文系统梳理其中五个核心方向:统一初始化、右值引用与移动语义、类型分类与完美转发、可变模板参数、lambda 与包装器。


第一部分:统一初始化({})

1.1 C++98 时代的初始化

C++98 中 {} 的使用范围非常有限:

// 数组初始化
int arr[] = {1, 2, 3, 4, 5};

// 仅限成员全为 public 的 struct(C 风格)
struct A { int a; int b; };
A a = {1, 2};

1.2 C++11 的统一初始化

C++11 的目标是让所有类型都能用 {} 初始化:

struct A {
    A(int _a = 10, int _b = 20, int _c = 30)
        : a(_a), b(_b), c(_c) {}
    int a, b, c;
};

// 内置类型
int d = {0};

// 自定义类型的隐式类型转换
string s = {"galgame"};
A a = {1, 2, 3};

// 单参数时可不写 {}
A b = {1};
A c = 1;

// 省略 =(调用构造函数)
A e{2, 3, 4};

// initializer_list:底层类似 C 数组,多了头尾指针
vector<int> v({1, 2, 3, 4, 5});
vector<int> vv = {1, 2, 3, 4, 5};

// 双层 {}:内层是隐式类型转换的构造函数,外层是 initializer_list
vector<A> o = {{1, 2, 3}, {4, 5, 6}};

关键点:vector<int> v = {1, 2, 3} 本质是调用形参为 initializer_list 的构造函数,而非逐元素插入。


第二部分:右值引用与移动语义

2.1 左值与右值

// 左值引用
type& a;

// 右值引用
type&& b;
概念 说明 示例
左值 数据自身,可取地址 *pa[0]
右值 常量或表达式产生的临时对象 x + y 的返回值、字面量

核心区别: 左值可取地址,可出现在 = 两边;右值不可取地址,只能出现在 = 右边。

2.2 引用规则总结

场景 结果
左值引用引用右值 不允许
const 左值引用引用右值 允许
右值引用引用左值 不允许
右值引用引用 move(左值) 允许

std::move 的功能是将左值强制转换为右值(语义上的资源转移标记)。

关键性质: 右值引用只能引用右值,但右值引用变量自身的属性是左值——这意味着可以通过右值引用去修改其指向的右值。这一性质是移动构造能够 swap 内部资源的基础。

左右引用(含 const 组合)可以构成函数重载,编译器优先匹配最精确的版本。

2.3 左值引用的局限

左值引用在函数传参和返回值时能减少拷贝,但当返回值为函数局部变量时,左值引用会变成野引用。

2.4 移动构造与移动赋值

两个函数的内部本质就是一个 swap 全部成员变量——从临时/匿名对象中掠夺资源:

// string 类内部的移动构造
string(string&& s) {
    swap(s);  // s 自身是左值,能修改,这就是右值引用为左值的意义
}

核心思想: 对于有深拷贝的自定义类型,移动构造/赋值避免了不必要的资源分配和拷贝,直接将源对象的资源"偷"过来,效率极高。

2.5 编译器返回值优化

仅有拷贝构造时
优化级别 行为
无优化 返回值 -> 拷贝构造临时变量 -> 拷贝构造外部变量
部分优化 返回值直接拷贝构造外部变量
完全优化 外部变量的引用就是返回值,相当于直接构造(地址相同)

注意:内置类型的返回值编译器不会做如此激进的优化,地址会不同。

既有拷贝构造也有移动构造

有了移动构造后,编译器不再调用拷贝构造,只调用移动构造。优化方式与拷贝构造完全一样,但即使不优化,移动构造本身只涉及变量转移,效率依然极高。

赋值的情况
string ret;
ret = "djada";

赋值本身无法被优化掉,但有了移动赋值后优先调用移动赋值,效率远高于拷贝赋值。

2.6 解决"返回值是局部变量"的三种方案

  1. 输出型参数: 从外部传入变量,函数直接修改
  2. 编译器优化: 两次拷贝 -> 一次拷贝 -> 直接构造
  3. 右值引用+移动语义: 即使不优化,移动构造/赋值效率也很高,本质是通过 swap 将资源传递出去

第三部分:类型分类、引用折叠与完美转发

3.1 C++11 的类型分类

C++11 对值的类别做了更精细的划分:

  • 右值 分为纯右值将亡值:纯右值即 C++98 的右值(字面量、表达式临时值),将亡值多为强制类型转换产生的右值(如 std::move 的返回值)。
  • 泛左值 包含左值将亡值

理解这套分类的关键在于:将亡值虽然属于右值,但它携带着"资源可被转移"的语义——这正是移动语义发挥作用的前提。

3.2 引用折叠

C++ 不允许直接写 int& &,但通过 typedef 或模板,引用的引用是可能出现的。这时触发引用折叠规则:

typedef int&  lref;
lref&&  a;  // a 是左值引用 (lref=左值, 一个左值+一个右值 -> 左值)
lref&   b;  // b 是左值引用 (两个左值 -> 左值)

typedef int&& rref;
rref&   c;  // c 是左值引用 (rref=右值, 一个右值+一个左值 -> 左值)
rref&&  d;  // d 是右值引用 (两个右值 -> 右值)

折叠规则:两个引用中只要有一个是左值引用,就折叠为左值引用;两个都是右值引用才折叠为右值引用。

const 不会影响引用折叠的结果。

引用折叠的核心应用——万能引用:

template<class T>
void func(T&& a) {}

T&& 配合模板参数推导时,根据实参类型的不同,a 的类型也不同:

int x = 10;
func(x);           // 传左值 -> T 推导为 int&  -> 折叠: int& &&  = int&
func(move(x));     // 传右值 -> T 推导为 int   -> int&&
func(10);          // 传右值 -> T 推导为 int   -> int&&

传左值时 T 被推导为 int&,然后 int& && 触发引用折叠得到 int&——这样同一个 T&& 形式的参数,既可以接收左值也可以接收右值,因此称为万能引用(Universal Reference)。

关键在于:编译器根据实参的左右值属性自动改变 T 的类型,从而改变函数模板形参的最终类型。

3.3 完美转发

问题的提出: 右值引用变量自身的属性是左值,这意味着在函数内部传递右值引用参数时会丢失右值属性:

template<class T>
void fun(T&& b) {
    T k = b;
    cout << &k << endl << &b << endl;  // 地址相同,说明 b 被视为左值
}

void func(int&& a) {
    fun(a);  // a 自身是左值,传入 fun 后 T 推导为 int&,b 变成 int&
}

右值的属性在层层传递中丢失了——这就是完美转发要解决的问题。

解决方案:std::forward

template<class T>
void fun(T&& b) {
    T k = b;
    cout << &k << endl << &b << endl;  // 地址不同,b 保持 int&&
}

void func(int&& a) {
    fun(forward<int>(a));  // 将 a 的右值属性原样传递到下一层
}

forward<type>(变量名) 的实质是一个函数模板,功能类似于 move 的强转——但它保留了变量的原有类型属性,左值传出左值,右值传出右值。

完美转发在 STL 中的应用——list::push_back 的底层:

// 传统写法:左右值各自重载
void push_back(const T& t)           { insert(end(), t); }
void push_back(T&& t)                { insert(end(), forward<T>(t)); }

bool insert(const T& t)              { Node* n = new Node(t); }
bool insert(T&& t)                   { Node* n = new Node(forward<T>(t)); }

这种写法函数间高度相似,利用引用折叠可以合并:

// 引用折叠写法:一个函数兼容两种引用
template<class X>
void push_back(X&& t)                { insert(end(), forward<X>(t)); }

template<class X>
bool insert(X&& t)                   { Node* n = new Node(forward<X>(t)); }

这样通过引用折叠 + 完美转发的组合,一个模板函数就能处理左值和右值两种情况,这也是 emplace 系列函数底层统一使用万能引用+完美转发的原因。

注意:引用折叠的形参一般不写 const——加了 const 的右值引用无法调用移动构造。


第四部分:可变模板参数与包扩展

4.1 可变模板参数

支持可变数量参数的函数模板和类模板。分为两类:

  • 模板参数包: 0~多个模板参数的集合
  • 函数参数包: 0~多个函数参数的集合
template<class ...Args>              // ... 位置不可省略
void Print(Args&&... args) {         // 函数模板中的右值引用参数都叫万能引用
    cout << sizeof...(args) << endl;  // sizeof... 可视为新关键字,计算参数包大小
}

可变参数模板可以理解为"模板的模板"——它能根据实参数量实例化出不同模板参数个数的模板:

Print(1, 2.3)  ->  template<class T1, class T2> -> template<int, double>
Print(1, 1)    ->  template<class T1, class T2> -> template<int, int>
Print(1)       ->  template<class T1>           -> template<int>

4.2 包扩展

包展开在编译时完成,通过编译期递归逐个取出参数包中的参数。每一次递归都生成一个新函数,各函数之间构成重载。

// 递归终止条件:空参版本
void Print() {}

template<class T, class... Args>
void Print(T&& first, Args&&... args) {
    cout << first << " ";
    Print(args...);  // 每次减少一个参数,递归到空参终止
}

... 的位置总结:

写法 含义
Args... args 类型后面的 ... 表示这是一个参数包
args... 变量后面的 ... 表示展开参数包
func(args)... 每次传一个参数,... 表示不断展开直到参数包为空

4.3 emplace 系列 vs push 系列

list<string> 为例:

方面 emplace push
参数类型 万能引用 左值/右值重载
插入左/右值时 调用拷贝/移动构造 调用拷贝/移动构造
插入字符串字面量 直接调用 const char* 构造函数 先构造临时 string,再移动/拷贝

区别一: emplace 作为可变模板参数,插入前未实例化,传 "111" 时直接以 const char* 接收后调构造函数;push 已实例化为 string,因此会先调用 const char* 构造函数生成临时对象,再调用一次移动/拷贝构造。

区别二: 多参构造的传参形式不同:

emplace(1, 1, 1, 1);     // 直接传多个参数
push({1, 1, 1, 1});      // 用 {} 调用隐式类型转换再拷贝

底层实现: 每个底层函数加上参数包形参的构造函数即可:

template<class... Args>
void emplace_back(Args&&... args) {
    insert(end(), std::forward<Args>(args)...);
}

万能引用的两个缺点:

  1. 无法接受 {} 的隐式类型转换传参
  2. 与参数包同时作为函数重载时,单参数调用会导致二义性

第五部分:lambda 表达式

5.1 基本语法

// [捕捉] (形参) -> 返回类型 { 函数体 }
auto add = [](int x, int y) -> int { return x + y; };
add(1, 2);

lambda 是一个匿名函数对象,可以定义在函数内部。

5.2 捕捉方式

int a = 10, b = 20;

// 值捕捉:a 为值拷贝,b 为引用拷贝
auto add = [a, &b](int x, int y) -> int { return x + y + a + b; };
捕捉方式 语法 说明
值捕捉 [a] 拷贝一份,不可修改
引用捕捉 [&a] 修改会影响外部变量
隐式值捕捉 [=] 捕捉函数内所有局部变量的值
隐式引用捕捉 [&] 捕捉函数内所有局部变量的引用
混合捕捉 [&, a, b] 除 a、b 为值捕捉,其余为引用捕捉

全局变量和局部静态变量可以直接使用,不允许被捕捉。同一变量不允许被捕捉两次。

5.3 mutable

允许值捕捉的变量在函数体内被修改(不影响外部):

auto add = [a, &b](int x, int y) mutable {
    a++;           // 允许修改 a 的副本
    return x + y;
};

5.4 lambda 不能递归

lambda 是匿名函数,必须初始化完成后才能使用。而递归要求在函数体内部调用自身——这在初始化完成前是不可能的,逻辑上类似于 int a = a + 10;

5.5 lambda 的原理:就是仿函数

编译器将 lambda 展开为一个匿名的仿函数类,捕捉的变量成为该类的成员变量,函数体成为 operator()。从反汇编可以验证:lambda 的构造过程和调用过程与手写仿函数几乎完全一致。

// lambda
auto r2 = [rate](double money, int year) { return money * rate * year; };

// 等价于
class __lambda {
    double _rate;
public:
    __lambda(double rate) : _rate(rate) {}
    double operator()(double money, int year) const {
        return money * _rate * year;
    }
};

第六部分:default、delete 与默认移动

6.1 默认移动构造/移动赋值的生成条件

只有没有自主实现移动构造、析构函数、拷贝构造、拷贝赋值中任意一个时,编译器才会生成默认移动构造/移动赋值。

功能:内置类型成员值拷贝,自定义类型成员优先调用移动构造,没有则调用拷贝构造。

6.2 default 与 delete

// 强制生成默认版本
List_Node() = default;
List_Node(const List_Node& a) = default;

// 禁止调用(如 ostream 的拷贝函数)
List_Node(const List_Node&) = delete;

第七部分:包装器(function 与 bind)

7.1 function

function 是一个函数模板,用于统一包装各种可调用对象,代替 C 语言的函数指针。定义在 <functional> 头文件中:

#include <functional>

int f(int a, int b) { return a + b; }

struct Functor {
    int operator()(int a, int b) { return a + b; }
};

int main() {
    // 包装常规函数、仿函数、lambda
    function<int(int, int)> f1 = f;
    function<int(int, int)> f2 = Functor();
    function<int(int, int)> f3 = [](int a, int b) { return a + b; };
}

包装成员函数时注意 this 指针:

class Plus {
public:
    static int plusi(int a, int b) { return a + b; }     // 静态成员,无 this
    double plusd(double a, double b) { return a + b; }    // 非静态,有 this
};

// 静态成员函数无需传对象
function<int(int, int)> f4 = &Plus::plusi;

// 非静态成员函数需要传入对象指针(或对象本身)
function<double(Plus*, double, double)> f5 = &Plus::plusd;
function<double(Plus, double, double)> f6 = &Plus::plusd;

7.2 bind

bind 是一个函数模板,用于调整可调用对象的参数个数和参数顺序,返回一个 function 类型。配合 placeholders 占位符使用:

using namespace std::placeholders;  // _1, _2, _3 ...

int Sub(int a, int b) { return (a - b) * 10; }
int SubX(int a, int b, int c) { return (a - b - c) * 10; }

// 基础绑定
auto sub1 = bind(Sub, _1, _2);  // 等价于 Sub(_1, _2)
sub1(10, 5);                     // Sub(10, 5)

// 调整参数顺序
auto sub2 = bind(Sub, _2, _1);
sub2(10, 5);                     // Sub(5, 10)

// 固定部分参数(调整参数个数)
auto sub3 = bind(Sub, 100, _1);  // Sub(100, _1),只需传一个参数
auto sub4 = bind(Sub, _1, 100);  // Sub(_1, 100)
auto sub5 = bind(SubX, 100, _1, _2);  // SubX(100, _1, _2)

// 绑定成员函数
function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
f7(1.1, 1.1);  // 无需每次传入 Plus 对象

总结

C++11 的这五大特性相互关联,共同构成了现代 C++ 编程的基础:

  • 统一初始化 消除了不同初始化场景下的语法差异
  • 右值引用与移动语义 从根本上解决了深拷贝的性能瓶颈,编译器优化则进一步减少不必要的对象创建
  • 类型分类与完美转发 建立了从值类别到引用折叠再到完美转发的完整链条,让模板函数无损传递参数属性
  • 可变模板参数 赋予模板编译期递归处理任意数量参数的能力,emplace 系列是其直接应用
  • lambda 与包装器 让函数式编程风格在 C++ 中成为一等公民,配合 function/bind 构建出灵活的可调用对象体系
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐