第14章 类
这一章介绍C++的类,类在程序中引入了用户自定义的类型,类可以包含数据和函数。
在传统程序设计语言中用户自定义类型是数据的集合。它们放在一起用以描述对象的属性和状态。C++中的类类型使用户不仅能够描述对象的属性和状态,还可以定义对象的行为。
本章包括下面一些主题:
* 类的概述
* 类名称
* 类成员
* 成员函数
* 静态数据成员
* 联合
* 位域
* 嵌套类说明
* 类范围中的类型名称
类类型用关键字class,struct和union定义。简单地说,用这三个关键字定义的类型都称作类说明(class declaration)。但是在讨论语言成分时,用不同的关键字定义的类,其行为是不同的。
语法
类名称:
标识符
一个类的变量和函数称为类的成员。在定义一个类的时候通常也提供一些如下的成员(尽管都是任选的):
* 类的数据成员,定义一个类类型的某个对象的属性和状态。
* 一个或多个构造函数,用来初始化类类型的对象。构造函数在第11章“特殊成员函数”中的“构造函数”一节中介绍。
* 一个或多个析构函数,主要完成一些清除工作,如:回收动态分配的存储器或关闭文件。析构函数在第11章“特殊成员函数”的“析构函数”一节中详述。
* 一个或多个成员函数用来定义对象的行为。
定义类类型
类类型的定义用类指示符(class-specifiers)。类类型的说明使用复杂类型指示符。在第6章“说明”中的“类型说明符”一节介绍。
语法
类说明符:
类头{成员表opt}
类头:
类关键字 imodel opt 标识符opt
基类说明opt 类关键字 imodel opt
类名称opt 基类说明opt类
关键字:
class
struct
unionimodel:
__declspec
当编译器处理完类名称以后(在处理类体之前),类名称立即被当作标识符,因此类名称可以用来说明类成员。这使得用户可以说明自引用型的数据结构。如下:
class Tree { public: void *Data; Tree *Left; Tree *Right; };
结构、类和联合
三种类类型分别是结构、类和联合。他们的说明是用struct、class和union关键字(见关键字语法)。表8.1显示了三种类类型之间的不同。
表8.1 结构、类和联合的访问控制和约束
结构 | 类 | 联合 |
类关键字struct | 类关键字是class | 类关键字是union |
缺省访问控制是公有的 | 缺省访问控制是私有的 | 缺省访问控制是公有的 |
无使用约束 | 无使用约束 | 任何时候只能使用一个成员 |
无名类类型
类可以是无名的。也即,你可以说明一个不带标识符的类。这在你用一个typedef名称去代替一个类的类名称时很有用。如下:
typedef struct { unsigned x; unsigned y; } POINT;
注意:上面例子中无名类的使用对保持已有C代码的兼容性是很有用的。在很多C代码中,typedef与无名结构一起使用是非常普遍的。当你要引用一个类的成员,并表现出这个成员并不是被包含在一个单独的类中时,无名类也同样是很有用的,如下所示:
struct PTValue { POINT ptLoc; union { intiValue; long lValue; }; };
PTValue ptv;
在上面的代码中,iValue可以用对象成员选择符(.)来访问。如下:
int i=ptv.iValue;
无名类要服从一些特定的限制(有关无名union的详情见本章后面的“联合”一节)。一个无名类:
* 不能有构造函数或析构函数
* 不能作为参数传递给函数(除非用“...”避开类型检查)
* 也不能作为函数的返回值
类的定义点
一个类的定义是在其类说明符之后。成员函数不必因为类的详细定义而也要马上定义。
研究下面的例子:
class Point//Point类 { //详细定义 public: Point() {cx=cy=0;}//定义构造函数 Point(int x,int y) {cx=x,cy=y;} //定义构造函数 unsigned &x(unsigned); //定义访问器 unsigned &y(unsigned); //定义访问器 private: unsigned cx,cy; };
尽管两个访问器函数(x和y)还没有被定义,但类Point已经详细定义了的(访问器函数主要用来提供对数据成员的安全访问)。
类类型对象
一个对象在执行环境中是一块类型化的存储区域;它不但保留了状态信息,也定义了行为。类类型对象用类名称来定义。考察下面的代码段:
class Account //类名是Account { public: Account(); //缺省构造函数 Account(double); //从double类型来构造 double& Deposit(double); double& Withdraw(double,int); ... };
Account CheckingAccount; //定义类类型对象
面的代码说明了一个类(新的类型)称为Account,然后用这种新的类型定义了一个对象叫CheckingAccount。
C++为类型对象提供了如下一些操作:
* 赋值。一个对象能够赋值给另一个。这一操作的缺省行为是成员方式(memberwise)的拷贝。用户可以提供一个自定义的赋值操作以取代缺省行为。
* 用拷贝构造函数进行初始化下面是用户自定义的拷贝构造函数进行的初始化的例子:
* 对某对象进行明确地初始化:
Point myPoint=thatPoint:
把myPoint说明为一个Point类型的对象并把它初始化为thatPoint的值。
* 作为传递参数而引起的初始化。对象能以传值或引用形式传递给函数。如果它们是以传值形式传递给函数的,则每个对象的拷贝会传递给函数。创建此拷贝的缺省办法是一个成员方式(memberwise)的拷贝。当然也可以用自定义的拷贝构造函数来代替缺省的拷贝构造函数(是一种构造函数,它带有唯一的对类的引用型的参数)。
* 由于初始化函数返回值而引起的初始化,对象有能够以传值或引用方式从函数返回。对于以传值方式返回对象的缺省方法也是成员方式的拷贝;当然同样可以由自定义的拷贝构造函数所代替。由引用方式(用指针或引用类型)返回的对象,对于调用函数来说是不能作为自动型或局部型对象的。如果这样的话,由返回值所指引的对象在其能被使用之前就超出了其范围。
第12章“重载”中的“重载操作符”一节,解释如何在基于类方式下重定义这些操作符。
空类
你可以说明一个空类,但是这种类型的对象有非零的值。下面的例子显示了这一点:
#include(iostream.h) class NoMembers { }; void main(){ NoMembers n; //NOMembers型对象 cout << "The size of an object of empty class is:" << sizeof n << endl; }
上面程序的输出是:
The size of an object of empty class is:1.
为这种对象分配的存储器是非零的。因而,不同的对象有不同的地址。由于具有不同的地址,就有可能通过比较对象指针鉴别不同的对象。同样,在数组中每个成员数组必须有不同的地址。
Microsoft特殊处
一个空基类在其派生类中占用零个字节。
Microsoft特殊处结束
在程序中类说明引入了新的类型(以类名称来称呼),这些类说明在给定的转换单元中就像类定义一样。在每个转换单元中,对于给定的类类型仅仅只可以有一个该类型的定义。用这些新的类型,用户可以定义对象。同编译器也可以通过类型检查发现对这些对象的不兼容的类型操作。
下面是类型检查的例子:
class Point { public: unsigned x,y; }; class Rect { public: unsigned x1,y1,x2,y2; }
//带有两个参数的函数原型,一个是Point类型,另一个是Rect型的引用int PtInRect (Point,Rect &);
...
Point pt;Rect
rect;
rect=pt; //错误,类型不匹配
pt=rect; //错误,类型不匹配
//错误,PtInRect的参数反了
cout << "Point is" << PtInRect(rect,pt) ? "" : "not"
<<"in rectangle "<< endl;
正如上面例子所显示的,对于一个类类型对象上的操作(如赋值和参量传递)和内部类型对象一样遵循着同一种类型检测约束。
因为编译器能够区分不同的类类型,函数可以在基于类类型参数以及内置参量的原则下被重载。有关重载函数的更多信息参见第7章“说明”中的“函数重载”以及第12章的“重载”。
说明及访问类名称
可以在全局范围或在类范围中说明类名。如果在类范围中说明类名称,它们实际上指的是“嵌套类”。
Microsoft特殊处
在Microsoft C++的局部类说明中不允许有函数定义。
Microsoft特殊处结束
在类的范围中引入的新的类名称会隐藏在同一封闭块中的同名元素。要引用因上述说明隐藏的名称,只能通过全类型指示符,下面的例子是一个用全类型指示符引用一个隐藏名称的例子:
struct A//全局范围中定义A { int a; }; void main() { char A='a';//把名称A重定义为一个对象 struct A AObject; ... }
因为名称A所指示的结构被由A所指示的char对象所隐藏了,因而定义一个类型A的对象AObject时必须用类关键字struct。
你当然可以用类关键字说明一个类而不提供该类的定义。这种无定义的类说明只是为超前引用而引入了一个类名称。这种技术在设计要在友元说明中引用其它的类的类中非常有用,当然在类名称必须在头文件中出现,而其定义又不需要时,这种技术也很有用。例如:
//RECT.H
class Point; //类Point的无定义说明
class Line { public: int Draw(Point &ptFrom,Point *ptTo); ... };
在上面的例子中,必须提供Point类名称,但不必引入该名称的定义性说明。
typedef语句和类
使用typedef语句去命名一个类类型是把一个typedef名称变成一个类名称。更多的详情参见第6章“说明”中的“类型指示符”。
类可以有如下类型的成员:
* 成员函数
* 数据成员
* 类(包括类、结构和联合),参见“嵌套类说明和联合”
* 枚举
* 位域
* 友元
* 类型名称
注意:友元是被类说明所包含的,它们包括在一个预先的列表中,然而它们并不是真正的类成员,因为它们不在类的范围之中。
语法
成员表: 成员说明 成员表opt
访问指示符:成员表opt
成员说明:
decl指示opt 成员说明符表opt;
函数定义opt;
限定名;
成员说明符表:
成员说明符
成员说明符表,成员说明符
成员说明符;
说明符 纯指示符opt
标识符opt:常量表达式
纯指示符:
=0成员表的目的:
* 说明给定类的全套成员
* 说明同各个类成员相联系的访问(公有的、私有的或保护的)在成员表的说明中,一个成员只能说明一次。成员的再次说明会引出错误消息。因为一个成员表就是一整套的类成员,故不能在随后的类说明中为给定类增加成员。
成员说明符中也不能包含有初始化器,提供初始化器会产生一个错误消息如下:
class CantInit { public: long l=7;//错误:试图初始化类成员 static inti=9;//错误:必须在类说明之外定义并初始化 };
因为对每个给定类类型的对象都要单独创建静态成员实例。正确的初始化办法是用类的构造函数去初始化成员的数据(构造函数在第11章“特殊成员函数”中的“构造函数”一节中论述)。对于一个给定类类型的所有对象仅有一个共享的静态数据成员拷贝,静态数据成员必须在文件范围中定义才能初始化(有关静态数据成员的详情见本章后面的“静态数据成员”)。下面的例子显示了如何进行这些初始化:
class CanInit { public: CanInit(){l=7;} //当新的CanInit对象创建时初始化l long l; static int i: static int j; }
int CanInit::i=15; //i在文件范围中定义并初始化为15
//初始化是在CanInit的范围中定值的
int CanInit::j=i; //初始化器的右边在要被初始化的对象范围中
注意:类名称CanInit必须前导于i以说明i被定义为CanInit的成员。
类成员说明语法
成员数据不能说明为auto、extern和register存储类型。然而它们能够被说明为static存储类型。
decl指示符在成员函数的说明中可以被省略(有关decl指示符的情况见第6章“说明”中的“指示符”一节,以及本章后面的成员函数和第7章“说明”中的“函数”一节)。因而下面的代码合法地说明了一个返回整型的函数:
class NoDeclspec { public; NoSpecifiers(); };
当你在成员表中说明一个友元类时,你可以省略成员说明符表。有关友元的更多信息参见第6章“说明”中的“友元指示符”以及第10章“成员访问控制”中的“友元”一节。甚至当一个类名称还没有被定义时,它也可以作为友元来说明,正是友元说明引入了该类名称。
然而在这种类的成员说明中,必须使用全类型指示符语法。见如下的例子:
class HasFriends { public: friend class NotDeclaredYet; };
在上面的例子中,类说明的后面没有成员说明符表。因为对NotDeclaredYet的说明还没有被处理到,故使用了全类型指示符的使用形式:classNotDeclaredYet。一个已经说明过的类型可以在友元成员说明的说明中使用一般的说明形式:
class AlreadyDeclared { ... }; class HasFriends { public; friend AlreadyDeclared; }
纯指示符(见下面的例子)表明对此说明的虚拟函数不提供其函数实现。因此纯指示符仅仅只能用在虚函数之上指示,考察如下的代码:
class StrBase //strings的基类 { public: virtual int IsLessThan (StrBase&)=0; virtual int IsEqualTo (StrBase&)=0; virtual strBase& CopyOf (StrBase&)=0; ... };
上面的代码中说明了一个抽象类,也即这个类设计为用作基类以派生出更多的特有的类。通过用纯指示符说明一个或多个虚拟函数为纯虚拟函数,这种基类能够强制执行一个特殊的协议(protocol)或机制。
从StrBase继承的类必须为这些纯虚拟函数提供实现。否则它们也会被认为是抽象基类。
抽象基类不能用来说明对象。例如,在一个从StrBase类继承的类的对象被说明之前,必须提供函数IsLessThan、IsEqualTo以及CopyOf的实现(有关抽象基类的更多信息参见第9章“派生类”中的“抽象类”)。
在成员表说明变长数组
Microsoft特殊处
如果一个程序未用ANSI兼容选项(/Za)来编译,则变长数组可以在类成员表中说明为最后一个数据成员。因为这是Microsoft的扩充,故而用这种方式使用变长数组会降低你的代码的可移植性。说明一个变长数组,要省略第一个维数,如:
class Symbol { public: int SymbolTYpe; char SymbolText[]; };
Microsoft特殊处结束
限制
如果一个类含有变长数组,它就不能用作其它类的基类,而且一个含有变长数组的类也不能随意说明为其它类的成员,只能说明为其它类的最后一个成员。一个含有变长数组的类也不能含有直接或间接虚拟基类。当sizeof运算符运用于一个含有变长数组的类时,将返回除变长数组以外的所有成员的存储总和。对于含有变长数组的类的实现,应该提供一个替换的办法以获得正确的类的大小。
不能够把含有变长数组组成成分的对象说明为某数组的成员,对于指向这类对象的指针进行数学运算也会产生错误。
类成员数据的存储
非静态成员数据以如下方式存储:所有在访问说明符之间的项是连续地向存储器地址增加的方向存放的。越过访问说明符的成员的存放顺序不作保证。
Microsoft特殊处
依赖于/Zp编译选项或包含pragma伪指令,可以引入干预空间以对成员数据进行按字或双字边界对齐。在Microsoft C++中,类成员是连续地按地址增加的方向存储的,尽管C++语言并不要求这一点。有关这种顺序的基本假设都会引起不可移植的代码。
Microsoft特殊处结束
成员命名限制
在类说明中跟类名称有同样名称的函数是构造函数。当该类的一个对象创建的时候,隐含地调用了构造函数(有关构造函数的更多信息参见第11章“特殊成员函数”中的“构造函数”一节。
下面各项在其说明的范围中,不能跟类名称有同样的名称:数据成员(静态或非静态)、封闭的枚举、无名联合成员及嵌套类。
类能够包含数据和函数,这些函数称为成员函数。任何在类说明中说明的一个非静态函数视为成员函数,并用成员选择符(.和->)调用。当在其它的成员函数中调用本类的成员函数时,对象和成员选择符可以省略。例如:
class Point { public: short x() { return _x; } short y() { return _y; } void Show(){ cout<<x()><","<<y()><"/n"; } private: short _x,_y; }; void main() { Point pt; pt.Show(); }
注意:在成员函数Show中调用了其它成员函数x和y,并未使用成员选择符。这些调用的隐含意义是 this->x()和this->y()。然而在主函数main中调用成员函数Show必须使用对象pt和成员选择符(.)。
在类中说明的静态成员函数的调用可以使用成员选择符或使用全限定函数名称(包括类名称)。
注意:用friend关键字说明的函数并不认为是类的成员。在类中此函数只是说明为友元(尽管此函数可以是别的类的成员)。
一个友元说明控制着非成员函数对类数据的访问。
下面的例子显示了如何说明成员函数:
class Point { public: unsigned GetX(); unsigned GetY(); unsigned SetX(unsigned x); unsigned SetY(unsigned y); private: unsigned ptX, ptY; }
在上面的类说明中,说明了四个函数:GetX,GetY,SetX和SetY。下面的例子显示如何在程序中调用这些函数:
void main() { //说明一个Point类型的对象 Point ptOrigin; //用成员选择符(.)调用成员函数 ptOrigin.SetX(0); ptOrigin.SetY(0); //说明一个指向Point类型对象的指针 Point *pptCurrent=new Point; //用成员选择符(->)调用成员函数 pptCurrent->SetX(ptOrigin.GetX()+10); pptCurrent->SetY(ptOrigin.GetY()+10); }
在上面的代码中,对象ptOrigin成员函数的调用是用成员选择符(.)来调用的。而由pptCurrent指向的对象的成员函数的调用使用的是成员选择符(->)。
成员函数概述
成员函数可以是静态的或非静态的。静态成员函数的行为同其它成员函数的行为是不同的,因为静态成员函数没有隐含的this参数。非静态成员函数有一个this指针。无论静态成员函数还是非静态成员函数成员均可在类说明之内或之外定义。
如果是在类说明之内定义的,则它被视为嵌入函数,也没有必要用类名称来限定函数名称。尽管定义在类说明之中的函数已经是作为嵌入函数对待,你仍可用关键字inline来文档化代码。
下面是在一个类说明中定义一个函数的例子:
class Account { public: //在类Account说明之中说明函数Deposit double Deposit(double HowMuch) { balance += HowMuch; return balance; } private: double balance; };
当在类说明的外面定义成员函数时,只有明确地把它说明为inline的,此成员函数才视为嵌入型成员函数。而且在定义时函数名必须用类名和范围分辨符(::)加以限定。
下面的例子除了在类说明之外定义函数Deposit外,同前面对类Account说明是等同的。
class Account { public: //说明成员函数Deposit,但不定义它。 double Deposit(double HowMuch); private: double balance; }; Inline double Account::Deposit (double HowMuch) { balance += HowMuch; return balance; }
注意:尽管成员函数既可以在类说明之中定义也可以在类说明之外单独定义,但当类定义了以后,就不能再为它增加成员函数了。
含有成员函数的类可以有多个说明,但成员函数本身在程序中仅有一次定义。多重定义会在链接时引出错误消息。如果一个类含有联编函数的定义,该函数的定义同样要满足“唯一定义”原则。
非静态成员函数
非静态成员函数有一个隐含的参数,this,它是一个指向调用此函数的对象的指针。
this指针的类型是type * const。这些函数被认为具有类的范围,可以直接使用处在同一类范围中的类数据和成员函数。在前面的例子中,表达式balance +=HowMuch把HowMuch的值加到类成员balance中。考察下面的语句:
Account Checking;
Checking Deposit(57.00);
在上面的例子中,说明了一个Account类型的对象,并调用其成员函数Deposit给它加上$57.00。在函数Account::Deposit中,balance作为Checking.balance(该对象的balance成员)。
非静态成员函数趋向于在它们自己的类类型对象之上操作。通过明确的类型转换在别的不同类型的对象上,调用此成员函数会引起不确定的行为。
this指针
所有的非静态成员函数能用this关键字,this是一个常量指针(不可修改)。它指向的对象是成员函数的调用者。成员数据可以由表达式this->成员名称的值来确定(尽管这种技术很少使用)。在成员函数中,在表达式中使用成员名称隐含着使用方式this->成员名称来选择正确的函数和数据成员。
注意:因为this指针是不可修改的,因而不允许对this赋值。在早期的C++的实现中允许对this指针赋值。
偶尔可以直接使用this指针。例如在操纵自引用型数据结构时,在这种情况下要取得当前对象的地址。
this指针的类型
在函数说明中通过const和volatile关键字可以修改this指针的类型。要说明一个具有这些关键字属性的函数,可使用cv-修饰符表语法。
语法:
cv-修饰符表:
cv-修饰符 cv-修饰符表
optcv-修饰符:
const
volatile
考虑下面的例子:
class Point { unsigned X() const; };
上面的例子中说明了一个成员函数X,其中this指针被当作一个指向常量对象的常量指针。可以混合使用cv-修饰符表选项,但它们通常修饰的是this指针所指的对象,而不是this指针本身。因此,下面的说明中说明了函数,它的this指针是一个指向常量对象的常量指针:
class point { unsigned X()__far const; };
下面的语法描述了this指针的类型,其中cv-修饰符表可以是const或volatile,类名称是类的名称:
cv-修饰符表opt 类类型 * const this
表8.2解释了有关这些修饰符的更多的作用。
表8.2
修饰符 | 意义 |
const | 不能改变成员数据,也不能调用非常量型成员函数 |
volatilee | 成员数据每次访问时是从存储器中调入的;并关闭了优化工作 |
把一个常量对象传递给一个非常量的成员函数会产生错误。同样地,把一个volatile对象传递给一个非volatile型的函数同样会产生错误。
成员函数说明为const型,则不能改变成员数据。在这种函数中,this指针是一个指向常量的指针。
注意:构造函数和析构函数是不能说明为const或volatile的。然而它们可以被const或volatile对象调用。
静态成员函数
静态成员函数被认为具有类的范围。同非静态成员相比,这些函数没有隐含的this参数;因而,它们只能直接使用静态的数据成员、枚举或嵌套类型。静态成员函数的存储可以不必有相应的类类型对象。考虑下面的代码:
class WindowManager { public: static int CountOf(); //返回打开窗口的个数 void Minimize();//最小化当前窗口 WindowManager SideEffects(); //边界效果(副作用)函数 ... private: static int wmWindowCount; };
int WindowManager::wmWindowCount=0;
...
//最小化(显示图标)所有窗口
for(int i=0;i<WindowManager::CountOf();++i)
rgwmWin[i].Minimize();
在上面的代码中,类WindowManager包含有静态成员函数CountOf。该函数返回打开窗口的个数,但却不必同某个给定的WindowManager类对象相联系。这一概念在循环中得以体现。在循环中CountOf用于控制表达式。因为CountOf是一个静态成员函数,因此无需引用对象,此函数即可调用。静态成员函数有外部连接。这些函数没有this指针(下一章中论述)。结果,这些函数必须遵循下面的一些约束:
* 不能用成员选择符(.或->)来访问非静态成员。
* 不能说明为虚函数。
* 不能同有相同参数类型的非静态成员函数同名称。
注意:选择了一个静态成员函数的成员选择符(.或->),其左边是不可定值的。在有副作用的函数同此函数一起使用时,这一点可能很重要。例如:表达式SideEffects().CountOF()中,并不会调用SideEffects函数。
类可以包含静态的成员数据或成员函数。当一个数据说明为static,则对于该类的所有对象仅维持该成员的一个拷贝(相关的情况见前一节的“静态成员函数”)。
静态数据成员并不是给定类类型对象的一部分;它们是单独的对象。结果,静态数据成员的说明不能看作是定义。数据成员的说明是在类范围之中的,但其定义
是在文件范围中实现的。这些静态成员具有外部连接。下面的例子显示了这一点:
class BufferedOutput { public: //返回该类对象所写的字节大小 short BytesWritten() { return bytecount;} //复位计数器 static void ResetCount() { bytecount=0; } //静态成员的说明 static long bytecount; };
//在文件范围中定义bytecount
long BufferedOutput::bytecount;
在前面的代码中,成员bytecount是在类BufferOutput中说明的,但它必须在类说明之外定义。
不必引用某类类型的对象即可引用静态数据成员。引用BufferedOutput对象所写的字节大小可以按如下方式得到:
long nBytes=BufferedOutput::bytecount;
静态成员不会因为该类类型的某个对象的结束而结束。静态成员当然也可以用成员选择符(.或->)来访问。例如:
BufferedOutput Console;
long nBytes=Console.bytecount;
在上面的例子中,对对象console的引用是不可求值的。返回的值是静态对象bytecount。
静态数据成员同样要受制于类成员的访问规则,因此私有存储的静态数据成员只允许类成员函数和友元访问。这些规则在第10章“成员访问控制”中描述。例外的是:静态数据成员必须在文件范围中定义,并忽视它们的存储约束。如果数据成员要明确地初始化,则必须同定义一起提供初始化器。
静态成员的类型并不被它的类名称所限定。因此:BufferedOutput::bytecount的类型是long型。
联合这种类类型在一个时间点只能包含一种数据元素(尽管数据元素可以是数组或者类类型)。联合的成员代表的是联合所能包含的数据种类。一个联合类型的对象需要足够的空间去容纳成员表中最大的成员。考虑下面的例子:
#include <stdlib.h> #include <string.h> #include <limits.h> union NumericType //说明一个可容纳下面数据的联合 { int iValue;//整型值 long lValue;//长整型值 double dValue;//浮点型值 } void main (int argc,char *argv[]) { NumericType *Values=new NumericType[argc-1]; for(int i=1;i<argc;++i) { if(strchr(argv[i],′. ′)!=0) //浮点型用dValue成员进行赋值 Values[i].dValue=atof(argv[i]); else //如果大于最大的整型数,则存到lValue成员中 if(atol(argv[i])>INT_MAX) Values[i].lValue=atol(argv[i]); else //否则,存到iValue成员中 Values[i].iValue=atoi(argv[i]); } }
NumericType联合在存储器中的位置示意图如图8.1。
联合中的成员函数
正如在成员函数中描述过的,除了有成员数据外,联合也可以有成员函数,尽管联合可以有特殊成员函数如构造函数和析构函数,但联合不能有虚拟函数(有关的更多的信息参见第11章“特殊成员函数”中的“构造函数”以及“析构函数”)。
作为类类型的联合联合
不可以有基类;因此,它们不能从其它的联合、结构和类中继承属性。联合也不能作为进一步继承的基类。
继承将在第9章“派生类”中详细讨论。
联合的成员数据
除了以下所述之外,联合可以在成员表中包含大多数的数据类型:
* 具有构造函数或析构函数的类类型
* 具有自定义的赋值操作符的类类型
* 静态数据成员
无名联合
无名联合是在说明时不带类名或说明符表的联合。
语法
union {成员表};
这种联合说明所说明的不是类型,而是对象。在无名联合中说明的名称不能跟在同一范围中的其它名称冲突。在无名联合中说明的名称可以直接使用,就像并不是成员变量一样。下面例子显示了这一点。
#include <iostream.h> struct DataForm { enum DataType {CharData=1,IntData,StringData}; DataType type; //说明一个无名联合 union { char chCharMem; char *szStrmem; int iIntMem; }; void print(); };
void DataForm::print() { //基于type的数据类型打印合适的数据类型 switch(type) { case CharData: cout << chCharMem; break; case IntData: cout<<szStrMem; break; case StringData: cout << iIntMem; } }
在函数DataForm::print中,三个成员(chCharMem、szStrMem和iIntMem)的访问就像他们是作为成员说明的一样(没有联合说明)。然而三个联合的成员是共享同一存储器的。
联合的成员数据除了上述所列的一些限制之外,还要受下列约束的限制:
* 如果在文件范围中说明,则必须说明为静态的。
* 它们仅能含有公有的成员;私有的和保护的成员在无名联合中会产生错误。
* 它们不能有成员函数。
注意:仅简单地省略语法上的类名部分并不会使一个联合成为无名联合,要把一个联合限制为无名联合,则说明一定不要说明对象。
类和结构可以拥有在存储空间上小于一个整型的成员。这些成员被说明为位域。位域成员说明符的语法规格如下:
语法
说明符opt:常量表达式
说明符是程序中用来存储成员的名称。它必须是一个整型(包括枚举型)。常量表达式说明了成员在结构中所占的位数。无名位域也即没有标识符的位域成员,可以用作填充。
注意:一个无名的零宽度位域会强制使下一个位域对齐到下一个类型边界。这里类型指的是成员的类型。
下面的例子说明了一个含有位域的结构:
struct Date { unsigned nWeekDay :3; //0..7(3位) unsigned nMonthDay :6; //0..31(6位) unsigned nMonth :5; //0..12(5位) unsigned nYear :8; //0..100(8位) };
一个Date类型对象的存储器布局如图8.2所示。
注意:nYear是8位长的而且超出了说明的unsigned int类型的字边界。因此nYear在一个新的unsigned int上继续。没有必要把所有的位域都塞到一个所基于的类型对象之中。根据说明中所需求的位域的大小,可以分配新的存储单元。
Microsoft特殊处
作为位域说明的数据的排列顺序是从低到高。
Microsoft特殊处结束
如果在结构说明中包含一个无名零长度位域,如下例所示:
struct Date { unsigned nWeekDay :3; //0..7(3位) unsigned nMonthDay :6; //0..31(6位) unsigned :0; //强迫对齐到下一个边界 unsigned nMonth :5; //0..12(5位) unsigned nYear :8; //0..100(8位) }
其存储器分布如图8.3所示。
位域所基于的类型必须是整型。见第2章“基本概念”中的基本类型。
使用位域的限制
下面列举了对位域的明细错误操作:
* 取一个位域的地址
* 初始化
类可以在其它类的范围之内说明。这种类称为“嵌套类”。嵌套类被视为在外围封闭类的范围之中,并只能在此作用之中有效地使用。要引用一个嵌套类而从它的直接外围类的范围之外,则必须使用全限定名。
下面的例子显示了如何说明嵌套类。
class BufferedIO { public: enum IOError { None,Access,General }; //说明嵌套类BufferedInput class BufferedInput { public: int read(); int good() { return _inputerror == None; } private: IOError _inputerror; } //说明嵌套类BufferedOutput class BuufferedOutput { //成员表 }; };
BufferedIO::BufferedIuput和BufferedIO::BufferedOutput是在BufferedIO之中说明的。这些类名称在类BufferedIO范围之外是不可见的。但是一个BufferedIO型对象不会含有任何BufferedIuput和BufferedOutput型的对象。嵌套类可以直接使用来自于外围类的名称、静态成员名称、类型名称以及枚举名称。
为了使用其它类成员,必须使用指针、引用或对象名称。
在上面的BufferedIO的例子中,枚举IOError可以由嵌套类BufferedIO::BufferedInput和BufferedIo::BufferedOutput的成员函数直接访问。如函数good中所示的。
注意:嵌套类仅仅只在类范围中说明了类型。它不会引起创建嵌套类的对象。上面的例子只是说明了两个嵌套类但并未说明任何这些类的对象。
访问权限和嵌套类
在别的类中的嵌套类并没有给嵌套类的成员函数以特殊的权限。同样,类中包括的成员函数对于嵌套类的成员也没有什么特殊的访问权限。
嵌套类中的成员函数
在嵌套类中说明的成员函数可以在文件范围中定义,前面的例子可以写为:
class BufferedIO { public: enum IOError { None,Access,General}; class BufferedInpu { public: int read();//说明但不定义成员函数read和good int good(); private: IOError _inputerror; } class BufferedOutput { //成员表 }; }
//在文件范围中定义成员函数read和good
int BufferedIO::BufferedInput::read() { ... }; int BufferedIO::BufferedInput::good() { return _inputerror==None; }
在上面的例子中,运用限定类型名称语法说明成员函数名称,说明为:
BufferedIO::BufferedInput::read()
意味着函数read是在类BufferedIO的范围中的BufferedInput类的成员函数。因为这一说明使用了限定的类型名语法,因而如下形式的构造是可能的:
typedef BufferedIO::BufferedInput BIO_INPUT
int BIO_INPUT::read()
上面的说明是同前一个说明等价的,但它用一个typedef名称代替了类名称。
友元函数和嵌套类
在嵌套类中说明的友元函数被视为在嵌套类的范围中,而不在类的范围中。因此对于包含在类中成员和成员函数,这些友元函数并没有获得多少特别的访问权限。如果你要在定义于文件范围中的友元函数中使用一个在嵌套类中说明的名称,必须像下面那样使用限定类型名称:
extern char *rgszMessage[]; class BufferedIO { public: ...class BufferedInput { public: friend int GetExtendedErrorStatus(); ... static char *message; int iMsgNo; }; };
char *BufferedIO::BufferedInput::message; int GetExtendedErrorStaus() { ... strcpy(BufferedIO::BufferedInput::message, rgszMessage[iMsgNo]); return iMsgNo; }
在上面的代码中函数GetExtendedErrorStatus是作为友元函数说明的,在这个函数中(它定义于文件范围),一条消息从静态数组中拷贝到类成员中。注意GetExtendedErrorStatus的一个较好的实现是说明:
int GetExtendedErrorStatus (char * message)
用前面的接口,几个不同的类可以使用此函数的功能,只要把要拷贝的错误消息的地址传给函数即可。
在类范围中定义的类型名称应视为局限于它们的类。它们不能在类的范围之外使用。
下面的例子显示了这一概念:
class Tree { public: typedef Tree *PTREE; PTREE Left; PTREE Right; void *vData; };
PTREE pTree; //错误:不在类的范围中