引言

C++11 是 C++ 语言发展史上的一座里程碑。从 C++98 到 C++11,经历了长达 13 年的蛰伏,C++11 带来了约 140 个新特性,以及对 C++03 标准中约 600 个缺陷的修正。它修复了 C++98/03 中的诸多痛点,引入了一系列革命性的特性,彻底改变了 C++ 的编程范式。可以说,C++11 更像是一门从 C++98/03 中孕育出的新语言

本文将系统地讲解 C++11 中最核心、最常用的特性,从语言特性到标准库扩充,力求全面深入,帮助读者从入门到精通。


第一篇:核心语言特性

一、类型推导:auto 与 decltype

在 C++11 之前,声明变量时必须明确指定其类型,这在处理复杂类型(如迭代器、模板类型)时会非常繁琐。C++11 引入了 auto 和 decltype 两个关键字,让编译器在编译期自动推导变量或表达式的类型。

1. auto —— 自动类型推导

auto 让编译器通过初始化表达式来推导变量的类型。auto 定义的变量必须有初始值

auto x = 42;                    // x 的类型是 int
auto y = 3.14;                  // y 的类型是 double
auto z = "hello";               // z 的类型是 const char*
auto it = vec.begin();          // it 的类型是 std::vector<int>::iterator

auto 的核心价值在于简化代码,特别是在处理 STL 迭代器和模板类型时。例如:

// 没有 auto 的时代
std::vector<std::map<int, std::string>>::iterator it = container.begin();

// 有了 auto 之后
auto it = container.begin();    // 简洁、清晰、不易出错

auto 的类型推导规则

  • auto 会忽略顶层 const(顶层 const 指指针/引用本身是 const,而非指向的对象是 const)。

  • auto 对引用的处理:会剥离引用,推导出被引用对象的类型。

const int ci = 0;
auto ai = ci;        // ai 的类型是 int,const 被忽略

int& ri = x;
auto ar = ri;        // ar 的类型是 int,引用被剥离

如果需要保留 const 或引用,可以显式声明:

const auto& ari = ri;    // ari 的类型是 const int&
2. decltype —— 查询表达式类型

decltype 用于查询表达式的类型,它不会计算表达式的值,只在编译期分析类型。与 auto 不同,decltype 保留顶层 const 和引用

int x = 0;
const int ci = 0;
int& ri = x;

decltype(x) y = x;       // y 的类型是 int
decltype(ci) di = ci;    // di 的类型是 const int
decltype(ri) dr = ri;    // dr 的类型是 int&

decltype 的一个重要用途是在模板中推导函数返回类型

template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
    return t + u;
}

由于 T 和 U 可以是任意类型,我们无法预先知道 + 操作符的结果类型,decltype 可以完美解决这个问题。

decltype 的微妙之处:表达式是否加括号会影响推导结果:

int x = 0;
decltype(x) a = x;      // a 的类型是 int
decltype((x)) b = x;    // b 的类型是 int&(因为 (x) 是左值表达式)
3. auto 与 decltype 的对比总结
特性 auto decltype
作用 根据初始化值推导变量类型 查询表达式的类型
顶层 const 忽略 保留
引用 剥离 保留
必须初始化
典型场景 简化变量声明 模板返回类型推导

二、统一的初始化与列表初始化

1. 大括号初始化 {}

在 C++98 中,只有数组和结构体(POD 类型)可以使用大括号初始化:

int arr[5] = {1, 2, 3, 4, 5};
struct STU { int a; double b; };
STU s = {3, 5.5};

C++11 之后,一切对象都可以用大括号进行初始化,这被称为列表初始化(List Initialization)

int a{2};                    // 直接初始化
int b = {2};                 // 拷贝初始化(赋值符号可省略)
const STU& rs{3, 5.5};       // 引用临时对象
std::vector<int> v{1, 2, 3}; // 容器初始化

大括号初始化在调用容器接口时尤为方便:

std::vector<std::pair<std::string, int>> v;
v.push_back({"hello", 1});   // 直接用大括号构造 pair
2. std::initializer_list

虽然大括号初始化已经很方便,但对于容器的多元素初始化,仍需要语言层面的支持。C++11 引入了 std::initializer_list(初始化列表),并为 STL 容器增加了接受 initializer_list 的构造函数:

