左值和右值、左值引用与右值引用、移动语句(2)「建议收藏」

以下来自IBM知识中心表达式可以分为以下值类别之一:左值Lvalue:如果表达式不是const限定的,则表达式可以出现在赋值表达式的左侧。 x值:要过期的右值引用。 右值(Prvalue)rvalue:非xvalue表达式,仅出现在赋值表达式的右侧。Rvalues包括xvalues和prvalues。Lvalues和xvalues可以称为glvalues。Note:类(p…

大家好,又见面了,我是你们的朋友全栈君。

以下来自IBM知识中心 

表达式可以分为以下值类别之一:

  • 左值Lvalue:如果表达式不是const限定的,则表达式可以出现在赋值表达式的左侧。
  • x值:要过期的右值引用。
  • 右值(Prvalue) rvalue:非xvalue表达式,仅出现在赋值表达式的右侧。Rvalues包括xvalues和prvalues。 Lvalues和xvalues可以称为glvalues。

Note:

  1. 类(prvalue)rvalues可以是cv限定的,但非class(prvalue)rvalues不能是cv限定的。
  2. Lvalues和xvalues可以是不完整的类型,但是(prvalue)rvalues必须是完整类型或void类型。
  3. 对象是可以检查和存储的存储区域。左值或x值是引用此类对象的表达式。左值不一定允许修改它指定的对象。例如,const对象是无法修改的左值。术语可修改的左值用于强调左值允许指定的对象被改变以及被检查。

左值和右值、左值引用与右值引用、移动语句(2)「建议收藏」

左值并不一定出现在表达式的左边:

以下对象类型是左值,但不是可修改的左值:

  1. 数组类型
  2. 不完整的类型
  3. const限定类型
  4. 结构或联合类型,其成员之一被限定为const类型

因为这些左值不可修改,所以它们不能出现在赋值语句的左侧。

术语rvalue右值指的是存储在存储器中某个地址的数据值。

rvalue是一个不能赋值的表达式。文字常量和变量都可以作为右值。当左值出现在需要右值的上下文中时,左值将隐式转换为右值。然而,相反的情况并非如此:rvalue无法转换为左值。 Rvalues始终具有完整类型或void类型。

只有C将函数指定符定义为具有函数类型的表达式。函数指示符不同于对象类型或左值。它可以是函数的名称或取消引用函数指针的结果。 C语言还区分它对函数指针和对象指针的处理。

另一方面,在C ++中,返回引用的函数调用是左值。否则,函数调用是rvalue表达式。在C ++中,每个表达式都会产生左值,x值,(prvalue)rvalue或无值。

在C和C ++中,某些运算符需要一些操作数的左值。下表列出了这些运算符以及对其用法的其他限制。

Operator Requirement
& (一元)操作数必须是左值。
++ — 操作数必须是左值。 这适用于前缀和后缀形式。
= += -= *= %= <<= >>= &= ^= |= 左操作数必须是左值。

例如,所有赋值运算符都会计算其右操作数并将该值赋给其左操作数。 左操作数必须是可修改的左值或对可修改对象的引用。

地址运算符(&)需要左值作为操作数,而增量(++)和减量( – )运算符需要可修改的左值作为操作数。 以下示例显示表达式及其对应的左值。

Expression Lvalue
x = 42 x
*ptr = new value *ptr
a++ a
C++ int& f() The function call to f()

仅限C ++ 11的开头。

以下表达式是xvalues:

  • 调用返回类型为右值引用类型的函数的结果
  • 强制转换为右值参考
  • 通过xvalue表达式访问的非引用类型的非静态数据成员
  • 指向成员访问表达式的指针,其中第一个操作数是xvalue表达式,第二个操作数是指向成员类型的指针

请参阅以下示例:

int a;
int&& b= static_cast<int&&>(a);

struct str{
   int c;
};

int&& f(){
   int&& var =1;
   return var;
}

str&& g();
int&& rc = g().c;

在此示例中,右值引用b的初始值设定项是x值,因为它是转换为右值引用的结果。 对函数f()的调用产生一个xvalue,因为该函数的返回类型是int &&类型。 rvalue reference rc的初始值设定项是xvalue,因为它是一个通过xvalue表达式访问非静态非引用数据成员c的表达式。仅限C ++ 11及以后版本。

将亡值就定义了这样一种行为:临时的值能够被识别、同时又能够被移动

 

左值到右值的转化:

需要拿到一个将亡值,就需要用到右值引用的申明:T &&,其中 T 是类型。右值引用的声明让这个临时值的生命周期得以延长、只要变量还活着,那么将亡值将继续存活。

C++11 提供了 std::move 这个方法将左值参数无条件的转换为右值,有了它我们就能够方便的获得一个右值临时对象,例如:

#include <iostream>
#include <string>

void reference(std::string& str) {
	std::cout << "左值" << std::endl;
}
void reference(std::string&& str) {
	std::cout << "右值" << std::endl;
}

int main()
{
	std::string  lv1 = "string,";			 // lv1 是一个左值
	//std::string&& rv2 = lv1 ;				// 非法,lv1 是一个左值
	// std::string&& r1 = s1;				// 非法, s1 在全局上下文中没有声明
	reference(std::move(lv1));				// std::move 可以将左值转移为右值
	std::string&& rv1 = std::move(lv1);		// 合法, std::move 可以将左值转移为右值
	std::cout << "rv1 = " << rv1 << std::endl;      // string,

	const std::string& lv2 = lv1 + lv1;		// 合法, 常量左值引用能够延长临时变量的生命周期
	// lv2 += "Test";						// 非法, 引用的右值无法被修改
	std::cout << "lv2 = " << lv2 << std::endl;      // string,string

	//std::string&& rv2 = lv1 + lv1;		// 合法,lv1 + lv1生成一个临时对象
	std::string&& rv2 = lv1 + lv2;			// 合法, 右值引用延长临时对象的生命周期
	rv2 += "string";					    // 合法, 非常量引用能够修改临时变量
	std::cout << "rv2 = " << rv2 << std::endl;      // string,string,string,

	reference(rv2);							// 输出左值
}

 输出:

右值
rv1 = string,
lv2 = string,string,
rv2 = string,string,string,string
左值

注意rv2 虽然引用了一个右值,但由于它是一个引用,所以 rv2 依然是一个左值。

 

如果在编译器期望rvalue的情况下出现左值,则编译器将左值转换为右值。下表列出了此例外情况:

转换前的情况 产生的行为
左值是一种函数类型。  
左值是一个数组。  
左值的类型是不完整的类型。 编译时错误
左值是指未初始化的对象。 未定义的行为
左值是指不是右值类型的对象,也不是从右值类型派生的类型。 未定义的行为
C ++ 左值是非类型类型,由任一类型限定 常量 要么 挥发物。 转换后的类型也不合格 常量 要么 挥发物。

 

将亡值

在C++11之前的右值和C++11中的纯右值是等价的。C++11中的将亡值是随着右值引用的引入而新引入的。换言之,“将亡值”概念的产生,是由右值引用的产生而引起的,将亡值与右值引用息息相关。所谓的将亡值表达式,就是下列表达式:

  1. 返回右值引用的函数的调用表达式
  2. 转换为右值引用的转换函数的调用表达式

读者会问:这与“将亡”有什么关系?

在C++11中,我们用左值去初始化一个对象或为一个已有对象赋值时,会调用拷贝构造函数或拷贝赋值运算符来拷贝资源(所谓资源,就是指new出来的东西),而当我们用一个右值(包括纯右值和将亡值)来初始化或赋值时,会调用移动构造函数或移动赋值运算符来移动资源,从而避免拷贝,提高效率。

当该右值完成初始化或赋值的任务时,它的资源已经移动给了被初始化者或被赋值者,同时该右值也将会马上被销毁(析构)。也就是说,当一个右值准备完成初始化或赋值任务时,它已经“将亡”了。而上面两种表达式的结果都是不具名的右值引用,它们属于右值(关于“不具名的右值引用是右值”这一点,后面还会详细解释)。又因为

  1. 这种右值是与C++11新生事物——“右值引用”相关的“新右值”
  2. 这种右值常用来完成移动构造或移动赋值的特殊任务,扮演着“将亡”的角色

       所以C++11给这类右值起了一个新的名字——将亡值。

        std::move()、tsatic_cast<X&&>(x)(X是自定义的类,x是类对象,这两个函数常用来将左值强制转换成右值,从而使拷贝变成移动,提高效率,关于这些,后续文章中会详细介绍。

        事实上,将亡值不过是C++11提出的一块晦涩的语法糖。它与纯右值在功能上及其相似,如都不能做操作符的左操作数,都可以使用移动构造函数和移动赋值运算符。当一个纯右值来完成移动构造或移动赋值任务时,其实它也具有“将亡”的特点。一般我们不必刻意区分一个右值到底是纯右值还是将亡值。 

我们可以在获取更多资料:精简版 、详细版  
      

以下为网友看法(正确性无法保证):

对左值和右值的一个最常见的误解是:等号左边的就是左值,等号右边的就是右值。

左值和右值都是针对表达式而言的,左值是指表达式结束后依然存在的持久对象,右值是指表达式结束时就不再存在的临时对象。一个区分左值与右值的便捷方法是:看能不能对表达式取地址,如果能,则为左值,否则为右值。下面给出一些例子来进行说明。

在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。

 int a = 10;
 int b = 20;
 int *pFlag = &a;
 vector<int> vctTemp;
 vctTemp.push_back(1);
 string str1 = "hello ";
 string str2 = "world";
 const int &m = 1;
请问,a,b, a+b, a++, ++a, pFlag, *pFlag, vctTemp[0], 100, string("hello"), str1, str1+str2, m分别是左值还是右值?1.a和b都是持久对象(可以对其取地址),是左值;2.a+b是临时对象(不可以对其取地址),是右值;3.a++是先取出持久对象a的一份拷贝,再使持久对象a的值加1,最后返回那份拷贝,而那份拷贝是临时对象(不可以对其取地址),故其是右值;4.++a则是使持久对象a的值加1,并返回那个持久对象a本身(可以对其取地址),故其是左值;5.pFlag和*pFlag都是持久对象(可以对其取地址),是左值;6.vctTemp[0]调用了重载的[]操作符,而[]操作符返回的是一个int &,为持久对象(可以对其取地址),是左值;7.100和string("hello")是临时对象(不可以对其取地址),是右值;8.str1是持久对象(可以对其取地址),是左值;9.str1+str2是调用了+操作符,而+操作符返回的是一个string(不可以对其取地址),故其为右值;10.m是一个常量引用,引用到一个右值,但引用本身是一个持久对象(可以对其取地址),为左值。
区分清楚了左值与右值,我们再来看看左值引用。左值引用根据其修饰符的不同,可以分为非·常量左值引用和常量左值引用。

左值引用、右值引用

左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型,事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。

右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。

左值引用通常也不能绑定到右值但常量左值引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。

int &a = 2;       # 左值引用绑定到右值,编译失败。非常量引用的初始值必须为左值,无法从“int”转换为“int &”	
int b = 2;        # 非常量左值
int &a = b;       # 非常量左值引用绑定到非常量左值,编译通过
const int &c = b; # 常量左值引用绑定到非常量左值,编译通过
const int d = 2;  # 常量左值
const int &e = c; # 常量左值引用绑定到常量左值,编译通过
const int &b =2;  # 常量左值引用绑定到右值,编程通过

右值值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值,例如:

int a;
int &&r1 = a;             # 编译失败,无法将右值绑定到左值
int &&r2 = std::move(a);  # 编译通过

下表列出了在C++11中各种引用类型可以引用的值的类型。值得注意的是,只要能够绑定右值的引用类型,都能够延长右值的生命期。 

左值和右值、左值引用与右值引用、移动语句(2)「建议收藏」

参考:https://stackoverflow.com/questions/4986673/how-to-return-an-object-from-a-function-considering-c11-rvalues-and-move-seman

非常量左值引用只能绑定到非常量左值,不能绑定到常量左值、非常量右值和常量右值。如果允许绑定到常量左值和常量右值,则非常量左值引用可以用于修改常量左值和常量右值,这明显违反了其常量的含义。如果允许绑定到非常量右值,则会导致非常危险的情况出现,因为非常量右值是一个临时对象,非常量左值引用可能会使用一个已经被销毁了的临时对象。

常量左值引用可以绑定到所有类型的值,包括非常量左值、常量左值、非常量右值和常量右值。

可以看出,使用左值引用时,我们无法区分出绑定的是否是非常量右值的情况。那么,为什么要对非常量右值进行区分呢,区分出来了又有什么好处呢?这就牵涉到C++中一个著名的性能问题——拷贝临时对象。考虑下面的代码:

vector<int> GetAllScores()
{
 vector<int> vctTemp;
 vctTemp.push_back(90);
 vctTemp.push_back(95);
 return vctTemp;
}

当使用vector<int> vctScore = GetAllScores()进行初始化时,实际上调用了三次构造函数(一次是vecTemp的构造,一次是return 临时对象的构造,一次是vecScore的复制构造)

尽管有些编译器可以采用RVO(Return Value Optimization)来进行优化,但优化工作只在某些特定条件下才能进行。可以看到,上面很普通的一个函数调用,由于存在临时对象的拷贝,导致了额外的两次拷贝构造函数和析构函数的开销。当然,我们也可以修改函数的形式为void GetAllScores(vector<int> &vctScore),但这并不一定就是我们需要的形式。另外,考虑下面字符串的连接操作: 

 string s1("hello");
 string s = s1 + "a" + "b" + "c" + "d" + "e";

在对s进行初始化时,会产生大量的临时对象,并涉及到大量字符串的拷贝操作,这显然会影响程序的效率和性能。怎么解决这个问题呢?

如果我们能确定某个值是一个非常量右值(或者是一个以后不会再使用的左值),则我们在进行临时对象的拷贝时,可以不用拷贝实际的数据,而只是“窃取”指向实际数据的指针(类似于STL中的auto_ptr,会转移所有权)。C++ 11中引入的右值引用正好可用于标识一个非常量右值。C++ 11中用&表示左值引用,用&&表示右值引用,如:

 int &&a = 10;

右值引用根据其修饰符的不同,也可以分为非常量右值引用和常量右值引用。

非常量右值引用只能绑定到非常量右值,不能绑定到非常量左值、常量左值和常量右值。如果允许绑定到非常量左值,则可能会错误地窃取一个持久对象的数据,而这是非常危险的;如果允许绑定到常量左值和常量右值,则非常量右值引用可以用于修改常量左值和常量右值,这明显违反了其常量的含义。

常量右值引用可以绑定到非常量右值和常量右值,不能绑定到非常量左值和常量左值(理由同上)。

 有了右值引用的概念,我们就可以用它来实现下面的CMyString类。

class CMyString
{
public:
// 构造函数
 CMyString(const char *pszSrc = NULL)
 {
      cout << "CMyString(const char *pszSrc = NULL)" << endl;
      if (pszSrc == NULL)
      {
       m_pData = new char[1];
       *m_pData = '
class CMyString
{
public:
// 构造函数
CMyString(const char *pszSrc = NULL)
{
cout << "CMyString(const char *pszSrc = NULL)" << endl;
if (pszSrc == NULL)
{
m_pData = new char[1];
*m_pData = '\0';
}
else
{
m_pData = new char[strlen(pszSrc)+1];
strcpy(m_pData, pszSrc);
}
}
// 拷贝构造函数
CMyString(const CMyString &s)
{
cout << "CMyString(const CMyString &s)" << endl;
m_pData = new char[strlen(s.m_pData)+1];
strcpy(m_pData, s.m_pData);
}
// move构造函数     ----        实质上就是·窃取·临时对象,注意参数的形式
CMyString(CMyString &&s)
{
cout << "CMyString(CMyString &&s)" << endl;
m_pData = s.m_pData;
s.m_pData = NULL;
}
// 析构函数
~CMyString()
{
cout << "~CMyString()" << endl;
delete [] m_pData;
m_pData = NULL;
}
// 拷贝赋值函数
CMyString &operator =(const CMyString &s)
{
cout << "CMyString &operator =(const CMyString &s)" << endl;
if (this != &s)
{
delete [] m_pData;
m_pData = new char[strlen(s.m_pData)+1];
strcpy(m_pData, s.m_pData);
}
return *this;
}
// move赋值函数
CMyString &operator =(CMyString &&s)
{
cout << "CMyString &operator =(CMyString &&s)" << endl;
if (this != &s)
{
delete [] m_pData;
m_pData = s.m_pData;
s.m_pData = NULL;
}
return *this;
}
private:
char *m_pData;
};
'; } else { m_pData = new char[strlen(pszSrc)+1]; strcpy(m_pData, pszSrc); } } // 拷贝构造函数 CMyString(const CMyString &s) { cout << "CMyString(const CMyString &s)" << endl; m_pData = new char[strlen(s.m_pData)+1]; strcpy(m_pData, s.m_pData); } // move构造函数 ---- 实质上就是·窃取·临时对象,注意参数的形式 CMyString(CMyString &&s) { cout << "CMyString(CMyString &&s)" << endl; m_pData = s.m_pData; s.m_pData = NULL; } // 析构函数 ~CMyString() { cout << "~CMyString()" << endl; delete [] m_pData; m_pData = NULL; } // 拷贝赋值函数 CMyString &operator =(const CMyString &s) { cout << "CMyString &operator =(const CMyString &s)" << endl; if (this != &s) { delete [] m_pData; m_pData = new char[strlen(s.m_pData)+1]; strcpy(m_pData, s.m_pData); } return *this; } // move赋值函数 CMyString &operator =(CMyString &&s) { cout << "CMyString &operator =(CMyString &&s)" << endl; if (this != &s) { delete [] m_pData; m_pData = s.m_pData; s.m_pData = NULL; } return *this; } private: char *m_pData; };

如果提供了move版本的构造函数,则不会生成默认的构造函数。另外,编译器永远不会自动生成move版本的构造函数和赋值函数,它们需要你手动显式地添加。

当添加了move版本的构造函数和赋值函数的重载形式后,某一个函数调用应当使用哪一个重载版本呢?下面是按照判决的优先级列出的3条规则:
1、常量值只能绑定到常量引用上,不能绑定到非常量引用上。
2、左值优先绑定到左值引用上,右值优先绑定到右值引用上。
3、非常量值优先绑定到非常量引用上。

当给构造函数或赋值函数传入一个非常量右值时,依据上面给出的判决规则,可以得出会调用move版本的构造函数或赋值函数。而在move版本的构造函数或赋值函数内部,都是直接“移动”了其内部数据的指针(因为它是非常量右值,是一个临时对象,移动了其内部数据的指针不会导致任何问题,它马上就要被销毁了,我们只是重复利用了其内存),这样就省去了拷贝数据的大量开销。

一个需要注意的地方是,拷贝构造函数可以通过直接调用*this = s来实现,但move构造函数却不能。

这是因为在move构造函数中,s虽然是一个非常量右值引用,但其本身却是一个左值(是持久对象,可以对其取地址),因此调用*this = s时,会使用拷贝赋值函数而不是move赋值函数,而这已与move构造函数的语义不相符。要使语义正确,我们需要将左值绑定到非常量右值引用上,C++ 11提供了move函数来实现这种转换,因此我们可以修改为*this = move(s),这样move构造函数就会调用move赋值函数。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/126824.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)
blank

相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号