第9章 类的构造函数、析构函数和赋值函数

2008-02-23 05:29:00来源:互联网 阅读 ()

新老客户大回馈,云服务器低至5折

</P><P>构造函数、析构函数和赋值函数是每个类最基本的函数。他们太普通以致让人容易麻痹大意,其实这些貌似简单的函数就象没有顶盖的下水道那样危险。</P><P>每个类只有一个析构函数和一个赋值函数,但能够有多个构造函数(包含一个拷贝构造函数,其他的称为普通构造函数)。对于任意一个类A,假如不想编写上述函数,C 编译器将自动为A产生四个缺省的函数,如
A(void); // 缺省的无参数构造函数
A(const A &a); // 缺省的拷贝构造函数
~ A(void); // 缺省的析构函数
A & operate =(const A &a); // 缺省的赋值函数</P><P>这不禁让人疑惑,既然能自动生成函数,为什么还要程式员编写?
原因如下:
(1)假如使用“缺省的无参数构造函数”和“缺省的析构函数”,等于放弃了自主“初始化”和“清除”的机会,C 发明人Stroustrup的好心好意白费了。
(2)“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。</P><P>对于那些没有吃够苦头的C 程式员,假如他说编写构造函数、析构函数和赋值函数很容易,能够不用动脑筋,表明他的认识还比较肤浅,水平有待于提高。
本章以类String的设计和实现为例,深入阐述被很多教科书忽视了的道理。String的结构如
下:
class String
{
public:
String(const char *str = NULL); // 普通构造函数
String(const String &other); // 拷贝构造函数
~ String(void); // 析构函数
String & operate =(const String &other); // 赋值函数
private:
char *m_data; // 用于保存字符串
};
9.1 构造函数和析构函数的起源
作为比C更先进的语言,C 提供了更好的机制来增强程式的安全性。C 编译器具备严格的类型安全检查功能,他几乎能找出程式中任何的语法问题,这的确帮了程式员的大忙。但是程式通过了编译检查并不表示错误已不存在了,在“错误”的大家庭里,“语法错误”的地位只能算是小弟弟。级别高的错误通常隐藏得很深,就象狡猾的罪犯,想逮住他可不容易。

根据经验,不少难以察觉的程式错误是由于变量没有被正确初始化或清除造成的,而初始化和清除工作很容易被人遗忘。Stroustrup在设计C 语言时充分考虑了这个问题并很好地予以解决:把对象的初始化工作放在构造函数中,把清除工作放在析构函数中。当对象被创建时,构造函数被自动执行。当对象消亡时,析构函数被自动执行。这下就不用担心忘了对象的初始化和清除工作。

构造函数和析构函数的名字不能随便起,必须让编译器认得出才能够被自动执行。Stroustrup的命名方法既简单又合理:让构造函数、析构函数和类同名,由于析构函数的目的和构造函数的相反,就加前缀‘~’以示区别。

除了名字外,构造函数和析构函数的另一个特别之处是没有返回值类型,这和返回值类型为void的函数不同。构造函数和析构函数的使命很明确,就象出生和死亡,光溜溜地来光溜溜地去。假如他们有返回值类型,那么编译器将不知所措。为了防止节外生枝,干脆规定没有返回值类型。(以上典故参考了文献[Eekel, p55-p56])

9.2 构造函数的初始化表
构造函数有个特别的初始化方式叫“初始化表达式表”(简称初始化表)。初始化表位于函数参数表之后,却在函数体 {} 之前。这说明该表里的初始化工作发生在函数体内的任何代码被执行之前。</P><P> 构造函数初始化表的使用规则:
* 假如类存在继承关系,派生类必须在其初始化表里调用基类的构造函数。
例如
class A
{…
A(int x); // A的构造函数
};
class B : public A
{…
B(int x, int y);// B的构造函数
};
B::B(int x, int y)
: A(x) // 在初始化表里调用A的构造函数
{

}
* 类的const常量只能在初始化表里被初始化,因为他不能在函数体内用赋值的方式来初始化(参见5.4节)。</P><P>* 类的数据成员的初始化能够采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。非内部数据类型的成员对象应当采用第一种方式初始化,以获取更高的效率。例如
class A
{…
A(void); // 无参数构造函数
A(const A &other); // 拷贝构造函数
A & operate =( const A &other); // 赋值函数
};</P><P> class B
{
public:
B(const A &a); // B的构造函数
private:
A m_a; // 成员对象
};</P><P>示例9-2(a)中,类B的构造函数在其初始化表里调用了类A的拷贝构造函数,从而将成员对象m_a初始化。
示例9-2 (b)中,类B的构造函数在函数体内用赋值的方式将成员对象m_a初始化。我们看到的只是一条赋值语句,但实际上B的构造函数干了两件事:先暗地里创建m_a对象(调用了A的无参数构造函数),再调用类A的赋值函数,将参数a赋给m_a。</P><P>B::B(const A &a)
: m_a(a)
{

}
示例9-2(a) 成员对象在初始化表中被初始</P><P>B::B(const A &a)
{
m_a = a;

}
示例9-2(b) 成员对象在函数体内被初始化</P><P>对于内部数据类型的数据成员而言,两种初始化方式的效率几乎没有区别,但后者的程式版式似乎更清楚些。若类F的声明如下:
class F
{
public:
F(int x, int y); // 构造函数
private:
int m_x, m_y;
int m_i, m_j;
}
示例9-2(c)中F的构造函数采用了第一种初始化方式,示例9-2(d)中F的构造函数采用了第二种初始化方式。
F::F(int x, int y)
: m_x(x), m_y(y)
{
m_i = 0;
m_j = 0;
}</P><P>示例9-2(c) 数据成员在初始化表中被初始化</P><P>F::F(int x, int y)

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇: 第8章 C 函数的高级特性

下一篇: 第10章 类的继承和组合