构造、拷贝、赋值、析构函数是类的四个特殊成员函数,其特殊之处在于,即使我们没有定义这些函数,编译器也会自动提供默认函数,但如果我们提供了这些函数的显式定义,那么编译器将不会再提供。这可能导致一些隐藏的问题,因此,我们需要对他们的实现进行充分的认识。
默认构造函数
假定有一个类Klunk,那么其默认构造函数定义如下:
Klunk::Klunk(){}
编译器为我们提供了一个不接受任何参数,也不执行任何操作的构造函数,我们可以像下面这样调用:
Klunk lunk;
但是这种不做任何操作的构造函数也许并不是我们需要的,那么我们就可以显式定义默认构造函数,如下:
Klunk::Klunk()
{
klunk_ct = 0;
}
复制构造函数
复制构造函数用于将一个对象复制到新创建的对象中,它接收一个指向类对象的常量引用作为参数,形式如下:
String(const String& );
何时调用
以下情形都会调用复制构造函数:
- String ditto(motto);
- String metoo = motto;
- String also = String(motto);
- String *pStr = new String(motto);
- 函数值传递对象
- 函数返回对象
值得注意的是,即使我们调用赋值运算符,也会调用拷贝构造函数。
有何功能
默认复制构造函数在复制过程中,会将成员的值进行逐个拷贝;如果成员中也有类成员存在,那么也将递归拷贝。但是需要注意的是,以上过程都是一种浅拷贝,这在碰到指针时将会带来灾难:创建了一个新的指针指向原指针指向的地址,而没有额外分配新的空间。这将导致:两个对象中都包含了指向同一地址的指针,在进行析构时,可能导致程序异常。
因此,我们需要深拷贝,与浅拷贝不同,深拷贝在进行复制时将会额外为指针指向额外分配新的空间,然后再进行值拷贝。如下所示:
String :: String(const String& st)
{
...
str = new char[len + 1];
strcpy(str,st.str);
...
}
赋值运算符
赋值运算符接收一个指向类对象的常量引用作为参数,而返回一个指向类对象的引用。形式如下:
String & String::operator=(const String&);
赋值运算符和复制拷贝类似,也是逐个拷贝,因此,也需要注意浅拷贝带来的问题。所以大多数时候需要我们实现自定义的复制运算符,在实现过程中,我们需要注意以下几个问题:
- 由于目标对象可能引用了以前分配的数据,所以函数应当使用delete[]来释放这些数据;
- 函数应当避免赋值给自身,否则,给对象重新赋值前,释放内存操作可能删除对象的内容。
- 返回一个指向调用对象的引用。
析构函数
析构函数,不必再作过多介绍,我们主要用它释放动态分配的内存。
总结
综上所述,我们可以完成一个类的基本实现了,以自定义String类为例,代码如下:
class String
{
public:
String(const char *str = NULL);
String(const String &str);
~String(void);
String &operator=(const String &str);
private:
char *m_data;
};
String::String(const char *str)
{
if (str == NULL)
{
m_data = new char[1];
*m_data = '\0';
}
else
{
int length = strlen(str);
m_data = new char[length + 1];
strcpy(m_data, str);
}
}
String::String(const String &str)
{
int length = strlen(str.m_data);
m_data = new char[length + 1];
strcpy(m_data, str.m_data);
}
String &String::operator=(const String &str)
{
if (this == &str)
return *this;
if (m_data)
delete[] m_data;
int length = strlen(str.m_data);
m_data = new char[length + 1];
strcpy(m_data,str.m_data);
return *this;
}