第 8章 类和对象
8.1 概述面向过程的程序设计方法采用函数(过程)来描述对数据的操作,但又将函数与数据分离。这种实质上的依赖与形式上的分离使得所编写出来的大型程序难于控制,严重时甚至可能导致程序的崩溃。
C++ 的类体现了 OOP 技术所要求的抽象和封装机制。类将欲处理目标的属性(数据)和对其进行操作的方法(函数)
封装成一个整体。这样,既把自然界中的一类实体抽象成一种具有自主行为的数据类型,同时又将它们的属性隐藏起来以阻止外界的干扰。
类描述了数据类型的组织形式;对象则是类的实例,是存放在内存中可操作的变量。
8.2 类
8.2.1 类的说明
class class_name {
<private:>
pri_members;
public:
pub_members;
<protected:
pro_members;>
};
其中:关键字 private,public 和 protected 为成员属性说明符,它们分别指出其后所定义的成员分别为 私有 成员,公有成员和 保护的 成员。由于类成员缺省的属性为私有的,所以在不致于引起二义性的情况下可以省略关键字 private。
类的成员分为两类,一类类似于结构成员 —— 是一些变量,
叫做数据成员(对象的属性),另一类是函数,叫做成员函数(对象的方法)。
成员属性规定了成员的访问权限:私有成员只允许对象自身的方法或友元函数(将在第 11 章中介绍)访问;公有成员则还允许对象的“外部”的访问;保护的成员(将在第 10
章中介绍)主要用来决定派生类中成员的访问权限。
通常,类中的数据成员被说明成私有的,以阻止外界的随意访问;而成员函数被说明成公有的,为外界访问对象中的数据提供一个安全的接口。根据实际需要,有时也将数据成员说明成公有的,以方便外部的访问,但这样的数据安全性将难以保证。成员函数也常常说明成私有的,这样的函数仅供对象内部调用。
// PERSON.H
#if !defined _PERSON_H_
#define _PERSON_H_
class Person {
private:
char Name[9];
int Age;
char Sex;
public:
void Register(const char*,int,char);
void GetName(char*);
int GetAge();
char GetSex();
};
#endif
一般而言,类的定义通常都写到一个头文件(,H 或,HPP)
中,供所有用到相应类类型的文件嵌入用。由于类中的成员函数是由代码组成的,所以通常都将它们写到一个独立的源程序(,CPP) 文件中,经编译后与使用对应类类型的模块链接成一个程序。
定义成员函数的一般形式为:
type class_name,,func_name(<agr_list>)
{
func_body;
}
// PERSON.CPP
#include <string.h>
#include "person.h”
void Person,,Register(const char* nm,int ag,char s)
{
strcpy(Name,nm);
Age = ag;
Sex = s;
}
void Person,,GetName(char* nm)
{
strcpy(nm,Name);
}
注意:成员函数若定义成:
char* Person,,GetName()
{
return Name;
}
岂不是更简便吗?实际上这样做将破坏数据有封装。由于函数返回了成员 Name 的首地址,就为外部修改该成员的提供了方便,从而也破坏了该数据的安全性。
int Person,,GetAge()
{
return Age;
}
char Person,,GetSex()
{
return Sex;
}
8.2.2 类与结构仅需记住一条,类与结构的唯一区别就在于,在类中定义的成员缺省成员属性为私有的;而在结构中定义的成员缺省成员属性为公有的 。
8.2.3 内联成员函数成员函数也可以定义成内联的,而且形式更为灵活。例:
在头文件 PERSON.H 中
class Person {
//...
public:
void Register(const char*,int,char);
void GetName(char*);
int GetAge() { return Age; }
char GetSex() { return Sex; }
在源程序文件 PERSON.CPP 中
inline void Person,,Register(const char* nm,int ag,char s)
{
strcpy(Name,nm);
Age = ag;
Sex = s;
}
inline void Person,,GetName(char* nm)
{
strcpy(nm,Name);
}
8.3 对象
8.3.1 对象说明从技术上讲,对象就是某一类类型的变量。与结构相似,类定义相当于结构的定义,而说明一个对象就如同说明一个结构变量。说明对象具有如下的一般形式:
<storage> class_name obj_name<...>;
例:
Person per1,per2;
8.3.2 使用对象由于对象中的数据成员通常都被说明成私有的,所以对对象中的数据成员的访问通常都是通过调用相应的公有成员函数来完成的。调用对象的一个成员函数就叫做向对象发送一个消息。
例:
char name[9];
Person Per;
Per.Register("张 三 ",21,'m');
Per.GetName(name);
cout << name << '\t' << Per.GetAge();
cout << '\t' << Per.GetSex() << endl;
应当说明的是,对于对象中的公有数据成员,则可以像结构变量那样通过对象名直接访问记住类与结构的唯一区别就可以理解类与结构存在许多相似之处,比如:访问对象的公有成员需要使用成员访问运算符、
同类对象之间可以整体赋值、对象用作函数的参数时属于赋值调用、函数可以返回一个对象、可以说明指向类类型的指针和对对象的引用、可以说明元素为类类型的数组,等等。
但是,一般情况下不能像结构变量那样对对象进行初始化,
除非所初始化的数据成员为公有成员。
8.3.3 类作用域类和结构中说明的标识符具有类作用域。例:
class X {
public:
int x;
//…
};
x = 3; // 错误!
int x = 5; // 正确
8.4 成员函数重载类中的成员函数与普通函数一样,也允许重载。例:
在 PERSON.H 中添加一个公有成员函数:
void Register(int ag)
{
Age = ag;
}
8.5 this 指针系统中存在一个唯一的指针,当一个对象接收到一个消息时,
系统就将该指针指向这个对象。由于该指针可以利用关键字
,this” 来访问,所以称其为 this 指针。
前面 Person 类的定义属于静态的说明。当程序运行时,对象则是动态的被访问,这时被访问对象中的成员函数将表现为如下的形式(以成员函数 Register 为例):
void Person,,Register(char* nm,int ag,char s)
{
strcpy(this->Name,nm);
this->Age = ag;
this->Sex = s;
}
当对象 Per 收到一个 Register( ) 消息时,系统将进行以下的说明并进行初始化:
Person *const this = &Per;
这样就确保函数修改的是对象 Per 中的数据成员,而不是其它 Person 类对象中的数据成员。
在程序中,常常还可以通过 this 指针来判断某些条件是否成立。关于这一点将在后续章节中进行介绍。
第 9章 构造函数和析构函数
9.1 构造函数
9.1.1 定义构造函数
<class_name,,>class_name(<arg_list>)
{
func_body;
}
可以看出,构造函数 ( Constructor)的函数名与类名完全相同。
另外,构造函数没有(返回值)类型。这不仅体现在构造函数不得返回任何值,而且函数根本就没有返回值类型说明,包括
void。
顾名思义,构造函数就是用来创建对象的。实际上,它还为初始化对象提供了接口。
// PERSON.H
#if !defined _PERSON_H_
#define _PERSON_H_
class Person {
private:
char cName[9];
unsigned uAge,7; // 节省 2 个字节存储单元
unsigned uSex,1;
public:
Person(const char*,int,char);
char* Name();
int Age() { return uAge; }
char Sex() { return uSex == 0? 'm','f'; }
};
#endif
// PERSON.CPP
#include <string.h>
#include "person.h”
Person,,Person(const char* nm,int ag,char s)
{
strcpy(cName,nm);
uAge = ag;
uSex = s == 'm'? 0,1;
}
char* Person,,Name()
{
static char temp[9]; // 必须说明成静态的
return strcpy(temp,cName);
}
9.1.2 构造函数和对象初始化有了构造函数所提供的接口,就可以很方便的对对象进行初始化。例:
Person per1("张三 ",21,'m'),per2("李四 ",22,'f');
这样,就在说明对象 per1 和 per2 的同时又分别对它们的数据成员进行了初始化。
注意初始化对象与初始化结构变量的区别,前者表现为函数调用的形式,而事实上对象的创建就是通过调用对应类的构造函数来实现的。
#include <iostream.h>
class X {
int x;
public:
X(int a = 0)
{
x = a;
cout << "Constructor is called." << endl;
}
}
void main()
{
X xArr[3];
}
该程序的输出为:
Constructor is called.
Constructor is called.
Constructor is called.
构造函数除了不存在返回值、不可通过对象来调用这样一些特殊之处外,其它方面与普通公有成员函数完全一样。因此,构造函数可以是内联的、允许重载。例:
class Person {
//…
public:
Person(int a) { Age = a; }
//…
}
该构造函数可以用来在创建一个对象的同时初始化其 Age 成员。
然而,这里存在一个问题:利用该构造函数所创建的对象其另外两个数据成员的值如何设置?在此情况下,必须为类 Per-
son 定义相应的数据成员访问接口。
下面将重新定义 Person 类。
// PERSON.H
#If !defined _PERSON_H_
#define _PERSON_H_
class Person {
private:
char *pName; // 更加灵活
unsigned uAge,7;
unsigned uSex,1;
public:
Person(char*,int,char);
Person(char* = 0); // 带有缺省参数
Person(int a),uAge(a) {} // 注意该函数的写法
Person(char s),pName(0) { uSex = s == 'm'? 0,1; }
char* Name(char* = 0);
int Age(int a = 0) {
int temp = uAge;
if(a != 0)
uAge = a;
return temp;
}
char Sex(char s = 'n') {
char temp = uSex == 0? 'm','f';
if(s != 'n')
uSex = s == 'm'? 0,1;
return temp;
}
};
#endif
// PERSON.CPP
#include <string.h>
#include "person.h"
Person,,Person(char* nm,int ag,char s)
{
pName = new char[strlen(nm) + 1];
strcpy(pName,nm);
uAge = ag;
uSex = s == 'm'? 0,1;
}
Person,,Person(char* nm)
{
if(!nm)
pName = 0;
else {
pName = new char[strlen(nm) + 1];
strcpy(pName,nm);
}
uAge = 0;
uSex = 0;
}
char* Person,,Name(char* nm)
{
static char temp[80];
if(pName)
strcpy(temp,pName);
else
temp[0] = 0; // 空串
if(nm) {
delete []pName;
pName = new char[strlen(nm) + 1];
strcpy(pName,nm);
}
return temp;
}
9.1.3 构造函数和 new 运算符运算符 new 可以创建生存期可控的对象。由于类从本质上讲是一种数据类型,因此可以利用 new 来创建动态对象的方式与创建动态变量相同。创建动态对象时对对象进行初始化,将调用相应的构造函数。例:
Person *pPer,*qPer,*sPer;
pPer = new Person("张三 ",22,'m’);
// 调用 Person(char*,int,char);
qPer = new Person(22); // 调用 Person(int);
sPer = new Person; // 调用 Person(char*);
//…
delete pPer;
delete qPer;
delete sPer;
9.1.4 缺省的构造函数前边介绍过,创建一个对象时需要调用类的构造函数。那么,
第 8 章中所有的类中均没有定义构造函数,它们的对象是通过什么东西创建的呢?是通过调用 缺省的构造函数 来创建的。
当一个类中没有显式地定义构造函数时,系统会为类自动生成一个形如:
class_name( ) {}
的构造函数,该函数就是所谓缺省的构造函数。
若为类显式地定义了任何一个构造函数,则系统将不再为类生成缺省的构造函数。
一个类若没有缺省的构造函数,则会为创建某些对象带来一些困难。比如,创建对象数组就必须调用缺省的构造函数(因此对象数组不得初始化)。为此,类中若显式地定义了任何一个构造函数,则必须显式地定义缺省的构造函数或所有参数都带有缺省值的构造函数 (如上例中的 Person(char*))。
9.2 析构函数
9.2.1 定义析构函数
<class_name,,>~class_name()
{
func_body;
}
与构造函数的作用相反,析构函数 ( Destructor)是用来撤销对象的。当一 个对象的生存期结束时,系统会自动调用类的析构函数来释放对象所占的内存,并做一些善后工作。
由析构函数的一般形式可以看出,析构函数是不能够重载的
(不带参数)。那么是否需要显式地定义析构函数呢?常常是需要的,对于涉及到动态内存分配的类必须显式地定义析构函数,以释放动态内存 。
#include <iostream.h>
class X {
int x;
public:
~X()
{
cout << "Destructor is called." << endl;
}
}
void main()
{
X xArr[3];
}
该程序的输出为:
Destructor is called.
Destructor is called.
Destructor is called.
9.2.2 析构函数和 delete 运算符我们知道,利用 new 运算符来创建一个动态对象时,需要调用类中相应的构造函数。同理,当利用 delete 来删除一个对象时,系统也会自动地调用类的析构函数。例:
X *pX = new X[3];
//…
delete []pX; // 输出同上例实际上,任何时候一个对象消亡 时都会调用析构函数。因为析构函数的作用就是销毁对象的。
9.2.3 缺省的析构函数与构造函数相同,若类中没有显式地定义析构函数,则系统将为其自动生成一个缺省的析构函数。同样,缺省的析构函数也是一个无函数体的函数。由于析构函数不允许重载,所以显式定义的析构函数自然可以代替缺省的析构函数。
因为缺省的析构函数仅执行系统预设的一些操作,这在许多场合下是不能满足实用要求的,所以常常需要显式地定义析构函数。
例如,对于前面定义的 Person 类,使用缺省的析构函数将导致严重的问题:当对象消亡时,缺省的析构函数释放了所有数据成员所占的内存,包括指针 pName 所占的内存,然而它不知道也不可能释放该指针所指的动态内存。另一方面,在对象消亡后,由于指向动态内存首地址的指针已经丢失,其所指的动态内存也就无法利用程序来释放。
鉴于此原因,必须为类 Person 显式地定义其析构函数。
在头文件 PERSON.H 中添加以下的析构函数:
~Person( ) { delete []pName; }
经这样定义后,析构函数在释放对象所占内存之前将首先释放指针 pName 所指的动态内存。
习题:
20,21
8.1 概述面向过程的程序设计方法采用函数(过程)来描述对数据的操作,但又将函数与数据分离。这种实质上的依赖与形式上的分离使得所编写出来的大型程序难于控制,严重时甚至可能导致程序的崩溃。
C++ 的类体现了 OOP 技术所要求的抽象和封装机制。类将欲处理目标的属性(数据)和对其进行操作的方法(函数)
封装成一个整体。这样,既把自然界中的一类实体抽象成一种具有自主行为的数据类型,同时又将它们的属性隐藏起来以阻止外界的干扰。
类描述了数据类型的组织形式;对象则是类的实例,是存放在内存中可操作的变量。
8.2 类
8.2.1 类的说明
class class_name {
<private:>
pri_members;
public:
pub_members;
<protected:
pro_members;>
};
其中:关键字 private,public 和 protected 为成员属性说明符,它们分别指出其后所定义的成员分别为 私有 成员,公有成员和 保护的 成员。由于类成员缺省的属性为私有的,所以在不致于引起二义性的情况下可以省略关键字 private。
类的成员分为两类,一类类似于结构成员 —— 是一些变量,
叫做数据成员(对象的属性),另一类是函数,叫做成员函数(对象的方法)。
成员属性规定了成员的访问权限:私有成员只允许对象自身的方法或友元函数(将在第 11 章中介绍)访问;公有成员则还允许对象的“外部”的访问;保护的成员(将在第 10
章中介绍)主要用来决定派生类中成员的访问权限。
通常,类中的数据成员被说明成私有的,以阻止外界的随意访问;而成员函数被说明成公有的,为外界访问对象中的数据提供一个安全的接口。根据实际需要,有时也将数据成员说明成公有的,以方便外部的访问,但这样的数据安全性将难以保证。成员函数也常常说明成私有的,这样的函数仅供对象内部调用。
// PERSON.H
#if !defined _PERSON_H_
#define _PERSON_H_
class Person {
private:
char Name[9];
int Age;
char Sex;
public:
void Register(const char*,int,char);
void GetName(char*);
int GetAge();
char GetSex();
};
#endif
一般而言,类的定义通常都写到一个头文件(,H 或,HPP)
中,供所有用到相应类类型的文件嵌入用。由于类中的成员函数是由代码组成的,所以通常都将它们写到一个独立的源程序(,CPP) 文件中,经编译后与使用对应类类型的模块链接成一个程序。
定义成员函数的一般形式为:
type class_name,,func_name(<agr_list>)
{
func_body;
}
// PERSON.CPP
#include <string.h>
#include "person.h”
void Person,,Register(const char* nm,int ag,char s)
{
strcpy(Name,nm);
Age = ag;
Sex = s;
}
void Person,,GetName(char* nm)
{
strcpy(nm,Name);
}
注意:成员函数若定义成:
char* Person,,GetName()
{
return Name;
}
岂不是更简便吗?实际上这样做将破坏数据有封装。由于函数返回了成员 Name 的首地址,就为外部修改该成员的提供了方便,从而也破坏了该数据的安全性。
int Person,,GetAge()
{
return Age;
}
char Person,,GetSex()
{
return Sex;
}
8.2.2 类与结构仅需记住一条,类与结构的唯一区别就在于,在类中定义的成员缺省成员属性为私有的;而在结构中定义的成员缺省成员属性为公有的 。
8.2.3 内联成员函数成员函数也可以定义成内联的,而且形式更为灵活。例:
在头文件 PERSON.H 中
class Person {
//...
public:
void Register(const char*,int,char);
void GetName(char*);
int GetAge() { return Age; }
char GetSex() { return Sex; }
在源程序文件 PERSON.CPP 中
inline void Person,,Register(const char* nm,int ag,char s)
{
strcpy(Name,nm);
Age = ag;
Sex = s;
}
inline void Person,,GetName(char* nm)
{
strcpy(nm,Name);
}
8.3 对象
8.3.1 对象说明从技术上讲,对象就是某一类类型的变量。与结构相似,类定义相当于结构的定义,而说明一个对象就如同说明一个结构变量。说明对象具有如下的一般形式:
<storage> class_name obj_name<...>;
例:
Person per1,per2;
8.3.2 使用对象由于对象中的数据成员通常都被说明成私有的,所以对对象中的数据成员的访问通常都是通过调用相应的公有成员函数来完成的。调用对象的一个成员函数就叫做向对象发送一个消息。
例:
char name[9];
Person Per;
Per.Register("张 三 ",21,'m');
Per.GetName(name);
cout << name << '\t' << Per.GetAge();
cout << '\t' << Per.GetSex() << endl;
应当说明的是,对于对象中的公有数据成员,则可以像结构变量那样通过对象名直接访问记住类与结构的唯一区别就可以理解类与结构存在许多相似之处,比如:访问对象的公有成员需要使用成员访问运算符、
同类对象之间可以整体赋值、对象用作函数的参数时属于赋值调用、函数可以返回一个对象、可以说明指向类类型的指针和对对象的引用、可以说明元素为类类型的数组,等等。
但是,一般情况下不能像结构变量那样对对象进行初始化,
除非所初始化的数据成员为公有成员。
8.3.3 类作用域类和结构中说明的标识符具有类作用域。例:
class X {
public:
int x;
//…
};
x = 3; // 错误!
int x = 5; // 正确
8.4 成员函数重载类中的成员函数与普通函数一样,也允许重载。例:
在 PERSON.H 中添加一个公有成员函数:
void Register(int ag)
{
Age = ag;
}
8.5 this 指针系统中存在一个唯一的指针,当一个对象接收到一个消息时,
系统就将该指针指向这个对象。由于该指针可以利用关键字
,this” 来访问,所以称其为 this 指针。
前面 Person 类的定义属于静态的说明。当程序运行时,对象则是动态的被访问,这时被访问对象中的成员函数将表现为如下的形式(以成员函数 Register 为例):
void Person,,Register(char* nm,int ag,char s)
{
strcpy(this->Name,nm);
this->Age = ag;
this->Sex = s;
}
当对象 Per 收到一个 Register( ) 消息时,系统将进行以下的说明并进行初始化:
Person *const this = &Per;
这样就确保函数修改的是对象 Per 中的数据成员,而不是其它 Person 类对象中的数据成员。
在程序中,常常还可以通过 this 指针来判断某些条件是否成立。关于这一点将在后续章节中进行介绍。
第 9章 构造函数和析构函数
9.1 构造函数
9.1.1 定义构造函数
<class_name,,>class_name(<arg_list>)
{
func_body;
}
可以看出,构造函数 ( Constructor)的函数名与类名完全相同。
另外,构造函数没有(返回值)类型。这不仅体现在构造函数不得返回任何值,而且函数根本就没有返回值类型说明,包括
void。
顾名思义,构造函数就是用来创建对象的。实际上,它还为初始化对象提供了接口。
// PERSON.H
#if !defined _PERSON_H_
#define _PERSON_H_
class Person {
private:
char cName[9];
unsigned uAge,7; // 节省 2 个字节存储单元
unsigned uSex,1;
public:
Person(const char*,int,char);
char* Name();
int Age() { return uAge; }
char Sex() { return uSex == 0? 'm','f'; }
};
#endif
// PERSON.CPP
#include <string.h>
#include "person.h”
Person,,Person(const char* nm,int ag,char s)
{
strcpy(cName,nm);
uAge = ag;
uSex = s == 'm'? 0,1;
}
char* Person,,Name()
{
static char temp[9]; // 必须说明成静态的
return strcpy(temp,cName);
}
9.1.2 构造函数和对象初始化有了构造函数所提供的接口,就可以很方便的对对象进行初始化。例:
Person per1("张三 ",21,'m'),per2("李四 ",22,'f');
这样,就在说明对象 per1 和 per2 的同时又分别对它们的数据成员进行了初始化。
注意初始化对象与初始化结构变量的区别,前者表现为函数调用的形式,而事实上对象的创建就是通过调用对应类的构造函数来实现的。
#include <iostream.h>
class X {
int x;
public:
X(int a = 0)
{
x = a;
cout << "Constructor is called." << endl;
}
}
void main()
{
X xArr[3];
}
该程序的输出为:
Constructor is called.
Constructor is called.
Constructor is called.
构造函数除了不存在返回值、不可通过对象来调用这样一些特殊之处外,其它方面与普通公有成员函数完全一样。因此,构造函数可以是内联的、允许重载。例:
class Person {
//…
public:
Person(int a) { Age = a; }
//…
}
该构造函数可以用来在创建一个对象的同时初始化其 Age 成员。
然而,这里存在一个问题:利用该构造函数所创建的对象其另外两个数据成员的值如何设置?在此情况下,必须为类 Per-
son 定义相应的数据成员访问接口。
下面将重新定义 Person 类。
// PERSON.H
#If !defined _PERSON_H_
#define _PERSON_H_
class Person {
private:
char *pName; // 更加灵活
unsigned uAge,7;
unsigned uSex,1;
public:
Person(char*,int,char);
Person(char* = 0); // 带有缺省参数
Person(int a),uAge(a) {} // 注意该函数的写法
Person(char s),pName(0) { uSex = s == 'm'? 0,1; }
char* Name(char* = 0);
int Age(int a = 0) {
int temp = uAge;
if(a != 0)
uAge = a;
return temp;
}
char Sex(char s = 'n') {
char temp = uSex == 0? 'm','f';
if(s != 'n')
uSex = s == 'm'? 0,1;
return temp;
}
};
#endif
// PERSON.CPP
#include <string.h>
#include "person.h"
Person,,Person(char* nm,int ag,char s)
{
pName = new char[strlen(nm) + 1];
strcpy(pName,nm);
uAge = ag;
uSex = s == 'm'? 0,1;
}
Person,,Person(char* nm)
{
if(!nm)
pName = 0;
else {
pName = new char[strlen(nm) + 1];
strcpy(pName,nm);
}
uAge = 0;
uSex = 0;
}
char* Person,,Name(char* nm)
{
static char temp[80];
if(pName)
strcpy(temp,pName);
else
temp[0] = 0; // 空串
if(nm) {
delete []pName;
pName = new char[strlen(nm) + 1];
strcpy(pName,nm);
}
return temp;
}
9.1.3 构造函数和 new 运算符运算符 new 可以创建生存期可控的对象。由于类从本质上讲是一种数据类型,因此可以利用 new 来创建动态对象的方式与创建动态变量相同。创建动态对象时对对象进行初始化,将调用相应的构造函数。例:
Person *pPer,*qPer,*sPer;
pPer = new Person("张三 ",22,'m’);
// 调用 Person(char*,int,char);
qPer = new Person(22); // 调用 Person(int);
sPer = new Person; // 调用 Person(char*);
//…
delete pPer;
delete qPer;
delete sPer;
9.1.4 缺省的构造函数前边介绍过,创建一个对象时需要调用类的构造函数。那么,
第 8 章中所有的类中均没有定义构造函数,它们的对象是通过什么东西创建的呢?是通过调用 缺省的构造函数 来创建的。
当一个类中没有显式地定义构造函数时,系统会为类自动生成一个形如:
class_name( ) {}
的构造函数,该函数就是所谓缺省的构造函数。
若为类显式地定义了任何一个构造函数,则系统将不再为类生成缺省的构造函数。
一个类若没有缺省的构造函数,则会为创建某些对象带来一些困难。比如,创建对象数组就必须调用缺省的构造函数(因此对象数组不得初始化)。为此,类中若显式地定义了任何一个构造函数,则必须显式地定义缺省的构造函数或所有参数都带有缺省值的构造函数 (如上例中的 Person(char*))。
9.2 析构函数
9.2.1 定义析构函数
<class_name,,>~class_name()
{
func_body;
}
与构造函数的作用相反,析构函数 ( Destructor)是用来撤销对象的。当一 个对象的生存期结束时,系统会自动调用类的析构函数来释放对象所占的内存,并做一些善后工作。
由析构函数的一般形式可以看出,析构函数是不能够重载的
(不带参数)。那么是否需要显式地定义析构函数呢?常常是需要的,对于涉及到动态内存分配的类必须显式地定义析构函数,以释放动态内存 。
#include <iostream.h>
class X {
int x;
public:
~X()
{
cout << "Destructor is called." << endl;
}
}
void main()
{
X xArr[3];
}
该程序的输出为:
Destructor is called.
Destructor is called.
Destructor is called.
9.2.2 析构函数和 delete 运算符我们知道,利用 new 运算符来创建一个动态对象时,需要调用类中相应的构造函数。同理,当利用 delete 来删除一个对象时,系统也会自动地调用类的析构函数。例:
X *pX = new X[3];
//…
delete []pX; // 输出同上例实际上,任何时候一个对象消亡 时都会调用析构函数。因为析构函数的作用就是销毁对象的。
9.2.3 缺省的析构函数与构造函数相同,若类中没有显式地定义析构函数,则系统将为其自动生成一个缺省的析构函数。同样,缺省的析构函数也是一个无函数体的函数。由于析构函数不允许重载,所以显式定义的析构函数自然可以代替缺省的析构函数。
因为缺省的析构函数仅执行系统预设的一些操作,这在许多场合下是不能满足实用要求的,所以常常需要显式地定义析构函数。
例如,对于前面定义的 Person 类,使用缺省的析构函数将导致严重的问题:当对象消亡时,缺省的析构函数释放了所有数据成员所占的内存,包括指针 pName 所占的内存,然而它不知道也不可能释放该指针所指的动态内存。另一方面,在对象消亡后,由于指向动态内存首地址的指针已经丢失,其所指的动态内存也就无法利用程序来释放。
鉴于此原因,必须为类 Person 显式地定义其析构函数。
在头文件 PERSON.H 中添加以下的析构函数:
~Person( ) { delete []pName; }
经这样定义后,析构函数在释放对象所占内存之前将首先释放指针 pName 所指的动态内存。
习题:
20,21