右值引用和移动语义以及C++11新增的类功能

发布时间:2023年12月26日

正文开始前给大家推荐个网站,前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站

右值引用和左值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,无论左值引用还是右值引用,都是给对象取别名。
什么是左值?什么是左值引用?
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

int main()
{
	// 以下的p、b、c、*p都是左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;
	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;

	delete p;
	return 0;
}

什么是右值?什么是右值引用?
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。

int main()
{
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);
	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
	//10 = 1;
	//x + y = 1;
	//fmin(x, y) = 1;
	return 0;
}

对于内置类型的右值我们称为纯右值,对于自定义类型的右值,它没有字面常量,和表达式,一般是函数的返回值,我们称为将亡值。
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,可以理解为右值被右值引用 引用以后它的属性就变成了左值。

左值引用和右值引用的比较
左值引用

  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右值。

右值引用

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

move可以改变变量的属性,可以将左值变为右值,但是变量本身的属性不会变,他是一个传值返回。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。

那么右值引用有什么用呢???

我们只到左值引用做参数和做返回值都可以提高效率。减少拷贝,但是当返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。如果返回的对象是一个内置类型,那消耗也不大,但是如果是一个深拷贝的对象,比如说string。

string tostring(int s)
{
	string ret;

	while (s)
	{
		ret += (s % 10) + '0';
		s /= 10;
	}
	reverse(ret.begin(), ret.end());
	return ret;
}

这种情况下是无法使用左值引用返回的。因为出了作用域ret就会销毁。但是如果使用传值返回,一定会面临深拷贝的问题,消耗太大。
在这里插入图片描述
使用传值返回将会面临2次深拷贝和两次析构,消耗很大。我们想一下,ret指向的资源是出了作用域就要销毁的值,但是他指向的空间是我们需要的,我们可不可以把它指向的空间交换出来,是不是就可以减少拷贝了。

在这里插入图片描述
像ret这种的将亡值,就是我们所说的右值,所以我们可以写一个右值的拷贝构造函数,也就是移动构造,我们可以把即将销毁的值转移出来,从而达到减少拷贝的作用。移动构造中没有新开空间,拷贝数据,所以效率提高了。

string(string&& s)
 :_str(nullptr)
 ,_size(0)
 ,_capacity(0)
{
	 swap(s);
}

有了移动构造以后,当发生这种右值拷贝拷贝时就能减少一次深拷贝,提升还是很大的,但是拷贝完以后,这个临时对象也是一个右值,要用它去赋值给s,所以和上面同理,我们还需要一个移动赋值。

// 移动赋值
string& operator=(string&& s)
{
	swap(s);
	return *this;
}

如果我们写了移动构造又写了拷贝构造,编译器会选择最合适的调用,如果拷贝的对象是右值,那么就会调用移动构造,左值就会调用拷贝构造,如果我们不写移动构造,那么编译器就只能调用拷贝构造,const修饰的左值引用也可以引用右值。

C++11,STL的容器都是增加了移动构造和移动赋值的。

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?
有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。

int main()
{
	string s1("hello world");
	// 这里s1是左值,调用的是拷贝构造
	string s2(s1);
	// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造
	// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的
	// 资源被转移给了s3,s1被置空了。
	string s3(std::move(s1));
 return 0;
}

C++11,STL容器插入接口函数也增加了右值引用版本

万能引用和完美转发

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,如果不加处理,接收的类型,后续使用中都退化成了左值。

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

template<typename T>
void PerfectForward(T&& t)
{
	//不加处理不管穿的是什么,这里的t都是左值
	// 如果加了move(t)那么又都会变为右值
	Fun(t);
}
int main()
{
	PerfectForward(10);  // 右值
	int a;
	PerfectForward(a);  // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b);  // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

我们希望能够在传递过程中保持它的左值或者右值的属性,那么就需要用到完美转发了。

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


template<typename T>
void PerfectForward(T&& t)
{
	//forward<T>就是完美转发
	Fun(forward<T>(t));
}
int main()
{
	PerfectForward(10);  // 右值
	int a;
	PerfectForward(a);  // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b);  // const 左值
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

新的类功能

原本C++的类有6个默认成员函数,C++11 新增了两个:移动构造函数和移动赋值运算符重载

如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

类成员变量初始化
C++11允许在类定义时给成员变量初始缺省值,默认生成构造函数会使用这些缺省值初始化。

class A
{
publicA(){}
private:
	int _a = 0;
};

强制生成默认函数的关键字default
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。

禁止生成默认函数的关键字delete
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明不定义,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。

那么今天的分享就到这里了,有什么不懂得可以私信博主,或者添加博主的微信,欢迎交流。

文章来源:https://blog.csdn.net/bushibrnxiaohaij/article/details/135088888
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。