【C++11】常用特性总结
这篇文章系统梳理了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试图实现一切对象皆可用{}初始化,这种机制称为列表初始化。
- 适用范围:内置类型与自定义类型均支持。对于自定义类型,底层会先用列表中的值构造一个临时对象,再去拷贝构造目标对象,最终被编译器直接优化为直接构造。
- 书写简化:在使用
{}初始化时,可以省略掉等号=。
使用场景:当对容器调用push或insert且需要传入多参数构造的对象时,直接传入{}能省去编写匿名对象的冗长代码。
#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_map 和 unordered_set,它们将查找的时间复杂度降到了O(1)。
- 新接口:容器全面支持了右值引用参数的
push、insert、emplace系列接口,极大地减少了内存分配与拷贝动作。
七、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存入map或vector中。例如用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把实例对象本身提前绑死,这样后续调用就不需要每次都传对象进去了。
完
更多推荐
所有评论(0)