std::vector<int> v = {1, 2, 3, 4, 5};
std::list<double> l = {3.1, 5.2, 4.8};
std::map<std::string, int> m = {{"hello", 1}, {"hehe", 3}};

initializer_list 的本质是一个数组,元素存放在栈区,其迭代器是原生指针:

auto il1 = {1, 2, 3, 4, 5};              // 类型为 std::initializer_list<int>
std::initializer_list<int> il2 = {1, 2, 3, 4, 5};

我们也可以自定义支持 initializer_list 的类:

class MyVector {
public:
    MyVector(std::initializer_list<int> list) {
        for (auto it = list.begin(); it != list.end(); ++it) {
            data.push_back(*it);
        }
    }
private:
    std::vector<int> data;
};

MyVector v = {1, 2, 3, 4, 5};  // 调用 initializer_list 构造函数

三、右值引用与移动语义

这是 C++11 中性能优化最重要的特性。要理解右值引用,首先需要理解 C++11 对值类别的重新划分。

1. 左值与右值的重新定义

C++98 中,左值(Lvalue)是可以取地址的表达式,右值(Rvalue)是不能取地址的临时值。C++11 对值类别进行了更精细的划分:

  • 泛左值(glvalue) = 左值(Lvalue)+ 将亡值(xvalue)

  • 右值(Rvalue) = 纯右值(prvalue)+ 将亡值(xvalue)

纯右值(prvalue) :字面量、表达式求值结果、传值返回的函数调用等:

42;                  // 字面量
a + b;               // 表达式结果
std::string("hello"); // 临时对象

将亡值(xvalue) :即将被销毁、但资源可以被“窃取”的对象,如 std::move 的返回值。

2. 右值引用 T&&

右值引用是一种绑定到右值的引用,语法为 T&&。它允许我们直接访问临时对象的资源:

int&& rref = 42;     // 右值引用绑定到字面量

右值引用的核心价值在于移动语义——它让我们能够“窃取”临时对象的资源,而不是复制。

3. 移动语义

在 C++11 之前,传递或返回大型对象(如 std::vectorstd::string)时会产生昂贵的拷贝操作。移动语义允许我们窃取右值对象的资源(如动态分配的内存),避免不必要的拷贝。

为了支持移动语义,C++11 为类引入了两个新的特殊成员函数:

  • 移动构造函数T(T&& other)

  • 移动赋值运算符T& operator=(T&& other)

class MyString {
public:
    // 移动构造函数:窃取 other 的资源
    MyString(MyString&& other) noexcept 
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;   // 将 other 置空
        other.size_ = 0;
    }
    
    // 移动赋值运算符
    MyString& operator=(MyString&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }
private:
    char* data_;
    size_t size_;
};

标准库容器已经实现了移动语义:

std::vector<int> make_large_vector() {
    std::vector<int> vec(1000000, 1);
    return vec;  // 触发移动构造,而非拷贝构造
}

std::vector<int> v = make_large_vector();  // 高效!
4. std::move

std::move 是一个类型转换函数,它将左值强制转换为右值引用,从而允许调用移动语义:

std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);  // 调用移动构造函数
// 此时 v1 处于“有效但未指定”的状态,不应再使用

需要注意的是,std::move 只是进行类型转换,并不真正移动任何东西。真正的移动发生在移动构造函数或移动赋值运算符中。

5. 完美转发(Perfect Forwarding)

完美转发允许我们在模板函数中保持参数的左值/右值属性不变,将其转发给另一个函数。

这需要结合引用折叠规则和 std::forward 实现:

template<typename T>
void relay(T&& arg) {
    process(std::forward<T>(arg));  // 完美转发 arg 到 process
}

void process(int& lval) { /* 处理左值 */ }
void process(int&& rval) { /* 处理右值 */ }

int x = 10;
relay(x);    // 转发左值 → 调用 process(int&)
relay(20);   // 转发右值 → 调用 process(int&&)

引用折叠规则

  • T& & → T&

  • T& && → T&

  • T&& & → T&

  • T&& && → T&&

T&& 在模板中被称为万能引用(Universal Reference) ,它可以绑定到左值或右值,具体取决于模板参数 T 的推导结果。


四、Lambda 表达式

Lambda 表达式允许我们在需要函数的地方直接定义匿名函数。它极大地简化了 STL 算法的使用。

