C++11 现代特性精华总结:从原理到实战
目录:
1. C++11 简介
C++11 是 C++ 发展史上的一个里程碑(“Modern C++”的开端)。相比于 C++98/03,它不仅修正了约 600 个缺陷,更引入了大量新特性。其核心目标是:提高开发效率(少写代码)、提升程序性能(移动语义)、增强语言安全性。
2. 列表初始化
2.1 {} 初始化
在 C++98 中,我们可以用 {} 初始化数组,但对象通常需要 () 或 =。C++11 扩大了 {} 的适用范围,使其成为统一初始化(Uniform Initialization)。
-
特点:可以使用
{}初始化一切,甚至可以省略=。
struct Point { int x, y; };
int main() {
int x = 1;
int y = {2}; // C++11 ok
int z{3}; // C++11 ok,省略赋值号
int arr[]{1, 2, 3};
Point p{1, 2}; // 初始化结构体
return 0;
}
2.2 std::initializer_list
为什么容器(如 std::vector)可以像数组一样用 {} 初始化?因为 C++11 引入了 std::initializer_list。它是一个轻量级的模板类,用于接收花括号列表。
// 模拟 vector 的构造接收
template<class T>
class MyVector {
public:
MyVector(std::initializer_list<T> l) {
for (auto e : l) {
// 插入数据...
std::cout << e << " ";
}
}
};
MyVector<int> v = {10, 20, 30}; // 调用 initializer_list 构造函数
3. 变量类型推导
auto:让编译器在编译期根据右边的值推导变量类型。最常见于替代冗长的迭代器类型。
decltype:推导表达式的类型,但不计算表达式的值。常用于模板编程中推导返回值类型。
std::map<std::string, std::string> dict;
// C++98 写法
std::map<std::string, std::string>::iterator it = dict.begin();
// C++11 写法
auto it_new = dict.begin();
// decltype 示例
int x = 0;
decltype(x) a = 20;
cout << typeid(a).name() << endl; // 输出Int
4. 范围 for 循环
这是为了配合 STL 容器而生的语法,底层实现依赖于迭代器(begin() 和 end())。
std::vector<int> v = {1, 2, 3, 4};
// 想要修改元素,使用引用 &
for (auto& e : v) {
e *= 2;
}
5. final 与 override
这两个关键字极大地增加了继承体系的安全性。
final:
修饰类:该类不能被继承。
修饰虚函数:该虚函数不能被重写。
override:
修饰子类的虚函数,强制编译器检查是否真的重写了父类虚函数(避免拼写错误或参数不匹配导致的隐藏 bug)。
final:
class Base1 final {
// 类定义
};
// 以下代码会导致编译错误,因为 Base 是 final 类
class Derived1 : public Base {
//
};
class Base2{
public:
void virtual func() final // 无法被派生类重写
{
//...
}
};
class Derive2 : public Base2{
public:
void func() override // 重写失败
{
// ..
}
};
override:
class Base {
public:
virtual void fun1() const;
};
class Derived : public Base {
public:
void fun1() override; // 编译错误,缺少 const
};
6. 右值引用与移动语义(核心难点)
这是 C++11 性能提升的关键。
6.1 左值引用与右值引用
-
左值(Lvalue):可以取地址、有名字的值(如变量、解引用的指针)。
-
右值(Rvalue):不能取地址、通常是匿名的值(如字面量
10、表达式返回值x+y、函数返回的临时对象)。
// 左值引用
int x = 1;
int& y = x;
Point p1{ 1,2 };
Point& p2 = p1;
// 右值引用
int &&a = 10;
int &&b = (1 + 2);
int&& c = Sub(1, 2);
6.2 左值引用与右值引用比较
左值引用通常用于给对象起别名,减少拷贝。
右值引用主要用于识别临时对象,从而实现移动语义。
6.3 右值引用使用场景和意义:移动语义
在 C++11 之前,如果一个对象包含指针(如 string 或 vector),发生赋值或传参时需要进行深拷贝。如果这个对象是临时的(即将销毁),深拷贝就是极大的浪费。
移动语义允许我们将临时对象的资源(指针所有权)“窃取”过来,而不是拷贝。
这里使用自己封装的string来演示这个效果:
当没有移动拷贝和移动赋值时:会白白多进行了两次的深拷贝

