C/C++(四)类和对象

时间:2024-10-21 14:40:29

3、移动语义(移动构造 + 移动赋值)(重点 + 难点)

3.1  左值引用 VS 右值引用

笔者在C/C++(二)中介绍的引用叫做左值引用,是传统C++语法中的引用;而右值引用,是C++11标准中新增的语法概念。

左值引用:对左值的引用,为左值取别名。所谓左值,就是可以出现在赋值符号左边(也可以出现在赋值符号右边,表示要赋值给其他对象的源对象)的值,一般都是变量 / 解引用的指针 / 对象标识符,可以获取其地址(左值最重要的核心特点),也可以对其赋值。(PS:const符号修饰后的左值,不能多次赋值,但是仍可以取其地址)

右值引用:右值的引用,为右值取别名。所谓右值,就是只能出现在赋值符号右边的值,  一般不能被修改,常见的右值有——常量、表达式的返回值、函数的返回值;右值又分为纯右值(往往是内置类型的右值)将亡值(往往是自定义类型的右值)纯右值不能取地址。

move函数:可以把左值转右值,右值转左值。(PS:但是不要轻易把自定义类型的左值 move 成右值,可能会被识别成将亡值,将亡值会在移动构造 / 移动赋值中导致资源被转移,导致源对象失去其资源)

注意

左值引用只能引用左值,但是const左值引用可以引用右值(因为右值不能修改, const左值引用也会让引用不能修改。)

右值引用只能引用右值 / move后的左值。

3.2  左值引用的使用场景

直接上代码:

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string.h>
#include <assert.h>
using namespace std;

namespace dfwm
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 构造函数
		string(const char* str = "") :_size(strlen(str)), _capacity(_size)
		{
			//cout << "string(char* str)" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// 拷贝构造函数
		string(const string& s):_str(nullptr)
		{
			cout << this << " string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}

		// 赋值运算符重载
		string& operator=(const string& s)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}

		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] = '\0';
		}

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

		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};
}

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

根据运行结果我们可以发现,传引用并没有调用拷贝构造函数进行深拷贝,提高了效率。

所以左值引用做参数和做返回值都可以有效提高效率

3.3  左值引用的短板

因为引用是别名,所以如果函数的返回对象是个局部变量,出了函数作用域就销毁了,这时候就不能左值引用返回了,否则会造成未定义行为。

继续上代码:

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string.h>
#include <assert.h>
using namespace std;

namespace dfwm
{
	class string
	{
	public:
		typedef char* iterator;
		iterator begin()
		{
			return _str;
		}
		iterator end()
		{
			return _str + _size;
		}

		// s1.swap(s2)
		void swap(string& s)
		{
			::swap(_str, s._str);
			::swap(_size, s._size);
			::swap(_capacity, s._capacity);
		}

		// 构造函数
		string(const char* str = "") :_size(strlen(str)), _capacity(_size)
		{
			//cout << "string(char* str)" << endl;
			_str = new char[_capacity + 1];
			strcpy(_str, str);
		}
		// 拷贝构造函数
		string(const string& s) :_str(nullptr)
		{
			cout << this << " string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}

		// 赋值运算符重载
		string& operator=(const string& s)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s);
			swap(tmp);
			return *this;
		}

		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] = '\0';
		}

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

		const char* c_str() const
		{
			return _str;
		}

		~string()
		{
			delete[] _str;
			_str = nullptr;
			_size = 0;
			_capacity = 0;
		}

	private:
		char* _str;
		size_t _size;
		size_t _capacity;
	};


	dfwm::string& to_string(int value)
	{
		bool flag = true;
		if (value < 0)
		{
			flag = false;
			value = 0 - value;
		}
		dfwm::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;
	}
}

int main()
{
	dfwm::string s1 = dfwm::to_string(1234);
	printf("%s", s1.c_str());
	return 0;
}

我们为 to_string函数传引用返回,由于 str 是个局部变量,会发现打印出来乱码,这是极其危险的。但是如果传值返回的话,因为是深拷贝,效率会显得很低。

这时候就需要右值引用和移动语义来解决了。

3.4  右值引用和移动语义

3.4.1  移动构造(使用时机:创建新的对象期间)

我们在dfwm::string 里面添加移动构造函数,移动构造函数的构成和拷贝构造函数类似,只是参数由左值引用变成右值引用

// 移动构造函数
string(string&& s) :_str(nullptr)
{
	cout << "string(string&& s) —— 移动构造" << endl;
	swap(s);
}

从运行结果可以发现,虽然仍是传值返回,但是因为有移动构造函数,调用了移动构造函数,可以提高效率

移动构造的原理

在介绍原理之前,我们给右值里面的将亡值下一个明确的定义:

将亡值其实就是出了作用域就会被销毁的对象。

移动构造,就是把这个将亡值(这也就是为什么移动构造的参数必须是右值引用)的资源直接窃取过来,不用再做深拷贝开辟新空间创建新对象拷贝数据了,提高了效率。

之所以叫移动构造,就是因为相当于把别人的资源移动过来构造自己

3.4.2  移动赋值(使用时机:两个都已经存在的对象之间赋值)

移动赋值就类似于拷贝赋值,只不过参数同样从左值引用变为右值引用

// 移动赋值
string& operator=(string&& s)
{
	cout << "string& operator=(string&& s) —— 移动赋值" << endl;
	swap(s);
	return *this;
}

int main()
{
	dfwm::string s;
	s = dfwm::to_string(1234);
	printf("%s", s.c_str());
	return 0;
}

运行可以发现,本来的传值赋值,变成了调用移动赋值,提高了效率。

移动赋值的原理

移动赋值的原理类似于移动构造,只不过,由于移动赋值是两个存在的对象之间进行赋值,所以底层原理是把将亡值的资源移动给自己,把自己的废弃资源转移给将亡值,随着将亡值的生命终结,废弃资源也被释放。

3.5  总结

右值引用与移动语义的出现,让传值调用和传值返回的效率都得到了有效提高;在 STL 容器里普遍都添加了移动构造和移动赋值。