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

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

2011-03-07 17:01 330 查看

第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]。

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