您的位置:首页 > 编程语言

高质量C /C编程指南---第9章 类的机关函数、析构函数与赋值函数

2011-03-07 15:42 302 查看

第9章 类的机关函数、析构函数与赋值函数

机关函数、析构函数与赋值函数是每个类最根蒂根基的函数。它们太通俗致使让人随便麻痹大意,真实这些貌似庞大的函数就象没有顶盖的下水道那样损伤。

每个类只需一个析构函数和一个赋值函数,但可以有多个机关函数(包含一个拷贝机关函数,其它的称为通俗机关函数)。关于任意一个类A,假若不想编写上述函数,C 编译器将主动为A孕育发作四个缺省的函数,如

A(void); // 缺省的无参数机关函数

A(const A &a); // 缺省的拷贝机关函数

~A(void); // 缺省的析构函数

A & operate =(const A &a); // 缺省的赋值函数

这不由让人迷惑,既然能主动天生函数,为什么还要程序员编写?

缘故因由如下:

(1)假若运用“缺省的无参数机关函数”和“缺省的析构函数”,即是摒弃了自主“初始化”和“清扫”的机遇,C 缔造人Stroustrup的好心好心白费了。

(2)“缺省的拷贝机关函数”和“缺省的赋值函数”均接纳“位拷贝”而非“值拷贝”的体式花式来完成,假使类中含有指针变量,这两个函数注定将出错。

关于那些没有吃够甜头的C 程序员,假若他说编写机关函数、析构函数与赋值函数很随便,可以不用动脑子,表明他的见识还斗劲肤浅,程度有待于提高。

本章以类String的设计与完成为例,深化阐述被很多教科书无视了的道理。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 机关函数的初始化表

机关函数有个不凡的初始化体式花式叫“初始化表达式表”(简称初始化表)。初始化表位于函数参数表之后,却在函数体 {} 之前。这剖析该表里的初始化义务发作在函数体内的任何代码被尝试之前。

机关函数初始化表的运用轨则:

u 假若类存在承继相关,派生类必需在其初始化表里调用基类的机关函数。

譬喻

>

{…

A(int x); // A的机关函数

};

>

{…

B(int x, int y);// B的机关函数

};

B::B(int x, int y)

: A(x) // 在初始化表里调用A的机关函数

{



}

u 类的const常量只能在初始化表里被初始化,由于它不能在函数体内用赋值的体式花式来初始化(参见5.4节)。

u 类的数据成员的初始化可以接纳初始化表或函数体内赋值两种体式花式,这两种体式花式的苦守不完全相同。

非内部数据典范的成员器材应当接纳第一种体式花式初始化,以获取更高的苦守。譬喻

>

{…

A(void); // 无参数机关函数

A(const A &other); // 拷贝机关函数

A & operate =( const A &other); // 赋值函数

};

>

{

public:

B(const A &a); // B的机关函数

private:

A m_a; // 成员器材

};

示例9-2(a)中,类B的机关函数在其初始化表里调用了类A的拷贝机关函数,从而将成员器材m_a初始化。

示例9-2 (b)中,类B的机关函数在函数体内用赋值的体式花式将成员器材m_a初始化。我们看到的只是一条赋值语句,但理想上B的机关函数干了两件事:先公开里创设m_a器材(调用了A的无参数机关函数),再调用类A的赋值函数,将参数a赋给m_a。

B::B(const A &a)

: m_a(a)

{



}

B::B(const A &a)

{

m_a = a;



}

示例9-2(a) 成员器材在初始化表中被初始化 示例9-2(b) 成员器材在函数体内被初始化

关于内部数据典范的数据成员而言,两种初始化体式花式的苦守险些没有区别,但后者的程序版式似乎更了然些。若类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;

}

F::F(int x, int y)

{

m_x = x;

m_y = y;

m_i = 0;

m_j = 0;

}

示例9-2(c) 数据成员在初始化表中被初始化 示例9-2(d) 数据成员在函数体内被初始化

9.3 机关和析构的顺序

机关从类条理的最根处初步,在每一层中,起首调用基类的机关函数,然后调用成员器材的机关函数。析构则严酷依照与机关相反的顺序尝试,该顺序是独一的,不然编译器将无法主动尝试析构进程。

一个幽默的气象是,成员器材初始化的顺序完全不受它们在初始化表中顺序的影响,只由成员器材在类中声明的顺序决议。这是由于类的声明是独一的,而类的机关函数可以有多个,是以会有多个差别顺序的初始化表。假若成员器材依照初始化表的顺序休止机关,这将招致析构函数无法获得独一的逆序。[Eckel, p260-261]

9.4 示例:类String的机关函数与析构函数

// String的通俗机关函数

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::~String(void)

{

delete [] m_data;

// 由于m_data是内部数据典范,也可以写成 delete m_data;

}

9.5 不要藐视拷贝机关函数与赋值函数

由于并非通通的器材都会运用拷贝机关函数和赋值函数,程序员可以对这两个函数有些藐视。请先记住以下的告诫,在阅读注释时就会多心:

u 本章扫尾讲过,假若不主动编写拷贝机关函数和赋值函数,编译器将以“位拷贝”的体式花式主动天生缺省的函数。假使类中含有指针变量,那么这两个缺省的函数就隐含了错误。以类String的两个器材a,b为例,假定a.m_data的内容为“hello”,b.m_data的内容为“world”。

现将a赋给b,缺省赋值函数的“位拷贝”意味实熟手b.m_data = a.m_data。这将形成三个错误:一是b.m_data原有的内存没被开释,形成内存走漏;二是b.m_data和a.m_data指向同一块内存,a或b任何一方更改都会影响另一方;三是在器材被析构时,m_data被开释了两次。