1. 基本语法
[capture](parameters) -> return_type { body }
  • 捕获列表 [] :定义哪些外部变量可以被 lambda 使用

  • 参数列表 () :与普通函数类似

  • 返回类型 -> type :可省略,编译器会自动推导

  • 函数体 {} :实际执行的代码

auto add = [](int a, int b) -> int { return a + b; };
std::cout << add(5, 3) << std::endl;  // 输出 8
2. 捕获列表

捕获列表决定了 lambda 可以访问哪些外部变量:

(1)不捕获任何变量

auto sayHello = []() { std::cout << "Hello, World!" << std::endl; };

(2)按值捕获 [=] :捕获所有外部变量的副本,lambda 内修改不影响外部

int x = 10;
auto f = [x]() { std::cout << x << std::endl; };
x = 20;
f();  // 输出 10(捕获的是副本)

(3)按引用捕获 [&] :捕获所有外部变量的引用,lambda 内修改会影响外部

int x = 10;
auto f = [&x]() { x += 10; };
f();
std::cout << x << std::endl;  // 输出 20

(4)混合捕获:可以指定某些变量按值、某些按引用

int a = 5, b = 10;
auto f = [a, &b]() { 
    // a 按值捕获(只读),b 按引用捕获(可修改)
    b = 20;
};

(5)捕获 this 指针:在类成员函数中,可以捕获 this 以访问成员变量

class MyClass {
    int value = 42;
public:
    void func() {
        auto f = [this]() { return value; };  // 捕获 this
    }
};
3. 可变 Lambda(mutable

默认情况下,按值捕获的变量在 lambda 内是只读的。使用 mutable 可以修改副本:

int x = 10;
auto f = [x]() mutable { x += 10; return x; };
std::cout << f() << std::endl;  // 输出 20
std::cout << x << std::endl;    // 输出 10(外部变量未变)
4. Lambda 在 STL 算法中的应用

Lambda 最常见的用途是在 STL 算法中作为谓词操作函数

std::vector<int> v = {5, 2, 8, 1, 9};

// 排序:按降序
std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });

// 查找第一个大于 5 的元素
auto it = std::find_if(v.begin(), v.end(), [](int x) { return x > 5; });

// 遍历并打印
std::for_each(v.begin(), v.end(), [](int x) { std::cout << x << " "; });

五、智能指针

C++11 引入了 std::unique_ptrstd::shared_ptr 和 std::weak_ptr 三种智能指针,用于自动管理动态内存,防止内存泄漏和悬挂指针。

1. 为什么需要智能指针?

使用裸指针(new/delete)容易出现两类问题:

  • 内存泄漏:忘记调用 delete

  • 悬挂指针:内存已被释放,但仍有指针指向它

智能指针基于 RAII(资源获取即初始化) 原则:在构造时获取资源,在析构时释放资源。

2. std::unique_ptr —— 独占所有权

unique_ptr 是独占所有权的智能指针,不能被拷贝,只能被移动。它是最轻量级的智能指针,适用于单一所有者场景。

std::unique_ptr<int> p1(new int(42));
std::unique_ptr<int> p2 = std::move(p1);  // 转移所有权
// p1 现在为空

// 使用 make_unique(C++14 引入,但推荐使用)
auto p3 = std::make_unique<int>(100);

unique_ptr 不支持拷贝构造和拷贝赋值,但支持移动语义:

std::unique_ptr<int> p1(new int(10));
std::unique_ptr<int> p2 = p1;  // 编译错误!不能拷贝
std::unique_ptr<int> p3 = std::move(p1);  // OK,转移所有权
3. std::shared_ptr —— 共享所有权

shared_ptr 采用引用计数的机制,多个 shared_ptr 可以共享同一个对象的所有权。当最后一个 shared_ptr 被销毁时,对象才会被释放。

// 创建方式
std::shared_ptr<int> p1(new int(42));
std::shared_ptr<int> p2 = std::make_shared<int>(100);  // 推荐方式[reference:67]

// 拷贝构造:引用计数 +1
std::shared_ptr<int> p3(p1);   // p1 和 p3 共享同一块内存

// 移动构造:p4 接管 p3 的资源,p3 变为空
std::shared_ptr<int> p4(std::move(p3));

