effectivec++(01):右值引用,移动构造,移动赋值,强制把做左值当右值使用

本文详细介绍了C++11中的右值引用及其重要特性——转移语义和精确传递。右值引用用于消除不必要的对象拷贝,提高效率,同时简化泛型函数定义。通过实例展示了如何利用右值引用实现移动构造函数和移动赋值运算符,从而优化资源管理。标准库函数std::move用于将左值引用转换为右值引用,以调用转移构造函数和转移赋值操作。精确传递则允许函数参数原封不动地传递给目标函数,减少了函数重载。
摘要由CSDN通过智能技术生成

一、 新特性的目的
右值引用 (Rvalue Referene) 是 C++ 新标准 (C++11, 11 代表 2011 年 ) 中引入的新特性 , 它实现了转移语义 (Move Sementics) 和精确传递 (Perfect Forwarding)。它的主要目的有两个方面:

一是消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。 二是能够更简洁明确地定义泛型函数。

二、 什么是右值,什么是左值以及右值引用
C++( 包括 C) 中所有的表达式和变量要么是左值,要么是右值。通俗的左值的定义就是非临时对象,那些可以在多条语句中使用的对象。 所有的变量都满足这个定义,在多条代码中都可以使用,都是左值。 右值是指临时的对象,它们只在当前的语句中有效。

int i = 0; // 在这条语句中,i 是左值,0 是临时值,就是右值。 在C++11之前,右值是不能被引用的,如:

int &a = 1; // error C2440: “初始化”: 无法从“int”转换为“int &”
我们最多只能用常量引用来绑定一个右值,如:

const int &a = 1; 在C++11中,我们可以引用右值,使用&&来实现:

int &&a = 1;

总的来说:

右值一般是临时对象或者子对象不能赋值
一,是否可以赋值左值可以,右值不可以
二,是否子在内存中找到标记的位置(但是右值是可以占用内存栈上数据区的内存的,但是一分配后马上释放,所以说你找不到他的标记)

右值引用:为了让右值引用绑定的右值的内存位置成为可能,比如如下:临时内存分配给右值引用后,可以用指针指向这边内存再操作他。

int&& rvaReference=1+2;
int * ptr=&rvaReference;
*ptr=20;

三、 应用场景
有如下string类,实现了拷贝构造函数和赋值运算符重载。

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")); 
}

template
void forward_value(T&& val) {
process_value(val);
}
运行结果:

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

总共执行了2次拷贝,MyString(“Hello”)和MyString(“World”)都是临时对象,临时对象被使用完之后会被立即析构,在析构函数中free掉申请的内存资源。 如果能够直接使用临时对象已经申请的资源,并在其析构函数中取消对资源的释放,这样既能节省资源,有能节省资源申请和释放的时间。 这正是定义转移语义的目的。

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

	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!

需要注意的是:右值引用并不能阻止编译器在临时对象使用完之后将其释放掉的事实,所以转移构造函数和转移赋值操作符重载函数 中都将_data赋值为了NULL,而且析构函数中保证了_data != NULL才会释放。

四、 标准库函数 std::move
既然编译器只对右值引用才能调用转移构造函数和转移赋值函数,又因为所有命名对象都只能是左值引用。 在这样的条件了,如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数,也就是把一个左值引用当做右值引用来使用,怎么做呢?标准库提供了函数 std::move,这个函数以非常简单的方式将左值引用转换为右值引用。

void ProcessValue(int& i) { 
	std::cout << "LValue processed: " << i << std::endl; 
} 

void 采用ProcessValue(int&& i) { 
	std::cout << "RValue processed: " << i << std::endl; 
} 

int main() { 
	int a = 0; 
	ProcessValue(a); 
	ProcessValue(std::move(a)); 
}

运行结果:
LValue processed: 0
RValue processed: 0

std::move在提高 swap 函数的的性能上非常有帮助,一般来说,swap函数的通用定义如下:

