✨✨所属专栏:C++✨✨
✨✨作者主页:嶔某✨✨
C++11的发展历史
2011年,C++标准委员会发布了C++11标准,这是C++的一次巨大飞跃,引入了许多重要的新特性,如智能指针、lambda表达式、并发编程支持等。这一版本的发布对C++社区产生了深远的影响,使得C++在现代软件开发中更具竞争力。C++11 是 C++ 的第⼆个主要版本,并且是从 C++98 起的最重要更新。C++03 与 C++11 期间花了 8 年时间,故⽽这是迄今为⽌最⻓的版本间隔。从那时起,C++ 有规律地每 3 年更新⼀次。
初始化列表
在C++98中我们可以用 { } 来初始化数组,结构体。
struct Point
{
int _x;
int _y;
};
int main()
{
int array1[] = { 1, 2, 3, 4, 5 };
int array2[5] = { 0 };
Point p = { 1, 2 };
return 0;
}
C++11以后想统⼀初始化⽅式,试图实现⼀切对象皆可⽤{}初始化,{}初始化也叫做列表初始化。
- 内置类型⽀持,⾃定义类型也⽀持,⾃定义类型本质是类型转换,中间会产⽣临时对象,最后优化了以后变成直接构造。
- { } 初始化的过程中,可以省略掉 = 。
- C++11列表初始化的本意是想实现⼀个⼤统⼀的初始化⽅式,其次他在有些场景下带来的不少便利,如容器push/inset多参数构造的对象时,{}初始化会很⽅便。
std::initializer_list
- 上⾯的初始化已经很⽅便,但是对象容器初始化还是不太⽅便,⽐如⼀个vector对象,我想⽤N个值去构造初始化,那么我们得实现很多个构造函数才能⽀持, vector v1 = {1,2,3}; vector v2 = {1,2,3,4,5};
- C++11库中提出了⼀个std::initializer_list的类, auto il = { 10, 20, 30 }; // the type of il is an initializer_list ,这个类的本质是底层开⼀个数组,将数据拷⻉过来,std::initializer_list内部有两个指针分别指向数组的开始和结束。
- 这是他的⽂档:initializer_list ,std::initializer_list⽀持迭代器遍历。
- 容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的 {x1,x2,x3...} 进⾏ 初始化。STL中的容器⽀持任意多个值构成的 {x1,x2,x3...} 进⾏初始化,就是通过 std::initializer_list的构造函数⽀持的。
STL中的容器都增加了⼀个initializer_list的构造
右值引用和移动语义
C++98的C++语法中就有引⽤的语法,⽽C++11中新增了的右值引⽤语法特性,C++11之后我们之前学习的引⽤就叫做左值引⽤。⽆论左值引⽤还是右值引⽤,都是给对象取别名。
左值和右值
- 左值是⼀个表示数据的表达式(如变量名或解引⽤的指针),⼀般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const 修饰符后的左值,不能给他赋值,但是可以取它的地址。
- 右值也是⼀个表⽰数据的表达式,要么是字⾯值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
- 值得⼀提的是,左值的英⽂简写为lvalue,右值的英⽂简写为rvalue。传统认为它们分别是left value、right value 的缩写。现代C++中,lvalue 被解释为loactor(定位) value的缩写,可意为存储在内存中、有明确存储地址可以取地址的对象,⽽ rvalue 被解释为 read value,指的是那些可以提供数据值,但是不可以寻址,例如:临时变量,字⾯量常量,存储于寄存器中的变量等,也就是说左值和右值的核⼼区别就是能否取地址。
int main()
{
// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]就是常⻅的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
cout << &c << endl;
cout << (void*)&s[0] << endl;
// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值
10;
x + y;
fmin(x, y);
string("11111");
//cout << &10 << endl;
//cout << &(x+y) << endl;
//cout << &(fmin(x, y)) << endl;
//cout << &string("11111") << endl;
return 0;
}
左值引用和右值引用
- Type& r1 = x; Type&& rr1 = y; 第⼀个语句就是左值引⽤,左值引⽤就是给左值取别名,第⼆个就是右值引⽤,同样的道理,右值引⽤就是给右值取别名。
- 左值引⽤不能直接引⽤右值,但是const左值引⽤可以引⽤右值
- 右值引⽤不能直接引⽤左值,但是右值引⽤可以引⽤move(左值)
- move是库⾥⾯的⼀个函数模板,本质内部是进⾏强制类型转换,但是底层实现还挺复杂的。
- 需要注意的是变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量变量表达式的属性是左值
- 语法层⾯看,左值引⽤和右值引⽤都是取别名,不开空间。从汇编底层的⻆度看左值引用和右值引用的实现,底层都是⽤指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要混到⼀起去理解,互相佐证,这样反⽽是陷⼊迷途。
引⽤延⻓⽣命周期
右值引⽤可⽤于为临时对象延⻓⽣命周期,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';// TestTestTest
return 0;
}
左值和右值的参数匹配
- C++98中,我们实现⼀个const左值引⽤作为参数的函数,那么实参传递左值和右值都可以匹配。
- C++11以后,分别重载左值引⽤、const左值引⽤、右值引⽤ 作为形参的f函数,那么实参是左值会匹配f(左值引⽤),实参是const左值会匹配f(const 左值引⽤),实参是右值会匹配f(右值引⽤)。
- 右值引⽤变量在⽤于表达式时属性是左值,这个设计这⾥会感觉很怪,但是在一些特定的地方就能体现出这样设计的目的(不要认为C++委员会的人是吃素的啊!魂淡!)
右值引⽤和移动语义
左值引⽤主要使⽤场景回顾
左值引⽤主要使⽤场景是在函数中左值引⽤传参和左值引⽤传返回值时减少拷⻉,同时还可以修改实参和修改返回对象的价值。
左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回,如addStrings和generate函数(返回的是一个局部对象,出了作用域会销毁),C++98中的解决⽅案只能是*使⽤输出型参数解决(类似LeetCode里用C语言些OJ的时候有一个renturn的参数在参数列表里面)。
string addStrings(string num1, string num2) {
string str;
int end1 = num1.size() - 1, end2 = num2.size() - 1;
// 进位
int next = 0;
while (end1 >= 0 || end2 >= 0)
{
int val1 = end1 >= 0 ? num1[end1--] - '0' : 0;
int val2 = end2 >= 0 ? num2[end2--] - '0' : 0;
int ret = val1 + val2 + next;
next = ret / 10;
ret = ret % 10;
str += ('0' + ret);
}
if (next == 1)
str += '1';
reverse(str.begin(), str.end());
return str;
}
vector<vector<int>> generate(int numRows) {
vector<vector<int>> vv(numRows);
for (int i = 0; i < numRows; ++i)
{
vv[i].resize(i + 1, 1);
}
for (int i = 2; i < numRows; ++i)
{
for (int j = 1; j < i; ++j)
{
vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
}
}
return vv;
}
那么C++11以后这⾥可以使⽤右值引⽤做返回值解决吗?显然是不可能的,因为这⾥的本质是返回 对象是⼀个局部对象,函数结束这个对象就析构销毁了,右值引⽤返回也⽆法概念对象已经析构销毁的事实
移动构造和移动赋值
- 移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
- 移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。
- 对于像string/vector这样的深拷⻉的类或者包含深拷⻉的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引⽤的类型,他的本质是要“掠夺”引⽤的右值对象的资源,⽽不是像拷⻉构造和拷⻉赋值那样去拷⻉资源,从而提⾼效率。以string为例:
// 移动构造 string(string&& s) { cout << "string(string&& s) -- 移动构造" << endl; swap(s); } // 移动赋值 string& operator=(string&& s) { cout << "string& operator=(string&& s) -- 移动赋值" << endl; swap(s); return *this; }
右值对象移动语义优化传值问题
在C++11以前(没有移动构造,移动赋值),只能返回局部对象的拷贝的临时对象再拷贝一次到返回的ret对象上,这样是很消耗资源的。C++没有解决问题的方法,只有考编译器的优化了。编译器直接把两次拷贝构造合二为一,就只用一次拷贝。
C++11出来以后(没有移动构造,移动赋值),在没有编译器优化的情况下,会调用两次移动构造相当于将局部对象的资源转移了两次,但即使这样,也不会消耗太多资源,其实就是几个swap,假如是一棵树,只用交换root,一个链表,只用交换头节点。这样其实没有编译器的优化效率也很高。编译器优化之后,只需调用一次移动构造(其实也没优化到什么)。
所以在编译器和C++委员会的工同努力下,这个问题得到了妥善的解决。
在Linux系统下,取消编译器的所有优化:g++ test.cpp -fno-elide-constructors
右值引⽤和移动语义在传参中的提效
- 查看STL⽂档我们发现C++11以后容器的push和insert系列的接⼝都增加了右值引⽤版本
- 当实参是⼀个左值时,容器内部继续调⽤拷⻉构造进⾏拷⻉(不能将其资源转走,之后可能还会用到该对象),将对象拷⻉到容器空间中的对象
- 当实参是⼀个右值(大多是临时变量,局部变量,反正都会销毁),容器内部则调⽤移动构造,将右值对象的资源移动到容器空间的对象上
类型分类(了解)
- C++11以后,进⼀步对类型进⾏了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值 (expiring value,简称xvalue)。
- 纯右值是指那些字⾯值常量或求值结果相当于字⾯值或是⼀个不具名的临时对象。如: 42、 true、nullptr 或者类似 str.substr(1, 2)、str1 + str2 传值返回函数调⽤,或者整 形 a、b,a++,a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于 C++98中的右值。
- 将亡值是指返回右值引⽤的函数的调⽤表达式和转换为右值引⽤的转换函数的调⽤表达,如 move(x)、static_cast(x)
- 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。
- 值类别 - cppreference.com和Value categories这两个关于值类型的中⽂和英⽂的官⽅⽂档。
- 有名字,就是glvalue;有名字,且不能被move,就是lvalue;有名字,且可以被move,就是 xvalu;没有名字,且可以被移动,则是prvalue。
引⽤折叠
- C++中不能直接定义引⽤的引⽤如 int& && r = i; ,这样写会直接报错,通过模板或 typedef 中的类型操作可以构成引⽤的引⽤。
- 通过模板或 typedef 中的类型操作可以构成引⽤的引⽤时,这时C++11给出了⼀个引⽤折叠的规则:右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤。
- 下⾯的程序中很好的展⽰了模板和typedef时构成引⽤的引⽤时的引⽤折叠规则,⼤家需要⼀个⼀个仔细理解⼀下。
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤ template<class T> void f1(T& x) {} // 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤ template<class T> void f2(T&& x) {} int main() { typedef int& lref; typedef int&& rref; int n = 0; lref& r1 = n; // r1 的类型是 int& lref&& r2 = n; // r2 的类型是 int& rref& r3 = n; // r3 的类型是 int& rref&& r4 = 1; // r4 的类型是 int&& // 没有折叠->实例化为void f1(int& x) f1<int>(n); f1<int>(0); // 报错 // 折叠->实例化为void f1(int& x) f1<int&>(n); f1<int&>(0); // 报错 // 折叠->实例化为void f1(int& x) f1<int&&>(n); f1<int&&>(0); // 报错 // 折叠->实例化为void f1(const int& x) f1<const int&>(n); f1<const int&>(0); // 折叠->实例化为void f1(const int& x) f1<const int&&>(n); f1<const int&&>(0); // 没有折叠->实例化为void f2(int&& x) f2<int>(n); // 报错 f2<int>(0); // 折叠->实例化为void f2(int& x) f2<int&>(n); f2<int&>(0); // 报错 // 折叠->实例化为void f2(int&& x) f2<int&&>(n); // 报错 f2<int&&>(0); return 0; }
- 像f2这样的函数模板中,T&& x参数看起来是右值引⽤参数,但是由于引⽤折叠的规则,他传递左值时就是左值引⽤,传递右值时就是右值引⽤,有些地⽅也把这种函数模板的参数叫做万能引⽤。
- Function(T&& t)函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引⽤折叠规则,就实现了实参是左值,实例化出左值引⽤版本形参的 Function,实参是右值,实例化出右值引⽤版本形参的Function。
template<class T> void Function(T&& t) { int a = 0; T x = a; //x++; cout << &a << endl; cout << &x << endl << endl; } int main() { // 10是右值,推导出T为int,模板实例化为void Function(int&& t) Function(10); // 右值 int a; // a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t) Function(a); // 左值 // std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t) Function(std::move(a)); // 右值 const int b = 8; // a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&t) // 所以Function内部会编译报错,x不能++ Function(b); // const 左值 // std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t) // 所以Function内部会编译报错,x不能++ Function(std::move(b)); // const 右值 return 0; }
完美转发
Function(T&& t)函数模板程序中,传左值实例化以后是左值引⽤的Function函数,传右值实例化以后是右值引⽤的Function函数。
但是我们之前有说,变量表达式都是左值属性,也就意味着⼀个右值被右值引⽤绑定后,右值引⽤变量表达式的属性是左值,也就是说Function函数中t的属性是左值,那么我们把t传递给下⼀层函数Fun,那么匹配的都是左值引⽤版本的Fun函数。这⾥我们想要保持t对象的属性, 就需要使⽤完美转发实现。
完美转发forward本质是⼀个函数模板,他主要还是通过引⽤折叠的⽅式实现,下⾯示例中传递给 Function的实参是右值,T被推导为int,没有折叠,forward内部t被强转为右值引⽤返回;传递给 Function的实参是左值,T被推导为int&,引⽤折叠为左值引⽤,forward内部t被强转为左值引⽤ 返回。
template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept
{ // forward an lvalue as either an lvalue or an rvalue
return static_cast<_Ty&&>(_Arg);
}
void Fun(int& x) { cout << "左值引⽤" << endl; }
void Fun(const int& x) { cout << "const 左值引⽤" << endl; }
void Fun(int&& x) { cout << "右值引⽤" << endl; }
void Fun(const int&& x) { cout << "const 右值引⽤" << endl; }
template < class T>
void Function(T && t)
{
Fun(t);
//Fun(forward<T>(t));
}
int main()
{
// 10是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(10); // 右值
int a;
// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)
Function(a); // 左值
// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)
Function(std::move(a)); // 右值
const int b = 8;
// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&t)
Function(b); // const 左值
// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)
Function(std::move(b)); // const 右值
return 0;
}
可变参数模板
- C++11⽀持可变参数模板,也就是说⽀持可变数量参数的函数模板和类模板,可变数⽬的参数被称为参数包,存在两种参数包:
- 模板参数包,表示零或多个模板参数;
- 函数参数包:表示零或多个函数参数。
template <class ...Args> void Func(Args... args) {}
template <class ...Args> void Func(Args&... args) {}
template <class ...Args> void Func(Args&&... args) {}
- 我们⽤省略号来指出⼀个模板参数或函数参数的表示⼀个包,在模板参数列表中,class...或 typename...指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后⾯跟...指出 接下来表⽰零或多个形参对象列表;函数参数包可以⽤左值引⽤或右值引⽤表⽰,跟前⾯普通模板 ⼀样,每个参数实例化时遵循引⽤折叠规则。
- 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
- 这⾥我们可以使⽤sizeof...运算符去计算参数包中参数的个数。
template <class ...Args>
void Print(Args&&... args)
{
cout << sizeof...(args) << endl;
}
可变参数模板先是根据实参的个数示例化出模板,之后再根据模板示例化出对应的函数(编译器是黑奴~~~)
可以这么理解
模板:不知道参数类型,但是知道参数个数
可变参数模板:参数类型和个数都不知道
包扩展
- 对于⼀个参数包,我们除了能计算他的参数个数,我们能做的唯⼀的事情就是扩展它,当扩展⼀个包时,我们还要提供⽤于每个扩展元素的模式,扩展⼀个包就是将它分解为构成的元素,对每个元素应⽤模式,获得扩展后的列表。我们通过在模式的右边放⼀个省略号(...)来触发扩展操作。底层的实现细节如图1所⽰。
- C++还⽀持更复杂的包扩展,直接将参数包依次展开依次作为实参给⼀个函数去处理。
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;
}
注意:以上过程是在编译时发生的,本质上是一个编译时递归。平常我们写的递归都是运行时递归。
template <class T>
const T& GetArg(const T& x)
{
cout << x << " ";
return x;
}
template <class ...Args>
void Arguments(Args... args)
{}
template <class ...Args>
void Print(Args... args)
{
// 注意GetArg必须返回或者到的对象,这样才能组成参数包给Arguments
Arguments(GetArg(args)...);
}
本质可以理解为编译器编译时,包的扩展模式将上⾯的函数模板扩展实例化为下⾯的函数,这样设计其实挺巧妙的。C++委员会的大佬还是有水平的哈哈!
void Print(int x, string y, double z)
{
Arguments(GetArg(x), GetArg(y), GetArg(z));
}
empalce系列接⼝
template void emplace_back (Args&&... args);
template iterator emplace (const_iterator position, Args&&... args);
- C++11以后STL容器新增了empalce系列的接⼝,empalce系列的接⼝均为模板可变参数,功能上兼容push和insert系列,但是empalce还⽀持新玩法,假设容器为container,empalce还⽀持直接插⼊构造T对象的参数,这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
- emplace_back总体⽽⾔是更⾼效,推荐以后使⽤emplace系列替代insert和push系列
- 在底层实现emplace_back的时候,我们不需要像push_back一样先构造一个对象,然后再把这个对象的 const引用 不断往下传。emplace_back只需要把参数包不断往下传,传到最后直接用参数包在与T匹配的类型构造T对象。emplace_back 相当于直接插入构造T对象的参数。
- 传递参数包过程中,如果是 Args&&... args 的参数包,要⽤完美转发参数包,⽅式如下 std::forward(args)... ,否则编译时包扩展后右值引⽤变量表达式就变成了左值。
int main()
{
list<string> lt;
//传左值,跟push_back⼀样,⾛拷贝构造
string s1 = "11111";
lt.emplace_back(s1);
//右值,跟push_back⼀样,⾛移动构造
lt.emplace_back(move(s1));
cout << "*********************************" << endl;
// 直接把构造string参数包往下传,直接⽤string参数包构造string
// 这⾥达到的效果是push_back做不到的
lt.emplace_back("111111111111");
cout << "*********************************" << endl;
list<pair<string, int>> lt1;
// 跟push_back⼀样
// 构造pair + 拷贝/移动构造pair到list的节点中data上
pair<string, int> kv("苹果", 1);
lt1.emplace_back(kv);
// 跟push_back⼀样
lt1.emplace_back(move(kv));
cout << "*********************************" << endl;
// 直接把构造pair参数包往下传,直接⽤pair参数包构造pair
// 这⾥达到的效果是push_back做不到的
lt1.emplace_back("苹果", 1);
cout << "*********************************" << endl;
//模拟实现list的emplace和emplace_back接⼝,这⾥把参数包不断往下传递,
//最终在结点的构造中直接去匹配容器存储的数据类型T的构造,
//所以达到了前⾯说的empalce⽀持直接插⼊构造T对象的参数,
//这样有些场景会更⾼效⼀些,可以直接在容器空间上构造T对象。
return 0;
}
新的类功能
默认的移动构造和移动赋值
- 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷⻉构造函数/拷⻉赋值重载/取地址重载/const 取地址重载,最后重要的是前4个,后两个⽤处不⼤,默认成员函数就是我们不写编译器会⽣成⼀个默认的。C++11 新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。
- 如果你没有⾃⼰实现移动构造函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意⼀个(深拷贝类型需要实现的默认成员函数)。那么编译器会⾃动⽣成⼀个默认移动构造。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调⽤移动构造,没有实现就调⽤拷⻉构造。
- 如果你没有⾃⼰实现移动赋值重载函数,且没有实现析构函数 、拷⻉构造、拷⻉赋值重载中的任意 ⼀个,那么编译器会⾃动⽣成⼀个默认移动赋值。默认⽣成的移动构造函数,对于内置类型成员会执⾏逐成员按字节拷⻉,⾃定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调⽤移动赋值,没有实现就调⽤拷⻉赋值。(默认移动赋值跟上⾯移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会⾃动提供拷⻉构造和拷⻉赋值。
defult和delete
- C++11可以让你更好的控制要使⽤的默认函数。假设你要使⽤某个默认的函数,但是因为⼀些原因这个函数没有默认⽣成。⽐如:我们提供了拷⻉构造,就不会⽣成移动构造了,那么我们可以使⽤default关键字显式指定移动构造⽣成。
- 如果能想要限制某些默认函数的⽣成,在C++98中,是该函数设置成private,并且只声明补丁已, 这样只要其他⼈想要调⽤就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语 法指⽰编译器不⽣成对应函数的默认版本,称=delete修饰的函数为删除函数。
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person & p)
:_name(p._name)
, _age(p._age)
{}
Person(Person&& p) = default;//强制编译器生成此类构造函数
//Person(const Person& p) = delete;
//编译器不会再自动生成此类构造函数
private:
string _name;
int _age;
};
final与override关键字
小老弟~ 忘了就老老实实去多态复习吧~ C++进阶:多态-****博客
STL中⼀些变化
- C++11中STL中的新容器有:<array><forward_list><unordered_map><unordered_set>,但是实际最有⽤的是unordered_map和unordered_set。C++进阶:哈希_c++ hash-****博客
- STL中容器的新接⼝也不少,最重要的就是右值引⽤和移动语义相关的push/insert/emplace系列 接⼝和移动构造和移动赋值,还有initializer_list版本的构造等,这些前⾯都讲过了。
- 容器的范围for遍历,这个在容器部分也讲过了。
- 如果忘记了老实复习奥!!!
lambda
lambda表达式语法
- lambda 表达式本质是⼀个匿名函数对象,跟普通函数不同的是它可以定义在函数内部。 lambda 表达式语法使⽤层⽽⾔没有类型,所以我们⼀般是⽤auto或者模板参数定义的对象去接收 lambda 对象。
- lambda表达式的格式:
[capture-list] (parameters)-> return type { function boby }
- [capture-list] : 捕捉列表,该列表总是出现在 lambda 函数的开始位置,编译器根据[]来判断接下来的代码是否为 lambda 函数,捕捉列表能够捕捉上下⽂中的变量供 lambda 函数使⽤,捕捉列表可以传值和传引⽤捕捉,具体细节下面有专门讲解。捕捉列表为空也不能省略。
- (parameters) :参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连 同()⼀起省略
- ->return type :返回值类型,⽤追踪返回类型形式声明函数的返回值类型,没有返回值时此 部分可省略。⼀般返回值类型明确情况下,也可省略,由编译器对返回类型进⾏推导。
- {function boby} :函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以 使⽤其参数外,还可以使⽤所有捕获到的变量,函数体为空也不能省略。
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 Qin" << endl;
return 0;
};
func1();
int a = 0, b = 1;
auto swap1 = [](int& x, int& y)
{
int tmp = x;
x = y;
y = tmp;
};
swap1(a, b);
cout << a << ":" << b << endl;
return 0;
}
捕捉列表
- lambda 表达式中默认只能⽤ lambda 函数体和参数中的变量,如果想⽤外层作⽤域中的变量就需要进⾏捕捉
- 第⼀种捕捉⽅式是在捕捉列表中显式的传值捕捉和传引⽤捕捉,捕捉的多个变量⽤逗号分割。[x, y, &z] 表⽰x和y值捕捉,z引⽤捕捉。
- 第⼆种捕捉⽅式是在捕捉列表中隐式捕捉,我们在捕捉列表写⼀个 = 表示隐式值捕捉,在捕捉列表写⼀个 & 表示隐式引⽤捕捉,这样我们 lambda 表达式中⽤了那些变量,编译器就会⾃动捕捉那些变量。(这里并不是把外部全部的变量都压入函数栈帧)
- 第三种捕捉⽅式是在捕捉列表中混合使⽤隐式捕捉和显式捕捉。[=, &x]表示其他变量隐式值捕捉, x引⽤捕捉;[&, x, y]表⽰其他变量引⽤捕捉,x和y值捕捉。当使⽤混合捕捉时,第⼀个元素必须是 &或=,并且&混合捕捉时,后⾯的捕捉变量必须是值捕捉,同理=混合捕捉时,后⾯的捕捉变量必须是引⽤捕捉。
- lambda 表达式如果在函数局部域中,他可以捕捉 lambda 位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉, lambda 表达式中可以直接使⽤。这也意味着 lambda 表达式如果定义在全局位置,捕捉列表必须为空。
- 默认情况下, lambda 捕捉列表是被const修饰的,也就是说传值捕捉的过来的对象不能修改, mutable加在参数列表的后⾯可以取消其常量性,也就说使⽤该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使⽤该修饰符后,参数列表不可省略(即使参数为空)。
int a = 0, b = 1, c = 2, d = 3;
// 传值捕捉本质是⼀种拷贝,并且被const修饰了
// mutable相当于去掉const属性,可以修改了
// 但是修改了不会影响外⾯被捕捉的值,因为是⼀种拷贝
auto func7 = [=]()mutable
{
a++;
b++;
c++;
d++;
return a + b + c + d;
};
lambda的应⽤
- 在学习 lambda 表达式之前,我们的使⽤的可调⽤对象只有函数指针和仿函数对象,函数指针的类型定义起来⽐较⿇烦,仿函数要定义⼀个类,相对会⽐较⿇烦。使⽤ lambda 去定义可调⽤对象,既简单⼜⽅便。
- lambda 在很多其他地⽅⽤起来也很好⽤。⽐如线程中定义线程的执⾏函数逻辑,智能指针中定制删除器等, lambda 的应⽤还是很⼴泛的。
struct Goods
{
string _name; // 名字
double _price; // 价格
int _evaluate; // 评价
// ...
Goods(const char* str, double price, int evaluate)
:_name(str)
, _price(price)
, _evaluate(evaluate)
{}
};
int main()
{
vector<Goods> v = { { "苹果", 2.1, 5 }, { "⾹蕉", 3, 4 }, { "橙⼦", 2.2, 3
}, { "菠萝", 1.5, 4 } };
// 类似这样的场景,我们实现仿函数对象或者函数指针支持商品中
// 不同项的⽐较,相对还是比较麻烦的,那么这⾥lambda就很好⽤了
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 的原理和范围for很像,编译后从汇编指令层的⻆度看,压根就没有 lambda 和范围for 这样的东西。范围for底层是迭代器,⽽lambda底层是仿函数对象,也就说我们写了⼀个 lambda 以后,编译器会⽣成⼀个对应的仿函数的类。
- 仿函数的类名是编译按⼀定规则⽣成的,保证不同的 lambda ⽣成的类名不同(这里用到;了一个东西叫uuid),lambda参数/返回类型/函数体就是仿函数operator()的参数/返回类型/函数体, lambda 的捕捉列表本质是⽣成的仿函数类的成员变量,也就是说捕捉列表的变量都是 lambda 类构造函数的实参,当然隐式捕捉,编译器要看使⽤哪些就传那些对象。
- 上⾯的原理,我们可以透过汇编层了解⼀下,下⾯第⼆段汇编层代码印证了上⾯的原理
class Rate
{
public:
Rate(double rate)
: _rate(rate)
{}
double operator()(double money, int year)
{
return money * _rate * year;
}
private:
double _rate;
};
int main()
{
double rate = 0.49;
// lambda
// 捕捉列表的rate,可以看到作为lambda_1类构造函数的参数传递了,这样要拿去初始化成员变量
auto r2 = [rate](double money, int year) {
return money * rate * year;
};
// 函数对象
Rate r1(rate);
r1(10000, 2);
r2(10000, 2);
auto func1 = [] {
cout << "hello world" << endl;
};
func1();
return 0;
}
包装器
function
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 异常。
以上是 function 的原型,他被定义头⽂件中。std::function - cppreference.com 是function的官⽅⽂档
函数指针、仿函数、 lambda 等可调⽤对象的类型各不相同, std::function 的优势就是统⼀类型,对他们都可以进⾏包装,这样在很多地⽅就⽅便声明可调⽤对象的类型,下⾯的第⼆个代码样例展示了 std::function 作为map的参数,实现字符串和可调⽤对象的映射表功能。
#include<functional>
int f(int a, int b)
{
return a + b;
}
struct Functor
{
public:
int operator() (int a, int b)
{
return a + b;
}
};
class Plus
{
public:
Plus(int n = 10)
:_n(n)
{}
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return (a + b) * _n;
}
private:
int _n;
};
int main()
{
// 包装各种可调⽤对象
function<int(int, int)> f1 = f;
function<int(int, int)> f2 = Functor();
function<int(int, int)> f3 = [](int a, int b) {return a + b; };
cout << f1(1, 1) << endl;
cout << f2(1, 1) << endl;
cout << f3(1, 1) << endl;
// 包装静态成员函数
// 成员函数要指定类域并且前⾯加&才能获取地址
function<int(int, int)> f4 = &Plus::plusi;
cout << f4(1, 1) << endl;
// 包装普通成员函数
// 普通成员函数还有⼀个隐含的this指针参数,所以绑定时传对象或者对象的指针过去都可以
function<double(Plus*, double, double)> f5 = &Plus::plusd;
Plus pd;
cout << f5(&pd, 1.1, 1.1) << endl;
function<double(Plus, double, double)> f6 = &Plus::plusd;
cout << f6(pd, 1.1, 1.1) << endl;
function<double(Plus&&, double, double)> f7 = &Plus::plusd;
cout << f7(move(pd), 1.1, 1.1) << endl;
cout << f7(Plus(), 1.1, 1.1) << endl;// 也可以传匿名对象
return 0;
}
150. 逆波兰表达式求值 - 力扣(LeetCode)
#include<map>
#include<stack>
using namespace std;
// 使⽤map映射string和function的⽅式实现
// 这种⽅式的最⼤优势之⼀是⽅便扩展,假设还有其他运算,我们增加map中的映射即可
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> st;
// function作为map的映射可调⽤对象的类型
map<string, function<int(int, int)>> opFuncMap = {
{"+", [](int x, int y) {return x + y; }},
{"-", [](int x, int y) {return x - y; }},
{"*", [](int x, int y) {return x * y; }},
{"/", [](int x, int y) {return x / y; }}
};
for (auto& str : tokens)
if (opFuncMap.count(str)) // 操作符
{
int right = st.top();
st.pop();
int left = st.top();
st.pop();
int ret = opFuncMap[str](left, right);// <--核心代码
st.push(ret);
}
else
st.push(stoi(str));
return st.top();
}
};
bind
- bind 是⼀个函数模板,它也是⼀个可调⽤对象的包装器,可以把他看做⼀个函数适配器,对接收的fn可调⽤对象进⾏处理后返回⼀个可调⽤对象。 bind 可以⽤来调整参数个数和参数顺序。 bind 也在这个头⽂件中。
- 调⽤bind的⼀般形式:
其中 newCallable本⾝是⼀个可调⽤对象,arg_list是⼀个逗号分隔的参数列表,对应给定的callable的参数。当我们调⽤newCallable时,newCallable会调⽤callable,并传给它arg_list中的参数。auto newCallable = bind(callable, arg_list);
- arg_list中的参数可能包含形如_n的名字,其中n是⼀个整数,这些参数是占位符,表示 newCallable的参数,它们占据了传递给newCallable的参数的位置。数值n表⽰⽣成的可调⽤对象中参数的位置:_1为newCallable的第⼀个参数,_2为第⼆个参数,以此类推。_1/_2/_3....这些占位符放到placeholders的⼀个命名空间中。
#include<functional>
using placeholders::_1;
using placeholders::_2;
using placeholders::_3;
int Sub(int a, int b)
{
return (a - b) * 10;
}
int SubX(int a, int b, int c)
{
return (a - b - c) * 10;
}
class Plus
{
public:
static int plusi(int a, int b)
{
return a + b;
}
double plusd(double a, double b)
{
return a + b;
}
};
int main()
{
auto sub1 = bind(Sub, _1, _2);
cout << sub1(10, 5) << endl;
// bind 本质返回的⼀个仿函数对象
// 调整参数顺序(不常用)
// _1代表第一个实参
// _2代表第二个实参
// ...
auto sub2 = bind(Sub, _2, _1);
cout << sub2(10, 5) << endl;
// 调整参数个数 (常用)
auto sub3 = bind(Sub, 100, _1);
cout << sub3(5) << endl;
auto sub4 = bind(Sub, _1, 100);
cout << sub4(5) << endl;
// 分别绑死第123个参数
auto sub5 = bind(SubX, 100, _1, _2);
cout << sub5(5, 1) << endl;
auto sub6 = bind(SubX, _1, 100, _2);
cout << sub6(5, 1) << endl;
auto sub7 = bind(SubX, _1, _2, 100);
cout << sub7(5, 1) << endl;
// 成员函数对象进行绑死,就不需要每次都传递了
function<double(Plus&&, double, double)> f6 = &Plus::plusd;
Plus pd;
cout << f6(move(pd), 1.1, 1.1) << endl;
cout << f6(Plus(), 1.1, 1.1) << endl;
// bind⼀般⽤于,绑死一些固定参数
function<double(double, double)> f7 = bind(&Plus::plusd, Plus(), _1, _2);
cout << f7(1.1, 1.1) << endl;
return 0;
}
bind绑定特定参数,function包装 计算复利
// 计算复利的lambda
auto func1 = [](double rate, double money, int year)->double {
double ret = money;
for (int i = 0; i < year; i++)
{
ret += ret * rate;
}
return ret - money;
};
// 绑死⼀些参数,实现出⽀持不同年化利率,不同金额和不同年份计算出复利的结算利息
function<double(double)> func3_1_5 = bind(func1, 0.015, _1, 3);
function<double(double)> func5_1_5 = bind(func1, 0.015, _1, 5);
function<double(double)> func10_2_5 = bind(func1, 0.025, _1, 10);
function<double(double)> func20_3_5 = bind(func1, 0.035, _1, 30);
cout << func3_1_5(1000000) << endl;
cout << func5_1_5(1000000) << endl;
cout << func10_2_5(1000000) << endl;
cout << func20_3_5(1000000) << endl;
未完待续……
本期博客到这里就结束了,如果有什么错误,欢迎指出,如果对你有帮助,请点个赞,谢谢!