详解 C++ 左值、右值、左值引用以及右值引用

这篇具有很好参考价值的文章主要介绍了详解 C++ 左值、右值、左值引用以及右值引用。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、左值和右值

1.左值

左值是一个表示数据的表达式,比如:变量名、解引用的指针变量。一般地,我们可以获取它的地址对它赋值,但被 const 修饰后的左值,不能给它赋值,但是仍然可以取它的地址。

总体而言,可以取地址的对象就是左值。

// 以下的a、p、*p、b都是左值
int a = 3;
int* p = &a;
*p;
const int b = 2;

2.右值

右值也是一个表示数据的表达式,比如:字面常量、表达式返回值,传值返回函数的返回值(是传值返回,而非传引用返回)右值不能出现在赋值符号的左边且不能取地址

总体而言,不可以取地址的对象就是右值。

double x = 1.3, y = 3.8;
// 以下几个都是常见的右值
10;                 // 字面常量
x + y;             // 表达式返回值
fmin(x, y);        // 传值返回函数的返回值

以下写法均不能通过编译:

  1. 10 = 4;x + y = 4;fmin(x, y) = 4;,VS2015 编译报错:error C2106: “=”: 左操作数必须为左值。原因:右值不能出现在赋值符号的左边。
  2. &10;&(x + y);&fmin(x, y);,VS2015 编译报错:error C2102: “&” 要求左值。原因:右值不能取地址。

3.总结

区分左值和右值,终究还是要看能否取地址。

二、左值引用和右值引用

传统的 C++ 语法中就存在引用语法,而 C++11标准中新增了右值引用的语法特性,因此为了区分两者,将C++11标准出现之前的引用称为左值引用。

无论左值引用还是右值引用,都是给对象取别名。

1.左值引用

左值引用就是对左值的引用,给左值取别名。

// 以下几个是对上面左值的左值引用
int& ra = a;
int*& rp = p;
int& r = *p;
const int& rb = b;

2.右值引用

右值引用就是对右值的引用,给右值取别名。

右值引用的表示是在具体的变量类型名称后加两个 &,比如:int&& rr = 4;

// 以下几个是对上面右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);

注意:
右值引用引用右值,会使右值被存储到特定的位置。
也就是说,右值引用变量其实是左值,可以对它取地址和赋值(const右值引用变量可以取地址但不可以赋值,因为 const 在起作用)。
当然,取地址是指取变量空间的地址(右值是不能取地址的)。

比如:

  1. double&& rr2 = x + y;
    &rr2;
    rr2 = 9.4;
    右值引用 rr2 引用右值 x + y 后,该表达式的返回值被存储到特定的位置,不能取表达式返回值 x + y 的地址,但是可以取 rr2 的地址,也可以修改 rr2 。
  2. const double&& rr4 = x + y;
    &rr4;
    可以对 rr4 取地址,但不能修改 rr4,即写成rr4 = 5.3;会编译报错。

现在我们知道左值引用可以引用左值,右值引用可以引用右值。
那么左值引用是否可以引用右值?右值引用是否可以引用左值呢?
下面的对比与总结给出了答案。

3.对比与总结

左值引用总结:

  1. 左值引用只能引用左值,不能直接引用右值。
  2. 但是const左值引用既可以引用左值,也可以引用右值。
// 1.左值引用只能引用左值
int t = 8;
int& rt1 = t;

//int& rt2 = 8;  // 编译报错,因为10是右值,不能直接引用右值


// 2.但是const左值引用既可以引用左值
const int& rt3 = t;

const int& rt4 = 8;  // 也可以引用右值
const double& r1 = x + y;
const double& r2 = fmin(x, y);

问:为什么const左值引用也可以引用右值?
答:在 C++11标准产生之前,是没有右值引用这个概念的,当时如果想要一个类型既能接收左值也能接收右值的话,需要用const左值引用,比如标准容器的 push_back 接口:void push_back (const T& val)
也就是说,如果const左值引用不能引用右值的话,有些接口就不好支持了。

