这篇文章系统梳理了C++11的核心语法与新特性,适合具备一定C/C++基础、需要进阶现代C++的开发者阅读。本文将通过“是什么、有什么用、底层原理”的逻辑,把原本晦涩的语法转化为直观的开发利器,帮助你彻底掌握这些在日常工程和面试中最常遇到的关键技术点。

一、C++11的发展历史

C++11是C++的第二个主要版本,也是从C++98起最重要的更新。它引入了大量更改,标准化了既有实践,并改进了对开发者可用的抽象。

在最终由ISO于2011年8月采纳前,它曾被称为C++0x,因为人们曾期待它能在2010年之前发布。从C++03到C++11历经了8年,这是迄今最长的版本间隔。此后,C++进入了每3年更新一次的规律周期。
在这里插入图片描述

二、列表初始化

2.1 C++98传统的{}

在C++98中,只有一般的数组和简单的结构体可以用{}进行初始化。

struct Point
{
    int _x;
    int _y;
};

int main()
{
    //C++98传统的聚合初始化
    int array1[] = {1, 2, 3, 4, 5};
    int array2[5] = {0};
    Point p = {1, 2};
    return 0;
}

这种传统方式的局限在于:无法用于自定义的复杂类对象,初始化方式不够统一。

2.2 C++11中的{}

C++11试图实现一切对象皆可用{}初始化,这种机制称为列表初始化

  • 适用范围:内置类型与自定义类型均支持。对于自定义类型,底层会先用列表中的值构造一个临时对象,再去拷贝构造目标对象,最终被编译器直接优化为直接构造。
  • 书写简化:在使用{}初始化时,可以省略掉等号=

使用场景:当对容器调用pushinsert且需要传入多参数构造的对象时,直接传入{}能省去编写匿名对象的冗长代码。

#include<iostream>
#include<vector>
using namespace std;

class Date
{
public:
    //带缺省值的构造函数
    Date(int year = 1, int month = 1, int day = 1)
        :_year(year), _month(month), _day(day)
    {}
private:
    int _year;
    int _month;
    int _day;
};

int main()
{
    //内置类型列表初始化,可省略=
    int x1 = {2};
    int x2{2}; 

    //自定义类型列表初始化
    Date d1 = {2025, 1, 1}; 
    Date d2{2024, 7, 25}; 

    vector<Date> v;
    //直接传入列表即可完成对象的构造并插入,比传匿名对象更简洁
    v.push_back({2025, 1, 1}); 

    return 0;
}

2.3 C++11中的std::initializer_list

{}虽然能解决多参数构造问题,但如果要用任意数量的元素来初始化一个容器(例如初始化一个装有100个数字的vector),重载无数个构造函数显然是不现实的。

为此,C++11引入了std::initializer_list类。

  • 底层逻辑:当编译器遇到一串大括号括起来的值时,会在栈上开辟一个临时数组存下这些值,并用std::initializer_list包装它们。这个类内部仅维护了两个指针,分别指向数组的头和尾。
  • 容器支持:STL中的容器都新增了以std::initializer_list为参数的构造函数和赋值运算符。这使得容器完美支持了变长参数的列表初始化。
#include<vector>
#include<map>
using namespace std;

int main()
{
    //自动推导为std::initializer_list类型
    auto il = {10, 20, 30}; 
    
    //调用vector的initializer_list版本构造函数
    vector<int> v1 = {1, 2, 3, 4, 5}; 
    
    //map初始化,外层是map的列表,内层是pair的列表
    map<string, string> dict = { {"sort", "排序"}, {"string", "字符串"} };
    
    return 0;
}

三、右值引用和移动语义

这是C++11优化性能的核心利器。以前的引用统一被称为左值引用。引用本质都是给对象取别名,但在C++11中,它们被严格区分以榨干每一次多余的内存拷贝开销。

3.1 左值和右值

