在 C++ 的发展历程中,2011 年发布的 C++11 是改动规模最大的一次升级,通常被看作传统 C++ 与 “现代 C++” 的分水岭。
前面我们学过的自动类型推导、范围循环、专用空指针、匿名函数和智能指针等高频语法,其实都是从 C++11 开始加入标准的。把这些知识点集中梳理一遍,以后我们在阅读旧项目代码或排查版本问题时会清晰很多。
C++11 新特性概览
在 C++11 中,标准委员会不仅大幅简化了日常高频语法,还一次性补齐了智能指针、移动语义、哈希容器与多线程等标准库工具。其中最常用的代表性特性如下表所示。
| 代表特性 | 所属类别 / 头文件 | 主要变化与作用 |
|---|---|---|
| 列表初始化 | 语言特性 | 支持使用花括号 {} 统一初始化变量、数组与 STL 容器 |
| auto 类型推导 | 语言特性 | 由编译器根据初始值自动推导变量类型,省去冗长的类型声明 |
| 范围 for 循环 | 语言特性 | 直接遍历数组或容器中的元素,无需手动管理下标或迭代器 |
| nullptr 空指针 | 语言特性 | 提供专门的空指针字面量,避免整数 0 与空指针混用引发歧义 |
| 强类型枚举(enum class) | 语言特性 | 限定枚举成员的作用域,并禁止隐式转换为整数 |
| Lambda 表达式 | 语言特性 | 在使用位置就近定义匿名函数,常用于自定义排序或筛选条件 |
| 右值引用与移动语义 | 语言特性 / <utility> | 引入 && 与 std::move(),支持直接转移底层资源而非深拷贝 |
| 智能指针 | <memory> | 提供 unique_ptr、shared_ptr、weak_ptr 自动管理堆内存释放 |
| override 与 final | 语言特性 | 在编译期检查虚函数重写是否正确,或禁止继续重写与继承 |
| constexpr | 语言特性 | 支持在编译期计算常量表达式的值,减少运行期计算开销 |
| 标准多线程库 | <thread> / <mutex> 等 | 在标准库中内置跨平台的线程、互斥锁与原子变量等并发工具 |
| 新增容器与工具库 | <array> / <unordered_map> 等 | 新增 array、tuple、unordered_map、chrono 时间库与 random 随机数库 |
提示 上表只是方便小伙伴们按需查阅,不需要死记硬背每个特性属于哪个版本。目前主流编译器默认都已开启 C++14 或更高标准,只有在维护早期旧项目并遇到版本报错时,才需要手动加上 -std=c++11 编译参数。
日常基础语法改进
在早期的 C++98 时代,哪怕只是给容器赋几个初始值、遍历里面的元素,或者把指针设为空,写起来都比较繁琐。C++11 对这些高频基础语法做了大幅简化,主要体现在以下两个方面。
1. 列表初始化、auto 与范围 for 循环
在 C++11 中,我们可以直接使用花括号({})给 std::vector 等容器赋初值,不再需要一行一行地调用 push_back()。同时配合 auto 关键字,编译器还能根据初始值自动推导出变量类型。
当我们需要遍历容器元素时,也不必再手写一长串迭代器类型,直接使用范围 for 循环就能从头到尾依次读取。下面我们来看一个例子。
示例 1:初始化与遍历容器
#include <iostream>
#include <vector>
int main() {
// 花括号列表初始化
std::vector<int> nums = {10, 20, 30, 40};
auto count = nums.size(); // 自动推导类型
std::cout << "元素个数:" << count << '\n';
std::cout << "容器元素:";
for (int n : nums) {
std::cout << n << ' ';
}
std::cout << '\n';
return 0;
}运行结果如下。
元素个数:4
容器元素:10 20 30 40分析:
在这个例子中,我们创建 nums 时直接通过 {10, 20, 30, 40} 填入了 4 个初始整数。下一行 auto count = nums.size(); 会根据右侧返回值,自动将 count 推导为无符号整型 std::size_t。
接着,for (int n : nums) 会依次取出 nums 中的每个元素并赋给变量 n。对比 C++11 写法与早期的 C++98 写法,下面 2 种方式是等价的。
// 方式 1(推荐)
std::vector<int> nums = {10, 20, 30, 40};
for (int n : nums) {
std::cout << n << ' ';
}
// 方式 2
std::vector<int> nums;
nums.push_back(10);
nums.push_back(20);
nums.push_back(30);
nums.push_back(40);
std::vector<int>::iterator it;
for (it = nums.begin(); it != nums.end(); ++it) {
std::cout << *it << ' ';
}对比可以看出,早期的方式 2 光声明一个迭代器类型 std::vector<int>::iterator 就要敲二十多个字符(如果换成 C++11 的 auto,直接写 auto it = nums.begin(); 就行了)。而使用方式 1 的花括号初始化配合范围 for 循环,短短几行代码就能搞定,可读性高出了不少。
2. 空指针字面量(nullptr)
在早期的 C++ 代码中,表示空指针通常使用宏 NULL,但 NULL 在底层其实就是整数 0 的宏定义。如果遇到同时接收整数 int 和指针 int* 的重载函数,传入 NULL 会被编译器当成整数 0,结果调错了函数。
为了解决这个混淆问题,C++11 引入了专门的空指针字面量 nullptr。它的类型是独立的 std::nullptr_t,可以自动转换为任意类型的指针,但不会再被当成普通整数。请看下面例子。
示例 2:区分空指针与整数 0
#include <iostream>
void check(int n) {
std::cout << "调用整数版本:" << n << '\n';
}
void check(int* p) {
if (p == nullptr) {
std::cout << "调用指针版本:空指针" << '\n';
}
}
int main() {
check(0); // 传入整数 0
check(nullptr); // 传入空指针
return 0;
}运行结果如下。
调用整数版本:0
调用指针版本:空指针分析:
check(0); 表示传入整数 0,因此匹配的是 void check(int n)。而 check(nullptr); 传入的是专用空指针字面量,编译器会准确匹配指针重载版本 void check(int* p)。
在现代 C++ 开发中,小伙伴们只需要记住这么一个规则:凡是需要表示空指针的地方,统一使用 nullptr,不要再使用旧式的 NULL 或 0。
Lambda 表达式
如果我们调用 std::sort()、std::find_if() 等标准库算法指定自定义规则,在函数外面单独定义一个普通函数往往会把逻辑拆得上下分散。
在 C++11 中,我们可以使用 Lambda 表达式在调用算法的位置直接写出匿名函数,随写随用,逻辑一目了然。
示例 3:Lambda 降序排列
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> nums = {5, 2, 8, 1, 9};
// 按从大到小降序排列
std::sort(nums.begin(), nums.end(), [](int a, int b) {
return a > b;
});
std::cout << "降序结果:";
for (int n : nums) {
std::cout << n << ' ';
}
std::cout << '\n';
return 0;
}运行结果如下。
降序结果:9 8 5 2 1分析:
在这个例子中,我们给 std::sort() 传入的第 3 个参数就是一个 Lambda 表达式。其中开头的方括号 [] 是捕获列表,圆括号 (int a, int b) 是参数列表,花括号内的 return a > b; 则是具体的比较规则。
默认情况下,std::sort() 会按从小到大的升序排列。这里写出 return a > b; 表示当左边的数 a 大于右边的数 b 时排在前面,最终就会得到从大到小的降序结果。
智能指针与移动语义
除了简化日常语法,C++11 在动态内存管理与资源传递上也带来了两项关键改进:
- 智能指针:在
<memory>头文件中提供了 std::unique_ptr、std::shared_ptr和std::weak_ptr,让堆内存随着对象销毁自动释放,大幅减少了手动new和delete引发的内存泄漏。 - 右值引用与移动语义:引入了右值引用(&&)与 std::move() 函数,支持将不再使用的对象内部的底层资源直接 “移交” 给新对象,省去了重复深拷贝的内存开销。
下面我们来看一个智能指针与移动语义配合使用的例子。
示例 4:转移智能指针所有权
#include <iostream>
#include <memory>
#include <string>
#include <utility>
int main() {
// 创建独占智能指针管理字符串
std::unique_ptr<std::string> p1(new std::string("lvyenet"));
// 将所有权从 p1 转移给 p2
std::unique_ptr<std::string> p2 = std::move(p1);
if (p1 == nullptr) {
std::cout << "转移后 p1 已变为空指针" << '\n';
}
std::cout << "p2 接管的内容:" << *p2 << '\n';
return 0;
}运行结果如下。
转移后 p1 已变为空指针
p2 接管的内容:lvyenet分析:
在上面例子中,std::unique_ptr 独占所管理的字符串,不允许直接写成 p2 = p1; 复制。当我们调用 std::move(p1) 时,p1 内部的堆内存指针会直接移交给 p2,同时 p1 自身变为空指针 nullptr,等函数结束时由 p2 自动释放内存。
可能有些小伙伴会问:“前面学习智能指针时不是推荐使用 std::make_unique() 吗?为什么这里写的是 new?” 其实原因很简单,C++11 当时只引入了 std::unique_ptr 类模板,配套的 std::make_unique() 是在随后的 C++14 中才补上的。
新增容器、时间库与多线程支持
在标准库方面,C++11 还补充了一批现代项目开发常用的工具组件,主要分为以下 3 类:
- 新增 STL 容器:提供了固定长度的
std::array、基于哈希表实现快速查找的 std::unordered_map 与std::unordered_set,以及能打包多个不同类型数据的元组std::tuple。 - 时间与随机数库:引入了高精度计时的
<chrono>时间库与<random>随机数库,我们不再需要依赖早期 C 语言中精度有限、分布不够均匀的time()和rand()。 - 跨平台多线程库:首次在标准库中内置了
<thread>、<mutex>、<condition_variable>、<atomic>和<future>,我们编写同一套并发代码就能在不同操作系统上直接编译运行。
传统 C++ 与 “现代 C++” 的分水岭
很多初学的小伙伴会好奇:为什么大家常把 C++11 称为 “现代 C++” 的起点?因为在早期的 C++98 和 C++03 时代,我们经常要手写冗长的迭代器类型,还得频繁使用裸指针配合 new 和 delete 管理内存,稍不留神就会引发内存泄漏或悬空指针。
到了 C++11,这门语言正式确立了 “用智能指针和移动语义管理资源,用 auto、范围 for 和 Lambda 简化日常表达” 的现代开发方式。后面发布的 C++14、C++17、C++20 等版本,都是在这套基础上继续完善语法和扩充标准库的。