下面就是 C++98标准中相关接口const左值引用引用右值的例子:

vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);

右值引用总结:

  1. 右值引用只能引用右值,不能直接引用左值。
  2. 但是右值引用可以引用被move的左值。

move,本文指std::move(C++11),作用是将一个左值强制转化为右值,以实现移动语义。
左值被 move 后变为右值,于是右值引用可以引用。

// 1.右值引用只能引用右值
int&& rr1 = 10;
double&& rr2 = x + y;
const double&& rr3 = x + y;

int t = 10;
//int&& rrt = t;  // 编译报错,不能直接引用左值


// 2.但是右值引用可以引用被move的左值
int&& rrt = std::move(t);
int*&& rr4 = std::move(p);
int&& rr5 = std::move(*p);
const int&& rr6 = std::move(b);

三、左值引用的使用场景及实际意义

1.使用场景

// 1.左值引用做参数
void func1(string s)
{...}

void func2(const string& s)
{...}


int main()
{
	string s1("Hello World!");
	func1(s1);  // 由于是传值传参且做的是深拷贝,代价较大
	func2(s1);  // 左值引用做参数减少了拷贝,提高了效率
	
	return 0;
}
// 2.左值引用做返回值(仅限于对象出了函数作用域以后还存在的情况)
string s2("hello");
// string operator+=(char ch)  传值返回存在拷贝且是深拷贝
// string& operator+=(char ch)  左值引用做返回值没有拷贝,提高了效率
s2 += '!';

2.实际意义

传值传参和传值返回都会产生拷贝,有的甚至是深拷贝,代价很大。而左值引用的实际意义在于做参数和做返回值都可以减少拷贝,从而提高效率。

3.短板

左值引用虽然较完美地解决了大部分问题,但对于有些问题仍然不能很好地解决。

当对象出了函数作用域以后仍然存在时,可以使用左值引用返回,这是没问题的。

string& operator+=(char ch)
{
	push_back(ch);
	return *this;
}

但当对象(对象是函数内的局部对象)出了函数作用域以后不存在时,就不可以使用左值引用返回了。

string operator+(const string& s, char ch)
{
	string ret(s);
	ret.push_back(ch);
	return ret;
}

// 拿现在这个函数来举例:ret是函数内的局部对象,出了函数作用域后会被析构,即被销毁了
// 若此时再返回它的别名(左值引用),也就是再拿这个对象来用,就会出问题

于是,对于第二种情形,左值引用也无能为力,只能传值返回。

四、右值引用

于是,为了解决上述传值返回的拷贝问题,C++11标准就增加了右值引用移动语义

1.移动语义(Move semantics)

将一个对象中的资源移动到另一个对象(资源控制权的转移)。

(1)移动构造

① 概念

转移参数右值的资源来构造自己。

// 这是一个模拟string类的实现的移动构造
string(string&& s)
	:_str(nullptr)
	, _size(0)
	, _capacity(0)
{
	swap(s);
}

拷贝构造函数和移动构造函数都是构造函数的重载函数,所不同的是:

  1. 拷贝构造函数的参数是 const左值引用,接收左值或右值;
  2. 移动构造函数的参数是右值引用,接收右值或被 move 的左值。

注:当传来的参数是右值时,虽然拷贝构造函数可以接收,但是编译器会认为移动构造函数更加匹配,就会调用移动构造函数。

总的来说,如果这两个函数都有在类内定义的话,在构造对象时:

  1. 若是左值做参数,那么就会调用拷贝构造函数,做一次拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次拷贝构造就会做一次深拷贝)。
  2. 若是右值做参数,那么就会调用移动构造,而调用移动构造就会减少拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次移动构造就会少做一次深拷贝)。

比如执行下面这几行代码:

string s("Hello World11111111111111111");
string s1 = s;  // s是左值,所以调用拷贝构造函数
string s2 = move(s);  // s被move后变为右值,所以调用移动构造函数,s的资源会被转移用来构造s2
// 要注意的是,move一般是不这样用的,因为s的资源被转走了

