C++11
1.C++11的发展历史
C++11 是 C++ 的第⼆个主要版本,并且是从 C++98 起的最重要更新。它引入了大量更改,标准化了既有实践,并改进了对 C++ 程序员可用的抽象。在它最终由 ISO 在 2011 年 8 月 12 日采纳前,人们曾使⽤名称“C++0x”,因为它曾被期待在 2010 年之前发布。
2. 列表初始化
C++11 中的列表初始化是一种非常强大和灵活的初始化方式,它为变量、数组、容器、结构体和类等的初始化提供了统一的语法形式。
2.1C++98的{}
C++98中⼀般数组和结构体可以用 {} 进行初始化。
struct Point
{
int _x;
int _y;
};
int main()
{
Point p = {1,2};//对结构体初始化
int a[] = { 1,2,3,4,5 };//对数组初始化
return 0;
}
2.2C++11的{}
•C++11引入了统一的列表初始化语法,使用花括号 {} 进行初始化, {} 初始化也叫做列表初始化。不仅可以用于数组、结构体,还能用于标准容器、类等。
•内置类型支持,自定义类型也⽀持,自定义类型本质是类型转换,中间会产⽣临时对象,最后优化
了以后变成直接构造。
• {} 初始化的过程中,可以省略掉 = 。
•C++11列表初始化的本意是想实现⼀个大统⼀的初始化方式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{} 初始化会很方便。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
Date(const Date& d)
:_year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date& d)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//定义容器的同时初始化多个元素,使代码更加简洁直观
vector<int> v{ 1,2,3,4,5 };
map<string, int> Mymap{ {"one",1},{"two",2}};
Date d1 = { 2025, 1, 1 };
// 这里d2引用的是{ 2024, 7, 25 }构造的临时对象
const Date& d2 = { 2024, 7, 25 };
// 需要注意的是C++98支持单参数时类型转换,也可以不用{}
Date d3 = { 2025 };
Date d4 = 2025;
return 0;
}
3.右值引用和移动语义
C++98的C++语法中就有引⽤的语法,而C++11中新增了的右值引用语法特性,C++11之后我们之前学习的引用就叫做左值引用。⽆论左值引用还是右值引用,都是给对象取别名。
3.1左值和右值
•左值是⼀个表示数据的表达式(如变量名或解引用的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
特点: 具有持久的状态,可被赋值和取地址。
int a = 10; // a 是左值,它有明确的内存地址,可以被赋值和取地址
int* ptr = &a; // 可以取 a 的地址
a = 20; // 可以对 a 进行赋值操作
•右值: 纯粹的临时对象、字面量值等,不能出现出现在赋值符号的左边,它们不代表任何具有持久状态的对象,右值不能取地址。
int b = 5 + 3; // 5 + 3 是纯右值,它是一个临时计算结果,没有自己的持久内存地址
int c = 10; // 10 是纯右值,它是一个字面量
区分方法:
•能否取地址:可以使用&运算符取地址的表达式通常是左值,否则是右值。例如&(a)
是合法的(a是左值)
,而&(5 + 3)
是不合法的(5 + 3是右值)
。
•是否可赋值:可以出现在赋值语句左边的表达式是左值。例如a = 10
;中a是左值
而5 = a
是错误的,因为5是右值
3.2左值引用和右值引用
左值引用是对左值的引用,使用&
符号来声明。它提供了一种为对象起别名的方式,使得多个名称可以指向同一个对象。
int num = 10;
// 声明一个左值引用 ref,它引用了变量 num
int& ref = num;
左值引用只能绑定到左值,不能直接绑定到右值。因为左值代表一个具有持久存储位置的对象,而右值通常是临时对象,生命周期短暂。
但是const左值引用可以引用右值: 是因为const的特性使得它不会去修改所引用对象的值。这样一来,当引用右值时,就不会出现修改临时对象(右值)的情况,从而保证了程序的安全性。同时,通过这种方式还可以延长右值的生命周期,使其在const左值引用的作用域内保持有效。
左值引用常用于函数参数传递,避免对象的拷贝,提高效率。同时,函数可以通过引用修改调用者提供的对象
右值引用是对右值的引用,使用&&
符号来声明。它主要用于实现移动语义和完美转发。
int&& rref = 20; // 声明一个右值引用 rref,绑定到右值 20
右值引用只能绑定到右值,右值引用不能直接引用左值(但是右值引用可以引用move(左值)
)。这使得右值引用能够区分临时对象和持久对象,从而实现不同的处理逻辑。
// int num = 10;
// int&& rref1 = num; // 错误,num 是左值,不能绑定到右值引用
int&& rref2 = 20; // 正确,20 是右值
右值引用的主要用途之一是在对象所有权转移时避免不必要的拷贝,提高性能。通过右值引用,可以将临时对象的资源(如动态分配的内存)直接转移到新对象中。
右值引用使用场景:
移动构造函数和移动赋值运算符: 右值引用在移动构造函数和移动赋值运算符中起着关键作用,允许对象在所有权转移时高效地释放和获取资源。
需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量变
量表达式的属性是左值。
语法层⾯看,左值引用和右值引用都是取别名,不开空间。从汇编底层的⻆度看下⾯代码中r1和rr1
汇编层实现,底层都是用指针实现的,没什么区别。
3.3移动构造和移动赋值
移动构造函数和移动赋值运算符是 C++11 引入的重要特性,它们主要用于高效地转移资源的所有权,避免不必要的深拷贝,提升程序性能。
移动构造函数是⼀种构造函数,类似拷贝构造函数,移动构造函数要求第⼀个参数是该类类型的引
⽤,但是不同的是要求这个参数是右值引引,如果还有其他参数,额外的参数必须有缺省值。
class BigObject {
private:
int* data;
size_t size;
public:
// 构造函数,动态分配内存
BigObject(size_t s) : size(s) {
data = new int[size];
for (size_t i = 0; i < size; ++i) {
data[i] = i;
}
}
// 拷贝构造函数,进行深拷贝
BigObject(const BigObject& other)
: size(other.size) {
data = new int[size];
for (size_t i = 0; i < size; ++i) {
data[i] = other.data[i];
}
cout << "Copy constructor called" << endl;
}
// 移动构造函数,转移资源所有权
BigObject(BigObject&& other) noexcept
: data(other.data)
, size(other.size)
{
other.data = nullptr;
other.size = 0;
cout << "Move constructor called" << endl;
}
// 析构函数,释放动态分配的内存
~BigObject() {
delete[] data;
}
};
int main() {
BigObject obj1(1000);
// 调用移动构造函数
BigObject obj2(std::move(obj1));
return 0;
}
移动构造函数的主要作用是在对象初始化时,将一个临时对象的资源(如动态分配的内存)直接转移到新对象中,避免深拷贝带来的性能开销。
移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引用。
移动赋值运算符是一种特殊的赋值运算符,用于在对象已经存在的情况下,从一个临时对象(右值)中接管资源。
class MyString {
private:
char* data;
size_t length;
public:
// 构造函数
MyString(const char* str = "") {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
}
// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
length = other.length;
other.data = nullptr;
other.length = 0;
}
return *this;
}
// 析构函数
~MyString() {
delete[] data;
}
// 打印字符串
void print() const {
cout << data << endl;
}
};
int main() {
MyString str1("Hello");
MyString str2("World");
// 使用移动赋值运算符将 str2 的资源转移到 str1
str1 = std::move(str2);
str1.print();
return 0;
}
打印结果为World
总结:对于像string/vector
这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。
3.4引用折叠
引用折叠是 C++ 模板编程和完美转发中的一个重要概念,它主要用于处理模板参数为引用类型时的复杂情况,尤其是在涉及右值引用和万能引用(也叫转发引用)的场景。
C++11给出了⼀个引用折叠的规则:
•T& &
折叠为 T&
(左值引用 + 左值引用 = 左值引用)
•T& &&
折叠为 T&
(左值引用 + 右值引用 = 左值引用)
•T&& &
折叠为 T&
(右值引用 + 左值引用 = 左值引用)
•T&& &&
折叠为 T&&
(右值引用 + 右值引用 = 右值引用)
简单来说,右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。
// 用于接收左值引用的函数
template<typename T>
void fun1(T& value) {
cout << value << endl;
}
// 用于接收右值引用的函数
template<typename T>
void fun2(T&& value) {
cout << value << endl;
}
//实现完美转发的函数模板
template<typename T>
void fun3(T&& arg) {
cout << value << endl;
}
int main() {
int num = 42;
fun1(num);
fun2(20);
fun3(20);//传右值
fun3(num);//传左值
return 0;
}
像fun3
这样的函数模板中,T&& arg
参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用,有些地⽅也把这种函数模板的参数叫做万能引用。
3.5完美转发
template<class T>
void Function(T&& t)
{
int a = 0;
T x = a;
//x++;
cout << &a << endl;
cout << &x << endl << endl;
}
Function(T&& t)
函数模板程序中,传左值实例化以后是左值引用的Function函数,传右值实例化
以后是右值引用的Function函数。
但是结合之前的讲解,变量表达式都是左值属性,也就意味着⼀个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引用版本的Fun函数。这里我们想要保持t对象的属性,就需要使用完美转发实现。
#include <iostream>
#include <utility>
void process(int& val) {
std::cout << "Lvalue: " << val << std::endl;
}
void process(int&& val) {
std::cout << "Rvalue: " << val << std::endl;
}
template<typename T>
void forwardValue(T&& arg) {
process(forward<T>(arg));//通过函数模板和 forward 实现了完美转发,确保参数在传递过程中保持原有的左值或右值属性。
}
int main() {
int num = 10;
forwardValue(num); // 传递左值
forwardValue(20); // 传递右值
return 0;
}
4.可变参数模板
C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。
• template <class ...Args> void Func(Args... args) {}
//可变参数函数模板
• template <class ...Args> void Func(Args&... args) {}
//可变参数函数模板(左值引用)
• template <class ...Args> void Func(Args&&... args) {}
////可变参数函数模板(右值引用)
// 可变参数函数模板
template<typename... Args>
void printArgs(Args... args) {
// 函数体
}
typename... Args:
这是一个模板参数包,Args
是一个模板参数包的名称,… 表示它可以包含零个或多个模板参数。
Args... args:
这是一个函数参数包,args
是函数参数包的名称,它对应于模板参数包 Args
,可以包含零个或多个函数参数。
可以使⽤sizeof...
运算符去计算参数包中参数的个数。
template <class ...Args>
void Print(Args&&... args)
{
cout << sizeof...(args) << endl;
}
int main()
{
double x = 2.2;
Print(); // 包里有0个参数
Print(1); // 包里有1个参数
Print(1, string("xxxxx")); // 包里有2个参数
Print(1.1, string("xxxxx"), x); // 包里有3个参数
return 0;
}
对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供用于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。
void ShowList()
{
// 编译器时递归的终⽌条件,参数包是0个时,直接匹配这个函数
cout << endl;
}
template <class T, class ...Args>
void ShowList(T x, Args... args)
{
cout << x << " ";
// args是N个参数的参数包
// 调⽤ShowList,参数包的第⼀个传给x,剩下N-1传给第⼆个参数包
ShowList(args...);
}
// 编译时递归推导解析参数
template <class ...Args>
void Print(Args... args)
{
ShowList(args...);
}
int main()
{
Print();
Print(1);
Print(1, string("xxxxx"));
Print(1, string("xxxxx"), 2.2);
return 0;
}
上面代码就可以对参数包进行扩展
运行结果:
代码原理:
总结:可变参数模板为 C++ 编程带来了更高的灵活性和通用性,允许模板处理任意数量和类型的参数。通过递归展开或折叠表达式,可以方便地使用参数包中的参数。在实现通用容器、日志记录函数等场景中,可变参数模板发挥着重要作用。
5.lambda表达式
在传统 C++ 中,如果需要一个简单的函数,通常要定义一个具名函数或函数对象。Lambda 表达式则允许在需要的地方直接定义一个匿名的函数对象,避免了额外的命名和定义,使代码更加简洁和灵活。
Lambda 表达式的基本语法如下:
[capture list] (parameter list) mutable(可选) exception(可选) -> return type(可选) { function body }
•捕获列表(capture list): 用于指定 Lambda 表达式可以访问的外部变量。可以是值捕获、引用捕获或混合捕获。
•参数列表(parameter list): 与普通函数的参数列表类似,用于传递参数给 Lambda 表达式。
•mutable(可选): 如果使用了 mutable,则可以修改通过值捕获的变量。
•exception(可选): 用于指定 Lambda 表达式可能抛出的异常。
•返回类型(return type): 可以显式指定返回类型,若省略,编译器会自动推导。
•函数体(function body): 包含 Lambda 表达式的具体实现代码。
其中:
1.捕捉为空也不能省略
2.参数为空可以省略
3.返回值可以省略,可以通过返回对象自动推导
4.函数题不能省略
int main()
{
// 一个简单的lambda表达式
auto add1 = [](int x, int y)->int {return x + y; };
cout << add1(1, 2) << endl;
// 1、捕捉为空也不能省略
// 2、参数为空可以省略
// 3、返回值可以省略,可以通过返回对象⾃动推导
// 4、函数体不能省略
auto func1 = []
{
cout << "hello bit" << endl;
return 0;
};
func1();
return 0;
}
5.1捕捉列表
捕获列表决定了 Lambda 表达式如何访问外部变量,常见的捕获方式有以下几种:
第⼀种捕捉方式是在捕捉列表中显⽰的传值捕捉和传引⽤捕捉,捕捉的多个变量⽤逗号分割。[x,y, &z]
表示x和y值捕捉,z引用捕捉。
值捕捉:
#include <iostream>
using namespace std;
int main() {
int x = 10;
auto lambda = [x]() {
cout << "Value of x: " << x << endl;
};
lambda();
return 0;
}
[x]
表示通过值捕获外部变量 x
,Lambda 表达式内部使用的是 x
的副本,对副本的修改不会影响外部的 x
。
引用捕捉:
#include <iostream>
using namespace std;
int main() {
int x = 10;
auto lambda = [&x]() {
x = 20;
cout << "New value of x: " << x << endl;
};
lambda();
cout << "Value of x outside lambda: " << x << endl;
return 0;
}
[&x]
表示通过引用捕获外部变量 x
,Lambda 表达式内部对 x
的修改会影响外部的 x
。
第⼆种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个=
表示隐式值捕捉,在捕捉列表写⼀个&
表示隐式引用捕捉,这样我们 lambda 表达式中⽤了那些变量,编译器就会自动捕捉那些变量。
#include <iostream>
using namespace std;
int main() {
int x = 10;
int y = 20;
auto lambda = [=]() {
cout << "Value of x: " << x << endl;
cout << "Value of y: " << y <<endl;
};
// 隐式值捕捉
// ⽤了哪些变量就捕捉哪些变量
auto func2 = [=]
{
int ret = a + b + c;
return ret;
};
cout << func2() << endl;
// 隐式引⽤捕捉
// ⽤了哪些变量就捕捉哪些变量
auto func3 = [&]
{
a++;
c++;
d++;
};
func3();
cout << a << " " << b << " " << c << " " << d << endl;
lambda();
return 0;
}
第三种捕捉⽅式是在捕捉列表中混合使用隐式捕捉和显示捕捉。[=, &x]
表示其他变量隐式值捕捉,x
引用捕捉;[&, x, y]
表⽰其他变量引用捕捉,x
和y
值捕捉。当使用混合捕捉时,第⼀个元素必须是&或=
,并且&
混合捕捉时,后面的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必须是引用捕捉。
#include <iostream>
using namespace std;
int main() {
int x = 10;
int y = 20;
auto lambda = [x, &y]() {
cout << "Value of x: " << x << endl;
y = 30;
cout << "New value of y: " << y << endl;
};
lambda();
cout << "Value of y outside lambda: " << y << endl;
return 0;
}
5.2lambda的应用
在学习 lambda
表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义⼀个类,相对会比较麻烦。使用 lambda
去定义可调用对象,既简单⼜方便。
#include<algorithm>
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
// ...
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
struct ComparePriceLess
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price < gr._price;
}
};
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3
}, { "菠萝", 1.5, 4 } };
// 类似这样的场景,我们实现仿函数对象或者函数指针⽀持商品中
// 不同项的比较,相对还是比较麻烦,那么这⾥lambda就很好⽤了
sort(v.begin(), v.end(), ComparePriceLess());
sort(v.begin(), v.end(), ComparePriceGreater());
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price < g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price > g2._price;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate < g2._evaluate;
});
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate > g2._evaluate;
});
return 0;
}
在这里例子中我们可以发现与仿函数相比,Lambda 表达式的优势在于它的简洁性和灵活性。使用仿函数需要定义一个结构体或类,并重载 () 运算符,代码相对繁琐。而 Lambda 表达式可以在需要的地方直接定义,无需额外的命名和定义,使代码更加简洁易读。特别是在需要临时定义一个简单的比较规则时,Lambda 表达式的优势更加明显。
Lambda的原理和范围for很像,编译后从汇编指令层的角度看,压根就没有 Lambda和范围for这样的东西。范围for底层是迭代器,而Lambda底层是仿函数对象,也就说我们写了⼀个Lambda以后,编译器会生成⼀个对应的仿函数的类。
6.包装器
在 C++ 里,包装器指的是能够对可调用对象(像函数、函数指针、成员函数指针、仿函数、Lambda 表达式等)进行封装,从而让它们能够以统一的方式被调用和管理的工具
6.1function
template <class T>
class function; // undefined
template <class Ret, class... Args>
class function<Ret(Args...)>;
std::function
是⼀个类模板,也是⼀个包装器。 std::function
的实例对象可以包装存储其他的可以调用对象,包括函数指针、仿函数、 lambda 、 bind
表达式等,存储的可调⽤对象被称为 std::function
的⽬标。若 std::function
不含⽬标,则称它为空。调用空std::function
的目标导致抛出 std::bad_function_call
异常。
#include <iostream>
#include <functional>
// 普通函数
int add(int a, int b) {
return a + b;
}
// 仿函数
struct Subtract {
int operator()(int a, int b) {
return a - b;
}
};
int main() {
// 包装普通函数
std::function<int(int, int)> func1 = add;
std::cout << "Add result: " << func1(3, 2) << std::endl;
// 包装仿函数
Subtract sub;
std::function<int(int, int)> func2 = sub;
std::cout << "Subtract result: " << func2(3, 2) << std::endl;
// 包装Lambda表达式
std::function<int(int, int)> func3 = [](int a, int b) {
return a * b;
};
std::cout << "Multiply result: " << func3(3, 2) << std::endl;
return 0;
}
6.2bind
std::bind
也在 <functional>
头文件中定义,它能够将可调用对象与其参数进行绑定,生成一个新的可调用对象。借助 std::bind
,可以预先绑定部分参数,或者调整参数的顺序。
#include <iostream>
#include <functional>
using namespace std;
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
// 普通函数
int add(int a, int b) {
return a + b;
}
int main() {
// 绑定部分参数
auto addFive = bind(add, _1, 5);
cout << "Add five result: " << addFive(3) <<endl;
// 调整参数顺序
auto reverseAdd = std::bind(add, _2, _1);
cout << "Reverse add result: " << reverseAdd(3, 2) << endl;
return 0;
}