一、左值和右值
左值:能用在赋值语句左侧的东西,它能够代表一个内存地址,必须有地址。左值必须要在内存中有实体;
右值:不能作为左值的值。右值不能出现在赋值语句中等号左侧,右值可以在内存也可以在CPU寄存器。
C++中的一条表达式,要么是右值,要么就是左值,不可能两者都不是。
i = i+1;// i是个左值不是右值,虽然出现在了等号右边。i用在等号右边的时候,我们说i有一种右值属性(不是右值)。
i出现再左边,用的是i代表的内存中的地址,所以我们说i有一种左值属性。
1.一个左值,可以同时具有左值和右值属性。2.左值有的时候能够被当作右值使用。
常见左值:返回左值引用的函数, 连同赋值, 下标解引用 和 前置递增递减运算符——赋值运算符、取地址符、string等[]
常见右值:返回非引用类型的函数,连同算数,关系,位操作和后置递增递减运算符
用到左值的运算符有哪些:
1、赋值运算符 =
整个赋值赋值语句的结果仍然是左值。
(a = 4) = 8;
2、取地址 &
int a = 5;
&a;
3、string、vector 下标[] 。迭代器都需要左值
string abc = "I love China";
abc[0];
vector<int>::iterator iter;
iter++;
4、通过看一个运算符再字面值上能不能操作,我们就可以判断运算符是否用到的是左值。
左值表达式(就是左值),右值表达式(就是右值)
左值:代表一个地址,所以左值表达式的求值结果就是一个对象,就得有地址。
结论:
一个对象被用作右值时,使用的是它的内容(值),被当作左值时,使用的是它的地址。
在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。举个例子,int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址;表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。
右值、将亡值
在理解C++11的右值前,先看看C++98中右值的概念:C++98中右值是纯右值,纯右值指的是临时变量值、不跟对象关联的字面量值。临时变量指的是非引用返回的函数返回值、表达式等,例如函数int func()的返回值,表达式a+b;不跟对象关联的字面量值,例如true,2,”C”等。
C++11对C++98中的右值进行了扩充。在C++11中右值又分为纯右值(prvalue,Pure Rvalue)和将亡值(xvalue,eXpiring Value)。其中纯右值的概念等同于我们在C++98标准中右值的概念,指的是临时变量和不跟对象关联的字面量值;将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。
将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。
二:引用分类
三种形式的引用:
1、左值引用(绑定到左值)。
int value = 10;
int &refval = value;
2、const引用,也是左值引用,我们不希望改变对象的值。
const int &refval2 = value;
3、右值引用(绑定到右值):它是个引用
int &&refrightvalue = 3;
refrightvalue = 5;
左值引用:
没有空引用的说法,有空指针的说法。
char *p =nullptr: //指针有 空指针的说法。
左值绑定到左值(cnost左值引用特殊)
int a = 1;
int &b{a}; //b绑定到a
//int &c;错误,引用必须要初始化
const int &i = 1;
//等价于:
int tmp = 1;
const int &i = tmp;
左值必须绑定到有名字的变量(临时变量不可以如:“This is a string”)
string strtest{ "I love China!" } ;
string &r1{ strtest}; //可以,左值引用绑定左值
string &r2 {"Ilove China"};//不可以,左值引用不能绑定到临时变量。临时变 量被系统当做右值
右值引用:
就是引用右值,绑定到右值。
主要是希望用右值引用来绑定一些即将销毁或是一些临时对象上
右值引用 也是 引用。大家理解成一个对象名。
能绑定到左值上的引用都不能绑定到右值。
int value = 10:
int &&refrightvalue = value: 右值引用也绑不到左值上。//错误
右值有一下特性:
- 可以直接绑定到右值。
- 不能直接绑定到左值。
- 本身是一个左值。可以进行 &运算。
- 右值能被 const 类型的引用绑定。
- 可以使用move()将左值绑定到右值引用上。
const 引用(常量引用):const常量引用既可以使用左值进行初始化也可以使用右值进行初始化
int &a = 2; # 左值引用绑定到右值,编译失败
int b = 2; # 非常量左值
const int &c = b; # 常量左值引用绑定到非常量左值,编译通过
const int d = 2; # 常量左值
const int &e = c; # 常量左值引用绑定到常量左值,编译通过
const int &b =2; # 常量左值引用绑定到右值,编程通过
const int &i = 1;
//等价于:
int tmp = 1;
const int &i = tmp;
string strtest{"hello world"};
string &r1{strtest}; //可以,左值引用绑定左值
const string &r2{strtest}; //可以
const string &r3{"hello world"}; //可以,const不仅可以绑定右值还可以绑定string的隐式类型转换并将所得到的值放到string临时变量中
string &&r4{"hello world"}; //右值引用绑定右值,临时变量被系统当作右值
string &&r5{strtest}; //不可以
int i = 100;
int &&r1 = i * 100; //可以
int &r2 = i*100; //不可以, i*100变成了右值
总结:
返回左值引用的函数,连同赋值、下标、解引用和前置递增递减运算符(--i\++i),都是返回左值表达式的例子。我们可以将左值引用绑定到这些表达式上。
返回非引用类型函数,连同算数,关系,位以及后置递增递减运算符,都生成右值,不能将一个左值引用绑定到这类表达上,但是我们可以将一个const引用或右值引用绑定到这个表达式上。
++i :直接个i加一,返回i本身,返回左值表达式
i++; 先用i,再加一。先产生临时变量,再给i加一,再返回临时变量的,右值表达式。
int i = 100;
(++i) = 199; //成立, ++i直接给变量i加1然后返回i本身。
i++ //先产生一个临时变量记录i的值用于使用,i的值被使用后,再给i+1;接着返回临时变量
int &&r1 = i++; //成功绑定右值,但是此后r1的值和i没有关系。
int &r2 = r1;
1、r1虽然是右值引用,但是r1本身是一个左值,要把r1看成一个变量。因为它再=左边。
2、所有变量都看成左值,因为他们是有地址的。
3、任何函数里的形参都是左值。void f(int && w);
4、临时对象都是右值
右值引用的目的:(c++11)
a、&&,代表一种新数据类型。
b、提高程序运行效率。把拷贝对象变成移动对象来提高程序运行效率。
左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。
右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是变量的别名,而右值引用则是不具名(匿名)变量的别名。
左值引用通常也不能绑定到右值,但常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。
左值地址。右值是数值。注意两个值之间的绑定问题。
三:std::move函数
C++11中的新新函数,建议使用时加上std::
移动:实际上没有做移动的操作 。只是把一个左值强制转换成一个右值的能力。
这样两个右值,因为右值本身是地址,所以,move可以实现两个值绑定到一起了。int i = 10;
int i = 10;
int &&ri = std::move(i); //把一个左值转换为右值,这就是move的能力
i = 20; //ri就代表i了
ri = 11; //ri就代表i了
string st = "hello";
string def = std::move(st);//执行到此时发现st被清空
//实际上是std::move()转换后的右值引用出发了string类的移动构造函数,把st的内容移动到了def中
//string的移动构造函数由于内存中的某些限制,并没有实现移动,而是重新开辟了一块内存
string st = "I Love China!";
const char *p = st.c_str() ;//看地址
string def = std::move(st) ;
const char *q = def.c_str() ;//看地址
地址是不同的
建议:在调用std::move()后不要再使用move()的参数
st 和 def 一样,没有移动构造函数。
std::forward()
#include <iostream>
int main ()
{
int &&rightV1 = 2;
//int &&rightV2 = rightV1;//报错
int &&rightV2 = std::move(rightV1);
rightV2 = 23;
std::cout << "r1 :" << rightV1 << std::endl;//23
std::cout << "r2 :" << rightV2 << std::endl;//23
std::cout <<"绑定左值同理" <<std::endl;
int i = 12;
int &&rightV3 = std::move(i);
rightV3 = 23;
std::cout << "i :" << i << std::endl;//23
std::cout << "r2 :" << rightV3 << std::endl;//23
return 0;
}
string st = " i Lou=ve China" ;
string def = std::move(st) ; //sting里的移动构造函数把st的内容转移到了def中,清空了st的值,而不是std::move移动了st
string st = " i Lou=ve China" ;
const char *p = st.c_str();
string &&deff = std::move(st); //不会使用移动构造函数。st并没有变。有的书要承诺:以后st不再使用
const char *q = def.c_str();
//查看p和q 的地址,是不相同的。开了新的内存。
int a = 10;
int&& p = move(a);
p = 100;
cout << p << " " << a << endl; //打印两个100
string str = "ljy";
string p_str = move(str); //调用了string的移动构造函数
p_str = "ljyljy";
cout << p_str << "---------" << str << endl; //p_str打印"ljyljy",str打印空
string str1 = "ljy";
string && p_str1 = move(str1);
p_str1 = "ljyljy";
cout << p_str1 << "---------" << str1 << endl; //p_str1和str1都打印"ljyljy"
std::forward()。 函数原型:std::forward<T>(u) 有两个参数:T 与 u。当T为左值引用类型时,u将被转换为T类型的左值,否则u将被转换为T类型右值。如此定义std::forward是为了在使用右值引用参数的函数模板中解决参数的完美转发问题。
//forward
template<typename _Tp>
inline _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t)
{ return static_cast<_Tp&&>(__t); }
int a = 10;
int &b = forward<int &>(a); //返回左值引用
int &&c = forward<int &&>(a); //返回右值引用
int &&d = forward<int>(a); //返回右值引用