执行string s1 = s;前:详解 C++ 左值、右值、左值引用以及右值引用
执行string s1 = s;后(也是执行string s2 = move(s);前):
详解 C++ 左值、右值、左值引用以及右值引用
执行string s2 = move(s);后:
详解 C++ 左值、右值、左值引用以及右值引用

② 移动构造有无的比较

比如执行语句cout << MyLib::to_string(1234) << endl;

只有拷贝构造没有移动构造:
详解 C++ 左值、右值、左值引用以及右值引用
在 to_string 函数栈帧销毁前,用局部对象 str 拷贝构造出临时对象返回到函数调用处。
详解 C++ 左值、右值、左值引用以及右值引用
既有拷贝构造也有移动构造:
详解 C++ 左值、右值、左值引用以及右值引用
在 to_string 函数栈帧销毁前,用局部对象 str (反正 str 要销毁,将 str 视为右值,直接转移 str 的资源 )移动构造出临时对象返回到函数调用处。
详解 C++ 左值、右值、左值引用以及右值引用

比如执行语句MyLib::string ret = MyLib::to_string(1234);

只有拷贝构造没有移动构造:详解 C++ 左值、右值、左值引用以及右值引用
在 to_string 函数栈帧销毁前,先用局部对象 str 拷贝构造出临时对象返回到函数调用处,to_string 函数栈帧销毁后,再用临时对象拷贝构造出 ret 。
但现在的编译器一般都会进行优化:因为临时对象有 ret 来接收,这样的话临时对象的创建和销毁就显得多余了,不如省略掉这一步,直接用 str 拷贝构造出 ret 。
详解 C++ 左值、右值、左值引用以及右值引用
既有拷贝构造也有移动构造:详解 C++ 左值、右值、左值引用以及右值引用
在 to_string 函数栈帧销毁前,由于局部对象 str 是左值(可以对它取地址),所以用 str 拷贝构造出临时对象返回到函数调用处,to_string 函数栈帧销毁后,由于临时对象是右值,所以用临时对象移动构造出 ret 。
但现在的编译器一般都会进行优化:因为临时对象有 ret 来接收,先拷贝构造出临时对象再用它移动构造出 ret ,临时对象好像没必要产生一样,不如省略掉。既然 str 是 to_string 函数栈帧的局部对象,最后还是要销毁,不如将 str 视为右值,直接转移 str 的资源用来构造 ret ,也就是直接用 str 移动构造出 ret 。
详解 C++ 左值、右值、左值引用以及右值引用

再比如执行下面的代码:

详解 C++ 左值、右值、左值引用以及右值引用
调用该函数后,需要传值返回这种占用很多资源的自定义类型,
在 C++98 中,没有移动构造,拷贝构造做深拷贝,花费的代价很大;
在 C++11 中,直接移动构造,转移 m 的资源给 ret ,提高了效率。

(2)移动赋值

① 概念

转移参数右值的资源来赋给自己。

// 这是一个模拟string类的实现的移动赋值
string& operator=(string&& s)
{
	swap(s);

	return *this;
}

拷贝赋值函数和移动赋值函数都是赋值运算符重载函数的重载函数,所不同的是:

  1. 拷贝赋值函数的参数是 const左值引用,接收左值或右值;
  2. 移动赋值函数的参数是右值引用,接收右值或被 move 的左值。

注:当传来的参数是右值时,虽然拷贝赋值函数可以接收,但是编译器会认为移动赋值函数更加匹配,就会调用移动赋值函数。

总的来说,如果这两个函数都有在类内定义的话,在进行对象的赋值时:

  1. 若是左值做参数,那么就会调用拷贝赋值,做一次拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次拷贝赋值就会做一次深拷贝)。
  2. 若是右值做参数,那么就会调用移动赋值,而调用移动赋值就会减少拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次移动赋值就会少做一次深拷贝)。

