C++(8)——移动语义

在现有的 C++ 机制中,我们可以定义拷贝构造函数和赋值函数。要实现转移语义,需要定义转移构造函数,还可以定义转移赋值操作符。对于右值的拷贝和赋值会调用转移构造函数和转移赋值操作符。

  • 移动语义:将内存的所有权从一个对象转移到另外一个对象,高效的移动用来替换效率低下的复制,对象的移动语义需要实现移动构造函数(move constructor)和移动赋值运算符(move assignment operator)。

接着上一篇博客的内容,我们来深入理解一下移动语义的运行过程

还是之前自定义string类:

class String
{
	char* str;
public:
	String(const char* p = NULL) :str(NULL)
	{
		if (p != NULL)
		{
			str = new char[strlen(p) + 1];
			strcpy(str, p);
		}
		else
		{
			str = new char[1];
			*str = '\0';
		}
	}
	~String()
	{
		if (str != NULL)
		{
			delete[] str;
		}
		str = NULL;
	}
	//深拷贝
	String(const String& s) :str(NULL)
	{
		str = new char[strlen(s.str) + 1];
		strcpy(str, s.str);
	}
	//深赋值
	String& operator=(const String& s)
	{
		if (&s != this)
		{
			delete[] str;
			str = new char[strlen(s.str) + 1];
			strcpy(str, s.str);
		}
		return *this;
	}

若调用过程如下:

String fun()
{
	String s2("star");
	return s2;
}
int main()
{
	String s1;
	s1 = fun();
	return 0;
}

上述过程一共产生了三个对象,在fun()函数的调用过程中,会调用拷贝构造在主函数栈帧中产生一个将亡值对象(因为是主函数调用的fun函数),随后调动赋值语句,完成后就会调动将亡值对象的析构函数。
不难看出,这一过程产生的三个对象,使用了三次堆区空间,临时对象的构建实际上对性能有较大影响。

写了移动构造和移动赋值之后:同样的函数调用,有哪些运行状况方面的变化呢?

//移动构造
	String(String&& s)
	{
		str = s.str;
		s.str = NULL;
	}
	//移动赋值
	String& operator=(String&& s)
	{
		if (this != &s)
		{
			str = s.str;
			s.str = NULL;
		}
		return *this;
	}
  1. 首先,在主函数的栈帧空间中构造一个名为s1的对象,其str指向一字节空间‘\0’
  2. 然后,在fun函数的栈帧空间中构建一个名为s2的对象,其str指向存储'star\0'的空间
  3. return时,将在主函数的栈帧空间中构建一个不具名对象,调用移动构造,将资源转接给当前不具名对象,使得s2.str = NULL,刚好生存期结束,再去自动析构s2,此时已经为空,直接通过。(注意:这里系统底层其实做了这样的工作:return std:move(s2);即将s2强转为右值对象)
  4. 再调动移动赋值函数,同样地将当前不具名对象的资源转接给s1 ,同时不具名对象的生存期也到了,也会自动调动析构,直接通过。

不难看出,这种方式只使用了一次堆区空间,尽可能地节约了成本,减少空间的使用和对象的构建,因此我们也明白:右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。

关于普通拷贝构造、普通赋值和移动拷贝和移动赋值的调用问题:

并不是这里优先调用了移动构造和移动赋值,而是因为这里产生了将亡值对象(不具名对象),我们所谓的移动构造也就是为了建立将亡值对象的一种构造函数,因为我们已经写了这个方法,就会优先调用,否则就会调用普通拷贝构造,移动拷贝构造在这里更加对口。

再比如,下面的程序:

//1
String s1("abc");
String s2("efg");
s1 = s2;

//2
String s1("abc");
String s2("efg");
s1 = std:move(s2);
  1. 此时会调动拷贝构造和赋值语句
  2. 若写了移动构造和赋值,优先调动,否则调用普通的构造和赋值
  3. 2此时没有重写拷贝构造和赋值语句,将会调动缺省的拷贝构造和赋值语句,但请注意,此时的这两个函数均采用浅赋值,浅拷贝的方式。

但是,上述程序的执行过程中还存在一个内存泄漏的问题,即初始化的s1自身指向的那一字节空间没有被释放,那么如何来解决这个内存泄漏的问题呢???

方法一:赋值时提前释放

	String& operator=(String&& s)
	{
		if (this != &s)
		{
			delete[]str;
			str = s.str;
			s.str = NULL;
		}
		return *this;
	}

方法二,使用一个交换函数

	String& operator=(String&& s)
	{
		if (this != &s)
		{
			s.str = Release(s.str);
		}
		return *this;
	}
	char *Release(char *p)
	{
		char *old = str;
		str = p;
		return old;
	}

此时,将不具名对象的资源交给s1,再将s1的资源交给将亡值对象,当赋值语句完成后,将亡值对象析构时,就会带动s1对象所拥有的资源释放。
——惊呼:好巧妙啊!!

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值