在现有的 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;
}
- 首先,在主函数的栈帧空间中构造一个名为
s1
的对象,其str指向一字节空间‘\0’
- 然后,在fun函数的栈帧空间中构建一个名为
s2
的对象,其str
指向存储'star\0'
的空间 - return时,将在主函数的栈帧空间中构建一个不具名对象,调用移动构造,将资源转接给当前不具名对象,使得
s2.str = NULL
,刚好生存期结束,再去自动析构s2
,此时已经为空,直接通过。(注意
:这里系统底层其实做了这样的工作:return std:move(s2);
即将s2强转为右值对象) - 再调动移动赋值函数,同样地将当前不具名对象的资源转接给
s1
,同时不具名对象的生存期也到了,也会自动调动析构,直接通过。
不难看出,这种方式只使用了一次
堆区空间,尽可能地节约了成本,减少空间的使用和对象的构建,因此我们也明白:右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。
关于普通拷贝构造、普通赋值和移动拷贝和移动赋值的调用问题:
并不是这里优先调用了移动构造和移动赋值,而是因为这里产生了将亡值对象(不具名对象),我们所谓的移动构造也就是为了建立将亡值对象的一种构造函数,因为我们已经写了这个方法,就会优先调用,否则就会调用普通拷贝构造,移动拷贝构造在这里更加对口。
再比如,下面的程序:
//1
String s1("abc");
String s2("efg");
s1 = s2;
//2
String s1("abc");
String s2("efg");
s1 = std:move(s2);
- 此时会调动拷贝构造和赋值语句
- 若写了移动构造和赋值,优先调动,否则调用普通的构造和赋值
- 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
对象所拥有的资源释放。
——惊呼:好巧妙啊!!