// 获取引用计数
long count = p1.use_count();   // 2(p1 和 p3)

make_shared vs new:《Effective Modern C++》建议优先使用 make_shared,因为它更高效(一次内存分配)且更安全。

// 不推荐
std::shared_ptr<int> p(new int(42));

// 推荐
auto p = std::make_shared<int>(42);
4. std::weak_ptr —— 弱引用

weak_ptr 是一种弱引用,它不增加引用计数。它主要用于解决 shared_ptr 的循环引用问题。

class B;  // 前向声明

class A {
public:
    std::shared_ptr<B> b_ptr;
};

class B {
public:
    std::weak_ptr<A> a_ptr;  // 使用 weak_ptr 打破循环
};

auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b_ptr = b;
b->a_ptr = a;  // 不会增加引用计数,不会造成内存泄漏

weak_ptr 不能直接访问对象,需要先通过 lock() 获取一个 shared_ptr

std::shared_ptr<A> pa = b->a_ptr.lock();
if (pa) {
    // 对象仍然存在,可以安全使用
}
5. 三种智能指针的选择建议
智能指针 所有权 适用场景
unique_ptr 独占 单一所有者,不需要共享
shared_ptr 共享(引用计数) 多个所有者共享资源
weak_ptr 弱引用 解决循环引用,观察者模式

六、其他重要语言特性

1. nullptr —— 类型安全的空指针

C++98 中使用 NULL(通常定义为 0 或 (void*)0)表示空指针,这在重载时会产生歧义。C++11 引入了 nullptr,它是一个类型安全的空指针常量。

void f(int);
void f(char*);

f(NULL);    // 可能调用 f(int),造成歧义
f(nullptr); // 明确调用 f(char*)
2. override 与 final
  • override:显式标明虚函数重写了基类的虚函数,编译器会检查是否正确重写。

  • final:阻止派生类继续重写该虚函数,或阻止类被继承。

class Base {
public:
    virtual void func() {}
    virtual void test() {}
};

class Derived : public Base {
public:
    void func() override {}    // 正确:重写 Base::func
    void test(int) override {} // 编译错误:没有匹配的基类虚函数
};

class FinalClass final {};     // 不能被继承
3. =default 与 =delete
  • =default:显式要求编译器生成默认的特殊成员函数。

  • =delete:禁止某个特殊成员函数的使用。

class MyClass {
public:
    MyClass() = default;                // 使用编译器生成的默认构造函数
    MyClass(const MyClass&) = delete;   // 禁止拷贝构造
    MyClass& operator=(const MyClass&) = delete;  // 禁止拷贝赋值
};
4. 委托构造函数(Delegating Constructor)

允许一个构造函数调用同一个类的另一个构造函数:

class MyClass {
public:
    MyClass() : MyClass(0) {}           // 委托给带参构造函数
    MyClass(int x) : value_(x) {}
private:
    int value_;
};
5. constexpr —— 编译期常量表达式

constexpr 用于声明在编译期就能计算出结果的函数或变量:

constexpr int square(int x) { return x * x; }
constexpr int result = square(5);  // 编译期计算,result = 25
6. static_assert —— 编译期断言

编译期进行断言检查,若条件为假则编译失败:

static_assert(sizeof(int) == 4, "int must be 4 bytes");
7. 范围 for 循环(Range-based for loop)

提供更简洁的遍历方式:

std::vector<int> v = {1, 2, 3, 4, 5};
for (auto& x : v) {
    x *= 2;  // 修改元素
}
for (const auto& x : v) {
    std::cout << x << " ";  // 只读访问
}
8. 可变参数模板(Variadic Templates)

允许模板接受任意数量的模板参数:

template<typename... Args>
void print(Args... args) {
    // 使用递归或折叠表达式展开
}
9. 外部模板(Extern Template)

用于显式实例化声明,减少编译时间:

extern template class std::vector<int>;  // 不在当前编译单元实例化

第二篇:标准库扩充

一、新容器

1. 无序容器(哈希表)

C++11 引入了基于哈希表实现的关联容器:

  • std::unordered_set / std::unordered_multiset

  • std::unordered_map / std::unordered_multimap

与 set/map 不同,无序容器不保证元素顺序,但查找、插入、删除的平均时间复杂度为 O(1)

