360 likes | 523 Views
8.2 类. 8.2.1 类的说明 class class_name { < private: > pri_members; public: pub_members; < protected: pro_members; > }; 其中:关键字 private , public 和 protected 为成员属性说明符,它们分别指出其后所定义的成员分别为 私有 成员、 公有 成员和 保护的 成员。由于类成员缺省的属性为私有的,所以在不致于引起二义性的情况下可以省略关键字 private 。.
E N D
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
9.3 拷贝初始化构造函数 <class_name ::>class_name(class_name&) { func_body; } 拷贝初始化构造函数为利用一个已存在的对象来创建另一个对象提供了接口。例如: Person per1("张三", 21, 'm'), per2(per1); 其中,对象 per2 就是利用对象 per1 来初始化的。因此这两 个对象具有其值完全相同的数据成员。 注意:前边并没有为 Person 类定义拷贝初始化构造函数,创建 per2 时调用的是什么呢?是缺省的拷贝初始化构造函数。 若类中没有显式地定义拷贝初始化构造函数,则系统将自动生成一个函数体为空的缺省的拷贝初始化构造函数。
然而,对于本例而言,利用缺省的拷贝初始化构造函数却会带来一个严重和问题:当对象 per1 生存期结束时,其析构函数会释放对象中数据成员 pName 所指的动态内存并释放整个对象所占的内存。而当 per2 消亡时,析构函数在释放整个对象所占内存前首先要释放 per2.pName 所指的动态内存,而这一块内存已经在撤消 per1 时释放掉了。这将导致一个“运行时间错误(Run-Time Error)”。 对于那些涉及到动态内存分配的类,必须显式地定义拷贝初始化构造函数。 从拷贝初始化构造函数的一般形式可以看出,该函数是不允许重载的,因此显式定义的拷贝初始化构造函数将覆盖缺省的拷贝初始化构造函数。
// 在 PERSON.H 中添加公有的成员函数原型: Person(Person&); // 在 PERSON.CPP 中添加函数定义: Person :: Person(Person& per) { pName = new char[strlen(per.pName) + 1]; strcpy(pName, per.pName); uAge = per.uAge; uSex = per.uSex; } 经过这样的定义,上述per2 在创建时将为自身的pName 申请一块动态内存,并将 per1 中的 pName 所指的字符串复制到该动态内存中。当任一对象消亡时,释放的均为自身的动态内存,从而防止了相应的运行时间错误发生。