目录
一、什么是左值和右值?
左值准确来说是:一个表示数据的表达式(如变量名或解引用的指针),且可以获取他的地址(取地址),可以对它进行赋值;它可以在赋值符号的左边或者右边。
右值准确来说是:一个表示数据的表达式(如字面常量、函数的返回值、表达式的返回值),且不可以获取他的地址(取地址);它只能在赋值符号的右边。右值也是通常不可以改变的值。
int main()
{
// 以下的p、b、c、*p都是左值
int a = 1;// a是左值,1作为普通字面量是右值
const int b = 2;
int* p = new int(0);
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 这些编译会报错:error C2106: “=”: 左操作数必须为左值
//10 = 1;
//x + y = 1;
//fmin(x, y) = 1;
二、什么是左值引用和右值引用?
左值引用:给左值取别名
右值引用:给右值取别名
需要注意的是:
- 左值引用只能引用左值;const左值引用可以左值,也可以引用右值(因为右值通常是不可以改变的值,所以用const左值引用是可以的);
- 右值只能引用右值;左值可以通过move(左值)来转化为右值,继而使用右值引用。
//int& ref1 = (x + y); - 权限放大
// 表达式返回的是临时对象,具有常性
const int& ref2 = (x + y); // 左值引用给右值取别名需加const
// 右值引用可以给move后的左值去别名
//int&& ref3 = a;
int&& ref3 = move(a);
}
void func(int& a){ cout << "void func(int& a)" << endl;}
void func(int&& a){ cout << "void func(int&& a)" << endl;}
int main()
{
int a = 1, b = 2;
func(a);
func(a + b);
// 没有右值引用前,第一个func函数形参变为const int& a既可以接收右值也可以接收左值
// 但有个问题,函数识别不了传进来的是左值还是右值
return 0;
}
此时我们已经了解了左值和左值引用,右值和右值引用。所以可以发现,左值引用就是我们通常使用的引用。那么左值引用和右值引用的意义或者区别在哪里呢?我们继续往下看。
三、右值引用使用场景和意义
左值引用的意义在于:
- 函数传参:实参传给形参时,可以减少拷贝。
- 函数传返回值时,只要是出了作用域还存在的对象,那么就可以减少拷贝。
但是左值引用却没有彻底的解决问题:函数传返回值时,如果返回值是出了作用域销毁的(出了作用域不存在的),那还需要多次的拷贝构造,导致消耗较大,效率较低。
所以这也就是为什么出现了右值引用,当然这是是右值引用价值中的一个!
1.补齐左值引用的短板——函数传返回值时的拷贝
首先我们需知道:
1.1纯右值、将亡值
纯右值和将亡值都属于右值。
纯右值
运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda表达式等都是纯右值。
举例:
- 除字符串字面值外的字面值
- 返回非引用类型的函数调用
- 后置自增自减表达式i++、i--
- 算术表达式(a+b, a*b, a&&b, a==b等)
- 取地址表达式等(&a)
将亡值
将亡值是指C++11新增的和右值引用相关的表达式,通常指将要被移动的对象、T&&函数的返回值、std::move函数的返回值、转换为T&&类型转换函数的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。
通过自己模拟实现的string帮助我们理解:
namespace yrj
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
// s1 = 将亡值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string operator+(char ch)
{
string tmp(*this);
tmp += ch;
return tmp;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
yrj::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
yrj::string str;
//while (value > 0)
//{
// int x = value % 10;
// value /= 10;
//
// str += ('0' + x);
//}
//
//if (flag == false)
//{
// str += '-';
//}
std::reverse(str.begin(), str.end());
return str;
}
}
2.移动语义
移动语义,在程序喵看来可以理解为转移所有权,之前的拷贝是对于别人的资源,自己重新分配一块内存存储复制过来的资源,而对于移动语义,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,别人不再拥有也不会再使用,通过C++11新增的移动语义可以省去很多拷贝负担,怎么利用移动语义呢,是通过移动构造函数和移动赋值函数。
2.1移动构造
直接上图:
这张图也就解释了移动构造也就是将原数据资源转移给其他变量
string的移动构造:
// 移动构造
string(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
举个例子:
yrj::string s1("hello world");
yrj::string ret1 = s1;
yrj::string ret2 = (s1+'!');
我们也可以将move后的左值(也就是右值)赋给左值,但需注意的是:除非你想把你原先的数据给搞没,否则不要随意使用move
yrj::string ret3 = move(s1);
再来看看98跟11的区别:
编译器会自动优化(连续的构造,但是不是所有的情况都优化),将两个拷贝构造优化为一个拷贝构造,直接跳过中间的临时变量,但是对于自定义类型时,虽然将两次拷贝构造优化为一次,拷贝构造仍然要消耗很大的空间,所以这时右值引用的第一个价值就要登场!
右值引用来补齐函数传返回值时的拷贝短板,当调用拷贝构造时,之前我们只有传左值,进行深拷贝,完成拷贝构造,但现在我们有了右值,可以传右值,那么传右值的拷贝构造是怎么搞的呢?
对于左值,我们后续还要使用,所以只能进行深拷贝,完成拷贝构造。但对于右值(将亡值),可以直接进行资源的交换,将this和将亡值交换资源。
在有了移动构造以后,再经过编译器的优化,就可以做到直接移动构造(资源的交换),实现0拷贝,效率极高!!
2.2移动赋值
string的移动赋值:
// s1 = 将亡值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动赋值" << endl;
swap(s);
return *this;
}
这里运行后,我们看到调用了一次移动构造和一次移动赋值。
因为如果是用一个已经存在的对象接收,编译器就没办法优化了。yrj::to_string函数中会先用str生成构造生成一个临时对象,但是我们可以看到,编译器很聪明的在这里把str识别成了右值,调用了移动构造。然后在把这个临时对象做为yrj::to_string函数调用的返回值赋值给ret,这里调用的移动赋值。(直接资源交换)
注意:移动语义仅针对于那些实现了移动构造函数的类的对象,对于那种基本类型int、float等没有任何优化作用,还是会拷贝,因为它们实现没有对应的移动构造函数。
3.插入接口右值版本
C++11以后,STL所有的容器都增加了移动构造,并且STL所有的容器插入数据接口函数都增加了右值引用版本
list的接口:
我们再来看看98跟11的区别:
int main()
{
list<yrj::string> lt;
yrj::string s1("good morning");
lt.push_back(s1);
lt.push_back(move(s1));
lt.push_back(yrj::string("good morning"));// 匿名对象也是右值
lt.push_back("good morning");
}
98插入数据就是老老实实的进行深拷贝:
11也就是资源转换,将将亡值和新节点中的数据进行资源交换,也可以理解为掠夺别人的资源:
总结:
左值引用减少拷贝,提高效率右值引用也是减少拷贝,提高效率。
但是他们的角度不同,左值引用是直接减少拷贝;
右值引用是间接减少拷贝,识别出是左值还是右值,如果是右值,则不再深拷贝,直接移动拷贝,提高效率。
四、完美转发
1.前置知识
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
以取到该位置的地址。(右值被右值引用以后就成为了左值)
引用折叠:编译器不允许我们写下类似int & &&这样的代码,但是它自己却可以推导出int & &&代码出来。它的理由就是:我(编译器)虽然推导出T为int&,但是我在最终生成的代码中,利用引用折叠规则,将int & &&等价生成了int &。推导出来的int & &&只是过渡阶段,最终版本并不存在,所以也不算破坏规定。引用折叠的规则如下:
- & + & -> &
- & + && -> &
- && + & -> &
- && + && -> &&
万能引用:对于函数模板中使用右值引用的参数来说,它既可以接收右值,也可以接收左值,这个情况下的右值引用也称为万能引用。
PerfectForward(10):10是右值,模板中T &&t这种为万能引用,右值10传到PerfectForward函数中变成了右值引用,但是调用Fun()时候,t变成了左值,因为它变成了一个拥有名字的变量 ,在继续调用Fun时,还是会因为属性导致结果并不是我们需要的:
所以我们就需一个东西来保持参数原本的属性——完美转发!
2.概念
完美转发指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参,转发函数实参是左值那目标函数实参也是左值,转发函数实参是右值那目标函数实参也是右值。那如何实现完美转发呢,答案是使用std::forward()。
std::forward
是一个实现完美转发的关键工具,它的作用是将参数的类型和值类别原封不动地传递给其他函数。std::forward 本质上是一个条件转换为右值引用的函数模板,当参数是左值引用时,它返回一个左值引用;当参数是右值引用时,它返回一个右值引用。std::forward 完美转发在传参的过程中保留对象原生类型属性。例如:
3. 完美转发的局限性
虽然完美转发可以大大提高参数传递的性能和准确性,但它也有一些局限性。首先,对于不支持移动语义的类型,完美转发无法带来性能优势。其次,完美转发可能导致代码变得复杂且难以阅读。因此,在使用完美转发时,需要权衡优势和劣势,根据实际情况进行选择。
五、新的类功能
1.默认成员函数
1. 构造函数2. 析构函数3. 拷贝构造函数4. 拷贝赋值重载5. 取地址重载6. const 取地址重载
- 如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任 意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类 型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造, 如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中 的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内 置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋 值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造 完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
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& operator=(const Person& p)
//{
// if(this != &p)
// {
// _name = p._name;
// _age = p._age;
// }
// return *this;
//}
//~Person(){}
private:
yrj::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
我们没写构造的话,会自己生产调用string的移动拷贝跟移动赋值,内置类型就会按照值进行拷贝
移动构造:s1资源被转移了,自己也一无所有了
移动赋值:s2跟s4资源实现了交换
我们再看看若自己实现了某一个函数(例如:析构函数)跟默认生成有什么区别:
可以看到自己实现了析构函数对象全部都调用的是深拷贝,其效率跟代价有点大,而默认生成却自己调用了移动函数,效率大大滴提升了。
2.强制生成默认函数的关键字default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原 因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以 使用default关键字显示指定移动构造生成。
Person(Person&& p) = default;
3.禁止生成默认函数的关键字delete
Person(const Person& p) = delete;