markdown
# 拥抱现代C++:C++11核心特性全景梳理
C++11 是"现代 C++"的开端。初学时总觉得知识点零散,直到我把它们串成一条线,才真正理解。这篇博客从统一初始化开始,把右值引用(重点)、`std::move`、完美转发、lambda、可变参数模板、新类功能、包装器一一理清,希望能帮到同样在路上的你。
## 1. 统一初始化与 `std::initializer_list`
### 1.1 大括号 `{}` 一统江湖
C++11 之前,初始化方式杂乱:小括号、等号、大括号混用。现在,**大括号初始化** 可以用于一切场合,并禁止窄化转换。
```cpp
int a{10};
int b = {20}; // 也可
std::vector<int> v{1,2,3,4};
std::map<int,std::string> m{{1,"one"},{2,"two"}};
struct Point { int x,y; };
Point p{3,4}; // 聚合初始化
int x = 3.14; // 仅警告
int y{3.14}; // 编译错误:窄化转换
1.2 幕后英雄 std::initializer_list
当用 {} 初始化容器时,编译器会生成一个 std::initializer_list,它是一种轻量只读容器。
cpp
#include <initializer_list>
#include <iostream>
#include <vector>
class MyVec {
public:
MyVec(std::initializer_list<int> list) {
for(auto& e : list)
data.push_back(e);
}
void print() {
for(auto e : data) std::cout << e << " ";
}
private:
std::vector<int> data;
};
MyVec v = {10, 20, 30}; // 调用 initializer_list 构造函数
v.print(); // 10 20 30
理解它,你就明白为什么 STL 容器都能直接用 {...} 填值了。
2. 新类功能
几个最常用的语法增强:
-
= default和= delete显式要求编译器生成或禁用特殊成员函数。
cppclass NoCopy { public: NoCopy() = default; NoCopy(const NoCopy&) = delete; // 禁止拷贝 }; -
override和final覆写虚函数时加
override,让编译器帮你检查签名;final阻止进一步覆写或继承。cppstruct Base { virtual void f() const; }; struct Derived : Base { void f() const override; // 签名不匹配就报错 }; -
委托构造
一个构造函数调用另一个构造函数,减少重复代码。
cppclass MyClass { int a, b; public: MyClass() : MyClass(0,0) {} MyClass(int x, int y) : a(x), b(y) {} }; -
继承构造
using Base::Base;直接"拿来"基类构造函数。cppstruct Base { Base(int); }; struct Derived : Base { using Base::Base; // Derived 也可用 Derived(5) };
3. 右值引用(重点)
右值引用是 C++11 最大的变革,支撑起了移动语义 和完美转发。我们沿着"值类别转换"这条线,一步步看清它的全貌。
3.1 左值、右值
- 左值 :有名字、能取地址、生命周期较长(如变量
int a)。 - 右值 :字面量、临时对象、表达式产生的无名结果(如
5、std::string("hello")、a+b)。
右值引用的语法是 T&&,它可以绑定到右值。这让我们有机会"窃取"临时对象的资源,而不是笨重地拷贝。
3.2 移动构造与移动赋值
假设一个管理动态内存的类:
cpp
class Buffer {
size_t sz;
char* data;
public:
Buffer(size_t n) : sz(n), data(new char[n]) {}
// 拷贝构造(昂贵)
Buffer(const Buffer& other) : sz(other.sz), data(new char[other.sz]) {
std::copy(other.data, other.data+sz, data);
}
// 移动构造(便宜)
Buffer(Buffer&& other) noexcept : sz(other.sz), data(other.data) {
other.sz = 0;
other.data = nullptr; // 源对象置于安全状态
}
~Buffer() { delete[] data; }
};
调用时机:
cpp
Buffer getBuffer() {
Buffer tmp(1024);
return tmp; // 返回时优先移动构造(也可能被 NRVO 优化)
}
Buffer b = getBuffer(); // 移动构造
Buffer b2 = std::move(b); // 显式移动(之后不要再用 b,除非重新赋值)
移动语义的意义:临时对象的资源直接"过户",避免深拷贝,大幅提升性能。
3.3 std::move 的本质与正确用法
std::move 不执行任何移动操作,它只做一件事:将左值强制转换为右值引用,让你能够匹配移动构造函数。
cpp
int x = 10;
int&& r = std::move(x); // 仅是类型转换,x 的值仍是 10
对基础类型,移动即拷贝;但对 std::string、std::vector 或含指针的类,就会触发资源窃取:
cpp
std::string s1 = "hello";
std::string s2 = std::move(s1);
// s1 现在处于"有效但未指定状态"(通常为空),不应再使用
什么时候用?
-
不要对局部返回值使用
std::move,它会抑制 NRVO 优化。cppstd::string getStr() { std::string s = "hello"; return s; // 正确,编译器会做 NRVO // return std::move(s); // 错误!反而降低性能 } -
当你确实要交出左值的所有权时,才显式使用:
cppstd::vector<std::string> v; std::string s = "test"; v.push_back(std::move(s)); // s 的资源被移入容器,s 变为空 -
对
const对象使用std::move基本无效 :const T&&无法匹配移动构造函数,最终退化为拷贝。
std::move 与 std::forward 的区别:
std::move |
std::forward |
|
|---|---|---|
| 作用 | 无条件转为右值引用 | 有条件转发:左值→左值,右值→右值 |
| 场景 | 主动交出所有权 | 模板中保持参数原始值类别 |
| 典型代码 | std::move(obj) |
std::forward<T>(obj) |
一句话:想交出所有权用 move,想原封不动转发用 forward。
3.4 值类别的转换与保持
上面几个概念背后有一条隐藏的逻辑线:左值和右值的"身份"如何转换与保持。
-
const T&能"接纳"右值常量左值引用可以绑定到右值,并延长其生命期。它不是转换了右值的类别,而是获得了绑定右值的权利。
cppconst int& ref = 10; // 10 是右值,但 ref 是左值引用很多只读参数因此写成
const T&,既能接受左值,又能接受右值,避免拷贝。 -
std::move让左值"充当"右值std::move返回右值引用,使左值能被移动构造"窃取"。但原变量本身仍然是左值。cppint a = 5; int&& r = std::move(a); // r 是右值引用,但 a 依然是左值 -
右值引用变量本身是左值 ------ 所以需要
forward保持"原籍"这是最容易犯错的地方:有名字的右值引用是左值。
cppint&& r = 10; // r 有名字,可以取地址,所以 r 是左值!如果你把
r传给另一个函数,它会匹配到左值版本,失去原本的右值属性。为了保持它最初的"右值性",必须用std::forward进行完美转发:cpptemplate<class T> void wrapper(T&& arg) { use(std::forward<T>(arg)); // 保持 arg 的原始值类别 }
这四条规则串在一起,就是 C++11 值类别体系的精髓:const& 接纳右值,&& 和 move 创造右值,forward 保持右值。
3.5 生命周期与资源安全
移动后,源对象处于 "有效但未指定状态" 。通常可以安全析构或赋予新值,但不应再使用其原有内容。这就是为什么移动构造要将源指针置为 nullptr。
3.6 引用折叠与类型推导
模板中 T&& 是万能引用,推导规则:
- 传入左值
int&,T推导为int&,折叠:int& &&→int& - 传入右值
int&&,T推导为int,结果int&&
引用折叠规则 :只有 && + && 才能折叠成 &&,其他组合全折叠为 &。这是完美转发的底层基础。
3.7 完美转发:std::forward
我们需要一个函数,能把参数的值类别原封不动地传递给另一个函数。结合万能引用和 std::forward 即可:
cpp
template<typename T, typename Arg>
std::unique_ptr<T> factory(Arg&& arg) {
return std::unique_ptr<T>(new T(std::forward<Arg>(arg)));
}
Arg&&是万能引用,可匹配任何实参。std::forward<Arg>(arg):如果arg原本是左值,转发为左值;原本是右值,转发为右值。
引用折叠与 std::forward 的配合,实现了参数的零开销中转。
4. 可变参数模板
C++11 允许模板参数列表接受任意数量参数:
cpp
void print() {} // 递归终止
template<typename T, typename... Args>
void print(T first, Args... rest) {
std::cout << first << " ";
print(rest...);
}
print(1, 2.5, "hello", 'c'); // 输出 1 2.5 hello c
Args... 是参数包,通过递归展开依次处理。它让 std::tuple、emplace_back 等成为可能。
5. Lambda 表达式
语法:[capture](params) -> ret { body }
捕获方式:
[=]:按值捕获所有(副本,默认不可改,除非加mutable)[&]:按引用捕获所有(修改会影响外部)[a, &b]:a按值,b按引用[this]:捕获当前对象指针
常见用例:
cpp
std::vector<int> v{3,1,4,1,5};
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });
int threshold = 3;
auto it = std::find_if(v.begin(), v.end(),
[threshold](int x) { return x > threshold; });
lambda 的底层是一个匿名的仿函数类,捕获列表是其成员变量。
6. 包装器:std::function 与 std::bind
6.1 std::function
通用多态函数包装器,可存储任意可调用物:函数、lambda、仿函数、成员函数等。
cpp
#include <functional>
int add(int a, int b) { return a+b; }
std::function<int(int,int)> func;
func = add; // 函数指针
func = [](int a,int b){ return a-b; }; // lambda
std::cout << func(4,2); // 2
存储成员函数时需传入对象指针或配合 std::bind。
6.2 std::bind
将参数与可调用物绑定,生成新的可调用对象,并可通过占位符调整参数顺序。
cpp
using namespace std::placeholders;
auto minus = [](int a, int b) { return a - b; };
auto five_minus = std::bind(minus, 5, _1);
std::cout << five_minus(3); // 5 - 3 = 2
// 绑定成员函数
struct Foo {
void display(int x) { std::cout << x; }
};
Foo obj;
auto f = std::bind(&Foo::display, &obj, _1);
f(100); // 打印 100
7. 结语
这篇笔记从统一初始化开始,到右值引用的移动语义、std::move、值类别转换与完美转发(重点),再到可变参数模板、lambda、新类功能和包装器,把 C++11 的核心骨架搭了出来。