区分两者的核心标准是能否使用&取地址

  • 左值 (lvalue):指存储在内存中有持久状态的数据变量。可以取地址。它可以出现在等号左边或右边(被const修饰的左值只能在右边,但依然能取地址)。
  • 右值 (rvalue):指字面值常量或表达式求值产生的临时对象。没有固定地址,不能取地址。它只能出现在等号右边。
int main()
{
    //左值:可以取地址
    int b = 1;
    const int c = b;
    string s("111111");

    //右值:不能取地址,都是临时产生的数据
    double x = 1.1, y = 2.2;
    10;
    x + y;
    string("11111");

    return 0;
}

3.2 左值引用和右值引用

  • 左值引用 (&):给左值取别名。不能直接绑定右值,但const左值引用可以绑定右值
  • 右值引用 (&&):给右值取别名。不能直接绑定左值,但可以绑定经过move()强转后的左值

核心易错点:右值引用变量本身拥有固定内存和名称,因此右值引用变量自身的属性是左值

int main()
{
    int b = 1;

    //const左值引用可以绑定右值
    const int& rx1 = 10;

    //右值引用可以绑定move后的左值
    int&& rrx1 = move(b);

    //rrx1绑定了右值,但rrx1作为变量本身是左值,可以直接被左值引用绑定
    int& r2 = rrx1; 

    return 0;
}

3.3 引用延长生命周期

无论是右值引用还是const左值引用,都能将被绑定临时对象的生命周期延长,直到引用本身被销毁。区别在于const左值引用无法修改该对象,而右值引用可以修改。

int main()
{
    std::string s1 = "Test";
    // std::string&& r1 = s1; // 错误:不能绑定到左值
    const std::string &r2 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期
    // r2 += "Test"; // 错误:不能通过到 const 的引⽤修改
    std::string &&r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期
    r3 += "Test";               // OK:能通过到⾮ const 的引⽤修改
    std::cout << r3 << '\n';
    return 0;
}

3.4 左值和右值的参数匹配

C++11之后,函数可以分别重载左值引用和右值引用的形参。编译器会进行精确匹配:实参是左值走左值引用版本,实参是右值走右值引用版本。

#include <iostream>
using namespace std;
void f(int &x)
{
    std::cout << "左值引⽤重载 f(" << x << ")\n";
}
void f(const int &x)
{
    std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}
void f(int &&x)
{
    std::cout << "右值引⽤重载 f(" << x << ")\n";
}

int main()
{
    int i = 1;
    const int ci = 2;
    f(i);            // 调⽤ f(int&)
    f(ci);           // 调⽤ f(const int&)
    f(3);            // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&)
    f(std::move(i)); // 调⽤ f(int&&)
    // 右值引⽤变量在⽤于表达式时是左值
    int &&x = 1;
    f(x);            // 调⽤ f(int& x)
    f(std::move(x)); // 调⽤ f(int&& x)
    return 0;
}

3.5 右值引用和移动语义的使用场景

3.5.1 左值引用主要使用场景回顾

在C++98中,如果函数需要在内部构造一个局部大对象(如巨大的vector)并返回,只能通过传值返回。局部对象销毁前会触发深拷贝,产生极大的性能损耗。

3.5.2 移动构造和移动赋值

针对深拷贝类,C++11引入了移动机制:

  • 移动构造:形参为该类的右值引用。它的本质是资源窃取。既然传入的是马上要销毁的右值,不如直接剥夺它的内部指针指向自己的空间,省去深拷贝。
  • 移动赋值:同理,接收右值引用参数,窃取资源并完成赋值。
//模拟string的移动构造机制
string(string&& s)
{
    //将右值s底层的资源指针直接窃取过来,并把s的指针置空
    swap(s); 
}

3.5.3 解决传值返回问题

有了移动构造后,当函数返回局部对象(将亡值)时,编译器会直接调用移动构造,将局部对象的资源直接“转移”给接收者,全程无深拷贝发生。
现代编译器还会进一步触发返回值优化(RVO),直接在接收者的内存空间上原地构造。

3.5.4 容器传参提效

