目录
1.右值引用
1.1 引用的概念
引用本质上就是为变量取一个别名。无论左右引用都是给一个对象取别名,引用变量与其引用实体公共同一块内存空间,而引用的底层是通 过指针来实现的,因此使用引用,可以提高程序的可读性。
C++98:左值引用(主要给左值起别名) C++11:右值引用(主要给右值起别名)。
void test()
{
int a = 1;
int& b = a;//左值引用,给a去了一个别名b
}
如上图所示左值引用是不能直接引用右值的,加上const修饰才可以
那么右值引用是加&&,如下所示:
int&& c = 10;
int&& d = x + y;
右值引用,是不能直接对左值进行引用的,但是move后的左值可以。
int a=0;
//int&& b=a; //这行时会报错的
int&& b=move(a);
1.2 左值与右值
在详细讲解右值引用前,先要明白什么是左值,什么是右值?
左值与右值是C语言中的概念,但C标准并没有给出严格的区分方式,一般认为:可以放在=左边的,或者能 够取地址的称为左值,只能放在=右边的,或者不能取地址的称为右值,但是也不一定完全正确。
因此关于左值与右值的区分不是很好区分,一般认为:
1. 普通类型的变量,因为有名字,可以取地址,都认为是左值。
2. const修饰的常量,不可修改,只读类型的,理论应该按照右值对待,但因为其可以取地址(如果只是 const类型常量的定义,编译器不给其开辟空间,如果对该常量取地址时,编译器才为其开辟空间), C++11认为其是左值。
3. 如果表达式运行结果或单个变量是一个引用则认为是左值。
4. 如果表达式的运行结果是一个临时变量或者对象,认为是右值。
5. 表达式或者函数返回值等临时对象为右值。
1.3 函数重载
右值引用是可以发生函数重载的,如下面代码所示:
template<class T>
void f(const T& a)
{
cout << "coid f(const T& a);" << endl;
}
template<class T>
void f(const T&& a)
{
cout << "coid f(const T&& a);" << endl;
}
int main()
{
int a = 10;
f(a); //这里会匹配左值引用参数的f
f(10); //这里会匹配右值引用参数的f
return 0;
}
如图所示,函数发生了重载
C++11中又将右值分为了:
纯右值: 基本类型的常量或临时对象 如:a+b,100等
将亡值:表达式的中间结果或者函数按照值的形式返回等
1.4 右值引用的应用--移动赋值和移动构造
给出如下的代码,在实例化对象时,会开一片新的空间给对象,然后将"shuaishuai"的内容拷贝给对象str1。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
using namespace std;
class String
{
public:
String(const char* str="")
{
cout << "String(const char* str="")" << endl;
_str = new char[strlen(str) + 1];
strcpy(_str, str);//将str复制到_str中
}
String(const String& str)
{
//深拷贝
cout << "String(const String& str)--深拷贝--效率低" << endl;
_str = new char[strlen(str._str) + 1];
strcpy(_str, str._str);
}
~String()
{
delete[] _str;
}
private:
char* _str;
};
int main()
{
String str1("shuaishuai");
//拷贝构造
String str2(str1);
return 0;
}
1.4.1 移动构造
上面两种实例化对象的方式都是进行深拷贝。
此时,若我们传一个将亡值时,由于有const 修饰的左值引用的构造函数重载,还是调用的深拷贝。这样就造成了效率较低。此时我们可以想到C++11中提出了右值引用的概念,可以写一个右值引用的函数重载,来避免这种情况。
String(String&& str)
:_str(nullptr)
{
cout << "String(String&& str)--移动构造--效率高" << endl;
strcpy(_str, str._str);
}
为什么要这样操作呢?因为将亡值是一个即将消失的临时对象,它所占用的这部分空间将被释放,那么我们可以设置另一个变量,继续使用这部分空间。这样空间消耗较少,效率高。
下面为调用右值引用构造函数重载的情况。
//在上面那代码的基础上给一个全局函数
String f(const char* str)
{
String tmp(str);
return tmp; //这里返回的实际是拷贝tmp的临时对象
}
这种情况在VS2022中会被编译器优化掉(在VS2015中是不会被优化的。)
第三行String(const char* str="")是全局函数里面调用的。最后的移动构造被编译器优化了,实际上是会进行移动构造的。
1.4.2 移动赋值
在C++11之前的赋值语句,也是进行深拷贝的。我们在String类中加入一个重载=的成员函数
String& operator=()(const String& str)
{
if(this!=&s)
{
cout << "String& operator=(const String& str)-深拷贝赋值-效率低" << endl;
//还要开空间,进行的是深拷贝
char* newchar = new char[len(str._str)+1];
strcpy(newchar, str._str);
delete[] _str;
_str = newchar;
}
return *this;
}
在新增右值引用后,当=后面为一个将亡值时,我们设计一个重载版本,让赋值不发生深拷贝,从而提高效率。代码如下:
// 引用的形式返回值,减少拷贝
String& operator()(String&& str)
{
if(this!=&str)
{
cout<<"String& operator()(String&& str)--移动赋值--效率高"<<endl;
swap(_str, str._str);
}
return *this;
}
下面为结果:
我们可以看到,当=后面为将亡值时,会调用=参数为右值引用的重载版本,以此来提高效率。
移动语义:顾名思义就是将一个对象的资源转移到另一个对象。
上面我们主要说了右值引用做函数参数时的情况。那我们下面就对比一下左值引用与右值引用:
左值引用 | 右值引用 | |
做参数 | void f1(const T& x) 传参过程中减少拷贝 | void push(T&& x) push内部不再使用拷贝构造x到容器的空间上,而是移动构造。 |
做返回值 | T& f2(),解决值的形式返回时拷贝的问题,在接受函数值时还需要拷贝构造。(但是这里有限制,如果返回对象除了作用域就消失的话,是不能传引用的) | C++中一般是没有右值引用做返回值的情况的。 T f2(){...} T ret = f2();解决了外面调用f2时返回对象的拷贝,在实例化ret这个对象时,这里就是右值引用的移动构造,减少了拷贝带来的低效率。 |
总结:
左值引用:解决的时传参过程中和返回值过程中的拷贝
右值引用:解决的是传参后,push/insert函数内部将对象移动到容器空间上的问题+传值返回时接收返回值参数的拷贝。
2.lambda表达式
2.1 lambda表达式语法
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement } lambda表达式各部分说明
- [capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来 的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
- (parameters):参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
- mutable:默认情况下,lambda函数总是一个const函数,mutable可以取消其常量性。使用该修 饰符时,参数列表不可省略(即使参数为空)。
- ->returntype:返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分 可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
- {statement}:函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量。 注意: 在lambda函数定义中,参数列表和返回值类型都是可选部分,而捕捉列表和函数体可以为空。 因此C++11中最简单的lambda函数为:[]{}; 该lambda函数不能做任何事情
//lambda表达式其实定义函数内部的匿名函数 int x7() { //[捕捉列表](参数列表)->返回值类型{函数体}; int a = 0, b = 1; auto add1 = [](int x1, int x2)->int {return x1 + x2; }; cout << add1(a, b) << endl; //add1是一个对象 //传值捕捉 [a]捕捉a [a,b]捕捉a,b [=]捕捉同一作用域中的所有对象 auto add2 = [a, b]()->int {return a + b; }; cout << add2() << endl; //传引用捕捉 [&a]捕捉a [&a,&b]捕捉a,b [&]捕捉同一作用域中的所有对象 //实现a和b的交换 auto swap1 = [](int& x1, int& x2) {int c = x1; x1 = x2; x2 = c; }; swap1(a, b); //传值捕捉的对象是不能被改变的,若想改变,需要加mutable 但是作用域的变量并没有改变 auto swap2 = [a,b]()mutable {int c = a; a = b; b = c; }; swap2(); //引用捕捉才可以改变外面的值 auto swap3 = [&a, &b]{int c = a; a = b; b = c; }; swap3(); return 0; }
情景:在对自定义的类进行降序排序时,当类中有很多不同的属性时,我们按照不同的属性进行排序,需要写不同的虚函数来进行控制,如下:
struct Goods
{
string _name;
double _price;
int _num;
};
//如果这里重载Goods的operator>/operator<手机不好的,因为你不知道要按照哪一项去比较
//按照价格比较
struct ComparePriceGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._price > gr._price;
}
};
//按照名字
struct CompareNameGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._name > gr._name;
}
};
// 按照编号
struct CompareNumGreater
{
bool operator()(const Goods& gl, const Goods& gr)
{
return gl._num > gr._num;
}
};
int main()
{
Goods gds[] = { { "苹果", 2.1, 3 }, { "相交", 3.0, 5 }, { "橙子", 2.2, 9 }, {"菠萝", 1.5, 10} };
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), ComparePriceGreater());
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), CompareNumGreater());
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), CompareNameGreater());
return 0;
}
像上面这样写会显得代码很繁琐,而且对编程的命名规范有很高的要求,如果命名不规范,我们还需要翻看源代码。这种情况下,我们可以使用lambda表达式来进行替换,如下:
//lambda
auto price_greater = [](const Goods& g1, const Goods& g2) {
return g1._price > g2._price;
};
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), price_greater);
//lambda表达式直接做参数 不给函数名 lambda表达式是更方便的
sort(gds, gds + sizeof(gds) / sizeof(gds[0]), [](const Goods& g1, const Goods& g2) {
return g1._price > g2._price;
});
可以有这两种形式的写法。简化代码
3.线程库
这一部分会在我学了linux操作系统后再做详细的讲解。
4.完美转发
void Fun(int& x) { cout << "lvalue ref" << endl; }
void Fun(const int& x) { cout << "const lvalue ref" << endl; }
void Fun(int&& x) { cout << "rvalue ref" << endl; }
void Fun(const int&& x) { cout << "const rvalue ref" << endl; }
template<class T>
void PerfectForward(T&& t)
{
Fun(t);
//Fun(std::forward<T>(t)); //右值引用会在第二次后的参数传递过程中属性丢失下一层调用会全部识别为左值
//加一个forward<T>就可以解决 完美转发
}
//9.完美转发
int main()
{
PerfectForward(10); //右值 rvalue ref
int a;
PerfectForward(a); //左值 lvalue ref
PerfectForward(move(a)); //move后,左值变右值 rvalue ref
const int b = 8;
PerfectForward(b); //const lvalue ref
PerfectForward(move(b)); //move后,const rvalue ref
string s1 = to_string(1111);
return 0;
}
如上图所示,右值引用在第二次传参过程中属性会丢失,下一层调用会全部识别为左值 ,当我们使用forward关键字的时候,这种情况就会被解决,得到正确的结果。