增加移动构造和移动赋值后,此时减少了两次深拷贝和两个临时变量的创建,以及两个临时tmp变量的析构。
6.4 右值引用引用左值及 std::move
如果想把一个左值当做右值使用(比如我确定这个变量后面不用了,想把它移动给别人),使用 std::move。 注意: std::move 不移动任何东西,它只是将类型强制转换为右值引用,以触移动构造函数。

6.5 完美转发
在模板编程中,我们希望函数模板将参数“原封不动”地传递给下一层函数(左值仍是左值,右值仍是右值)。
-
万能引用(Universal Reference):模板中的
T&&不代表右值引用,而是万能引用,既能接左值也能接右值。 -
std::forward:保持参数原本的属性。
当使用万能引用接受传参,参数原本属性是右值,但是为了能够转移右值资源,此时这个参数会退化成左值,如果没有使用完美转发,它的下一层函数调用会默认将这个参数视为左值。

增加完美转发,保留参数原本的属性,此处传参右值确保调用的是对应的右值引用函数。

万能引用与完美转发一般会用在一些插入数据的函数中,让右值数据使用移动构造,减少一些深拷贝。
使用案例:
template<class T>
struct ListNode
{
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
T _data;
};
template<class T>
class List
{
typedef ListNode<T> Node;
public:
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
void PushBack(T&& x)
{
//Insert(_head, x);
Insert(_head, std::forward<T>(x));
}
void Insert(Node* pos, T&& x)
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = std::forward<T>(x); // 关键位置
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
Node* _head;
};
7. 可变参数模板
允许模板定义中包含任意数量的参数。
用于实现类似 printf 的函数:
// 匹配最后一次参数包为0个参数
void ShowList()
{}
template<class T,class ...Args>
void ShowList(T value, Args... args)
{
cout << value << " ";
ShowList(args...); // 递归调用,参数包不断变小
}
ShowList(1, "hello", 3.14); // 打印 1 hello 3.14
初次之外,可变模板参数的最大用处在于:在 C++98 中,push_back 只能接受一个参数(容器存储的类型对象)。如果你想放一个对象进去,通常需要先在外部构造好,然后再拷贝/移动进去。
而emplace_back 的目标是:“你把构造函数的参数传给我,我替你在容器内部直接构造。” 因为对象的构造函数参数个数和类型是不确定的,所以必须使用可变参数模板。
(1)为了证明参数包可以直接构造对象,我们定义一个包含不同构造函数的类 A,然后写一个工厂函数来构造它。
using namespace std;
class A {
public:
int _x;
string _s;
// 构造函数 1:无参
A() : _x(0), _s("default") {
cout << "调用无参构造" << endl;
}
// 构造函数 2:两个参数
A(int x, string s) : _x(x), _s(s) {
cout << "调用双参构造: " << x << ", " << s << endl;
}
// 构造函数 3:拷贝构造
A(const A& other) : _x(other._x), _s(other._s) {
cout << "调用拷贝构造" << endl;
}
// 移动构造
A(A&& other) noexcept : _x(other._x), _s(std::move(other._s)) {
cout << "调用移动构造" << endl;
}
};
// --- 工厂函数 ---
template <typename... Args>
A createA(Args&&... args) {
// 必须使用 forward 保持参数属性,... 用于解包
return A(std::forward<Args>(args)...);
}
int main() {
cout << "--- 1. 构造无参对象 ---" << endl;
A a1 = createA();
cout << "\n--- 2. 构造带参对象 ---" << endl;
// 自动匹配到了 A(int, string)
A a2 = createA(10, "hello");
cout << "\n--- 3. 构造拷贝 ---" << endl;
// 传入一个对象,匹配到了 A(const A&)
A a3 = createA(a2);
return 0;
}
(2)为了展示可变参数模板的威力,我们对比 push_back 和 emplace_back。
假设我们有一个复杂的类 Student:
struct Student {
string _name;
int _age;
Student(string name, int age) : _name(name), _age(age) {
cout << "构造 Student" << endl;
}
Student(const Student& s) : _name(s._name), _age(s._age) {
cout << "拷贝构造" << endl;
}
Student(Student&& s) : _name(move(s._name)), _age(s._age) {
cout << "移动构造" << endl;
}
};
场景 1:使用 push_back
std::vector<Student> v;
// 必须先构造一个临时对象,再传进去
v.push_back(Student("张三", 20));
/* 执行流程:
1. Student("张三", 20) -> 调用【构造函数】生成临时对象
2. push_back 接收临时对象 -> 调用【移动构造】 将其放入 vector
3. 临时对象析构
结果:1次构造 + 1次移动 + 1次析构
*/
场景 2:使用 emplace_back(利用可变参数模板)
std::vector<Student> v;
// 直接传递构造函数的参数
v.emplace_back("李四", 22);
/* 执行流程:
1. 可变参数模板捕获 "李四" (const char*) 和 22 (int)
2. 内部直接在 vector 内存中调用 new (addr) Student("李四", 22) -> 【构造函数】
结果:只有 1 次构造!
*/