比如下面这几行代码:

string s("11111111111111111");
string s1("22222222222222222");
s1 = s;  // s是左值,所以调用拷贝赋值函数

string s2("333333333333333333");
s2 = std::move(s);  // s被move后变为右值,所以调用移动赋值函数,s的资源会被转移用来赋给s2
// 要注意的是,move一般是不这样用的,因为s的资源被转走了
② 移动赋值有无的比较

比如执行下面的语句:
MyLib::string ret("111111111111111111111111");
ret = MyLib::to_string(12345);

没有移动赋值(有移动构造和拷贝赋值):
详解 C++ 左值、右值、左值引用以及右值引用
用 str(编译器视 str 为右值)移动构造出临时对象作为返回值,再用临时对象拷贝赋值给 ret 。
详解 C++ 左值、右值、左值引用以及右值引用
有移动赋值:
详解 C++ 左值、右值、左值引用以及右值引用
用 str(编译器视 str 为右值)移动构造出临时对象作为返回值,由于临时对象是右值,再用临时对象移动赋值给 ret 。
详解 C++ 左值、右值、左值引用以及右值引用

2.右值引用的使用场景

除了上面的使用场景之外,C++11标准的STL 容器的相关接口函数也增加了右值引用版本。

比如:

详解 C++ 左值、右值、左值引用以及右值引用详解 C++ 左值、右值、左值引用以及右值引用

3.完美转发(Perfect forwarding)

(1)引入原因

在此之前我们需要知道什么是万能引用:

确定类型的 && 表示右值引用(比如:int&& ,string&&),
但函数模板中的 && 不表示右值引用,而是万能引用,模板类型必须通过推断才能确定,其接收左值后会被推导为左值引用,接收右值后会被推导为右值引用

注意区分右值引用和万能引用:下面的函数的 T&& 并不是万能引用,因为 T 的类型在模板实例化时已经确定。

template<typename T>
class A
{
	void func(T&& t);  // 模板实例化时T的类型已经确定,调用函数时T是一个确定类型,所以这里是右值引用
};

让我们通过下面的程序来认识万能引用:

template<typename T>
void f(T&& t)  // 万能引用
{
	//...
}

int main()
{
	int a = 5;  // 左值
	f(a);  // 传参后万能引用被推导为左值引用
	
	const string s("hello");  // const左值
	f(s);  // 传参后万能引用被推导为const左值引用
	
	f(to_string(1234));  // to_string函数会返回一个string临时对象,是右值,传参后万能引用被推导为右值引用

	const double d = 1.1;
	f(std::move(d));  // const左值被move后变成const右值,传参后万能引用被推导为const右值引用
	
	return 0;
}

在调试下开监视窗口可看到传参后参数 t 的类型:详解 C++ 左值、右值、左值引用以及右值引用

于是我们会用万能引用去做一些有意义的事,比如下面的代码:

void Func(int& x) {	cout << "左值引用" << endl; }

void Func(const int& x) { cout << "const左值引用" << endl; }

void Func(int&& x) { cout << "右值引用" << endl; }

void Func(const int&& x) { cout << "const右值引用" << endl; }


template<typename T>
void f(T&& t)  // 万能引用
{
	Func(t);  // 根据参数t的类型去匹配合适的重载函数
}

int main()
{
	int a = 4;  // 左值
	f(a);
	
	const int b = 8;  // const左值
	f(b);
	
	f(10); // 10是右值
	
	const int c = 13;
	f(std::move(c));  // const左值被move后变成const右值
	
	return 0;
}

运行程序后,我们本以为打印的结果是:
左值引用
const左值引用
右值引用
const右值引用

但实际的结果却是:
详解 C++ 左值、右值、左值引用以及右值引用
后两行的运行结果跟我们预想的不一样。

那么这是怎么一回事呢?
其实在本文的前面已经讲过了,右值引用变量其实是左值,所以就有了上面的运行结果。

