C++11的复习
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;
| 概念 | 说明 | 示例 |
|---|---|---|
| 左值 | 数据自身,可取地址 | *p、a[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 解决"返回值是局部变量"的三种方案
- 输出型参数: 从外部传入变量,函数直接修改
- 编译器优化: 两次拷贝 -> 一次拷贝 -> 直接构造
- 右值引用+移动语义: 即使不优化,移动构造/赋值效率也很高,本质是通过 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)...);
}
万能引用的两个缺点:
- 无法接受
{}的隐式类型转换传参 - 与参数包同时作为函数重载时,单参数调用会导致二义性
第五部分: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 构建出灵活的可调用对象体系
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)