在类属性中使用动态变量有一个问题。在函数中即使使用局部指针变量创建动态变量,且局部指针变量在函数调用结束时离去,除非调用 delete
,否则动态变量仍会保存在内存中。不调用 delete
销毁动态变量,动态变量就会一直占用内存空间,这会导致程序因耗尽自由存储而终止。此外,将动态变量嵌入类的实现中,由于使用该类的程序员并不知道动态变量的存在,所以不能指望他们帮你调用 delete
。事实上,由于数据成员通常是私有成员,所以程序员通常不能访问所需的指针变量,所以根本不能为这些指针变量调用 delete
。为了解决这个问题,C++ 提供了称为 析构函数 的特殊成员函数。
析构函数(destructor) 是成员函数,在类的对象离开作用域时自动调用。换言之,如函数包含局部变量,而且这个局部变量是提供了析构函数的对象,那么函数调用终止时会自动调用析构函数。如果正确定义了析构函数,析构函数就会调用 delete
销毁由对象创建的所有动态变量。为达到 “析构” 的目的,可能只需要调用一次 delete
,也可能需要调用多次。可让析构函数执行其他清理工作,但将内存回收到自由存储是析构函数的主职。
析构函数的定义为 ~
+ 类名,其与构造函数的定义类似,只是前面多出一个 ~
符号,比如 StringVar
类的析构函数就为 ~StringVar
。析构函数不能指定返回值类型,无参数,所以每个类只能有一个析构函数,不能为类重载析构函数。除了这些区别,析构函数的定义方式与其他成员函数相同。
下面借助下面的示例进行说明:
#include
#include
#include
#include
using namespace std;
class StringVar
{
public:
StringVar(const char a[]);
// 前条件:数组 a 包含以 '\0' 终止的一组字符
// 初始化对象,使它的值成为a中存储的字符串
// 并使其以后能设置成最大长度为strlen(a)的字符串值
~StringVar();
// 析构函数
friend ostream& operator <<(ostream& outs, const StringVar& theString);
private:
char *value; //指向容纳字符串值的动态数组的指针
};
void conversation();
// 开始与用户的对话
int main()
{
conversation();
return 0;
}
StringVar::StringVar(const char a[])
{
value = new char[strlen(a)+1];
strcpy(value, a);
}
StringVar::~StringVar()
{
delete [] value;
}
ostream& operator <<(ostream& outs, const StringVar& theString)
{
outs << theString.value;
return outs;
}
void conversation()
{
StringVar ourname("Borg");
cout <<"We are "<< ourname <<endl;
}
上面定义的析构函数 ~StringVar()
调用 delete
销毁成员指针变量 value
指向的动态数组。分析 conversation
函数,局部变量 ourName
会创建动态数组。如果类没有析构函数,在对 conversation
的调用结束后,动态数组仍会占用内存,即使它们对于程序来说已完全无用。对于这个示范程序,似乎不是很严重的问题,因为在对 conversation
的调用结束之后,程序会马上终止。但如果写程序反复调用 conversation
这样的函数,而且 StringVar
类没有合适的析构函数,函数调用就会不断消耗自由存储中的内存,直至所有内存都消耗殆尽,造成程序不得不异常终止。
**拷贝构造函数(copy constructor)**要求获取一个参数,该参数具有与类相同的类型。该参数必须传引用,而且通常要在前面附加 const
参数修饰符,使它成为常量参数。除此之外,拷贝构造函数的定义和用法与其它任何构造函数完全相同。
例如,我们在上面的代码中使用拷贝构造函数。
StringVar ourname("Brog");
StringVar myname(ourname);
成员变量 myname.value
不能简单地设置成与 ourname.value
相同的值,那样会造成两个指针指向同一个动态数组,即浅拷贝。
StringVar::StringVar(const StringVar& stringObject)
{
value = new char[10];
strcpy(value, stringObject.value);
}
而应该像上面的代码新建一个动态数组,并将一个动态数组的内容拷贝到另一个动态数组。这就是深拷贝。
具体地说,拷贝构造函数会在三种情况下自动调用:
下面举一个例子说明没有拷贝构造函数会出现什么问题。
void showString(StringVar theString)
{
cout<<"The string is: "
<< theString << endl;
}
再给定以下代码,其中包括一个函数调用:
StringVar greeting("Hello");
showString(greeting);
cout << "After call: " << greeting << endl;
假定没有拷贝构造函数,那么具体过程是:执行函数调用时,greeting
的值复制给局部变量 theString
,所以 theString.value
被设置成与 greeting,value
相等。但是,这些是指针变量,所以在函数调用期间,theString.value
和greeting.value
指向同一个动态数组。
函数调用结束之后,会调用 StringVar
的析构函数,将 theString
使用的内存返回给自由存储。由于 greeting.value
和 theString.value
指向同一个动态数组,所以删除 theString.value
就是删除 greeting.value
。那么之和执行 cout<<"After call:"<
greeting
对象是某些函数的局部变量,就会出现重大问题。在这种情况下,析构函数调用等价于:
delete [] greeting.value
重复调用 delete
来删除同一个动态数组,可能会造成严重的系统错误,并导致程序崩溃。
假设 string1
和 string2
像下面这样声明:
StringVar string1("abc"), string2("xyz");
下面的赋值函数,会将 string2
的属性拷贝给 string1
。
string1 = string2;
当然这里为浅拷贝,如果需要深拷贝,要重载赋值操作符。重载赋值操作符与重载其他操作符不同,重载赋值操作符必须是类的成员,而不能是类的友元。
class StringVar
{
public:
void operator =(const StringVar& rightSide);
// 重载赋值操作符=,将字符串从一个对象复制到另一个
函数定义如下:
void StringVar::operator =(const StringVar& rightSide)
{
for(int i=0; i<strlen(rightSide.value); ++i)
value[i] = rightSide.value[i];
value[strlen(rightSide.value)] = '\0';
}
拷贝构造函数、操作符= 以及 析构函数统称为 Big Three。专家认为,如果需要定义其中一个,就必须定义全部三个。缺少任何一个,编译器都会帮你创建它,只是可能达不到你预期的效果。所以,有必要每次都自己定义。假如所有成员变量都具有预定义了类型(比如 int
和 double
),那么编译器生成的拷贝构造函数和重载的 =
能很好的工作。但假如类中包含类或指针成员变量,它们就可能表现失常,对于使用指针和操作符 new
的任何类,最保险的做法就是定义自己的拷贝构造函数、重载的操作符=
以及析构函数。