C++11 新特性完全指南:从入门到精通
引言
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::vector、std::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_ptr、std::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::list,forward_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_clock、steady_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_ptr、shared_ptr、weak_ptr、nullptr |
| 泛型编程 | 可变参数模板、decltype、外部模板 |
| 并发编程 | thread、mutex、atomic |
| 标准库扩充 | 无序容器、tuple、regex、chrono、random |
掌握 C++11 的这些特性,是写出现代化、高效、安全的 C++ 代码的基础。无论是日常开发还是算法竞赛,这些特性都会让你事半功倍。
C++11 告诉我们:语言的进化不是为了炫技,而是为了让程序员能够更专注地解决问题,而不是与语言本身的缺陷作斗争。
本文为 C++11 新特性完全指南,后续可针对每个特性进行更深入的专题学习。
更多推荐



所有评论(0)