template <class T> 
void swap(T& a, T& b) 
{ 
	T tmp(a);   // copy a to tmp 
	a = b;      // copy b to a 
	b = tmp;    // copy tmp to b 
}

有了std::move,再结合右值引用,就可以避免不必要的拷贝了。 swap函数的定义变为 :

template <class T>
void swap(T& a, T& b) 
{ 
	T tmp(std::move(a)); // move a to tmp 
	a = std::move(b);    // move b to a 
	b = std::move(tmp);  // move tmp to b 
}

可以使用第三节中的MyString类进行测试:

int main() { 
	MyString a("a");
	MyString b("b");

	swap(a, b);

	return 0;
}

五、 精确传递(Perfect Forwarding)

精确传递就是在参数传递过程中,所有这些属性和参数值都不能改变。在泛型函数中,这样的需求非常普遍。 举例说明比较好理解。

forward_value函数只有一个参数val,定义如下:

template <typename T> 
void forward_value(const T& val) { 
	process_value(val); 
} 

template <typename T> 
void forward_value(T& val) { 
	process_value(val); 
}

函数 forward_value 为每一个参数必须重载两种类型,T& 和 const T&,否则,下面四种不同类型参数的调用中就不能同时满足:

int a = 0;
const int &b = 1;
forward_value(a); // int&
forward_value(b); // const int&
forward_value(2); // const int&

对于一个参数就要重载两次,也就是函数重载的次数和参数的个数是一个正比的关系。这个函数的定义次数对于程序员来说,是非常低效的。我们看看右值引用如何帮助我们解决这个问题:

template <typename T> 
void forward_value(T&& val) { 
	process_value(val); 
}

只需要定义一次,接受一个右值引用的参数,就能够将所有的参数类型原封不动的传递给目标函数。
————————————————
本文部分采用原文链接:https://blog.csdn.net/china_jeffery/article/details/78520237

六,移动构造和移动赋值的触发条件是右边是右值(&&)

#include<iostream>

class Test
{
private:
    char *ptr;
    int number;
public:
    Test()
    {
        number=0;
        ptr=NULL;
    }

    explicit Test(int k)
    {
        number=k;
        ptr=new char[number];    
    }

    Test(int k,char ch):number(k)
    {
        number=k;
        ptr=new char[number]; 
        for (int i = 0; i < k; i++)
        {
            /* code */
            ptr[i]=ch+i;    
        }
    }

    Test& operator +(const Test&rhs)const
    {
        Test tmp(number+rhs.number);
        int record=0;
        for(int i=0;i<number;i++)
        {
            tmp.ptr[i]=ptr[i];
            record=i;
        }
        for (int  k = record; k < tmp.number; k++)
        {
            /* code */
            tmp.ptr[k]=rhs.ptr[k-record];
        }
        
        return tmp;
    }
    //move constructor
    Test( Test && rhs)
    {
        ptr=rhs.ptr;
        number=rhs.number;
        rhs.ptr=NULL;              //防止同一对象被析够两次
        rhs.number=0;
       std::cout<<"move constructor()"<<std::endl;
    }
    //move copy assignment
    Test &operator =(Test && rhs)
    {
        std::cout<<"move copy assignment()"<<std::endl;
        if(this==&rhs)
        return *this;

        delete []ptr;//delet forward ptr
        ptr=rhs.ptr;
        number=rhs.number;
        rhs.ptr=NULL;//只需要一个指针指向内存,这里的rhs离开局部后会修够
        rhs.number=0;
        return *this;

    }

    void show()const
    {  
        for(int i=0;i<number;i++)
        std::cout<<ptr[i];
        std::cout<<std::endl;
    }

    ~Test(){if(ptr!=NULL)delete []ptr;}//

};
#include"test.h"
#include<cstdio>

int main()
{
    Test one(2,'a');
    Test two(3,'b');
    Test tree=one+two;
    tree.show();
    Test four;
    four=one+tree;
    four.show();
    getchar();
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值