具体解释:

  1. f(10);
    10是右值,传参后万能引用被推导为右值引用,但该右值引用变量其实是左值,因此实际调用的函数是void Func(int& x)
  2. f(std::move(c));
    const左值被move后变成const右值,传参后万能引用被推导为const右值引用,但该const右值引用变量其实是const左值,因此实际调用的函数是void Func(const int& x)

也就是说,右值引用失去了右值的属性

但我们希望的是,在传递过程中能够保持住它的原有的左值或右值属性,于是 C++11标准提出完美转发。

(2)概念

完美转发是指在函数模板中,完全依照模板的参数类型,将参数传递给当前函数模板中的另外一个函数。

因此,为了实现完美转发,除了使用万能引用之外,我们还要用到std::forward(C++11),它在传参的过程中保留对象的原生类型属性

这样右值引用在传递过程中就能够保持右值的属性。

void Func(int& x) { cout << "左值引用" << endl; }

void Func(const int& x) { cout << "const左值引用" << endl; }

void Func(int&& x) { cout << "右值引用" << endl; }

void Func(const int&& x) { cout << "const右值引用" << endl; }


template<typename T>
void PerfectForward(T&& t)  // 万能引用
{
	Func(std::forward<T>(t));  // 根据参数t的类型去匹配合适的重载函数
}

int main()
{
	int a = 4;  // 左值
	PerfectForward(a);

	const int b = 8;  // const左值
	PerfectForward(b);

	PerfectForward(10); // 10是右值

	const int c = 13;
	PerfectForward(std::move(c));  // const左值被move后变成const右值

	return 0;
}

运行结果如下:
详解 C++ 左值、右值、左值引用以及右值引用

实现完美转发需要用到万能引用和 std::forward 。

(3)使用场景

除了上面的使用场景之外,C++11标准的 STL 容器的相关接口函数也实现了完美转发,这样就能够真正实现右值引用的价值。

比如 STL 库中的容器 list :
详解 C++ 左值、右值、左值引用以及右值引用

上面四个接口函数都调用 _Insert 函数,_Insert 函数模板实现了完美转发。

再比如自己模拟实现的 list(这里只写出主要部分):

template<class T>
struct ListNode
{
	ListNode* _next = nullptr;
	ListNode* _prev = nullptr;
	T _data;
};


template<class T>
class List
{
	typedef ListNode<T> Node;
public:
	List()
	{
		_head = new Node;
		_head->_next = _head;
		_head->_prev = _head;
	}

	void PushBack(const T& x)  // 左值引用
	{
		Insert(_head, x);
	}

	void PushFront(const T& x)  // 左值引用
	{
		Insert(_head->_next, x);
	}

	void PushBack(T&& x)  // 右值引用
	{
		Insert(_head, std::forward<T>(x));  // 关键位置:保留对象的原生类型属性
	}

	void PushFront(T&& x)  // 右值引用
	{
		Insert(_head->_next, std::forward<T>(x));  // 关键位置:保留对象的原生类型属性
	}

	template<class TPL>    // 该函数模板实现了完美转发
	void Insert(Node* pos, TPL&& x)  // 万能引用
	{
		Node* prev = pos->_prev;
		Node* newnode = new Node;
		newnode->_data = std::forward<TPL>(x);  // 关键位置:保留对象的原生类型属性

		// prev newnode pos
		prev->_next = newnode;
		newnode->_prev = prev;
		newnode->_next = pos;
		pos->_prev = newnode;
	}

private:
	Node* _head;
};

只要是右值引用,由当前函数再传递给其它函数调用,要保持右值属性,必须实现完美转发。

4.重大意义

右值引用(及其支持的移动语义和完美转发)是 C++11 中加入的最重要的新特性之一,它使得 C++ 程序的运行更加高效。文章来源地址https://www.toymoban.com/news/detail-402998.html