STL容器全面新增了右值引用版本的接口。例如push_back,当传入的参数是右值时,会直接调用元素的移动构造,把数据零拷贝地挂载到容器中。

3.6 类型分类

C++11对值类型做了更细致的分类:

  • 纯右值 (prvalue):纯粹的字面值常量或不具名的临时对象。
  • 将亡值 (xvalue):即将销毁并被移走资源的对象(如move的返回值,返回右值引用的函数调用)。
  • 泛左值 (glvalue):包含左值和将亡值。

3.7 引用折叠

在模板参数推导中,可能会出现引用的引用,此时遵循引用折叠规则
只有右值引用遇上右值引用才会保持为右值引用(&& && -> &&),其他所有组合一律折叠为左值引用(& && -> & 等)。

这种机制造就了模板中的万能引用

//这里的T&&不是单纯的右值引用,而是万能引用
template<class T>
void f2(T&& x) {}

//传左值时,折叠为左值引用;传右值时,折叠为右值引用

3.8 完美转发

由于右值引用变量自身属性会退化为左值,当我们在万能引用函数内部把参数继续往下层函数传递时,下层函数永远只会将其当作左值处理。
完美转发std::forward的作用,就是在传参时保持对象原有的左值或右值属性不丢失。

template<class T>
void Function(T&& t)
{
    //如果没有forward,t一律当作左值传递
    //使用完美转发,保持t的原生属性传递给Fun
    Fun(std::forward<T>(t)); 
}

四、可变参数模板

4.1 基本语法及原理

C++11允许模板接收可变数量的参数,这些参数统称为参数包
声明时用省略号...表示,可以通过sizeof...(args)获取包内参数的个数。

template <class... Args>
void Print(Args&&... args)
{
    //获取参数包中参数的数量
    cout << sizeof...(args) << endl; 
}

4.2 包扩展

参数包不能直接当作数组遍历,必须通过解包操作来处理。最常用的是编译时递归推导:编写一个接收单参数和剩余参数包的模板函数,递归调用自身,直到参数包为空并匹配到终止函数。

//递归终止函数
void ShowList() { cout << endl; }

//递归解析参数包
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
    cout << x << " ";
    //剥离第一个参数后,剩下的参数包继续往下传
    ShowList(args...); 
}

4.3 emplace系列接口

STL容器新增了emplace系列接口(如emplace_back),底层正是基于可变参数模板和完美转发。

  • 核心优势push_back需要你在外部先构造好对象,再拷贝或移动进容器。而emplace_back允许你直接传入构造该对象所需的底层参数,它会在容器的内存空间中直接原地构造对象
  • 结论:在任何场景下,emplace_back的效率都大于或等于push_back,日常开发应优先使用emplace系列。

五、新的类功能

5.1 默认的移动构造和移动赋值

C++11在类的默认成员函数中新增了移动构造和移动赋值。
生成的条件极为苛刻:只有当你没有显式实现任何移动操作,并且没有实现析构、拷贝构造、拷贝赋值中的任何一个时,编译器才会自动生成。
默认生成的版本会对内置类型按字节拷贝,对自定义类型尝试调用其移动版本(若没有则退化为调用拷贝版本)。

5.2 成员变量声明时给缺省值

类定义时可以直接为成员变量赋缺省值。这个值是提供给初始化列表的备胎,如果在构造函数的初始化列表中没有显式为其赋值,就会采用这个缺省值。

5.3 default和delete

  • default:强制编译器生成某个被拦截的默认函数。例如你写了拷贝构造导致默认移动构造不生成,可以通过= default强制生成。
  • delete:指示编译器彻底禁用某个函数。例如禁止对象拷贝,只需在拷贝构造声明后加上= delete,将其标记为删除函数。

5.4 final与override

  • final:修饰类,表示该类不可被继承;修饰虚函数,表示该虚函数不可被子类重写。
  • override:修饰子类的虚函数,强制编译器检查该函数是否真实、正确地重写了父类的虚函数。

