C++11 新特性

在 C++ 的发展历程中,2011 年发布的 C++11 是改动规模最大的一次升级,通常被看作传统 C++ 与 “现代 C++” 的分水岭。

前面我们学过的自动类型推导、范围循环、专用空指针、匿名函数和智能指针等高频语法,其实都是从 C++11 开始加入标准的。把这些知识点集中梳理一遍,以后我们在阅读旧项目代码或排查版本问题时会清晰很多。

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 等版本,都是在这套基础上继续完善语法和扩充标准库的。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号