对象的复制是每个C++程序员都会遇到的问题。同样提供复制的方式也各种各样,有拷贝构造,赋值操作等等。他们的目的都是一样的将数据从一个对象拷贝到另外一个对象。本实用经验将同你讨论关于对象复制的注意事项。
对于普通的数据类型,他们之间的复制是很简单的,例如:
int a = 10;
int b =a;
但对于class类型的复制,事情就变的有些复杂了。C++通过拷贝构造和赋值操作实现对象的拷贝。
拷贝构造
- 拷贝构造是一种特殊的构造函数,这种构造函数具有单个形参,该形参是该类类型的引用。当定义一个新对象并用一个同类型的对象对他进行初始化时,复制构造函数将被调用。
- 如果自定义的类没有提供拷贝构造函数,编译器将会为我们合成一个。合成的复制构造函数将逐个成员初始化,将新对象初始化为原对象的副本。
我们知道,如果不给类提供一个拷贝构造函数,编译器会给我默认提供一个。如果你提供了一个拷贝构造函数,则编译器将不在为你默认生成构造函数。
小心陷阱:如果自己声明了拷贝构造函数,就表示你拒绝编译器为你提供的合成默认构造函数。这时即使你的代码出现了错误,编译也不会提醒你。这是你声明拷贝构造函数时需要注意的。
下面的代码是CString拷贝构造和赋值操作的一个例子。
// CString字符串类实现
class cstring
{
public:
cstring();
cstring(const char* pszbuffer);
~cstring();
cstring(const cstring& other);
const cstring& operator=(const cstring& other);
private:
char* m_pszbuffer;;
};
cstring::cstring()
{
m_pszbuffer = null;
return;
}
cstring::cstring(const char* pszbuffer)
{
m_pszbuffer = pszbuffer != null ? strdup(pszbuffer) : null;
return;
}
cstring::~cstring()
{
if(m_pszbuffer != null)
{
free(m_pszbuffer);
m_pszbuffer = null;
}
return;
}
cstring::cstring(const cstring& other)
{
if(this == &other)
{
return;
}
m_pszbuffer = other.m_pszbuffer != null ? strdup(other.m_pszbuffer) : null;
}
// CString 字符串赋值操作符实现
const cstring& cstring::operator=(const cstring& other)
{
if(this == &other)
{
return *this;
}
if(m_pszbuffer != null)
{
free(m_pszbuffer);
m_pszbuffer = null;
}
m_pszbuffer = other.m_pszbuffer != null ? strdup(other.m_pszbuffer) : null;
return *this;
}
通过上例可看出:无论采用拷贝构造函数抑或是赋值操作符。复制过程中,对象的所有成员都必须进行复制。
接着,我们在讨论存在继承关系的对象复制。我们看下面的例子:
// CWnd窗口类
class CWnd
{
public:
CWnd(const CWnd &rhs);
CWnd &operator = (const CWnd &rhs);
private:
string m_strTitle; // 窗口名称
}
CWnd::CWnd(const CWnd &rhs)
{
m_strTitle = rhs.m_strTitle;
}
// 窗口赋值操作符实现类
CWnd &CWnd::operator = (const CWnd &rhs)
{
if (this != &rhs)
{
m_strTitle = rhs.m_strTitle;
}
return *this;
}
// Form 窗口实现类
class CForm : public CWnd
{
public:
CForm(const CForm &rhs);
CForm &operator = (const CForm &rhs);
private:
int m_iType; // Form类型
}
CForm::CForm(const CForm &rhs)
{
m_iType = rhs.m_iType;
}
CForm &CForm::operator = (const CForm &rhs)
{
if (this != &rhs)
{
m_iType = rhs.m_iType;
}
return *this;
}
按照上述实现,你会发现此时的拷贝构造和赋值操作没有对基类的数据成员进行初始化。因此会默认的调用基类的默认构造函数,这样就会导致基类的数据成没有被复制初始化。这种错误在某些情况下,会导致致命的问题。为了避免上述问题的出现,我们需要调用基类的拷贝构造函数(赋值函数)进行显示初始化。
CForm::CForm(const CForm &rhs) : CWnd(rhs)
{
m_iType = rhs.m_iType;
}
CForm &CForm::operator = (const CForm &rhs)
{
if (this != &rhs)
{
CWnd::operator=(rhs);
m_iType = rhs.m_iType;
}
return *this;
}
因此,当你书写一个拷贝构造或(赋值操作)函数时,有这么两点你必须做到:
(1)复制所有的成员变量;
(2)在复制时,不仅要复制派生类的数据成员,基类的数据成员同样也要进行复制。
最后,再讨论一种复制实现–通过memcpy是否对象拷贝复制。对于struct,我们一般可通过memcpy或memset实现对象的拷贝。对于类对象,这种实现可能会存在风险。因为memset和memcpy对于POD对象可安全的进行拷贝复制。如果对于非POD对象,这种实现存在很多的风险。你可参考实用经验54。这里就不多做讨论了。但是一些共识我们是可达成的:
- 对于非继承关系的类或未包含virtual函数的类,可以通过memset或memcpy等实现对象的拷贝。因为这时的class是POD对象和struct无异。
- 对于存在继承关系或含virtual函数的类,请不要使用memset或memcpy实现对象的拷贝。因为这时的对象是非POD对象。
请谨记
- 对象复制时,请记得复制对象的所有成员,及基类的所有成员。