网站开发gxjzdrj,做企业网站 长春,网站建设公司销售提成,个人主页推荐右值与左值
在讲解右值引用之前#xff0c;我们就需要先辨析一下左值与右值的区别。
左值
左值是一个表示数据的表达式#xff0c;我们可以获取它的地址并且对其赋值#xff0c;左值可以出现在赋值操作符的左边#xff0c;但是右值不能。
int i 0;
int* p i;
do…右值与左值
在讲解右值引用之前我们就需要先辨析一下左值与右值的区别。
左值
左值是一个表示数据的表达式我们可以获取它的地址并且对其赋值左值可以出现在赋值操作符的左边但是右值不能。
int i 0;
int* p i;
double d 3.14;变量ipd都是左值一方面来说它们出现在了的左边另一方面来说我们可以对其取地址并修改它的值。
const int ci 0;
int const* cp i;
const double cd 3.14;变量cicpcd都是左值它们出现在了的左边我们可以对其取地址。但是由于具有const属性我们不能修改它。
因此 左值最显著的特征是可以取地址但是不一定可以被修改。 右值
右值也是一个表达数据的表达式比如字面常量表达式返回值函数返回值等等右值可以出现在赋值操作符的右边但是不能出现在的左边右值不能取地址。
double func()
{return 3.14;
}int x 10;
int y 20;
int z x y;
double d func();以上代码中1020x yfunc()都是右值它们出现在的右边。1020对应了字面常量x y对应了表达式返回值func()对应函数返回值。这些都是右值它们最显著的特点就是无法取地址。 右值引用语法
先回顾一下左值引用的语法
int i 0;
int* p nullptr;int ri i;
int* rp p;左值引用的语法是type右值引用的语法是type。
接下来我们尝试对刚刚的值进行右值引用
int ri 0;
int* rp nullptr;
double rd 3.14; 左值引用不能直接引用右值const左值引用 可以引用右值 用左值引用直接引用右值
int i 5; // 非法
以const引用的形式那么就可以引用右值
const int i 5; // 合法 右值引用不能直接引用左值右值引用可以引用move后的左值 右值引用不能直接引用左值
int i 5;
int rri i; // 非法
函数move其可以把一个左值强制转化为一个右值。
int i 5;
int rri move(i); // 合法
move(i)之后i依然是左值但是move(i)这个表达式返回了一个右值的i。 右值引用底层
既然存在右值引用这个语法那么我们来看看右值引用到底干了些啥。
右值引用的工作主要有两种情况一种是右值引用了常量另外一种是右值引用了move后的左值。
右值引用了常量 当右值引用了常量引用会把常量区中的数据拷贝一份到栈区然后该引用指向栈区中拷贝后的数据 看到一段代码
int r 5;
r 10;
以上过程中我们先用r右值引用了常量5然后通过右值引用把5改为了10。
如果这个过程中右值常量5存储在常量区r右值引用后如果r指向常量区的5会发生什么此时我们的r 10操作就相当于把常量区的5修改为了10从此以后整个程序中只要去常量区拷贝5都会变成拷贝10这可就完蛋了。因此我们的右值引用常量绝对不能直接引用常量区的数据 因此右值引用常量时的真实操作是把常量区的数据拷贝到栈区中然后这个引用指向这一块栈区内存。 当右值引用了常量引用会把常量区中的数据拷贝一份到栈区然后该引用指向栈区中拷贝后的数据该数据可以修改当const左值引用了常量引用会把常量区中的数据拷贝一份到栈区然后该引用指向栈区中拷贝后的数据但是该数据是常量不能修改 右值引用了move后的左值 当右值引用了move后的左值右值引用直接指向该左值 看到以下代码
int i 5;
int rri move(i);rri 10;cout i endl;
cout rri endl;
程序输出结果为
10
10
我们可以通过修改右值引用来修改左值或者说以通俗点的说法此时右值引用就是这个左值的别名。确实是这样的当右值引用了move后的左值其实和直接左值引用这个左值没有任何区别。 左值引用解决了传参时存在的拷贝问题 string add_string(string s1, string s2)
{string s s1 s2;return s;
}int main()
{string str;string hello Hello;string world world;str add_string(hello, world);return 0;
}以上代码中add_string函数需要接收两个string类型的参数此时我们使用传引用传参就可以避免两个string的拷贝消耗。 2.左值引用解决了一部分返回值的拷贝问题 string say_hello()
{static string s hello world;return s;
}int main()
{string str1;string str2;str1 say_hello();return 0;
}以上代码中函数say_hello生成了一个string并把它返回给外部如果我们直接返回那么str1接收参数时就会先拷贝构造出一个临时变量然后临时变量再拷贝构造str1。这个过程发生了两次拷贝构造。但是返回值s指向的string是全局的其出了函数依然存在因此我们传引用返回可以不用拷贝构造一个临时变量直接拿返回值s去拷贝构造节省了一次拷贝构造。
也就是说左值引用通过传引用传参和传引用返回节省了拷贝。 右值引用其实更多的是一种标记。
先来看看什么情况下会产生可以被右值引用的左值 当一个左值被move后可以被右值引用C会把即将离开作用域的非引用类型的返回值当成右值这种类型的右值也称为将亡值 右值的意思就是这个变量的资源可以被迁移走 移动语义
为了讲解移动语义先写一个简单的mystring类
class mystring
{
public://构造函数mystring(const char* str ){_str new char[strlen(str) 1];strcpy(_str, str);}//析构函数~mystring(){delete[] _str;}// 赋值重载mystring operator(const mystring s){cout 赋值重载 endl;return *this;}// 拷贝构造mystring(const mystring s){cout 拷贝构造 endl;}private:char* _str nullptr;
}; mystring get_string()
{mystring str(hello);return str;
}int main()
{mystring s2 get_string();return 0;
}s2通过函数get_string来获得字符串并构造自己。这个过程中由于str是局部变量会发生拷贝构造临时变量临时变量再拷贝构造s2的过程。
但是由于str是一个将亡值具有右值属性我们可以写一个函数直接把它的资源转移走
class mystring
{
public:// 移动构造mystring(mystring s){cout 移动构造 endl;std::swap(_str, s._str);}
};函数主体部分通过一个swap函数把参数s的_str指针成员与自己的_str成员进行交换。由于指针指向字符串数组此时相当于把s的字符串数组交换给自己这样就完成了对右值引用的数据转移。 除了移动构造我们还有原先的拷贝构造
class mystring
{
public:// 移动构造mystring(mystring s){cout 移动构造 endl;std::swap(_str, s._str);}// 拷贝构造mystring(const mystring s){cout 拷贝构造 endl;}
};虽然说我们的左值引用也可以达到这样的移动构造但是有一个问题并不是所有的对象资源都是可以被转移走的。移动构造之所以这么叫就是因为移走了别人的资源。这部分资源之所以会被移走就是因为它有右值属性。而它之所以有右值属性要么就是这个变量是个将亡值资源不转移就浪费了要么就是被程序员亲自move了程序员许可把这个对象的资源转移走。
就是这样的一个逻辑闭环右值引用以一个既安全又高效的方式完成了局部变量的资源拷贝问题。而这个过程也叫做右值引用的移动语义。 移动改语法实现了通过移走别人的资源实现高效的创建对象避免大量拷贝语义在这个过程中右值引用只提供语义层面的功能即许可一个对象资源被转移的右值语义 因为右值引用的出现C11后类的默认成员函数从6个变成了8个。新增两个成员函数移动构造移动赋值重载。
//移动赋值重载
mystring operator(mystring s)
{std::swap(_str, s._str);return *this;
}// 移动构造
mystring(mystring s)
{std::swap(_str, s._str);
}它们的特点是参数为右值引用函数体内部通过交换别人的指针到自己手上实现高效的资源转移。 引用折叠
看到以下代码
template class T
void func(T t)
{cout T 右值引用 endl;
}template class T
void func(const T t)
{cout const T const左值引用 endl;
}int main()
{int a 5;func(a);//左值func(move(a));//右值return 0;
}程序输出结果如下
T 右值引用
T 右值引用
C在模板中推出了引用折叠也叫做万能引用规则如下 T 推演为 TT 推演为 T 如果你希望当参数为左值引用和右值引用的时候函数的功能是一样的你就可以只写一个函数
template class T
void func(T t)
{
}
此时参数T就已经是一个引用折叠了。现在我们来调用这个函数
int a 5;
func(a);
func(move(a)); 第一次传参func(a);模板参数T的类型为int但是参数类型为int 此时根据折叠引用规则int 等于int 第二次传参func(move(a));模板参数T的类型为int但是参数类型为int 此时根据折叠引用规则int 等于int 我们刚才的模板如果作用于int类型就可以推演出四套函数重载
void func(int){};
void func(const int){};
void func(int){};
void func(const int){}; 完美转发
看到以下代码
void fuc1(int rri)
{cout func1 左值引用 endl;
}void fuc1(int rri)
{cout func1 右值引用 endl;
}int main()
{int i 5;int rri move(i);fuc1(rri);return 0;
}输出结果
func1 左值引用 右值引用后右值引用指向的对象是右值属性但是引用本身是左值属性 再来看到一个案例
void func2(int x)
{cout func2 左值引用 endl;
}void func2(int x)
{cout func2 右值引用 endl;
}template class T
void fuc1(T t)
{func2(t);
}int main()
{int i 5;fuc1(i);//左值fuc1(move(i));//右值return 0;
}由于在func1中我们经过了折叠引用这一步T这个参数类型是不确定的。 如果T是右值的话传参后t会变成左值那么我们可以对其进行move操作 如果T是左值的话传参后t还是左值我们无需对其进行操作 这个地方就不能粗暴的进行move了不然会把原本就是左值的参数给move成右值。为了解决这个情况C提供了一个函数模板forward称为完美转发其可以识别到参数的左右值类型从而将其转化为原来的值。
我们只需要在引用折叠中这样进行调用
template class T
void fuc1(T t)
{func2(forwardT(t));
}
在forward的模板参数中传入引用折叠的模板参数T那么forwardT就可以根据t的类型自动返回其原始的左右值属性了。