u 拷贝机关函数和赋值函数特别非常随便稠浊,常招致错写、错用。拷贝机关函数是在器材被建即速调用的,而赋值函数只能被已经存在了的器材调用。以下程序中,第三个语句和第四个语句很雷同,你分得清晰哪个调用了拷贝机关函数,哪个调用了赋值函数吗?

String a(“hello”);

String b(“world”);

String c = a; // 调用了拷贝机关函数,最好写成 c(a);

c = b; // 调用了赋值函数

本例中第三个语句的作风较差,宜改写成String c(a) 以区别于第四个语句。

9.6 示例:类String的拷贝机关函数与赋值函数

// 拷贝机关函数

String::String(const String &other)

{

// 答应利用other的公有成员m_data

int length = strlen(other.m_data);

m_data = new char[length 1];

strcpy(m_data, other.m_data);

}

// 赋值函数

String & String::operate =(const String &other)

{

// (1) 搜寻自赋值

if(this == &other)

return *this;

// (2) 开释原有的内存资源

delete [] m_data;

// (3)分派新的内存资源,并复制内容

int length = strlen(other.m_data);

m_data = new char[length 1];

strcpy(m_data, other.m_data);

// (4)前往本器材的援用

return *this;

}

类String拷贝机关函数与通俗机关函数(参见9.4节)的区别是:在函数入口处无需与NULL休止斗劲,这是由于“援用”弗成以是NULL,而“指针”可感觉NULL。

类String的赋值函数比机关函数庞大得多,分四步完成:

(1)第一步,搜寻自赋值。你可以会感觉添枝加叶,难道有人会愚笨到写出 a = a 这样的自赋值语句!确切其实不会。但是直接的自赋值仍有可以出现,譬喻

// 内容自赋值

b = a;



c = b;



a = c;

// 所在自赋值

b = &a;



a = *b;

大约有人会说:“纵然出现自赋值,我也可以不睬睬,大不了化点时分让器材复制本人罢了,反正不会出错!”

他真的说错了。看看第二步的delete,自尽后还能复制本人吗?以是,假若缔造自赋值,应该即速截至函数。看重不要将搜寻自赋值的if语句

if(this == &other)

错写成为

if( *this == other)

(2)第二步,用delete开释原有的内存资源。假若现在不开释,今后就没机遇了,将形成内存走漏。

(3)第三步,分派新的内存资源,并复制字符串。看重函数strlen前往的是有效字符串长度,不包含终了符‘\0’。函数strcpy则连‘\0’一路复制。

(4)第四步,前往本器材的援用,目的是为了实气象 a = b = c 这样的链式表达。看重不要将 return *this 错写成 return this 。那么可否写成return other 呢?成绩不是一样吗?

弗成以!由于我们不知道参数other的生命期。有可以other是个姑且器材,在赋值终了后它即速消失,那么return other前往的将是渣滓。

9.7 偷懒的设备处置惩罚拷贝机关函数与赋值函数

假若我们真实不想编写拷贝机关函数和赋值函数,又不答应别人运用编译器天生的缺省函数,若何办?

偷懒的设备是:只需将拷贝机关函数和赋值函数声明为公有函数,不用编写代码。

譬喻:

>

{ …

private:

A(const A &a); // 公有的拷贝机关函数

A & operate =(const A &a); // 公有的赋值函数

};

假若有人试图编写如下程序:

A b(a); // 调用了公有的拷贝机关函数

b = a; // 调用了公有的赋值函数

编译器将指出错误,由于外界弗成以利用A的公有函数。

9.8 若何在派生类中完成类的根蒂根基函数

基类的机关函数、析构函数、赋值函数都不能被派生类承继。假若类之间存在承继相关,在编写上述根蒂根基函数时应看重以下工作:

u 派生类的机关函数应在其初始化表里调用基类的机关函数。

u 基类与派生类的析构函数应该为虚(即加virtual关键字)。譬喻

#include <iostream.h>

>

{

public:

virtual ~Base() { cout<< "~Base" << endl ; }

};

>

{

public:

virtual ~Derived() { cout<< "~Derived" << endl ; }

};

void main(void)

{

Base * pB = new Derived; // upcast

delete pB;

}

输出成绩为:

~Derived

~Base

假若析构函数不为虚,那么输出成绩为

~Base

u 在编写派生类的赋值函数时,看重不要忘失对基类的数据成员重新赋值。譬喻:

>

{

public:



Base & operate =(const Base &other); // 类Base的赋值函数

private:

int m_i, m_j, m_k;

};

>

{

public:



Derived & operate =(const Derived &other); // 类Derived的赋值函数

private:

int m_x, m_y, m_z;

};

Derived & Derived::operate =(const Derived &other)

{

//(1)搜寻自赋值

if(this == &other)

return *this;

//(2)对基类的数据成员重新赋值

Base::operate =(other); // 由于不能直接利用私无数据成员

//(3)对派生类的数据成员赋值

m_x = other.m_x;

m_y = other.m_y;

m_z = other.m_z;

//(4)前往本器材的援用

return *this;

}

9.9 一些心得体味

有些C 程序设计书籍称机关函数、析构函数和赋值函数是类的“Big-Three”,它们确切其实是任何类最主要的函数,不容藐视。

大约你感觉本章的内容已经够多了,学会了就能平安无事,我不能作这个保证。假若你平息吃透“Big-Three”,请好好阅读参考文献[Cline] [Meyers] [Murry]。

版权声明:
原创作品,答应转载,转载时请务必以超链接形式标明文章 原始因由 、作者信息和本声明。不然将究查轨则责任。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: