C++ 移动构造函数

参考链接

rvalue & lvalue
左值引用右值引用
右值引用的好处


左值,右值

左值就是通过变量名指向具体地址的值,如普通变量,指针,和返回值为引用的函数调用;右值就是不指向具体地址的值,如常量,临时变量,计算表达式(的中间结果),返回值不为引用的函数调用。左值在生存期持续存在,而右值要么不存在,要么只是暂时存在。在表达式中,左值可以出现在等号的左右两边,但是右值只能存在于等号的右边

or

C++( 包括 C) 中所有的表达式和变量要么是左值,要么是右值。通俗的左值的定义就是非临时对象,那些可以在多条语句中使用的对象。 所有的变量都满足这个定义,在多条代码中都可以使用,都是左值。 右值是指临时的对象,它们只在当前的语句中有效。
对右值的取地址是错误的,因为内存中不存在这样一块确定的区域;同时,取地址得到的也是右值,如下

int var = 10;
int* bad_addr = &(var + 1); // ERROR: lvalue required as unary '&' operand
int* addr = &var;           // OK: var is an lvalue
&var = 40;                  // ERROR: lvalue required as left operand
                            // of assignment

一般而言,对右值的引用是错误的,如

int &a = 5; // ERROR
std::string& sref = std::string();  // ERROR: invalid initialization of
                                    // non-const reference of type
                                    // 'std::string&' from an rvalue of
                                    // type 'std::string'

这些被称为“左值引用”。非常量左值引用不能分配右值,因为这需要无效的右值到左值转换
可以为常量左值引用分配右值。因为它们是常量,所以不能通过引用修改值,因此不存在修改右值的问题。这使得非常常见的 C++ 习惯用法成为可能,即通过对函数的常量引用来接受值,从而避免了不必要的临时对象复制和构造。

void foo(const string& str);

//可以通过以下方法调用
string mystr("123");
foo(mystr);
foo(string("123"));

第二种调用方法就是相当于常量左值引用分配右值


Move constructor

移动构造函数允许将右值对象拥有的资源移动到左值中,而无需创建其副本。

代码示例

class MyString {
private:
	char* _data;
	size_t   _len;
	void _init_data(const char *s) {
		_data = new char[_len + 1];
		memcpy(_data, s, _len);
		_data[_len] = '\0';
	}
public:
	MyString() {
		_data = NULL;
		_len = 0;
	}

	MyString(const char* p) {
		_len = strlen(p);
		_init_data(p);
	}

	MyString(const MyString& str) {
		_len = str._len;
		_init_data(str._data);
		std::cout << "Copy Constructor is called! source: " << str._data << std::endl;
	}

	MyString& operator=(const MyString& str) {
		if (this != &str) {
			_len = str._len;
			_init_data(str._data);
		}
		std::cout << "Copy Assignment is called! source: " << str._data << std::endl;
		return *this;
	}

	virtual ~MyString() {
		if (_data != NULL) {
			std::cout << "Destructor is called! " << std::endl; 
			free(_data);
		}
	}
};

int main() { 
	MyString a; 
	a = MyString("Hello"); 
	std::vector<MyString> vec; 
	vec.push_back(MyString("World")); 
}

运行结果

Copy Assignment is called! source: Hello
Destructor is called!
Copy Constructor is called! source: World
Destructor is called!
Destructor is called!
Destructor is called!

这里调用了两次拷贝构造函数,MyString(“Hello”)和MyString(“World”)都是临时对象,临时对象被使用完之后会被立即析构。这里一共发生了几次内存分配,拷贝,释放呢?a创造时没有发生内存分配,首先临时变量MyString(“Hello”)发生一次内存分配,拷贝过程中有一次内存分配加拷贝,用完之后然后析构释放内存。另一个临时变量也一样。
如果能够直接使用临时对象已经申请的资源,并在其析构函数中取消对资源的释放,这样既能节省资源,有能节省资源申请和释放的时间。 这正是定义转移语义的目的。

通过加入定义转移构造函数和转移赋值操作符重载来实现右值引用(即复用临时对象):

MyString(MyString&& str) { 
		std::cout << "Move Constructor is called! source: " << str._data << std::endl; 
		_len = str._len; 
		_data = str._data; 
		str._len = 0; 
		str._data = NULL;   // ! 防止在析构函数中将内存释放掉
	}

	MyString& operator=(MyString&& str) { 
		std::cout << "Move Assignment is called! source: " << str._data << std::endl; 
		if (this != &str) { 
			_len = str._len; 
			_data = str._data; 
			str._len = 0; 
			str._data = NULL;  // ! 防止在析构函数中将内存释放掉
		} 
		return *this; 
	}

这里引入了右值引用符号&&,运行结果

Move Assignment is called! source: Hello
Move Constructor is called! source: World
Destructor is called!
Destructor is called!

这里就避免了很多不必要的拷贝和分配操作,但是注意,这里临时对象用完之后依然会调用析构函数,所以需要将临时对象的相关地址内存给置为nullptr
所以move constructor的核心是临时变量(右值)的拷贝和赋值


上述代码存在内存泄漏:!!!!!!!

  1. 赋值操作只是新分配内存然后拷贝,对于原来如果已经存在的内存却没有释放
  2. 移动赋值也存在一样的问题
  3. 赋值操作和拷贝构造函数不同的关键在于,拷贝构造函数是从无到有,赋值操作是从一个已经存在的到另一个已经存在的,因此需要将原来已经存在的分配的内存给释放掉,避免内存泄漏。

你可能感兴趣的:(C/C++,c++)