词条 | 派生类 |
释义 | 利用继承机制,新的类可以从已有的类中派生。那些用于派生的类称为这些特别派生出的类的“基类”。 语法说明基类说明:在C++中要定义的新的数据类型不仅拥有新定义的成员,而且还同时拥有旧的成员,我们称已存在的用来派生新类的类为C++基类,又称为父类。 基类表:基类表中存放各个基类名称 基类说明符:基类类体中类成员的访问说明符 单一继承在“单一继承”这种最普通的形式中,派生类仅有一个基类。 在类的层次设计中,可以发现一些普遍的特性,即派生类总是同基类有“kind of”关系。 另一个值得注意点是Book既是派生类(从PrintedDocument中派生),也是基类(PaperbackBook是从Book派生的)。下面的例子是这种类层次的一个轮廓性的说明。 class PrintedDocument { //成员表 }; //Book是从PrintedDocument中派生的 class Book:public PrintedDocument { //成员表 }; //PaperbackBook是从Book中派生 class PaperbackBook: public Book { //成员表 }; PrintedDocument作为Book的直接基类,它同时也是PaperbackBook的非直接基类。直接基类和非直接基类的区别在于直接基类出现在类说明的基类表中,而非直接基类不出现在基类表中。 每个派生类的说明是在基类的说明之后说明的, 因此对于基类仅只给出一个前向引用的说明是不够的,必须是完全的说明。 一个类可以作为很多特别类的基类。 在继承中,派生类含有基类的成员加上任何你新增的成员。结果派生类可以引用基类的成员(除非这些成员在派生类中重定义了)。当在派生类中重定义直接基类或间接基类的成员时,可以使用范围分辨符(::)引用这些成员。考虑下面的代码: class Document { public: char * Name;//文档名称 void PrintNameOf(); //打印名称 }; //实现类Document的PrintNameOf函数 void Document::PrintNameOf() { cout << Name << end ; } class Book:public Document { public: Book(char *name, long pagecount); private: long PageCount; }; //class Book 构造函数 Book::Book (char *name, long pagecount) { Name=mew char [strlen(name)+1]; strcpy (Name,name); PageCount=pagecount; }; 注意,Book的构造函数(Book::Book)具有对数据成员Name的访问权。在程序中可以按如下方式创建Book类对象并使用之。 //创建一个Book类的新对象,这将激活构造函数Book:BookBook LibraryBook ("Programming Windows,2nd Ed",994); ... //使用从Document中继承的函数PrintNameOf. LibraryBook.PrintNameOf();如前面例子所示,类成员和继承的数据与函数以一致的方式引用。如果类Book所调用的PrintNameOf是由类Book重新定义实现的,则原来属于类Document的PrintNameOf函数只能用范围分辩符(::)才能使用: class Book:public Document { Book(char *name,long pagecount); void PrintNameOf(); long PageCount; }; void Book::PrintNameOf() { cout<<"Name of Book:"; Document::PrintNameOf(); } 只要有一个可访问的、无二义性的基类,派生类的指针和引用可以隐含地转换为它们基类的指针和引用。下面的例子证实了这种使用指针的概念(同样也适用于引用): #include <iostream.h> void main() { Document * DocLib[10]; //10个文档的库 for (int i=0; i<10; ++i) { cout<<"Type of document:" <<"P)aperback,M)agazine,H)elp File,C)BT" << endl; char CDocType; cin >>CDocType; switch(tolower(CDocType)) { case 'p': DocLib=new PaperbackBook; break; case 'm': DocLib=new Magazine; break; case 'h': DocLib=new HelpFile; break; case 'c': DocLib=new ComputerBasedTraining; break; default: --i; break; } } for (i=0; i<10; ++i) DocLib->PrintNameOf(); } 在前面例子的SWITCH语句中,创建了不同类型的对象。这一点依赖于用户对CDocType对象所作出的说明。然而这些类型都是从类Document中派生出来的,故可以隐含地转换为Document*。结果是DocLib成为一个“相似链表”(heterogeneous list)。此链表所包含的是不同种类的对象,其中的所有对象并不是有相同的类型。 因为Document类有一个PrintNameOf函数。因此它能够打印图书馆中每本书的名称,但对于Document类型来说有一些信息会省略掉了(如:Book的总页数,HelpFile的字节数等)。 注意:强制基类去实现一个如PrintNameOf的函数,通常不是一个很好的设计,本章后面的“虚拟函数”中提供了一个可替换的设计方法。 多重继承C++的后期的一些版本为继承引入了“多重继承”模式。在一个多重继承的图中,派生类可以有多个直接基类。 对于一个特定的程序如果每个类的属性并不是全部要求使用,则每个类可以单独使用或者同别的类联合在一起使用。 虚基类层次 有一些类层次很庞大,但有很多东西很普遍。这些普遍的代码在基类中实现了,然而在派生类中又实现了特殊的代码。 对于基类来说重要的是建立一种机制,通过这种机制派生类能够完成大量的函数机能。 这种机制通常是用虚函数来实现的。有时,基类为这些函数提供了一个缺省的实现。 了解到所有的Identify和WhereIs的函数实现返回的是同种类型的信息,这一点很重要。在这个例子中,恰好是一种描述性字符串。 这些函数可以作为虚拟函数来实现,然后用指向基类的指针来调用,对于实际代码的联结将在运行时决定,以选择正确的Identify和WhereIs函数。 类协议的实现 类可以实现为要强制使用某些协议。这些类称为“抽象类”,因为不能为这种类类型创建对象。它们仅仅是为了派生别的类而存在。 当一个类中含有纯虚拟函数或当他们继承了某些纯虚拟函数却又没有为它们提供一个实现时,该类称为抽象类。纯虚拟函数是用纯说明符定义的虚拟函数。如下: virtual char *Identify()=0; 基类Document把如下一些协议强加给派生类。 * 为Identify函数提供一个合适的实现 * 为WhereIs函数提供一个合适的实现 在设计Document类时,通过说明这种协议,类设计者可以确保如不提供Identify和WhereIs函数则不能实现非抽象类。因而Document类含有如下说明: class Document { public: ... //对派生类的要求,它们必须实现下面这些函数 virtual char *Identify()=0; virtual char *WhereIs()=0; ... }; 基 类如前面讨论的,继承过程创建的新的派生类是由基类的成员加上由派生类新加的成员组成。在多重继承中,可以构造层次图,其中同一基类可以是多个派生类的一部分。图9.4显示了这种图。 多重基类如同多重继承中所描述的,一个类可以从多个基类中派生出来。在派生类由多个基类派生出来的多重继承模式中,基类是用基类表语法成份来说明的。 class CollectionOfBook:public Book,public Collection { //新成员 }; 基类的说明顺序一般没有重要的意义,除非在某些情况下要调用构造函数和析构函数的时候。在这些情况下,基类的说明顺序会对下面所列的有影响。 由构造函数引起的初始化发生的顺序。如果你的代码依赖于CollectionOfBook的Book部分要在Collection部分之前初始化,则此说明顺序将很重要。初始化是按基类表中的说明顺序进行初始化的。 激活析构函数以作清除工作的顺序。同样,当类的其它部分正在被清除时,如果某些特别部分要保留,则该顺序也很重要。析构函数的调用是按基类表说明顺序的反向进行调用的。 注意:基类的说明顺序会影响类的存储器分布。不要对基类成员在存储器中的顺序作出任何编程的决定。 在你说明基类表时,不能把同一类名称说明多次。但是对于一个派生类而言,其非直接基类可以有多个相同的。 虚拟基类因为一个类可以多次作为一个派生类的非直接基类。C++提供了一个办法去优化这种基类的工作。 注意,在LunchCashierQueue对象中,有两个Queue子对象。下面的代码说明Queue为虚拟基类: class Queue { //成员表 }; class CashierQueue:virtual public Queue { //成员表 }; class LunchQueue: virtual public Queue { //成员表 }; class LunchCashierQueue:public LunchQueue, public CashierQueue { //成员表 }; 一个类对于给定的类型既可以有虚拟的组成部分,也可以有非虚拟的组成部分。 如果一个派生类重载了一个从虚拟基类中继承的虚拟函数,而且该派生类以指向虚拟基类的指针调用这些构造函数和析构函数时,编译器会引入一个附加的隐含的“vtordisp”域到带有虚拟基类的类中。/vd0编译器选项禁止了这个增加的隐含vtordisp构造/析构位置成员。/vd1选项(缺省),使得在需要时可以解除禁止。只有在你确信所有类的构造函数或析构函数都虚拟地调用了虚拟函数,vtordisp才可以关掉。 /vd编译器选项会影响全局编译模式。使用vtordisp编译指示可以在基于类方式上打开或禁止vtordisp域: #pragma vtordisp(off) class GetReal:virtual public{...}; #pragma vtordisp(on) 名称的二义性实例多重继承使得从不同的路径继承成员名称成为可能。沿着这些路径的成员名称并不必然是唯一的。这些名称的冲突称为“二义性”。 任何引用类成员的表达式必须使用一个无二义性的引用。下面的例子显示了二义性是如何发生的。 //说明两个基类A和B class A { public: unsigned a; unsigned b(); }; class B { public: unsigned a(); //注意类A也有一个成员"a"和一个成员"b" int b(); char c; }; //定义从类A和类B中派生出的类C class C : public A, public B { }; 分析按上面所给出的类说明,如下的代码就会引出二义性,因为不清楚是引用类A的b呢,还是引用类B的b: C *pc=new C; pc->b(); 考虑一下上面的代码,因为名称a既是类A又是类B的成员,因而编译器并不能区分到底调用哪一个a所指明的函数。访问一个成员,如果它能代表多个函数、对象、类型或枚举则会引起二义性。 编译器通过下面的顺序执行以检测出二义性: 1. 如果访问的名称是有二义性的(如前述),则产生一条错误信息。 2. 如果重载函数是无二义性的,它们就没有什么问题了 3. 如果访问的名称破坏了成员访问许可,则产生一条错误信息 在一个表达式产生了一个通过继承产生的二义性时,通过用类名称限制发生问题的名称即可人工解决二义性,要使前面的代码以无二义性地正确编译,要按如下使用代码: C *pc = new C; pc->B::a(); 注意:在类C说明之后,在C的范围中引用B就会潜在地引起错误。但是,直到在C的范围中实际使用了一个对B的无限定性的引用,才会产生错误。 二义性和虚拟基类如果使用了虚拟基类、函数、对象、类型以及枚举可以通过多重继承的路径到达,但因为只有一个虚拟基类的实例,因而访问这些名称时,不会引起二义性。 访问任何类A的成员,通过非虚拟基类访问则会引起二义性;因为编译器没有任何信息以解释是使用同类B联系在一起的子对象,还是使用同类C联系在一起的子对象,然而当A说明为虚拟基类时,则对于访问哪一个子对象不存在问题了。 通过继承图可能有多个名称(函数的、对象的、枚举的)可以达到。这种情况视为非虚拟基类引起的二义性。但虚拟基类也可以引起二义性,除非一个名称“支配”(dominate)了其它的名称。一个名称支配其它的名称发生在该名称定义在两个类中,其中一个是由另一个派生的,占支配地位的名称是派生类中的名称,在此名称被使用的时候,相反不会产生二义性,如下面的代码所示: class A { public: int a; }; class B: public virtual A { public: int a(); }; class C: public virtual A { ... }; class D: public B,public C { public: D() {a();} //不会产生二义性,B::a()支配了A::a }; 转换的二义性显式地或隐含地对指向类类型的指针或引用的转换也可引起二义性。 实例1虚拟函数可以确保在一个对象中调用正确的函数,而不管用于调用函数的表达式。 假设一个基类含有一个说明为虚拟函数同时一个派生类定义了同名的函数。派生类中的函数是由派生类中的对象调用的,甚至它可以用指向基类的指针和引用来调用。下面的例子显示了一个基类提供了一个PrintBalance函数的实现: class Account { public: Account(double d); //构造函数 virtual double GetBalance(); //获得平衡 virtual void PrintBalance(); //缺省实现 private: double _balance; }; //构造函数Account的实现 double Account::Account(double d) { _balance=d; } //Account的GetBalance的实现 double Account::GetBalance() { return _balance; } //PrintBalance的缺省实现 void Account::PrintBalance() { cerr<<"Error.Balance not available for base type". <<endl; } 两个派生类CheckingAccount和SavingsAccount按如下方式创建: class CheckingAccount:public Account { public:void PrintBalance(); }; //CheckingAccount的PrintBalance的实0现 void CheckingAccount::PrintBalance() { cout<<"Checking account balance:" << GetBalance(); } class SavingsAccount:public Account { public: void PrintBalance(); }; //SavingsAccount中的PrintBalance的实 现void SavingsAccout::PrintBalance() { cout<<"Savings account balance:" << GetBalance(); } 函数PrintBalance在派生类中是虚拟的,因为在基类Account中它是说明为虚拟的,要调用如PrintBalance的虚拟函数,可以使用如下的代码: //创建类型CheckingAccount和SavingsAccount的对象 CheckingAccount *pChecking=new CheckingAccount(100.00); SavingsAccount *pSavings=new SavingsAccount(1000.00); //用指向Account的指针调用PrintBalance Account *pAccount=pChecking; pAccount->PrintBalance(); //使用指向Account的指针调用PrintBalance pAccount=pSavings; pAccount->PrintBalance(); 分析1在前面的代码中,除了pAccount所指的对象不同,调用PrintBalance的代码是相同的。 因为PrintBalance是虚拟的,将会调用为每个对象所定义的函数版本,在派生类CheckingAccount和SavingsAccount中的函数“覆盖”了基类中的同名函数。如果一个类的说明中没有提供一个对PrintBalance的覆盖的实现,则将采用基类Account中的缺省实现。 实例2派生类中的函数重载基类中的虚拟函数,仅在它们的类型完全相同时才如此。派生类中的函数不能仅在返回值上同基类中的虚拟函数不同;参量表也必须不同。当指针或引用调用函数时,要遵循如下规则: * 对虚拟函数调用的解释取决于调用它们的对象所基于的类型。 * 对非虚函数调用的解释取决于调用它们的指针或引用的类型。 下面例子显示了在使用指针调用虚拟或非虚拟函数时它们的行为:#include //说明一个基类 class Base { public: virtual void NameOf(); //虚拟函数 void InvokingClass(); //非虚拟函数 }; //两个函数的实现 void Base::NameOf() { cout<<"Base::NameOf\"; } void Base::InvokingClass() { cout<<"Invoked by Base\"; } //说明一个派生类 class Derived:public Base { public: void NameOf(); //虚拟函数 void InvokingClass(); //非虚拟函数 }; //两个函数的实现 void Derived::NameOf() { cout<<"Derived::NameOf\"; } void Derived::InvokingClass() { cout<<"Invoked by Derived\"; } void main() { //说明一个Derived类型的对象 Derived aDerived; //说明两个指针,一个是Derived*型的,另一个是Base*型的,并用 //aDerived初始化它们。 Derived *pDerived=&aDerived; Base *pBase =&aDerived; //调用这个函数 pBase->NameOf(); //调用虚拟函数 pBase->InvokingClass();//调用非虚拟函数 pDerived->NameOf();//调用虚拟函数 pDerived->InvokingClass(); //调用非虚拟函数 } 分析2该程序的输出是: Derived::NameOf Invoked by Base Derived::NameOf Invoked by Derived 注意,不管调用NameOf函数的指针是通过指向基类的指针还是指向派生类的指针,它调用的函数是派生类的。因为NameOf是虚拟函数,而且pBase和pDerived指向的对象都是派生类的,故而调用函数是派生类的。 因为虚拟函数只能为类类型的对象所调用,所以你不能把一个全局的或静态函数说明为虚拟的。 在派生类中说明一个重载函数时可以用virtual关键字,但是这并不是必须的,因为重载一个虚拟函数,此函数就必然是虚拟函数。 基类中的虚拟函数必须有定义,除非它们被说明为纯的。 虚拟函数调用机制可以用范围分辨符(::)明确地限定函数名称的方法来加以限制。考虑前面的代码,用下面的代码调用基类的PrintBalance。 CheckingAccount *pChecking=new CheckingAccount(100.00); pChecking->Account::PrintBalance(); //明确限定 Account *pAccount=pChecking; //调用Account::PrintBalance pAccount->Account::PrintBalance();//明确限定 上面例子中的两个对PrintBalance的调用都限制了虚拟函数的调用机制。 -------------------------------------------------------------------------------- 抽象类抽象类就像一个一段意义上的说明,通过它可以派生出特有的类。你不能为抽象类创建一个对象,但你可以用抽象类的指针或引用。 至少含有一个纯虚拟函数的类就是抽象类。从抽象类中派生出的类必须为纯虚拟函数提供实现,否则它们也是抽象类。 把一个虚拟函数说明为纯的,只要通过纯说明符语法,考虑一下本章早些时候在“虚拟函数”中提供的例子。类Account的意图是提供一个通常意义的函数功能,Account类型的对象太简单而没有太多用处。因此Account是作为抽象类的一个很好的候选: 实例1class Account { public: Account(double d); //构造函数 virtual double GetBalance();//获得平衡 virtual void PrintBalance()=0; //纯虚拟函数 Private: double _balance; }; 分析1这里的说明同前一次的说明的唯一不同是PrintBalance是用纯说明符说明的。 使用抽象类的限制 抽象类不能用于如下用途: * 变量或成员数据 * 参量类型 * 函数的返回类型 * 明确的转换类型 另外一个限制是如果一个抽象类的构造函数调用了一个纯虚拟函数,无论是直接还是间接的,结果都是不确定的。但抽象类的构造函数的析构函数可以调用其它成员函数。 抽象类的纯虚拟函数可以有定义,但它们不能用下面语法直接调用: 抽象类名称::函数名称() 实例2在设计基类中含有纯虚拟析构函数的类层次时,这一点很有用。因为在销毁一个对象的过程中通常都要调用基类的析构函数,考虑下面的例子:#include //说明一个带有纯虚拟析构函数的抽象类 class base { public: base() { } virtual ~base()=0; }; //提供一个析构函数的定义 base::~base() { }; class derived:public base { public: derived(){ }; ~derived() { }; }; void main() { derived *pDerived=new derived; delete pDerived; } 分析2当一个由pDerived所指的对象销毁的时候,会调用类derived的析构函数,进而调用基类base中的析构函数。纯虚拟函数的空的实现保证了该函数至少存在着一些操作。 注意:在前面例子中,纯虚拟函数base::~base是在derived::~derived中隐含调用的。当然明确地用全限定成员函数名称去调用纯虚拟函数是可能的。 -------------------------------------------------------------------------------- 概念描述这一节补充一些有关类的新的概念: * 二义性 * 全局名称 * 名称和限定名 * 函数的参量名称 * 构造函数初始化器 二义性名称的使用在其范围中必须是无二义性的(直到名称的重载点)。如果这个名称表示了一个函数,那么这个函数必须是关于参量的个数和类型是无二义性的。如果名称存在着二义性,则要运用成员访问规则。 全局名称一个对象、函数或枚举的名称如果在任何函数、类之外引入或前缀有全局单目范围操作符(::),并同时没有同任何下述的双目操作符连用。 * 范围分辨符(::) * 对象和引用的成员选择符(.) * 指针的成员选择符(->) 名称及限定名同双目的范围分辨符(::)一起使用的名称叫“限定名”。在双目范围分辨符之后说明的名称必须是在该说明符左边所说明的类的成员或其基类的成员。 在成员选择符(.或->)后说明的名称必须是在该说明符左边所说明的类类型对象的成员或其基类的成员。在成员选择符的右边所说明的名称可以是任何类类型对象,只要该说明符的左边是一个类类型对象,而且该对象的类定义了一个重载的成员选择符(->),它把指针所指的对象变为特殊的类类型。 编译器按下面的顺序搜索一个名称,发现以后便停止: 1. 如果名称是在函数中使用,则在当前块范围中搜索,否则在全局范围中搜 索。 2. 向外到每一个封闭块范围中搜索,包括最外面函数范围(这将包括函数的参量)。 3. 如果名称在一个成员函数中使用,则在该类的范围中搜索该名称。 4. 在该类的基类中搜索该名称。 5. 在外围嵌套类范围(如果有)或其基类中搜索,这一搜索一直到最外层包裹的类的范围搜索之后。 6. 在全局范围中搜索。 然而你可以按如下方式改变搜索顺序: 7. 如果名称的前面有::,则强制搜索在全局范围之中。 8. 如果名称的前面有class、struct和union关键字,将强制编译器仅搜索 class,struct或union名称。 9. 在范围分辨符的左边的名称,只能是class,struct和union的名称。如果在一个静态成员函数中引用了一个非静态的成员名,将会产生一条错误消息。同样地,任何引用包围类中的非静态组员会产生一条错误消息,因为被包围的类没有包围类的this指针。 函数参量名称函数的参量名称在函数的定义中视为在函数的最外层块的范围中。因此,它们是局部名称并且在函数结束之后,范围就消失了。 函数的参量名称是在函数说明(原型)的局部范围中,并且在说明结束以后的范围中消失。 缺省的参量名称是在参量(它们是缺省的)范围中,如前面两段描述的,然而它们不能访问局部变量和非静态类成员。缺省参量值的确定是在函数调用的时候,但它们的给定是在函数说明的原始范围中。因此成员函数的缺省参量总是在类范围中的。 |
随便看 |
百科全书收录4421916条中文百科知识,基本涵盖了大多数领域的百科知识,是一部内容开放、自由的电子版百科全书。