C++11 - 现代 C++ 的开端
📑 C++ 演进系列:总览 | C++11 | C++14 | C++17 | C++20 | C++23 | C++26
C++11 是 C++ 历史上最重要的标准之一,它彻底改变了 C++ 的编程方式,被称为"现代 C++"的起点。在 C++11 之前,C++98/03 沿用了十余年,语言表达力落后于时代。C++11 一次性引入了自动类型推导、Lambda、智能指针、移动语义、并发支持等重量级特性,使 C++ 摆脱了"带类的 C"的旧面貌,转向类型安全、资源安全、表达力强的现代风格。
自动类型推导(auto)
auto 让编译器从初始化表达式推导变量类型,取代冗长难读的类型名(如 std::vector<std::string>::const_iterator)。它不是动态类型–类型在编译期就已确定,只是省去手写。主要收益是可读性(关注"做什么"而非"是什么类型")和可维护性(重构时类型自动跟随)。
推导规则
auto 的推导规则复用模板实参推导,默认按值:丢弃引用与顶层 const。
1 | |
要从容器取可修改元素需写 auto&,只读用 const auto&。数组与函数名在按值推导时退化为指针:
1 | |
auto&& 万能引用
auto&& 不是右值引用:经过引用折叠,左值右值都能绑,且保留 const。
1 | |
折叠规则:& 与 && 任意组合,只要出现 & 结果就是 &;两个 && 才是 &&。
尾置返回类型
C++11 还允许把返回类型放到参数表之后,让返回类型可以引用参数名:
1 | |
代理类型陷阱
1 | |
vector<bool> 的 operator[] 返回按位代理对象,auto 会原样接住;容器变动后代理可能悬挂。确实要 bool 时写 bool x = v[0]; 强制转换。
使用陷阱
auto x = {1, 2, 3} 推导出 std::initializer_list<int>,与预期可能不符;函数返回 auto(返回类型推导)要到 C++14 才支持。auto 适合类型名长、明显或无关紧要的场合,类型对理解逻辑重要时仍应显式写出。
Lambda 表达式
Lambda 让你在用到可调用对象的地方就地定义它,无需另起一个函数或仿函数。这在传给 STL 算法(std::sort、std::find_if、std::for_each)时尤其有价值–以前要么写函数指针、要么定义仿函数类,都很啰嗦。
语法为 [捕获](参数) -> 返回类型 { 函数体 },返回类型可省略(由 return 推导)。捕获决定了 lambda 如何"看见"外部变量:值捕获拷贝一份、引用捕获持有引用。[=]/[&] 是按值/按引用捕获所有外部变量的简写。
1 | |
闭包类型是什么
每个 lambda 表达式都会让编译器生成一个唯一的匿名类(闭包类型):捕获的变量成为其数据成员,函数体成为 operator(),且默认是 const 成员函数。这就是值捕获的变量在 lambda 内默认改不了的原因。无捕获的 lambda 可隐式转换为函数指针,能与 C API 交互:
1 | |
mutable:修改值捕获的拷贝
1 | |
捕获 this
成员函数里定义的 lambda 常需访问成员,[this] 按指针捕获:
1 | |
闭包里存的是 this 指针的拷贝,Widget 销毁后再调用是未定义行为。需要整个对象拷贝时用 C++20 的 [*this]。
移动捕获:C++11 做不到
捕获列表只能拷贝或引用,move-only 对象(如 unique_ptr)捕获不了。C++11 的变通是改用 shared_ptr;C++14 的初始化捕获才是正解:
1 | |
注意:引用捕获([&])要小心 lambda 生命周期超出被引用变量–如 lambda 存入容器后异步执行,引用的对象可能已销毁,导致悬挂引用。默认优先值捕获;确需引用且能保证生命周期时再用 [&]。
智能指针
智能指针用 RAII 管理动态内存,析构自动释放,从根本上治理裸 new/delete 的内存泄漏与异常安全问题。C++11 引入了三种:
unique_ptr:独占所有权,不可拷贝、只能移动。零开销抽象(大小与裸指针相同,无引用计数开销)。默认选择。shared_ptr:共享所有权,引用计数。多个shared_ptr可指向同一对象,最后一个销毁时释放。有原子计数开销。weak_ptr:对shared_ptr的弱引用,不增加计数。用于打破循环引用、观察但不影响生命周期。
1 | |
make_unique/make_shared 优先于裸 new:一次分配(make_shared 把对象和控制块合并到一次分配)、异常安全(避免 f(unique_ptr(new A), unique_ptr(new B)) 求值顺序导致的泄漏)。注意 make_unique 实际是 C++14 才加入的,C++11 可手写一份或暂时退回 new。
weak_ptr 使用前需 lock() 提升为 shared_ptr 检查对象是否仍存活。shared_ptr 控制块的原子操作有开销,热路径上应优先 unique_ptr。
unique_ptr 细节
1 | |
无状态删除器不占额外空间;带状态的删除器(如函数指针)会使 unique_ptr 变大。
shared_ptr 的控制块
每个被共享的对象伴随一个控制块,含两个计数:强计数(shared_ptr 个数,归零时析构对象)与弱计数(weak_ptr 个数加一,归零才释放控制块)。make_shared 把对象与控制块合并为一次分配,代价是只要还有 weak_ptr 存活,整块内存(含已析构对象占用的部分)都不能归还。
enable_shared_from_this
对象内部要获取指向自身的 shared_ptr(典型:注册异步回调)时,直接持有成员 shared_ptr 会造成两个独立控制块、双重释放;正确做法是继承 enable_shared_from_this:
1 | |
weak_ptr 的正确用法
1 | |
右值引用与移动语义
移动语义让"资源转移"成为一等操作:把昂贵的拷贝(如 vector 拷贝整个堆缓冲)换成廉价的指针交换。它区分了"拷贝"(复制底层资源,源对象不变)与"移动"(转移资源所有权,源对象置空)。
核心机制是右值引用 T&&——绑定到即将销毁的临时对象(右值)。std::move 是个无操作转换,把左值强转为右值引用,表示"我不再需要这个值,你可以掏空它"。移动构造/移动赋值用 T&& 接管资源后把源对象置为有效但未指定状态(通常置空)。
1 | |
关键点:移动操作应标记 noexcept——vector 扩容时只有 noexcept 移动才会真正移动,否则退化为拷贝以保证强异常安全。std::move 后不要再使用源对象(除赋值或销毁外)。返回局部变量时不要 std::move——会阻止 RVO。现代代码里手写移动构造已少见,编译器生成的版本(零规则)通常足够。
转发引用与引用折叠
T&& 出现在推导语境时是转发引用(不是右值引用):实参为左值时 T 被推导为 X&,折叠成 X&;实参为右值时 T 为 X,得到 X&&。
1 | |
注意 void f(Widget&&) 没有推导,就是普通的右值引用;auto&& 同样是转发引用。
std::forward 与 std::move 的分工
两者都只是类型转换、不生成任何代码:std::move 无条件转成右值;std::forward<T> 有条件–T 是左值引用就转左值,否则转右值。move 用于你确定要掏空的对象,forward 用于模板中对转发引用的透传,转发时误用 move 会把左值实参也掏空。
moved-from 状态
被移动的对象处于"有效但未指定"状态:可以安全析构、可以赋新值,但不能假设它的内容。惯用法:移动之后只做销毁或重新赋值。
std::function 和 std::bind
std::function 是可调用对象的类型擦除包装器:能统一存储函数指针、仿函数、Lambda、成员函数指针等任何可调用对象,只要签名匹配。这让回调、事件处理、策略注入等场景可以把"任意可调用物"当一等对象传递。
代价是有一点运行期开销(类型擦除、可能的堆分配、间接调用),热路径上不如模板或直接 Lambda。C++14 起 Lambda 捕获初始化、C++23 std::move_only_function 进一步削弱了它的地位,但 std::function 仍是"存储任意回调"的标准选择。
实现机制
std::function 内部用一个小缓冲区(通常两三个指针大小)做 SBO(小缓冲优化):闭包能塞进去就直接存对象里,塞不下才堆分配。调用走一次虚表/函数指针间接跳转,捕获大的 lambda 还可能堆分配。签名匹配是"兼容"而非精确:返回类型可隐式转换、实参按值传递时发生拷贝。
1 | |
std::bind 的经典坑:重载与转发
1 | |
成员函数绑定还需要 std::mem_fn 或 &Class::method 加对象指针,参数占位符的顺序也容易写错。Lambda 没有这些问题。
std::unordered_map 和 std::unordered_set
基于哈希表的容器,提供平均 O(1) 的查找/插入/删除。与之相对,std::map/std::set 基于红黑树,是有序的 O(log n)。选择标准:需要按键排序遍历时用 map/set,只需要快速查找且不关心顺序时用 unordered_map/unordered_set。
内部结构:桶数组 + 链表
unordered_map 是开链哈希表:一个桶数组,每个桶挂一条单链表(GCC 实现里整张表共用一条大链表,桶只存首尾指针)。负载因子 size / bucket_count 超过 max_load_factor(默认 1.0)时触发 rehash–重新分配更大的桶数组并重挂所有节点。
1 | |
rehash 的连带后果:节点本身不搬家(指针稳定),但桶的归属关系变化导致迭代器全部失效;map 则相反–插入删除只失效指向被删元素的迭代器。
自定义键类型
1 | |
要求:hash 与 operator== 语义一致–相等的键必须有相同哈希值,否则同一键会掉进不同的桶,永远查不到。异或组合字段是常见写法,但注意两字段相等时异或结果为 0,混入移位可缓解。
哈希容器的其余代价:最坏情况 O(n)(哈希冲突,恶意输入可能触发)、无序遍历。若想保留插入顺序可配合存一个 vector 记录键的顺序。operator[] 在键不存在时会插入默认值–只想查找时用 find 或 at(后者不存在时抛异常)。
std::tuple
std::tuple 是异质值的固定大小容器:把不同类型的若干值打包成一个对象。相比自定义结构体,它无需命名字段、可用模板泛化处理任意类型组合,适合函数多返回值、临时聚合、编译期元编程。
访问用 std::get<I>(t)(按索引)或 std::get<T>(t)(按类型,要求唯一)。C++17 结构化绑定让它解包更优雅。std::tuple_cat 拼接多个 tuple。
元组工具
1 | |
元组比较是字典序:std::tie 把左值引用打包成 tuple,做"多字段比较"很方便:
1 | |
std::tie 还能做多重回赋:std::tie(a, b) = std::make_pair(b, a); 交换两个变量(忽略某个返回值用 std::ignore)。
权衡:需要语义明确字段名时还是用结构体–point.x 比 std::get<0>(point) 可读得多。tuple 适合类型组合在编译期才能确定、或临时聚合无需命名的场合。
std::chrono
std::chrono 是类型安全的时间库,用编译期类型系统区分时间点(time_point)、时长(duration)与时钟(clock)。它消除了 C 风格 time_t/gettimeofday 的无类型算术与可移植性问题——不同单位的时长(秒、毫秒、纳秒)是不同类型,混算会编译报错而非静默出错。
三个时钟:system_clock(挂钟时间,可转 time_t)、steady_clock(单调递增,适合计时)、high_resolution_clock(最小分辨率,通常即 steady_clock)。C++14 起的 chrono_literals(100ms、2s)让字面量更直观。
duration 的表示
duration<Rep, Period> 是"计数值 + 单位比例":milliseconds 是 duration<int64, milli>,nanoseconds 是 duration<int64, nano>。单位不同的 duration 相加时自动向更精细的单位对齐;反过来 duration_cast 往粗单位转时截断(毫秒转秒丢小数)。
1 | |
C++17 起非截断语境有 floor/ceil/round 可选。
1 | |
测量代码耗时优先用 steady_clock(不受系统时间调整影响);跨进程/持久化时间用 system_clock。duration_cast 在单位不整除时截断。
常见计时模板
1 | |
注意 high_resolution_clock 的单位是实现定义的(可能纳秒也可能别的),benchmark 应固定用 steady_clock。
std::array
std::array 是 C 风格数组的零开销包装:大小在编译期固定、栈分配、不退化为指针、提供 STL 容器接口(size()、begin()/end()、at()、front()/back())。它取代了既不安全(退化为指针、无边界检查)又缺少接口的裸数组 T[]。
与 std::vector 的区别:array 大小固定、栈分配、无动态扩容开销;vector 大小可变、堆分配。已知大小且不需要增删时用 array,否则用 vector。
为什么说"零开销"
std::array<T, N> 内部唯一的数据成员就是 T data[N]:没有容量、没有指针、没有析构逻辑。std::to_raw(a)(C++17)直接取出内部数组;std::get<I> 走的是编译期检查的元素访问。它还是聚合体,所以花括号初始化走聚合规则(未列出的元素补零)。
一个注意点:std::array<int, 0> a; 是合法的空数组,但 a.begin() == a.end(),front()/back() 是未定义行为。
1 | |
at() 做边界检查(越界抛 std::out_of_range),operator[] 不检查但更快。std::get<I>(arr) 提供编译期边界检查。array 是值类型–拷贝会复制全部元素,传参用 const& 或 span。
array 支持比较运算(逐元素字典序)与 std::swap(逐元素交换,O(n));解构出指针可用 a.data(),与 C API 交互时它就是裸数组。
std::forward_list
std::forward_list 是单向链表,相比 std::list(双向)每个节点少存一个指针、更省内存。它提供 O(1) 的头部插入/删除,以及任意位置 O(1) 的"插入/删除后继"操作(insert_after/erase_after)。
适用场景:内存极其紧张、只需单向遍历、频繁头部增删。但要注意现代硬件上链表缓存不友好–元素巨大且确需中间 O(1) 增删时才考虑,否则 vector 往往更快。Core Guidelines 不推荐链表作为默认选择。
为什么基于"后继"操作
单向链表中拿到节点指针后找不到前驱,删除节点必须改前驱的 next 指针。所以接口设计成"给我你确定存在的前驱,我操作它的后继":insert_after(pos, val)、erase_after(pos)。相应地,"删除第一个元素"是特例 pop_front,"在头部插入"是 push_front。
遍历中想删除当前元素的标准写法:
1 | |
需要 O(n) 拿长度时可遍历统计或改用 std::list(有 size());C++14 起也提供 resize。
1 | |
由于单向,只能前向遍历、没有 size()(为保 O(1) 接口而省略)、操作基于"前驱"而非"当前节点"(因为删除当前节点需要修改前驱的指针)。
std::random
C++11 的 <random> 是对 C 风格 rand() 的彻底升级。rand() 问题重重:分布不均、周期短、全局状态非线程安全、% 取模引入模偏差。新库把"随机源"与"分布"解耦:引擎(如 std::mt19937)产生均匀随机位,分布(如 uniform_int_distribution)把位映射成所需分布。
这套设计质量远超 rand()–Mersenne Twister 周期长达 2^19937-1,分布数学正确(无偏差),引擎可实例化避免全局状态。
为什么要解耦引擎与分布
rand() % 6 取 1-6 的骰子时,若 RAND_MAX + 1 不是 6 的倍数,低位区间的概率偏高,这是模偏差。分离后,uniform_int_distribution 内部做拒绝采样把偏差消掉;normal_distribution 用 Box-Muller 之类的算法从均匀位生成正态值。引擎只管"均匀随机位",分布只管"数学正确的映射",可以自由组合。
random_device 在 Linux 上默认读 /dev/urandom(每次调用有系统调用开销),只用于播种;主流平台可用它播种 mt19937。想复现结果(测试、回放)就固定种子:std::mt19937 gen(42);。
1 | |
引擎对象较重(mt19937 状态约 2.5KB),应在作用域内复用而非频繁构造。安全敏感场景(密码学、token 生成)不要用 <random>——它的输出可预测,应用平台密码学 API。
std::regex
C++11 把正则表达式纳入标准库,支持 ECMAScript、POSIX 等多种语法。它统一了之前各平台各库(Boost.Regex、PCRE)的碎片化,提供编译期/运行期正则、匹配/搜索/替换、迭代器接口。
需注意 <regex> 的实现普遍较慢(尤其 GCC),不适合高频热路径–能用手写字符串算法(find/substr)解决就别上正则。适合一次性解析、配置校验等低频场景。
语法与匹配语义
默认语法是 ECMAScript;std::regex::extended、awk、grep 等可切换。几个关键语义:
regex_match:整串匹配才算成功,适合"验证格式"。regex_search:找第一个匹配子串,适合"提取"。smatch里matches[0]是整体匹配,matches[1]起是捕获组;matches.str(i)/matches.position(i)取内容与位置。
1 | |
regex_iterator 遍历所有匹配(每次解引用是一个 smatch);regex_token_iterator 还能直接取捕获组或"匹配之外的部分"(分割字符串用)。
1 | |
std::regex 构造开销大,应构造一次复用,不要在循环里反复构造。原始字符串字面量 R"(...)" 让写正则时不用双重转义反斜杠。regex_search 找首次匹配,regex_match 要求整串匹配。
范围 for 循环
范围 for 循环遍历可迭代范围(容器、初始化列表、数组),消除了手写迭代器/索引的样板与 off-by-one 错误。它等价于对 begin()/end() 迭代器的循环,但意图清晰、不易写错。
展开形式与自定义类型支持
编译器把 for (decl : expr) body 展开为:
1 | |
要点:begin/end 通过 ADL 找(成员函数或自由函数都行),所以任何类型只要提供这对函数就能被范围 for 遍历;范围表达式只求值一次;循环变量每轮都重新声明。注意临时容器的坑:for (auto x : make_vec()) 在 C++11/14 是未定义行为(临时在循环体前已析构),C++17 起通过延长 auto&& 的生命周期修复。
1 | |
关键:遍历非 trivial 类型(string、自定义类)时用 const auto& 避免拷贝;要修改元素用 auto&;只在元素是廉价值类型(int 等)时用 auto(按值)。遍历过程中不要修改容器结构(增删元素会使迭代器失效)。
初始化列表
C++11 之前,初始化语法碎片化:内置类型用 =,数组用 {},对象用 (),容器只能逐个 push_back,动态数组根本没法初始化。C++11 引入统一初始化——用花括号 {} 统一初始化一切:变量、对象、容器、成员、堆分配、返回值、函数参数。
花括号初始化的形式
花括号可以带 =,也可以不带,两者基本等价(不带 = 的称为直接列表初始化,能调用 explicit 构造函数)。
1 | |
数组初始化的细节
数组是花括号初始化最古老的用户(C 语言就有),但 C++11 后它有三种等价形式,且规则比看上去细。
1 | |
关键规则:
- 未列出的元素零初始化:
c的第三个元素是0,不是垃圾值。只要用了花括号,数组就不会有未初始化的元素;而int a[3];在局部作用域是未初始化的(静态/全局数组则默认零初始化)。 {}是惯用清零写法:int a[3]{};与int a[3] = {};等价,全部置零,取代memset。- 窄化检查对
= {...}同样生效:带=的花括号仍是列表初始化,int a[2] = {3.14, 2}编译错误。 - 大小省略时按元素个数推导;显式给出且小于列表长度则报错,大于则余下补零。
字符数组初始化自字符串字面量时,末尾的 '\0' 计入大小:
1 | |
(C 允许 char s3[3] = "abc" 并丢弃 '\0',C++ 不允许。)
嵌套数组按聚合体逐层初始化:
1 | |
省略内层花括号(括号省略)按顺序填充,编译器不检查行列对齐,易错,建议保留分组。std::array 按同样规则做聚合体初始化:std::array<int, 3> a = {1, 2}; 第三个元素零初始化。按索引指定初始化(int a[3] = {[1] = 2})是指派初始化器,要到 C++20 才进入语言。
std::initializer_list
容器之所以能写 vec = {1, 2, 3},靠的是 std::initializer_list<T>。编译器遇到同类型的花括号列表时,会在列表出现的语句处生成一个隐藏的 const T[N] 临时数组,把元素逐个拷贝进去,再构造一个指向该数组的轻量视图(本质是两个字段:首指针 + 长度)传给构造函数。
1 | |
底层数组的物化过程有几个关键细节:
- 元素逐个拷贝初始化:数组是
const T[N],长度编译期已知。元素写右值(临时对象)时可直接在数组元素上构造,写左值则要先拷贝一次。 - 存储位置跟随语境:局部作用域里通常是栈上临时;静态初始化语境则放静态存储区。任何情况下都不涉及堆分配。
- 容器侧还有一次拷贝:构造函数要把元素从
const数组拷进自己的存储,且因为元素是const,这一步无法优化成移动–vector<string> v{...}的每个元素至少"构造一次 + 拷贝一次"。 - 嵌套列表是逐层构造,不是嵌套数组:
map<string, int> m{{"a", 1}, {"b", 2}}里,内层{"a", 1}直接在外层const pair<const string, int>[2]数组的元素上构造出 pair,map 再把每个 pair 拷进节点–字符串内容至少被构造两次。
使用上有两条铁律:
- 元素只读:底层数组是
const T[N],不能就地修改;拷贝initializer_list只是拷贝视图,不复制元素,多个视图共享同一数组。 - 生命周期绑定列表对象:C++17 起标准明确,数组的存活期与它所初始化的
initializer_list对象一致;但拷贝列表不会延长数组寿命。因此把列表存入成员、或从函数返回,视图指向的数组都已在别处销毁:
1 | |
需要"存起来延后使用"的元素列表,应拷贝进 vector(值语义、拥有元素),而不是保存 initializer_list。
优先匹配 initializer_list 构造函数
这是最容易踩的坑:只要类有 initializer_list 构造函数,花括号初始化就优先匹配它,哪怕其他构造函数看起来更合适。
1 | |
括号 () 走普通构造函数,花括号 {} 优先走 initializer_list 构造函数–语义完全不同。只有当 initializer_list 构造函数无法匹配(元素类型不符等)时,编译器才回退到普通构造函数。
禁止窄化转换
花括号初始化在编译期拒绝可能丢数据的隐式转换,这是它相对 =/() 初始化的安全优势。
1 | |
判定规则:浮点转整数、大范围整数转小范围整数、双精度转单精度等都是窄化;常量表达式若目标类型装得下则放行(char c{65} 合法)。
解决 most vexing parse
C++ 的"最烦人的解析":Widget w(); 会被解析为"返回 Widget 的函数声明",而不是默认构造对象。花括号没有这种歧义。
1 | |
使用建议
- 容器装已知元素列表 -> 用
{}(要的就是initializer_list)。 - 调用特定构造函数(如
vector(n, val)预分配)-> 用(),明确绕过initializer_list。 - 内置类型、聚合体、需要窄化检查 -> 用
{}。 - 模板代码中对类型参数 T 初始化要小心:
T t{args...}和T t(args...)语义可能不同,保持约定一致。
nullptr
nullptr 是类型安全的空指针常量,类型为 std::nullptr_t,可隐式转换为任意指针类型,但不转换为整型。它取代了有歧义的 NULL(在 C 里 NULL 常被定义为 0 或 (void*)0,在 C++ 里通常是 0)。
NULL 的根本问题:func(NULL) 在 void func(int*) 与 void func(int) 重载时,NULL 作为 0 会匹配 func(int)——出乎意料。nullptr 明确表示指针,无歧义地匹配指针重载。
1 | |
现代 C++ 一律用 nullptr,不用 NULL 或 0 表示空指针。模板推导时 nullptr 的类型 nullptr_t 也比 0 更准确。
nullptr 的类型规则
std::nullptr_t 与整型之间没有隐式转换,指针之间的转换是单向的:
1 | |
它自己可以比较(nullptr == nullptr 为 true);sizeof(nullptr_t) 通常与指针相同。
constexpr
constexpr 表示"可在编译期求值",让常量计算、表格生成、模板参数等从运行期挪到编译期。相比 C 风格 #define 和 const,constexpr 既类型安全又能用于更多语境(数组大小、模板参数、static_assert)。
C++11 的 constexpr 限制较严:函数体只能有一条 return(递归实现)。C++14 起大幅放宽,C++23 进一步扩展。但即便在 C++11,constexpr 递归已能做可观的编译期计算。
1 | |
constexpr 函数既可在编译期(用于常量语境)也可在运行期调用——是否编译期取决于调用语境。若要求强制编译期,C++20 起用 consteval。constexpr 变量是真正的编译期常量,不像 const 只读不一定是编译期。
C++11 constexpr 的限制
1 | |
限制清单:函数体单条 return;无局部变量、无循环;参数与返回值必须是字面值类型;递归深度受编译器默认限制(clang 约 512 层)。传运行期变量调用同一函数则退化为普通调用–这正是"是否编译期取决于语境"。
constexpr 构造函数让用户类型也能进编译期:
1 | |
类型别名
using 是 typedef 的现代替代,语法从右到左更直观,且支持模板别名——typedef 无法做到。模板别名让"带部分参数的模板"成为一等公民,极大简化了模板元编程与复杂类型的可读性。
1 | |
using 与 typedef 语义等价(都不引入新类型,只是别名),但 using 语法更清晰(using Name = Type 像赋值)且能模板化。新代码一律用 using。
模板别名的威力
模板别名与"typedef 组合"不同:它接受模板参数并延迟展开,可在元编程里当"返回类型的函数"用。C++14 标准库据此补齐了 _t 后缀:
1 | |
别名不创建新类型,重载决议与原类型完全一致;这也意味着错误信息里仍会出现原始长类型。
委托构造函数
委托构造允许一个构造函数调用同类的另一个构造函数,消除多个构造函数间重复的成员初始化代码。之前要么每个构造函数各写一遍初始化列表、要么抽个 init() 函数(后者不能用于 const 成员/引用成员初始化)。
1 | |
注意:委托构造后,初始化列表不能再初始化其他成员(委托即把初始化完全交给目标构造函数);构造函数体可在委托调用之后执行。避免循环委托(A 委托 B、B 委托 A)。
1 | |
委托目标完成初始化与函数体后才回到委托者的函数体;若委托目标构造中抛异常,委托者的函数体不会执行。
override 和 final
override 显式声明"此虚函数重写基类的虚函数",让编译器检查签名是否真的匹配——若基类没有匹配的虚函数(拼错名字、签名不一致、基类非虚),编译报错。这抓住了 C++ 最常见的继承 bug 之一:以为重写了其实没有(静默创建了一个新函数)。
final 禁止进一步重写(用于虚函数)或禁止进一步继承(用于类),表达"此处就是终态"的设计意图,有时还能助优化(去虚化)。
1 | |
现代 C++ 规约:凡重写虚函数必加 override——零成本地获得编译期检查。final 谨慎使用,过度使用会妨碍扩展;但在叶子类或安全敏感的虚函数上用它防误重写很合理。
override 检查的匹配项:函数名、参数类型列表、cv 限定、引用限定、noexcept 是否一致;返回类型要求协变。任何一个不符(包括 const 漏写)都会从"重写"降级为"新函数",加 override 后这类错误在编译期暴露。基类函数没有 virtual 而派生类标 override 也报错。
enum class
enum class(强类型枚举)解决了裸 enum 的两大问题:枚举值会隐式转为 int(类型不安全)、枚举值泄漏到外层作用域(命名冲突)。
enum class 的枚举值不会隐式转整型(必须 static_cast)、不会泄漏作用域(必须 Color::Red 限定)、可以指定底层类型(enum class Color : uint8_t)。这让枚举真正安全。
1 | |
C++23 起取底层值用 std::to_underlying(c) 取代啰嗦的 static_cast。枚举转名字的 switch 末尾可用 std::unreachable() 标记不可达分支。新代码一律用 enum class,不用裸 enum。
底层类型与前置声明
1 | |
不指定底层类型时,由实现选择能装下所有枚举值的整型。指定底层类型后可以前置声明枚举:enum class Color : uint8_t;–两个头文件要互引枚举而不知道对方枚举值时非常有用,裸 enum 做不到(必须见到完整定义)。
位标志用法
1 | |
静态断言
static_assert 在编译期断言,条件为假时编译报错并输出消息。它让"对类型/常量的假设"在编译期就暴露,而非运行期崩溃。常用于模板约束、平台假设、布局检查。
1 | |
C++17 起 static_assert 的消息可省略(static_assert(cond);)。与运行期 assert 区别:static_assert 在编译期、NDEBUG 无关;assert 在运行期、NDEBUG 下被移除。模板中 static_assert 是早期做概念约束的常见手段(C++20 Concepts 取代了大部分这种用法)。
模板里的惯用形态是"检查类型属性并给出可读错误":
1 | |
C++11 <type_traits> 与 static_assert 是一对搭档;C++17 的 if constexpr 让很多"断言 + 分支"能直接在编译期选择代码路径。
变参模板
变参模板接受任意数量、任意类型的模板参数(参数包 Args...),让"类型安全的可变参数"成为可能。它取代了 C 风格 printf 的可变参数(无类型检查)和 Java 风格 Object...(类型擦除),是实现 make_shared、tuple、function 等的基础设施。
C++11 时代展开参数包靠递归模板(定义一个基础 case + 一个递归 case)。C++17 折叠表达式大幅简化了常见模式。
1 | |
变参模板的递归展开较繁琐,C++17 折叠表达式能省掉大部分递归样板。转发参数包用 std::forward<Args>(args)...(完美转发)保持值类别。sizeof...(Args) 取参数个数。
sizeof… 与模式展开
参数包的三种展开位置:模板实参列表(tuple<Args...>)、函数实参列表(f(args...))、初始化列表({args...})。sizeof...(pack) 是编译期常量,配合数组技巧可强制求值顺序:
1 | |
C++11 里这个"逗号 + 数组"技巧是替代折叠表达式的标准手段;包为空时数组大小为 0,需补一个哨兵元素({0, (process(args), 0)...})。
emplace_back 的实现骨架
变参模板最著名的应用就是完美转发构造:
1 | |
这里 Args&&... 每个都是转发引用,std::forward<Args>(args)... 逐元素保持值类别,把"多参数原地构造"的能力透传给底层容器。
常用特性总结
最常用的 C++11 特性:
auto类型推导- Lambda 表达式
- 智能指针(
unique_ptr,shared_ptr) - 范围 for 循环
nullptroverride关键字enum class






