由始至终
----构造与析构
作者:HolyFire
我们在平时的生活中一般会总结出一些规律,早上起床会刷牙洗脸,晚上会洗澡睡觉,这些都成了惯例。使用瓶装调味品时先将瓶盖打开,用完后将瓶盖盖上。这是一种好习惯。但是有些人不同,他们往往偷懒,一个常常不刷牙不洗脸不洗澡的人会有体味,东西放得乱七八糟的人生房间很不整洁。这些都是我们不希望看到的。当然编程中我们也不希望代码乱七八糟。
使用一个未初始化的变量简直就是灾难,使用一个未初始化的指针将导致崩溃。这是我的忠告。在C++中初始化不会有附加的效果,不会降低效率,我们要做的是养成好习惯,产生一个对象的时候就将它初始化。
对于
Object.Init();
Object.Free();
这样的调用并不是很困难,要记住他也不是难事,但是谁都不能保证他永远不会忘记,更糟糕的是
Object.Init();
Object.Free();
没有配对使用
Object.Init();
Object.Free();
Object.Free();
或
Object.Init();
Object.Init();
Object.Free();
会带来什么样的结果,谁也不知道,而且这样的错误,编译器不会报错。这是多么可怕的错误,一个程序员最怕遇上的就是这样的逻辑错误,它可能为了找这样的一个错误花上一整天时间。
让我们看看有什么好的办法。
一个对象按时间来分析,一般有三个阶段,出生,活动,死亡。与我们要做的有什么相关之处呢,初始化,运行,释放。很好,对照一下,我们发现在对象出生的时候初始化,死亡的时候释放,如果这一切能用这样的机制来操作,我们就再也不用担心会由于忘记或错误的使用带来麻烦了。
C++里就提供了这样的机制。使用他有个约定
class Object{
public:
Object(); //与类同名的函数,该函数没有返回值,叫做构造函数
~Object(); //类似的,在构造函数名前加一个取反符号,叫做析构函数
};
构造函数将在对象产生的时候调用
析构函数将在对象销毁的时候调用
调用的过程和实现方法由编译器完成,我们只要记住他们调用的时间就行了,而且他们的调用是自动完成的,不需要我们控制。
#include <iostream>
using namespace std;
class Object{
public:
Object(){ cout << "Object ON!" << endl; }
~Object(){ cout << "Object OFF!" << endl; }
};
void main()
{
Object o;
}
运行结果
Object ON!
Object OFF!
构在函数和析构函数确实的执行了
现在我们来一个应用的例子
一个字符串类,它需要保存字符串的内容,但是它不知道字符串的大小,那么设计这个字符串类的时候,保存字符串的成员变量就不能用固定大小的数组,而是用可以间接操作数组的指针。
#include <iostream>
#include <string.h>
using namespace std;
class string{
private:
char * data;
public:
string(){ data = NULL; }
string( char * str )
{
cout << "Copy string: " << str << endl;
data = new char[ strlen(str) + 1 ];
memcpy( data , str , strlen(str) + 1 );
}
char * Data(){ return data; }
~string()
{
if( data )
{
cout << "Free string: " << data << endl;
delete data;
}
}
};
void main()
{
{
string s("abcd");
cout <<"Show String: " << s.Data() <<endl;
}
cin.get();
}
Copy string: abcd //执行了string::string( char * str ) 构造函数
Show String: abcd
Free string: abcd //由于在{}中产成的对象是临时对象,它的生命期在}后就结束了,所以string::~string() 析构函数被调用
申请内存和释放内存的操作自动完成了,构造函数和析构函数的目的在于一个类可以象普通类型一样初始化和释放,从而保证了封装。
上面的例子有两个构造函数,这么什么大不了的,我们看过《面面俱到----重载》得都知道,重载的把戏。
要注意的是构造函数可以有参数,在继承中如何处理呢。
class mystring : public string{
public:
mystring( char * str ):string( str ){ }
}
mystring( char * str ):string( str )
记住这样的形式,给自己的父类传递函数就用这样的书写格式,这是一个约定。
构造函数后面加上一个:表示后面是一个初始化序列,说它是一个序列是因为它可以初始化多个成员变量,在初始化序列里调用向父类传递参数是为了保证类的产生的顺序,先产生父类,然后是子类。使用初始化有个好处就是可以提高效率。
string(){ data = NULL; }
可以改写成
string():data(NULL){ }
他的作用是产生成员变量char * data时将他的值置为NULL。从而少了data = NULL;这步操作。
注意,这里构造和析构有一个顺序问题,就是构造时应该从基类开始按继承的层次顺序调用,析构的时候顺序正好相反。这样处理是因为,子类可能在构造函数里使用父类的成员变量,如果父类还没有创建,那就会有问题,而析构的时候,如果父类先析构,也会有这样的问题。
析构函数还有一个能否正确运行的问题。
#include <iostream>
using namespace std;
class One{
public:
One(){ cout << "One ON!" << endl; }
~One(){ cout << "One OFF!" << endl; }
};
class Two : public One{
public:
Two(){ cout << "Two ON!" << endl; }
~Two(){ cout << "Two OFF!" << endl; }
};
class Three : public Two{
public:
Three(){ cout << "Three ON!" << endl; }
~Three(){ cout << "Three OFF!" << endl; }
};
void main()
{
Three three;
}
运行结果
One ON!
Two ON!
Three ON!
Three OFF!
Two OFF!
One OFF!
正确
void main()
{
Three * three = new Three;
delete three;
}
运行结果
One ON!
Two ON!
Three ON!
Three OFF!
Two OFF!
One OFF!
正确
void main()
{
One * three = new Three;
delete three;
}
运行结果
One ON!
Two ON!
Three ON!
One OFF!
不好了,Two和Three的析构都没有运行,怎么会这样,原来One * three指出了指针指向的是一个One类的对象。如何得到正确的结果呢,如果能让One类记住被继承后的变化就好了。
对了!虚函数,在《后入为主----虚函数》中可以知道,虚函数有这个特性,不信试试看。
class One{
public:
One(){ cout << "One ON!" << endl; }
virtual ~One(){ cout << "One OFF!" << endl; }
};
void main()
{
One * three = new Three;
delete three;
}
运行结果
One ON!
Two ON!
Three ON!
Three OFF!
Two OFF!
One OFF!
正确
这个特点很重要,我们要牢牢记住,我们称这种方法为“虚析构”,在多态里运用非常广泛,也是编写可复用代码的一个重要技巧。
构造和析构的作用机制就是自动化,简化编程的复杂度。还有要记住的是,在一个类的构造函数里分配了的资源尽量要记得在该类的析构函数里释放,当然也允许提前释放,你可以在析构函数里判断它是否已经释放,如果没有就释放。这就是----由始至终,它间接的描述了一个对象的生和死(记住这一点很重要,因为我以后会讲到如何运用这个特性控制对象的生死)。
2001/8/23
丁宁