到了这里,关于详解 C++ 左值、右值、左值引用以及右值引用的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • MordernC++之左值(引用)与右值(引用)

    C++中左值与右值的概念是从C中继承而来,一种简单的定义是 左值能够出现再表达式的左边或者右边,而右值只能出现在表达式的右边 。 另一种区分左值和右值的方法是: 有名字、能取地址的值是左值,没有名字、不能取地址的值是右值 。比如上述语句中a,b, c是变量可以

    2023年04月09日
    浏览(41)
  • 【C++11】左值引用和右值引用

    需要云服务器等云产品来学习Linux的同学可以移步/--腾讯云--/--阿里云--/--华为云--/官网,轻量型云服务器低至112元/年,新用户首次下单享超低折扣。     目录 一、新的类功能 1、新的默认成员函数 2、类成员变量初始化 3、强制生成默认函数的default 4、禁止生成默认函

    2023年04月17日
    浏览(49)
  • 【送书】【C++11】左值引用和右值引用

    需要云服务器等云产品来学习Linux的同学可以移步/--腾讯云--/--阿里云--/--华为云--/官网,轻量型云服务器低至112元/年,新用户首次下单享超低折扣。     目录 一、新的类功能 1、新的默认成员函数 2、类成员变量初始化 3、强制生成默认函数的default 4、禁止生成默认函

    2023年04月09日
    浏览(39)
  • 左值引用、右值引用,std::move() 的汇编解释

    1:左值引用 引用其实还是指针,但回避了指针这个名字。由编译器完成从地址中取值。以vs2019反汇编: 如图,指针和引用的汇编代码完全一样。但引用在高级语言层面更友好,对人脑。比如可以少写一个 * 号和 - 。 ,以下是指针和引用的使用: 以上就是左值引用,引用的

    2024年02月04日
    浏览(39)
  • C++ 左值和右值

    在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。举个例子,int a = b+c, a 就是左值,其有变量名为a,通过a可以获取该变量的地址;

    2024年02月14日
    浏览(39)
  • C++左值右值完美转发转移

    英文含义: 左值(Lvalue) : Locator value ,意味着它指向一个具体的内存位置。 右值(Rvalue) : Read value ,指的是可以读取的数据,但不一定指向一个固定的内存位置。 定义 左值 :指的是一个持久的内存地址。左值可以出现在赋值操作的左侧或右侧。例如,变量、数组的元

    2024年03月10日
    浏览(54)
  • C++面试八股文:什么是左值,什么是右值?

    某日二师兄参加XXX科技公司的C++工程师开发岗位第16面: 面试官:什么是左值,什么是右值? 二师兄:简单来说,左值就是可以使用 符号取地址的值,而右值一般不可以使用 符号取地址。 二师兄:一般左值存在内存中,而右值存在寄存器中。 二师兄:严格意义上分,右值

    2024年02月09日
    浏览(41)
  • C++:深入理解C++11新特性:Chapter3:左值和右值

    在C语言中,我们常常会提起左值(lvalue),右值(rvalue)这样的称呼,而在编译程序时,编译器有时也会报出错误信息中包含 左值,右值说法。不过左值、右值通常不是通过一个严谨的定义而为人所知。下面我通过这样一个例子,来引导大家认识: 左值,右值,左值引用,右

    2024年02月04日
    浏览(53)
  • C++右值引用,右值引用与const引用的区别

    左值:可以取地址的、有名字的变量,有持久性; 右值:一般是不可寻址的常量,或在表达式求值过程中创建的无名临时对象,短暂性的。 C++11新增了另一种引用——右值引用。这种引用可指向右值,使用声明。 右值引用只能引用临时变量和常量值。 const引用:可以引用普

    2024年01月18日
    浏览(64)
  • 【C++】右值引用

    引用是给对象取别名,本质是为了减少拷贝 。以前我们学习的引用都是左值引用,右值引用是C++11新增的语法,它们的共同点都是给对象取别名。既然如此,有了左值引用,为什么还要有右值引用?右值引用具体是怎样的?以及它有哪些应用场景?接下来,会详细分析~~ 左值

    2024年04月11日
    浏览(44)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包