3.3 右值引用:移动语义和完美转发
类别:类作者
3.3.1 指针成员与拷贝构造
对C++程序员来说,编写C++程序有一条必须注意的规则,就是在类中包含了一个指针成员的话,那么就要特别小心拷贝构造函数的编写,因为一不小心,就会出现内存泄露。我们来看看代码清单3-16中的例子。
代码清单3-16
include <iostream>
using namespace std;
class HasPtrMem{
public:
HasPtrMem():d(new int(0)){}
HasPtrMem(const HasPtrMem&h):
d(new int(h.d)){}//拷贝构造函数,从堆中分配内存,并用h.d初始化
~HasPtrMem(){delete d;}
int*d;
};
int main(){
HasPtrMem a;
HasPtrMem b(a);
cout<<*a.d<<endl;//0
cout<<*b.d<<endl;//0
}//正常析构
//编译选项:g++3-3-1.cpp
在代码清单3-16中,我们定义了一个HasPtrMem的类。这个类包含一个指针成员,该成员在构造时接受一个new操作分配堆内存返回的指针,而在析构的时候则会被delete操作用于释放之前分配的堆内存。在main函数中,我们声明了HasPtrMem类型的变量a,又使用a初始化了变量b。按照C++的语法,这会调用HasPtrMem的拷贝构造函数。这里的拷贝构造函数由编译器隐式生成,其作用是执行类似于memcpy的按位拷贝。这样的构造方式有一个问题,就是a.d和b.d都指向了同一块堆内存。因此在main作用域结束的时候,a和b的析构函数纷纷被调用,当其中之一完成析构之后(比如b),那么a.d就成了一个“悬挂指针”(dangling pointer),因为其不再指向有效的内存了。那么在该悬挂指针上释放内存就会造成严重的错误。
这个问题在C++编程中非常经典。这样的拷贝构造方式,在C++中也常被称为“浅拷贝”(shollow copy)。而在未声明构造函数的情况下,C++也会为类生成一个浅拷贝的构造函数。通常最佳的解决方案是用户自定义拷贝构造函数来实现“深拷贝”(deep copy),我们来看看代码清单3-17中的修正方法。
代码清单3-17
include <iostream>
using namespace std;
class HasPtrMem{
public:
HasPtrMem():d(new int(0)){}
HasPtrMem(HasPtrMem&h):
d(new int(h.d)){}//拷贝构造函数,从堆中分配内存,并用h.d初始化
~HasPtrMem(){delete d;}
int*d;
};
int main(){
HasPtrMem a;
HasPtrMem b(a);
cout<<*a.d<<endl;//0
cout<<*b.d<<endl;//0
}//正常析构
//编译选项:g++3-3-2.cpp
在代码清单3-17中,我们为HasPtrMem添加了一个拷贝构造函数。拷贝构造函数从堆中分配新内存,将该分配来的内存的指针交还给d,又使用(h.d)对d进行了初始化。通过这样的方法,就避免了悬挂指针的困扰。