std::unordered_map<std::string, int> umap;
umap["apple"] = 5;
umap["banana"] = 3;

for (const auto& p : umap) {
    std::cout << p.first << ": " << p.second << std::endl;
}
2. std::tuple —— 元组

tuple 是 pair 的泛化,可以存储任意数量不同类型的值:

std::tuple<int, double, std::string> t(42, 3.14, "hello");
auto t2 = std::make_tuple(100, 2.718, "world");

// 访问元素
int i = std::get<0>(t);
double d = std::get<1>(t);
std::string s = std::get<2>(t);

// 使用 auto 解包(C++17)
auto [a, b, c] = t;
3. std::forward_list —— 单向链表

相比 std::listforward_list 是单向链表,节省了内存,但只能向前遍历。

std::forward_list<int> fl = {1, 2, 3, 4, 5};
fl.push_front(0);  // O(1)

二、正则表达式库 <regex>

C++11 正式将正则表达式纳入标准库:

#include <regex>

std::string text = "The quick brown fox jumps over the lazy dog";
std::regex pattern("\\b\\w{3}\\b");  // 匹配三个字母的单词

std::smatch matches;
while (std::regex_search(text, matches, pattern)) {
    std::cout << matches[0] << std::endl;
    text = matches.suffix();
}

三、随机数库 <random>

C++11 提供了比 rand() 更强大、更可控的随机数生成设施:

#include <random>

std::random_device rd;                    // 真随机数种子
std::mt19937 gen(rd());                   // 梅森旋转算法
std::uniform_int_distribution<> dis(1, 100);  // 均匀分布

int random_number = dis(gen);  // 生成 1~100 的随机整数

四、时间库 <chrono>

chrono 提供了类型安全的时间处理功能:

#include <chrono>

auto start = std::chrono::high_resolution_clock::now();
// ... 执行某些操作 ...
auto end = std::chrono::high_resolution_clock::now();

auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "耗时: " << duration.count() << " ms" << std::endl;

chrono 的三个核心组件

  • duration:表示一段时间(如 30 秒、1 小时)

  • time_point:表示时间点

  • clock:提供当前时间(如 system_clocksteady_clock


五、并发编程支持

C++11 首次在语言标准中引入了多线程支持

1. std::thread —— 线程
#include <thread>

void worker(int id) {
    std::cout << "线程 " << id << " 开始工作" << std::endl;
}

int main() {
    std::thread t1(worker, 1);
    std::thread t2(worker, 2);
    
    t1.join();  // 等待 t1 结束
    t2.join();  // 等待 t2 结束
    return 0;
}
2. std::mutex 与 std::lock_guard —— 互斥锁
#include <mutex>

std::mutex mtx;
int counter = 0;

void increment() {
    std::lock_guard<std::mutex> lock(mtx);  // RAII 加锁
    counter++;
    // 离开作用域自动解锁
}
3. std::atomic —— 原子操作

原子操作提供无锁的线程安全操作,性能优于互斥锁:

#include <atomic>

std::atomic<int> counter(0);

void increment() {
    counter.fetch_add(1);  // 原子操作,无需加锁
}

六、其他标准库特性

  • std::bind 与 std::function:函数绑定与包装

  • std::move 与 std::forward:移动语义与完美转发的辅助函数

  • std::initializer_list:初始化列表支持(已在第一篇详述)

  • <type_traits>:编译期类型检查与转换


总结

C++11 是一次革命性的更新。它从多个维度彻底改变了 C++ 编程的面貌:

维度 核心改进
语法简洁性 auto、范围 for、Lambda、列表初始化
性能优化 右值引用、移动语义、完美转发
内存安全 unique_ptrshared_ptrweak_ptrnullptr
泛型编程 可变参数模板、decltype、外部模板
并发编程 threadmutexatomic
标准库扩充 无序容器、tupleregexchronorandom

掌握 C++11 的这些特性,是写出现代化、高效、安全的 C++ 代码的基础。无论是日常开发还是算法竞赛,这些特性都会让你事半功倍。

C++11 告诉我们:语言的进化不是为了炫技,而是为了让程序员能够更专注地解决问题,而不是与语言本身的缺陷作斗争。


本文为 C++11 新特性完全指南,后续可针对每个特性进行更深入的专题学习。

更多推荐