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:要从容器取可修改元素需写 auto&,只读用 const auto&。范围 for 循环与 auto 配合是惯用法。
1 | |
陷阱: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 生命周期超出被引用变量——如 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)) 求值顺序导致的泄漏)。weak_ptr 使用前需 lock() 提升为 shared_ptr 检查对象是否仍存活。shared_ptr 控制块的原子操作有开销,热路径上应优先 unique_ptr。
右值引用与移动语义
移动语义让"资源转移"成为一等操作:把昂贵的拷贝(如 vector 拷贝整个堆缓冲)换成廉价的指针交换。它区分了"拷贝"(复制底层资源,源对象不变)与"移动"(转移资源所有权,源对象置空)。
核心机制是右值引用 T&&——绑定到即将销毁的临时对象(右值)。std::move 是个无操作转换,把左值强转为右值引用,表示"我不再需要这个值,你可以掏空它"。移动构造/移动赋值用 T&& 接管资源后把源对象置为有效但未指定状态(通常置空)。
1 | |
关键点:移动操作应标记 noexcept——vector 扩容时只有 noexcept 移动才会真正移动,否则退化为拷贝以保证强异常安全。std::move 后不要再使用源对象(除赋值或销毁外)。返回局部变量时不要 std::move——会阻止 RVO。现代代码里手写移动构造已少见,编译器生成的版本(零规则)通常足够。
std::function 和 std::bind
std::function 是可调用对象的类型擦除包装器:能统一存储函数指针、仿函数、Lambda、成员函数指针等任何可调用对象,只要签名匹配。这让回调、事件处理、策略注入等场景可以把"任意可调用物"当一等对象传递。
代价是有一点运行期开销(类型擦除、可能的堆分配、间接调用),热路径上不如模板或直接 Lambda。C++14 起 Lambda 捕获初始化、C++23 std::move_only_function 进一步削弱了它的地位,但 std::function 仍是"存储任意回调"的标准选择。
1 | |
std::bind 部分应用参数(柯里化),现代代码更倾向用 Lambda 替代——auto add5 = [](int x) { return add(x, 5); }; 更清晰、更易内联、不占额外类型。std::bind 在绑定重载函数或完美转发时容易踩坑,能不用就不用。
std::unordered_map 和 std::unordered_set
基于哈希表的容器,提供平均 O(1) 的查找/插入/删除。与之相对,std::map/std::set 基于红黑树,是有序的 O(log n)。选择标准:需要按键排序遍历时用 map/set,只需要快速查找且不关心顺序时用 unordered_map/unordered_set。
哈希容器的代价:最坏情况 O(n)(哈希冲突,恶意输入可能触发)、无序遍历、对自定义类型需提供 std::hash 特化与 operator==。元素指针/引用在 rehash 时可能失效(不同于 map 的稳定指针)。
1 | |
自定义键类型需特化 std::hash<Key> 并提供 operator==;若想保留插入顺序可配合存一个 vector 记录键的顺序。operator[] 在键不存在时会插入默认值——只想查找时用 find 或 at(后者不存在时抛异常)。
std::tuple
std::tuple 是异质值的固定大小容器:把不同类型的若干值打包成一个对象。相比自定义结构体,它无需命名字段、可用模板泛化处理任意类型组合,适合函数多返回值、临时聚合、编译期元编程。
访问用 std::get<I>(t)(按索引)或 std::get<T>(t)(按类型,要求唯一)。C++17 结构化绑定让它解包更优雅。std::tuple_cat 拼接多个 tuple。
1 | |
权衡:需要语义明确字段名时还是用结构体——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)让字面量更直观。
1 | |
测量代码耗时优先用 steady_clock(不受系统时间调整影响);跨进程/持久化时间用 system_clock。duration_cast 在单位不整除时截断。
std::array
std::array 是 C 风格数组的零开销包装:大小在编译期固定、栈分配、不退化为指针、提供 STL 容器接口(size()、begin()/end()、at()、front()/back())。它取代了既不安全(退化为指针、无边界检查)又缺少接口的裸数组 T[]。
与 std::vector 的区别:array 大小固定、栈分配、无动态扩容开销;vector 大小可变、堆分配。已知大小且不需要增删时用 array,否则用 vector。
1 | |
at() 做边界检查(越界抛 std::out_of_range),operator[] 不检查但更快。std::get<I>(arr) 提供编译期边界检查。array 是值类型——拷贝会复制全部元素,传参用 const& 或 span。
std::forward_list
std::forward_list 是单向链表,相比 std::list(双向)每个节点少存一个指针、更省内存。它提供 O(1) 的头部插入/删除,以及任意位置 O(1) 的"插入/删除后继"操作(insert_after/erase_after)。
适用场景:内存极其紧张、只需单向遍历、频繁头部增删。但要注意现代硬件上链表缓存不友好——元素巨大且确需中间 O(1) 增删时才考虑,否则 vector 往往更快。Core Guidelines 不推荐链表作为默认选择。
1 | |
由于单向,只能前向遍历、没有 size()(为保 O(1) 接口而省略)、操作基于"前驱"而非"当前节点"(因为删除当前节点需要修改前驱的指针)。
std::random
C++11 的 <random> 是对 C 风格 rand() 的彻底升级。rand() 问题重重:分布不均、周期短、全局状态非线程安全、% 取模引入模偏差。新库把"随机源"与"分布"解耦:引擎(如 std::mt19937)产生均匀随机位,分布(如 uniform_int_distribution)把位映射成所需分布。
这套设计质量远超 rand()——Mersenne Twister 周期长达 2^19937-1,分布数学正确(无偏差),引擎可实例化避免全局状态。
1 | |
引擎对象较重(mt19937 状态约 2.5KB),应在作用域内复用而非频繁构造。安全敏感场景(密码学、token 生成)不要用 <random>——它的输出可预测,应用平台密码学 API。
std::regex
C++11 把正则表达式纳入标准库,支持 ECMAScript、POSIX 等多种语法。它统一了之前各平台各库(Boost.Regex、PCRE)的碎片化,提供编译期/运行期正则、匹配/搜索/替换、迭代器接口。
需注意 <regex> 的实现普遍较慢(尤其 GCC),不适合高频热路径——能用手写字符串算法(find/substr)解决就别上正则。适合一次性解析、配置校验等低频场景。
1 | |
std::regex 构造开销大,应构造一次复用,不要在循环里反复构造。原始字符串字面量 R"(...)" 让写正则时不用双重转义反斜杠。regex_search 找首次匹配,regex_match 要求整串匹配。
范围 for 循环
范围 for 循环遍历可迭代范围(容器、初始化列表、数组),消除了手写迭代器/索引的样板与 off-by-one 错误。它等价于对 begin()/end() 迭代器的循环,但意图清晰、不易写错。
1 | |
关键:遍历非 trivial 类型(string、自定义类)时用 const auto& 避免拷贝;要修改元素用 auto&;只在元素是廉价值类型(int 等)时用 auto(按值)。遍历过程中不要修改容器结构(增删元素会使迭代器失效)。
初始化列表
C++11 的统一初始化(花括号 {})试图统一各种初始化语法:变量、对象、容器、成员、堆分配都能用 {}。它还引入了 std::initializer_list,使容器能像 vec = {1, 2, 3} 这样初始化。
一个重要安全特性:花括号初始化禁止窄化转换(如 int x{3.14} 编译错误),而 int x = 3.14 会静默截断。这让初始化更安全。
1 | |
注意"最烦人的解析"(most vexing parse)问题:Widget w(); 会被解析为函数声明,Widget w{} 才是默认构造。花括号在此更明确。但花括号会优先匹配 initializer_list 构造函数——vector<int> v(5, 0) 是 5 个 0,vector<int> v{5, 0} 是元素 5 和 0,行为不同需留意。
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 更准确。
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 只读不一定是编译期。
类型别名
using 是 typedef 的现代替代,语法从右到左更直观,且支持模板别名——typedef 无法做到。模板别名让"带部分参数的模板"成为一等公民,极大简化了模板元编程与复杂类型的可读性。
1 | |
using 与 typedef 语义等价(都不引入新类型,只是别名),但 using 语法更清晰(using Name = Type 像赋值)且能模板化。新代码一律用 using。
委托构造函数
委托构造允许一个构造函数调用同类的另一个构造函数,消除多个构造函数间重复的成员初始化代码。之前要么每个构造函数各写一遍初始化列表、要么抽个 init() 函数(后者不能用于 const 成员/引用成员初始化)。
1 | |
注意:委托构造后,初始化列表不能再初始化其他成员(委托即把初始化完全交给目标构造函数);构造函数体可在委托调用之后执行。避免循环委托(A 委托 B、B 委托 A)。
override 和 final
override 显式声明"此虚函数重写基类的虚函数",让编译器检查签名是否真的匹配——若基类没有匹配的虚函数(拼错名字、签名不一致、基类非虚),编译报错。这抓住了 C++ 最常见的继承 bug 之一:以为重写了其实没有(静默创建了一个新函数)。
final 禁止进一步重写(用于虚函数)或禁止进一步继承(用于类),表达"此处就是终态"的设计意图,有时还能助优化(去虚化)。
1 | |
现代 C++ 规约:凡重写虚函数必加 override——零成本地获得编译期检查。final 谨慎使用,过度使用会妨碍扩展;但在叶子类或安全敏感的虚函数上用它防误重写很合理。
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。
静态断言
static_assert 在编译期断言,条件为假时编译报错并输出消息。它让"对类型/常量的假设"在编译期就暴露,而非运行期崩溃。常用于模板约束、平台假设、布局检查。
1 | |
C++17 起 static_assert 的消息可省略(static_assert(cond);)。与运行期 assert 区别:static_assert 在编译期、NDEBUG 无关;assert 在运行期、NDEBUG 下被移除。模板中 static_assert 是早期做概念约束的常见手段(C++20 Concepts 取代了大部分这种用法)。
变参模板
变参模板接受任意数量、任意类型的模板参数(参数包 Args...),让"类型安全的可变参数"成为可能。它取代了 C 风格 printf 的可变参数(无类型检查)和 Java 风格 Object...(类型擦除),是实现 make_shared、tuple、function 等的基础设施。
C++11 时代展开参数包靠递归模板(定义一个基础 case + 一个递归 case)。C++17 折叠表达式大幅简化了常见模式。
1 | |
变参模板的递归展开较繁琐,C++17 折叠表达式能省掉大部分递归样板。转发参数包用 std::forward<Args>(args)...(完美转发)保持值类别。sizeof...(Args) 取参数个数。
常用特性总结
最常用的 C++11 特性:
auto类型推导- Lambda 表达式
- 智能指针(
unique_ptr,shared_ptr) - 范围 for 循环
nullptroverride关键字enum class








