• 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
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    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'
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    void foo(const string& str);
    
    //可以通过以下方法调用
    string mystr("123");
    foo(mystr);
    foo(string("123"));
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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


    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")); 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    运行结果

    Copy Assignment is called! source: Hello
    Destructor is called!
    Copy Constructor is called! source: World
    Destructor is called!
    Destructor is called!
    Destructor is called!
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里调用了两次拷贝构造函数,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; 
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

    Move Assignment is called! source: Hello
    Move Constructor is called! source: World
    Destructor is called!
    Destructor is called!
    
    • 1
    • 2
    • 3
    • 4

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


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

    1. 赋值操作只是新分配内存然后拷贝,对于原来如果已经存在的内存却没有释放
    2. 移动赋值也存在一样的问题
    3. 赋值操作和拷贝构造函数不同的关键在于,拷贝构造函数是从无到有,赋值操作是从一个已经存在的到另一个已经存在的,因此需要将原来已经存在的分配的内存给释放掉,避免内存泄漏。
  • 相关阅读:
    RemObjects Elements 12.0 Crack
    2020架构真题(四十六)
    我对React原理的理解
    C++入门之缺省参数与函数重载
    Stable Diffusion的模型选择,采样器选择,关键词
    GEE15:获取不同遥感指数的时间序列及不同指数间的关系
    YOLOv7训练数据集
    用关键词搜索店铺列表详情
    OD_2024_C卷_200分_7、5G网络建设【JAVA】【最小生成树】
    PMP大家都是怎么备考的?使用什么工具可以分享一下吗?
  • 原文地址:https://blog.csdn.net/good_jiojio/article/details/128121981