很多人在发帖或文章时根本就没有做过必要的验证;事实上从外文翻译过来的经典教材也不是完全没有失误,当然我更相信是翻译造成的问题,但对于理解的人来说错误的例子会造成极恶劣的恶果,就是距离真正的概念失之毫厘,谬以千里 |
C++一个多继承的例子中,由类A,类B1和类B2以及类C组成了类继承的层次结构。在该结构中,类C的对象将包含两个类A的子对象。由于类A是派生类C两条继承路径上的一个公共基类,那么这个公共基类将在派生类的对象中产生多个基类子对象。如下图当构造一个Class C 实例时会构造出2个Class A实例;(java因为不允许从多个类派生;所以避免了这个问题;Class C extend B1,B2是错误的)
A{ f(), a } A{ f(), a }
/ /
B1{b} B2{c}
/ /
C{f2(),d}
/ /
B1{b} B2{c}
/ /
C{f2(),d}
如果要想使这个公共基类在派生类中只产生一个基类子对象,则必须将这个基类设定为虚基类。
虚基类的引入和说明
前面简单地介绍了要引进虚基类的原因。实际上,引进虚基类的真正目的是为了解决二义性问题。
虚基类说明格式如下:
virtual <继承方式><基类名>
其中,virtual是虚类的关键字。虚基类的说明是用在定义派生类时,写在派生类名的后面。例如:
解释一下什么是纯虚类(抽象):至少有一个成员函数为<纯虚函数>,即没有任何实现但定义明确的虚函数很象java里的接口(Interface);注意一定是纯虚函数
解释一下什么是纯虚类(抽象):至少有一个成员函数为<纯虚函数>,即没有任何实现但定义明确的虚函数很象java里的接口(Interface);注意一定是纯虚函数
//file classtest.h complied pass by vc60 #ifndef CLASSTEST_H #define CLASSTEST_H
#include <stdio.h>
#include <iostream.h> #include <string.h>
class A {
public: virtual int f1(int sv1)=0; virtual int f2(int sv2)=0; virtual int f3(int sv3)=0;
};
class B:virtual public A
{ public: virtual int f1(int sv1){cout<<"B::f1()"<<endl;return sv1;}; virtual int f3(int sv3){cout<<"B::f3()"<<endl;return sv3+3;}; public: B(){cout<<"B::Constructed"<<endl;}; virtual ~B(){cout<<"B::Destoryed"<<endl;}; };
class C :virtual public A
{ public: virtual int f2(int sv2){cout<<"C::f1()"<<endl;return sv2+2;}; virtual int f3(int sv3){cout<<"C::f3()"<<endl;return sv3+6;}; public: C(){cout<<"C::Constructed"<<endl;}; virtual ~C(){cout<<"C::Destoryed"<<endl;}; };
class D :public B,public C
{ public: D(){} virtual ~D(){} int f1(int sv1){int in=sv1*5; return in;}; int f2(int sv2){return f1(sv2);}; int f3(int sv3){return sv3*9;}; }; #endif |
由于使用了虚基类,使得类A,类B,类C和类D之间关系用DAG图示法表示如下:
A{ f1(), a }
/ /
B{b} C{c}
/ /
D{f2(),d}
/ /
B{b} C{c}
/ /
D{f2(),d}
从该图中可见不同继承路径的虚基类子对象被合并成为一个对象。这便是虚基类的作用,这样将消除了合并之前可能出现的二义性。这时,在类D的对象中只存在一个类A的对象。因此,下面的引用都是正确的:
D n;
n.f1(); //对f()引用是正确的。
int D::f2()
{
return f1(sv2); //对f1()引用是正确的。
}
n.f1(); //对f()引用是正确的。
int D::f2()
{
return f1(sv2); //对f1()引用是正确的。
}
//file classtest.cpp #include "classtest.h" int main(int argc, char* argv[]) { //下面程序段是正确的。 D n; A *pa; pa = &n; cout<<"D obj test:"<<n.f1(100)<<endl; cout<<"A obj test:"<<pa->f1(100)<<endl; return 0; } |
-------------------------------------
Output:
B::Constructed
C::Constructed
D obj test:500
A obj test:500
C::Destoryed
B::Destoryed
其中,pa是指向类A对象的指针,n是类D的一个对象,&n是n对象的地址。pa=&n是让pa指针指向类D的对象,这是正确的,并且也无二义性。
Pa是基类的指针但最后指向一个真正的对象却是子类的对象的实例;与JAVA中的动态绑定作用 dynamic binding/runtime binding是相同的;但C++继承了C语言的规范;更多的是使用所谓"前绑定的方式-early banding";简单的说就是编译器明确知道实例指向的对象类型;类型是严格定义的;JAVA中除了static和 final方法<private方法隐含有final的意思>以外通常都是后绑定
相对于C++的虚基类;JAVA的概念叫做"抽象类"abstract class 概念不再解释;多态性与动态绑定举个例子
import java.util.*;
abstract class A {
int i; // storage allocated for each public abstract void play(); public String what() { return "A"; } public abstract void adjust(); }
class B1 extends A {
public void play() { System.out.println("B1.play()"); } public String what() { return "B1"; } public void adjust() {} }
class B2 extends A {
public void play() { System.out.println("B2.play()"); } public String what() { return "B2"; } public void adjust() {} }
class C extends B1 {
public void play() { System.out.println("C.play()"); } public void adjust() { System.out.println("C.adjust()"); } }
public class D {
// Doesn't care about type, so new types // added to the system still work right: static void tune(A i) { i.play(); } static void tuneAll(A[] e) { for(int i = 0; i < e.length; i++) tune(e[i]); } public static void main(String[] args) { A[] orchestra = new A[4]; int i = 0; orchestra[i++] = new B1(); orchestra[i++] = new B2(); orchestra[i++] = new C(); tuneAll(orchestra); } } |
与C++的例子相同;Class A是个抽象基类(父类);定义了一个基类类型后;对象却可以绑定为任何子类的实例;抽象方法的作用与C++的虚函数作用基本相同;目的都是为了使子类的方法能够取代这个非实体的函数;重点是覆盖override,所以参数表应该相同;如果有相同的标记和不同的参数表就成为重载overload的概念了,重载的概念应该是发生在同一层次(同一个)的函数 如上例中的代码片断可改写为
class C extends B1 {
public void play() { System.out.println("C.play()");//overriden } public void play(String sv) { System.out.println("C.play()"+sv); //overload } public void adjust() { System.out.println("C.adjust()"); } } |
在Java和C++中抽象方法/虚函数的隐含方法都是public
虚基类的构造函数
前面讲过,为了初始化基类的子对象,派生类的构造函数要调用基类的构造函数。对于虚基类来讲,由于派生类的对象中只有一个虚基类子对象。为保证虚基类子对象只被初始化一次,这个虚基类构造函数必须只被调用一次。由于继承结构的层次可能很深,规定将在建立对象时所指定的类称为最派生类。C++规定,虚基类子对象是由最派生类的构造函数通过调用虚基类的构造函数进行初始化的。如果一个派生类有一个直接或间接的虚基类,那么派生类的构造函数的成员初始列表中必须列出对虚基类构造函数的调用。如果未被列出,则表示使用该虚基类的缺省构造函数来初始化派生类对象中的虚基类子对象。
从虚基类直接或间接继承的派生类中的构造函数的成员初始化列表中都要列出这个虚基类构造函数 的调用。但是,只有用于建立对象的那个最派生类的构造函数调用虚基类的构造函数,而该派生类的基类中所列出的对这个虚基类的构造函数调用在执行中被忽略,这样便保证了对虚基类的对象只初始化一次。
C++又规定,在一个成员初始化列表中出现对虚基类和非虚基类构造函数的调用,则虚基类的构造函数先于非虚基类的构造函数的执行。
下面举一例子说明具有虚基类的派生类的构造函数的用法。
#include <iostream.h> class B : virtual public A
class C : virtual public A
{ public: C(const char *s1, const char *s2):A(s1) { cout<<s2<<endl; } };
class D : public B, public C
{ public: D(const char *s1, const char *s2, const char *s3, const char *s4) :B(s1, s2), C(s1, s3), A(s1) { cout<<s4<<endl; } };
void main()
{ D *ptr = new D("class A", "class B", "class C", "class D"); delete ptr; } |
该程序的输出结果为:
class A
class B
class C
class D
class B
class C
class D
在派生类B和C中使用了虚基类,使得建立的D类对象只有一个虚基类子对象。
在派生类B,C,D的构造函数的成员初始化列表中都包含了对虚基类A的构造函数。
在建立类D对象时,只有类D的构造函数的成员初始化列表中列出的虚基类构造函数被调用,并且仅调用一次,而类D基类的构造函数的成员初始化列表中列出的虚基类构造函数不被执行。这一点将从该程序的输出结果可以看出。