【C++进阶学习】第十一弹——C++11(上)——右值引用和移动语义

2024-08-05 08:48:03 浏览数 (2)

前言:

前面我们已经将C 的重点语法讲的大差不差了,但是在C 11版本之后,又出来了很多新的语法,其中有一些作用还是非常大的,今天我们就先来学习其中一个很重要的点——右值引用以及它所扩展的移动定义

一、左值引用和右值引用

左值引用

左值引用是最常见的引用类型,通常用于绑定到一个左值。左值是一个具有名称的对象,可以取地址,通常出现在赋值操作符的左边。(简单的说,能取地址的就是左值)

语法:

代码语言:javascript复制
类型 &引用名 = 左值;

示例:

代码语言:javascript复制
int a = 10;
int &refA = a;  // refA是一个左值引用,绑定到左值a

特点:

  • 左值引用必须初始化,并且只能绑定到左值。
  • 左值引用可以修改绑定的对象。
右值引用

右值引用是C 11引入的新特性,用于绑定到一个右值。右值是一个临时对象,通常没有名称,不能取地址,通常出现在赋值操作符的右边。(右值不能取地址,比如常量)

语法:

代码语言:javascript复制
类型 &&引用名 = 右值;

示例:

代码语言:javascript复制
int &&refB = 20;  // refB是一个右值引用,绑定到右值20

特点:

  • 右值引用必须初始化,并且只能绑定到右值。
  • 右值引用主要用于实现移动语义和完美转发。

有一个需要强调的是,常变量虽然也属于常量,但是它可以取地址,所以它属于左值

二、左值引用与右值引用的比较

左值引用:

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

代码语言:javascript复制
int main()
{
    // 左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a;   // ra为a的别名
    //int& ra2 = 10;   // 编译失败,因为10是右值
    // const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10;
    const int& ra4 = a;
    return 0;
}

右值引用:

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

代码语言:javascript复制
int main()
{
 // 右值引用只能右值,不能引用左值。
 int&& r1 = 10;
 
 // error C2440: “初始化”: 无法从“int”转换为“int &&”
 // message : 无法将左值绑定到右值引用
 int a = 10;
 int&& r2 = a;
 // 右值引用可以引用move以后的左值
 int&& r3 = std::move(a);
 return 0;
}

三、右值引用的使用

在上面我们也已经讲到了,左值引用及可以引用左值,又可以引用右值,那么C 11为什么还要设计右值引用呢?下面我们来看一下原因。

我们借助string类来讲解

先来看一下下面所出现的所有代码,可以先思考看看思考思考

代码语言:javascript复制
namespace zda
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str   _size;
		}
		string(const char* str = "")
			:_size(strlen(str))
			, _capacity(_size)
		{
			//cout << "string(char* str)" << endl;
			_str = new char[_capacity   1];
			strcpy(_str, str);
		}
		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}
		// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}
		// 赋值重载
		string& operator=(const string& s)
		{
			cout << "string& operator=(string s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}
		// 移动构造
		string(string&& s)   //右值引用
			:_str(nullptr)
			, _size(0)
			, _capacity(0)
		{
			cout << "string(string&& s) -- 移动语义" << endl;
			swap(s);
		}
		// 移动赋值
		string& operator=(string&& s)    //右值引用
		{
			cout << "string& operator=(string&& s) -- 移动语义" << endl;
			swap(s);
			return *this;
		}
		~string()
		{
			delete[] _str;
			_str = nullptr;
		}
		char& operator[](size_t pos)
		{
			assert(pos < _size);
			return _str[pos];
		}
		void reserve(size_t n)
		{
			if (n > _capacity)
			{
				char* tmp = new char[n   1];
				strcpy(tmp, _str);
				delete[] _str;
				_str = tmp;
				_capacity = n;
			}
		}
		void push_back(char ch)
		{
			if (_size >= _capacity)
			{
				size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
				reserve(newcapacity);
			}
			_str[_size] = ch;
			  _size;
			_str[_size] = '';
		}
		//string operator =(char ch)
		string& operator =(char ch)
		{
			push_back(ch);
			return *this;
		}
		const char* c_str() const
		{
			return _str;
		}
	private:
		char* _str;
		size_t _size;
		size_t _capacity; // 不包含最后做标识的
	};
	string to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}
		string str;
		while (value > 0)
		{
			int x = value % 10;
			value /= 10;
			str  = ('0'   x);
		}
		if (flag == false)
		{
			str  = '-';
		}
		std::reverse(str.begin(), str.end());
		return str;
	}
}

左值引用使用场景:

代码语言:javascript复制
void func1(bit::string s)
{}
void func2(const bit::string& s)
{}
int main()
{
 bit::string s1("hello world");
 // func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
 func1(s1);
 func2(s1);
 // string operator =(char ch) 传值返回存在深拷贝
 // string& operator =(char ch) 传左值引用没有拷贝提高了效率
 s1  = '!';
 return 0;
}

左值引用短板:

当函数返回对象为临时变量的时候,左值引用就派不上用场了,就只能传值返回,就需要拷贝至少一次(老一点的编译器为两次)

右值引用和移动语义:

对于上面这种问题,我们就可以通过右值引用和移动语义来实现

移动构造

移动构造的本质就是将参数的右值窃取过来,占为己有,这样它就不用再深度拷贝了,所以叫做移动构造

代码语言:javascript复制
// 移动构造
string(string&& s)
 :_str(nullptr)
 ,_size(0)
 ,_capacity(0)
{
 cout << "string(string&& s) -- 移动语义" << endl;
 swap(s);
}
int main()
{
 zda::string ret2 = bit::to_string(-1234);
 return 0;
}

当返回值是右值时,因为移动构造并没有开辟空间进行深拷贝,所以效率就会更高

需要注意的是,当拷贝构造和移动构造同时存在时,编译器默认的也会调用移动构造,因为编译器会默认调用效率更高的函数

移动赋值
代码语言:javascript复制
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
int main()
{
 zda::string ret1;
 ret1 = zda::to_string(1234);
 return 0;
}

// 运行结果:
// string(string&& s) -- 移动语义
// string& operator=(string&& s) -- 移动语义

这里运行后发现,调用了一次移动构造和一次移动赋值,因为这里的ret1是一个已经存在的对象,用它来接受函数返回值的时候编译器就无法再优化了,所以会在移动构造后创建一个临时变量,且这个临时变量会被编译器识别为右值,从而调用移动赋值

四、总结

上面我们就简单的先提了一下右值引用的应用:移动语义,下一篇我们再重点讲解一下右值引用的另一个重点语法:完美挥发

0 人点赞