六、STL中一些变化

  • 新容器:最实用的是基于哈希表实现的无序关联容器 unordered_mapunordered_set,它们将查找的时间复杂度降到了O(1)。
  • 新接口:容器全面支持了右值引用参数的pushinsertemplace系列接口,极大地减少了内存分配与拷贝动作。

七、lambda

7.1 lambda表达式语法

Lambda表达式本质是一个匿名函数对象,它允许你在函数内部直接定义一段临时执行逻辑,省去了专门去写一个仿函数类的麻烦。
完整语法格式:[capture-list] (parameters) mutable -> return type { function body }

  • [capture-list] 捕捉列表:不可省略。不仅标志着lambda的开始,还能将外部局部变量抓取到内部使用。
  • (parameters) 参数列表:无参数时可连同括号省略。
  • mutable:默认传值捕捉进来的变量带有const属性,加上它可取消常量性。
  • -> return type 返回值类型:通常由编译器自动推导,一般直接省略。
  • { function body } 函数体:具体的业务逻辑,不可省略。
int main()
{
    //一个实现相加功能的简易lambda
    auto add1 = [](int x, int y) { return x + y; };
    cout << add1(1, 2) << endl;
    return 0;
}

7.2 捕捉列表

默认情况下lambda内部无法看到外层的局部变量,必须通过捕捉列表引入:

  • 显式捕捉[x, &y] 表示对x传值捕捉,对y传引用捕捉。
  • 隐式捕捉[=] 表示自动把内部用到的外层变量全部传值捕捉;[&] 表示全部传引用捕捉。
  • 混合捕捉[=, &x] 表示除x外其他全部传值捕捉。混合捕捉的首个元素必须是=&

易错点:传值捕捉本质是一种值拷贝,加上mutable虽然能在内部修改它,但完全不会影响外层的实参原值。静态变量和全局变量不需要捕捉即可直接使用。

7.3 lambda的应用

向STL算法(如排序)传递自定义比较规则时,直接就地写一个Lambda是最优雅的做法。

struct Goods {
    string _name;
    double _price;
};

int main() {
    vector<Goods> v = { {"苹果", 2.1}, {"香蕉", 3.0} };

    //直接传入lambda实现按价格升序排列
    sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
        return g1._price < g2._price;
    });
    return 0;
}

7.4 lambda的原理

从汇编层面看,编译器遇到lambda表达式时,会自动在底层生成一个未命名的仿函数类
捕捉列表里的变量,就是这个仿函数类构造函数的入参,用来初始化类的成员变量;lambda的参数和主体逻辑,则被转化为该类重载的operator()的内容。

八、包装器

8.1 function

std::function是一个定义在<functional>中的类模板包装器。
在C++中,函数指针、仿函数、Lambda表达式都具备可调用属性,但它们的类型完全不同。std::function的核心价值就是统一类型,把它们统统装进同一个标准包装器中。

#include <functional>

int f(int a, int b) { return a + b; }

int main()
{
    //统一包装函数指针和Lambda
    function<int(int, int)> f1 = f; 
    function<int(int, int)> f2 = [](int a, int b) { return a + b; }; 
    
    return 0;
}

实战价值:你可以将std::function存入mapvector中。例如用map<string, function<int(int, int)>>将加减乘除的符号与对应的Lambda映射起来,从而彻底干掉庞杂的switch-case语句。

8.2 bind

std::bind是一个函数适配器,主要用来调整可调用对象的参数个数和参数顺序
占位符_1_2用于指定生成的新对象接收参数的位置。

#include<functional>
using placeholders::_1;

int Sub(int a, int b) { return (a - b) * 10; }

int main()
{
    //将Sub的第一个参数强制绑死为100
    //sub3现在只需要接收1个参数即可,它对应_1的位置
    auto sub3 = bind(Sub, 100, _1);
    
    //实际相当于调用Sub(100, 5)
    cout << sub3(5) << endl; 
    return 0;
}

在使用面向对象编程时,由于类的成员函数隐式带有一个this指针,常常会用bind把实例对象本身提前绑死,这样后续调用就不需要每次都传对象进去了。


更多推荐