这一章在干嘛?
全书的最后一章,也是 C++11 的总结陈词。前 17 章零散介绍过的 C++11 特性在这里先做一轮盘点;然后集中火力讲四个此前没讲透的大件:移动语义与右值引用(C++11 最深刻的性能革新)、lambda 表达式、function 包装器、可变参数模板。读完这章,你对”C++11 相对于 C++98 多了什么”就有完整地图了。
18.1 C++11 特性大盘点
先快速回顾散落在前面各章的 C++11 改动,一张清单过一遍:
- 新类型:
long long/unsigned long long(64 位整数)、char16_t/char32_t、原始字符串。 - 统一初始化:花括号列表适用于一切类型——
int x = {5};、double y {2.75};、Stump s2{5, 43.4};,连new表达式都行:int * ar = new int[4] {2, 4, 6, 7};。 - 收窄检查:花括号初始化禁止有信息丢失的转换。
char c2 = 459585821;悄悄截断;char c2 {459585821};直接编译错误。这是花括号相对圆括号的核心优势。 - 声明简化:
auto类型推导;decltype(x) y;让 y 与表达式 x 同类型(模板里的救星);后置返回类型auto f2(double, int) -> double;解决”T、U 还没进作用域没法写返回类型”的难题:template<typename T, typename U> auto eff(T t, U u) -> decltype(T*U) {...}。 - 模板别名:
using itType = std::vector<std::string>::iterator;,且支持 typedef 做不到的部分特化:template<typename T> using arr12 = std::array<T,12>;。 - nullptr:专用的空指针关键字。0 身兼整数常量与指针常量二职容易引发歧义重载,nullptr 是纯指针类型,
nullptr == 0为 true 但编译器会拒绝把 nullptr 传给 int 参数。 - 智能指针换代:auto_ptr 弃用,unique_ptr/shared_ptr/weak_ptr 上位,且为配合 STL 容器和移动语义专门设计。
- 异常规范弃用:
throw(bad_dog)这套声明退出舞台,只保留noexcept表示”本函数不抛异常”。 - 作用域内枚举:
enum class New1 {never, sometimes};——成员必须用New1::never访问,不同枚举不再撞名,底层类型也更可控。 - explicit 转换运算符:explicit 的管辖范围从单参构造函数扩展到转换函数:
explicit operator double() const;阻止隐式转换。 - 类内成员初始化:
int mem1 = 10; double mem2 {1966.54};直接写在类定义里,构造函数初始化列表里的值可以覆盖它。从此不用在每个构造函数里重复写默认值。 - STL 更新:新增容器 forward_list 与四个哈希容器;cbegin()/cend() 返回 const 迭代器;容器获得移动构造/移动赋值;
>>相邻的模板尖括号不再要求空格:std::vector<std::list<int>> v1;合法了。
18.2 右值引用
先分清两种”值”:
- 左值(lvalue):能取地址的、有持久身份的数据。变量名、解引用的指针都是。传统引用(现在叫左值引用
&)只能绑定左值。 - 右值(rvalue):字面常量(13)、表达式计算结果(x+y)、函数按值返回的临时值。它们”飘在空中”,取不了地址。右值引用
&&专门绑定右值。
// rvref.cpp -- simple uses of rvalue references
#include <iostream>
inline double f(double tf) {return 5.0*(tf-32.0)/9.0;}
int main()
{
using namespace std;
double tc = 21.5;
double && rd1 = 7.07; // 绑定字面量
double && rd2 = 1.8 * tc + 32.0; // 绑定表达式结果 70.7
double && rd3 = f(rd2); // 绑定函数返回的临时值
cout << "rd1 value and address: " << rd1 << ", " << &rd1 << endl;
return 0;
}最有趣的一点:右值本来取不了地址,但绑定到右值引用之后就有地址了(&rd1 合法)——数据被安放到了具体位置,右值引用就是通向它的门。rd2 绑定的是当时 x+y 的值,之后改 x、y 都影响不了它。
右值引用的核心使命是实现移动语义,下一节揭晓。
18.3 移动语义
问题:深拷贝有时候纯属浪费。
vector<string> vstr_copy1(vstr); // #1:真复制,20 万字符一份份拷
vector<string> vstr_copy2(allcaps(vstr)); // #2:拷贝 allcaps 返回的临时对象#2 里 allcaps() 返回的临时对象马上就要销毁,却还要为它做一次 2000 万字符级别的深拷贝再整体销毁——等于把整套家具搬进新家又连夜扔掉旧房子。更好的做法是:数据原地不动,把”房本”直接过户。这就是移动语义——名叫”移动”,实际移动的只是簿记信息(指针),数据本身纹丝不动。
实现需要两步:
- 编译器能区分”该拷贝”还是”该移动”——靠重载:左值实参匹配拷贝构造
Useless(const Useless &),右值实参匹配移动构造Useless(Useless &&)。 - 移动构造里写”过户”逻辑。
看移动构造的真身:
// useless.cpp -- 拷贝构造 vs 移动构造(核心对比)
Useless::Useless(const Useless & f) : n(f.n) // 深拷贝:一字节一字节抄
{
++ct;
pc = new char[n];
for (int i = 0; i < n; i++)
pc[i] = f.pc[i];
}
Useless::Useless(Useless && f) : n(f.n) // 移动构造:直接偷指针
{
++ct;
pc = f.pc; // 窃取地址(pilfering)
f.pc = nullptr; // 原对象置空,防止析构时 delete 两次
f.n = 0;
}两个要点:① 移动构造会修改源对象,所以参数不能是 const;② 接管指针后必须把源指针置 nullptr——对空指针 delete [] 是安全的,delete 同一地址两次是灾难。程序中 Useless four (one + three); 里 one+three 是右值,自动匹配移动构造:four 的数据地址与 operator+ 里临时对象的地址完全相同,数据没有搬动过一寸。
移动赋值同理:
Useless & Useless::operator=(Useless && f) // 移动赋值
{
if (this == &f)
return *this;
delete [] pc; // 先释放自己原有的数据
n = f.n;
pc = f.pc; // 过户
f.n = 0;
f.pc = nullptr;
return *this;
}强制移动 std::move():左值默认走拷贝,但有时你就是想把左值”处理掉”换效率(比如从候选数组里挑出一个,其余丢弃)。<utility> 里的 std::move(one) 把左值 one 伪装成右值,从而触发移动赋值:
four = one; // 拷贝赋值,one 完好无损
four = std::move(one); // 移动赋值,one 被掏空(变成空对象)注意 std::move() 本身不做任何移动,它只是”改标签”;如果类没定义移动赋值,编译器会退回用拷贝赋值。
常见坑:move 过的对象别再用
two = std::move(one);之后 one 已被掏空(指针置 nullptr),再解引用或读它的数据是未定义行为。move 的语义就是”我承诺不再用它”——签名式的所有权交接。
对大多数程序员的实际意义在于:你不用天天写右值引用,但库替你写好了——STL 容器现在都有移动构造和移动赋值,容器搬家、扩容、传值的开销因此大幅下降。
18.4 类的新特性
C++11 给类设计加了一堆减负工具:
特殊成员函数从 4 个变 6 个:默认构造、拷贝构造、拷贝赋值、析构,加上移动构造和移动赋值。规则(重要):
- 定义了析构/拷贝构造/拷贝赋值中的任何一个 → 移动构造和移动赋值不再自动生成。
- 定义了移动构造或移动赋值 → 拷贝构造和拷贝赋值不再自动生成。
- 需要被用到且你没定义时,编译器才生成默认版本(defaulted)。
defaulted 与 deleted 方法:
class Someclass
{
public:
Someclass() = default; // 显式要求编译器生成默认版
Someclass(const Someclass &) = delete; // 禁用拷贝(比 private 方案直白)
Someclass & operator=(const Someclass &) = delete;
Someclass(Someclass &&) = default; // 允许移动
Someclass & operator=(Someclass &&) = default;
};delete 还能禁用特定重载:void redo(double); 加上 void redo(int) = delete; 后,sc.redo(5) 会被编译器拦下(而不是悄悄把 5 提升为 5.0)——deleted 函数参与重载决议,只是选中即报错。
委托构造:构造函数调用构造函数,公共逻辑只写一份:
Notes::Notes(int kk, double xx, std::string stt) : k(kk), x(xx), st(stt) {}
Notes::Notes() : Notes(0, 0.01, "Oh") {} // 委托给全参版
Notes::Notes(int kk) : Notes(kk, 0.01, "Ah") {}继承构造:using BS::BS; 把基类构造函数引入派生类(默认/拷贝/移动构造除外)。继承来的构造只初始化基类成员,派生类新增成员走默认初始化;签名冲突时派生类自己的版本优先。
override 与 final:修虚函数手滑的神器。签名写错(如 f(char * ch) 对 f(char ch))不会覆盖而是隐藏基类版本,运行时才发现多态失效。加上 override 后缀,签名对不上直接编译报错:
class Action { public: virtual void f(char ch) const; };
class Bingo : public Action {
public:
virtual void f(char * ch) const override; // 编译错误:签名不匹配!
};final 则禁止后续派生类再覆盖:virtual void f(char ch) const final;。两者是”有特殊含义的标识符”而非关键字,别的场合仍可当普通变量名。
18.5 lambda 函数
lambda(λ 表达式)是匿名函数,长这样:
[](int x) { return x % 3 == 0; }它解决的问题是:给 STL 算法传”一个小判断”,用函数指针要全局定义、用 functor 要写整个类,都离使用点太远。lambda 的三段式:[捕获列表](参数列表){函数体},返回类型自动推导(多 return 语句时用后置语法 -> double 显式声明)。
函数指针、functor、lambda 三种写法对比同一个需求”数出能被 3/13 整除的随机数个数”:
// lambda0.cpp -- 三种函数对象的对比(节选)
std::vector<int> numbers(Size);
std::generate(numbers.begin(), numbers.end(), std::rand);
// ① 函数指针:要先在全局定义 bool f3(int x) {...}
int count3 = std::count_if(numbers.begin(), numbers.end(), f3);
// ② functor:要定义一个类,但可参数化
class f_mod {
int dv;
public:
f_mod(int d = 1) : dv(d) {}
bool operator()(int x) { return x % dv == 0; }
};
count3 = std::count_if(numbers.begin(), numbers.end(), f_mod(3));
// ③ lambda:定义即使用
count3 = std::count_if(numbers.begin(), numbers.end(),
[] (int x) { return x % 3 == 0; });捕获列表是 lambda 的独门武功——它能”看见”作用域内的局部变量:
[z]:按值捕获 z;[&count]:按引用捕获 count(lambda 内改它就是改原件)[&]:全部按引用;[=]:全部按值- 混搭:
[ted, &ed]、[&, ted]、[=, &ed]
于是统计可以写成一行 lambda 双杀:
// lambda1.cpp -- 按引用捕获,一个 lambda 统计两个数
int count3 = 0, count13 = 0;
std::for_each(numbers.begin(), numbers.end(),
[&](int x){ count3 += x % 3 == 0; count13 += x % 13 == 0; });x % 13 == 0 是 bool,转成 0/1 加进计数器——把谓词当累加器用,这是传统函数指针做不到的。lambda 用两次也不用复制粘贴:auto mod3 = [](int x) { return x % 3 == 0; }; 起个名,之后 mod3 既能传给算法也能当普通函数调。
18.6 function 包装器
问题:模板的低效实例化。函数指针、functor、lambda 都是”可调用类型”,但对模板来说它们是不同的类型:
// somedefs.h
template <typename T, typename F>
T use_f(T v, F f) { ... return f(v); }
// callable.cpp -- 六种调用
use_f(y, dub); // F = double(*)(double)
use_f(y, square); // 同上,共用一次实例化
use_f(y, Fp(5.0)); // F = Fp,新的实例化!
use_f(y, Fq(5.0)); // F = Fq,又一种!
use_f(y, [](double u) { return u*u; }); // 编译器内部类型,又一种!
use_f(y, [](double u) { return u+u/2.0; }); // 又一种!虽然六种调用的行为签名都是 double(double),模板参数 F 却被推导成五种不同类型,use_f() 被实例化了五份,可执行文件白白膨胀。
解法:std::function<double(double)>(头文件 <functional>)。function 按调用签名(返回类型 + 参数类型列表)统一封装一切可调用体:
// wrapped.cpp -- using a function wrapper(核心)
function<double(double)> ef1 = dub;
function<double(double)> ef2 = square;
function<double(double)> ef3 = Fq(10.0);
function<double(double)> ef5 = [](double u) { return u*u; };
use_f(y, ef1); // 六次调用的 F 全是 function<double(double)>
use_f(y, ef5); // → 只实例化一份,count 地址始终相同也可以反过来在模板形参上用 function:T use_f(T v, std::function<T(T)> f),调用时写 use_f<double>(y, dub)。顺带一提,C++11 还有一批新包装器:bind(比 bind1st/bind2nd 灵活的绑定器)、mem_fn(成员函数转普通可调用体)、reference_wrapper(可拷贝的”引用”)。
18.7 可变参数模板
可变参数模板(variadic template)让函数/类接受任意数量、任意类型的参数。目标:show_list(n, x) 和 show_list(x*x, '!', 7, mr) 都能用一个模板搞定。四个关键词:参数包、展开、递归、终止。
// variadic2.cpp -- 可变参数模板的最终形态
#include <iostream>
#include <string>
// 0 个参数:终止递归
void show_list() {}
// 恰好 1 个参数:打印换行而非逗号,同时是递归的终点
template<typename T>
void show_list(const T& value)
{
std::cout << value << '\n';
}
// 2 个及以上:打印第一个,递归处理剩下的
template<typename T, typename... Args>
void show_list(const T& value, const Args&... args)
{
std::cout << value << ", ";
show_list(args...); // 展开:把包拆回一个个参数
}
int main()
{
int n = 14;
double x = 2.71828;
std::string mr = "Mr. String objects!";
show_list(n, x);
show_list(x*x, '!', 7, mr);
return 0;
}拆解机制:
typename... Args是模板参数包(一串类型),Args... args是函数参数包(一串值)。Args能匹配任意个类型,包括 0 个。args...放在包名右侧表示展开:show_list(args...)把包还原成独立的参数列表。- 每次递归用普通参数 T value 吃掉第一个,包里剩下的传给下一层;包空了就落到无参重载,递归终止。
const Args&... args给包里的每个参数统一套上 const 引用模式,避免大对象按值传递的开销。
没有下标访问包的能力(不能写 Args[2]),所以”处理一个、递归其余”是标准套路——和链表遍历一个思路。
18.8 更多新特性与 Boost
并发编程:多核时代需要线程。C++11 定义了支持线程的内存模型,新增 thread_local 关键字(变量的生命周期与线程绑定),并提供 <atomic> 原子操作库与 <thread>、<mutex>、<condition_variable>、<future> 线程支持库。
库扩容:<random>(多种随机数引擎和分布)、<chrono>(时间时长)、<tuple>(pair 的任意元推广)、<ratio>(编译期有理数)、<regex>(正则表达式——配合原始字符串 R"(\d\t\w\d)" 使用,不用再写双反斜杠)。
低层支持:放宽 POD 定义便于字节级复制;联合允许含构造/析构的成员;alignof()/alignas 管内存对齐;constexpr 扩大编译期常量求值范围(const 变量可放入只读存储);static_assert 把断言提前到编译期(调试模板利器);支持用户自定义字面量(如二进制字面量 1001001b)。
语言演进模式值得一提:标准委员会节奏慢,而社区通过库来先行创新。STL 就是这么被 HP 实验室”用脚投票”进标准的。Boost 项目(始于 1998,www.boost.org,100+ 个库)延续这条路:先在 Boost 社区孵化、评审,成熟后经 TR1(Technical Report 1)过渡进 C++11。tuple、array、bind、function、智能指针、regex、random 都出自这条流水线。标准之外的 Boost 依然值得挖掘,比如 lexical_cast(数字与字符串互转,boost::lexical_cast<string>(weight) 一行搞定,缺点是对浮点格式控制不足,精细格式化还得用第 17 章的 stringstream)。
学完本书之后:掌握规则只是起点,更长的旅程是学会高效地用 C++——多读好代码,了解 UML、用例分析、CRC 卡片这些分析与设计方法,按”迭代演化”而非”一步到位”的思路构建大型系统。
通关标准:
能说清左值/右值/左值引用/右值引用的关系;能手写移动构造(记住偷指针 + 置空两步);能用捕获列表写出带状态累加的 lambda;能解释 function 包装器如何把 5 次模板实例化压成 1 次;能默写”参数包 + 递归 + 终止重载”的可变参数模板三件套。
常见坑:移动构造的参数不能是 const
移动构造/移动赋值要修改源对象(置空它的指针),参数必须是
Useless &&而非const Useless &&。另外接管指针后忘记把源指针置 nullptr,会导致析构阶段对同一地址 delete 两次,程序当场崩溃。
自测:Useless four(one + three); 走的是拷贝构造还是移动构造?为什么?
移动构造。one+three 产生一个临时对象(右值),匹配
Useless(Useless &&)重载;而 Useless two = one 中 one 是左值,匹配Useless(const Useless &)。一组拷贝/移动重载让编译器自动把初始化分成”需要深拷贝”和”只需过户”两队。
自测:std::move(one) 一定会触发移动操作吗?
不一定。std::move 只是把 one 标记为右值,真正调用的是移动赋值/移动构造——如果类没定义它们,编译器会退回调用拷贝版本;连拷贝版也没有则编译失败。move 是”许可”而非”命令”。
自测:为什么把六大特殊成员函数里的移动构造写出来后,拷贝构造就"消失"了?
C++11 的规则:显式定义移动构造或移动赋值,编译器就不再自动生成拷贝构造与拷贝赋值(防止移动语义与拷贝语义混用出事)。此时若仍需要拷贝,用
Someclass(const Someclass &) = default;显式找编译器要回来。
、[=]、[&, ted] 分别是什么意思?
[&] 把作用域内所有自动变量按引用捕获(修改即改原件);[=] 全部按值捕获(副本,不可改原件);[&, ted] 表示 ted 按值、其余全部按引用。同理 [=, &ed] 是 ed 按引用、其余按值。捕获是 lambda 访问局部变量并携带状态的唯一通道。
自测:可变参数模板为什么必须有一个无参(或单参)的重载版本?
递归展开包需要一个终点。每次递归吃掉一个参数、包缩短一格;当包空了再调 show_list(args…) 就是无参调用,必须有无参重载接住,否则无限递归编译不过。带单参版本则能把最后一项的输出从”逗号结尾”换成”换行结尾”,一举两得。