欢迎光临
我们一直在努力

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 的对比总结
特性autodecltype
作用 根据初始化值推导变量类型 查询表达式的类型
顶层 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)
未经允许不得转载:171主机测评 » C++11 新特性完全指南:从入门到精通
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址