8. Lambda 表达式
8.1 C++98 的痛点
在 C++98 中,为了使用 std::sort 自定义排序,我们需要单独写一个函数或者一个仿函数类,非常繁琐且破坏代码连贯性。
8.2 Lambda 语法
Lambda 就是一个匿名函数对象。 语法格式:[capture-list] (parameters) mutable -> return-type { statement }
[=]:按值捕获所有外部变量。
[&]:按引用捕获所有外部变量。
[this]:捕获当前类指针
int a = 10, b = 20;
// 简单的加法 lambda
auto add = [](int x, int y) { return x + y; };
// 捕获外部变量交换
auto swap_val = [&a, &b]() {
int temp = a; a = b; b = temp;
};
8.3 函数对象与 Lambda
底层原理:Lambda 表达式在编译时,编译器会通过生成一个匿名类来实现。
[]中的捕获列表,对应匿名类的成员变量(构造函数初始化)。
{}中的函数体,对应匿名类的operator()重载。 因此,Lambda 在汇编层面上和仿函数(Function Object)是一模一样的。
9. 包装器
9.1std::function
C++ 中“可调用对象”太多了:函数指针、仿函数、Lambda。类型各不相同,难以存储在同一个容器中。std::function 是一个能够容纳所有可调用对象的类型安全的包装器。
#include <functional>
int func(int a, int b) { return a + b; }
int main() {
// 包装普通函数
std::function<int(int, int)> f1 = func;
// 包装 Lambda
std::function<int(int, int)> f2 = [](int a, int b) { return a + b; };
// 既然类型统一了,就可以放在 map 里做命令分发表
std::map<string, std::function<int(int, int)>> opFuncMap;
opFuncMap["+"] = f1;
opFuncMap["-"] = f2;
}
9.2std::bind
用于调整可调用对象的参数(参数绑定/偏函数应用)。比如固定某个参数,或者调整参数顺序。
int Sub(int a, int b) { return a - b; }
// 将 Sub 的第一个参数固定为 100
auto sub100 = std::bind(Sub, 100, std::placeholders::_1);
// 调用:sub100(5) 相当于 Sub(100, 5) -> 结果 95
cout << sub100(5) << endl;
结语
技术的学习往往是一个螺旋上升的过程。C++11 的特性繁多且复杂,特别是像右值引用和完美转发这样的概念,初看可能晦涩难懂,但一旦在实际项目中用上一次(比如优化了一次深拷贝,或者写出了一个优雅的回调),你就会感叹它的精妙。
这篇文章是对 C++11 核心特性的一个阶段性梳理,但“纸上得来终觉浅”。建议大家在以后的编码中,尝试用 auto 替代冗长的类型,用 emplace_back 替代 push_back,用 Lambda 替代繁琐的仿函数。只有在不断的写代码、看汇编、调 Bug 的过程中,这些知识点才能真正内化为你的技能。共勉!
更多推荐


所有评论(0)