目录:

1. C++11 简介

2. 列表初始化

2.1 {} 初始化

2.2 std::initializer_list

3. 变量类型推导

4. 范围 for 循环

5. final 与 override

6. 右值引用与移动语义(核心难点)

6.1 左值引用与右值引用

6.2 左值引用与右值引用比较

6.3 右值引用使用场景和意义:移动语义

6.4 右值引用引用左值及 std::move

6.5 完美转发

7. 可变参数模板

8. Lambda 表达式

8.1 C++98 的痛点

8.2 Lambda 语法

8.3 函数对象与 Lambda

9. 包装器

9.1std::function

9.2std::bind

结语


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 的过程中,这些知识点才能真正内化为你的技能。共勉!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