虛基類、純虛函數和抽象類
1、虛基類
在《多繼承》中講過的例子中,由類A,類B1和類B2以及類C組成了類繼承的層次結構。在該結構中,類C的對象將包含兩個類A的子對象。由於類A是派生類C兩條繼承路徑上的一個公共基類,那麼這個公共基類將在派生類的對象中產生多個基類子對象。如果要想使這個公共基類在派生類中只產生一個基類子對象,則必須將這個基類設定為虛基類。
虛基類的引入和說明
前面簡單地介紹了要引進虛基類的原因。實際上,引進虛基類的真正目的是為了解決二義性問題。
虛基類說明格式如下:
virtual <繼承方式><基類名>
其中,virtual是虛類的關鍵字。虛基類的說明是用在定義派生類時,寫在派生類名的後面。例如:
class A
{
public:
void f();
protected:
int a;
};
class B : virtual public A
{
protected:
int b;
};
class C : virtual public A
{
protected:
int c:
};
class D : public B, public C
{
public:
int g();
private:
int d;
};
由於使用了虛基類,使得類A,類B,類C和類D之間關系用DAG圖示法表示如下:
A{ f(), a }
/ /
B{b} C{c}
/ /
D{g(),d}
從該圖中可見不同繼承路徑的虛基類子對象被合並成為一個對象。這便是虛基類的作用,這樣將消除了合並之前可能出現的二義性。這時,在類D的對象中只存在一個類A的對象。因此,下面的引用都是正確的:
D n;
n.f(); //對f()引用是正確的。
void D::g()
{
f(); //對f()引用是正確的。
}
下面程序段是正確的。
D n;
A *pa;
pa = &n;
其中,pa是指向類A對象的指針,n是類D的一個對象,&n是n對象的地址。pa=&n是讓pa指針指向類D的對象,這是正確的,並且也無二義性。
虛基類的構造函數
前面講過,為了初始化基類的子對象,派生類的構造函數要調用基類的構造函數。對於虛基類來講,由於派生類的對象中只有一個虛基類子對象。為保證虛基類子對象只被初始化一次,這個虛基類構造函數必須只被調用一次。由於繼承結構的層次可能很深,規定將在建立對象時所指定的類稱為最派生類。C++規定,虛基類子對象是由最派生類的構造函數通過調用虛基類的構造函數進行初始化的。如果一個派生類有一個直接或間接的虛基類,那麼派生類的構造函數的成員初始列表中必須列出對虛基類構造函數的調用。如果未被列出,則表示使用該虛基類的缺省構造函數來初始化派生類對象中的虛基類子對象。
從虛基類直接或間接繼承的派生類中的構造函數的成員初始化列表中都要列出這個虛基類構造函數 的調用。但是,只有用於建立對象的那個最派生類的構造函數調用虛基類的構造函數,而該派生類的基類中所列出的對這個虛基類的構造函數調用在執行中被忽略,這樣便保證了對虛基類的對象只初始化一次。
C++又規定,在一個成員初始化列表中出現對虛基類和非虛基類構造函數的調用,則虛基類的構造函數先於非虛基類的構造函數的執行。
下面舉一例子說明具有虛基類的派生類的構造函數的用法。
#include <iostream.h>
class A
{
public:
A(const char *s) { cout<<s<<endl; }
~A() {}
};
class B : virtual public A
{
public:
B(const char *s1, const char *s2):A(s1)
{
cout<<s2<<endl;
}
};
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
在派生類B和C中使用了虛基類,使得建立的D類對象只有一個虛基類子對象。
在派生類B,C,D的構造函數的成員初始化列表中都包含了對虛基類A的構造函數。
在建立類D對象時,只有類D的構造函數的成員初始化列表中列出的虛基類構造函數被調用,並且僅調用一次,而類D基類的構造函數的成員初始化列表中列出的虛基類構造函數不被執行。這一點將從該程序的輸出結果可以看出。
2、純虛函數和抽象類
純虛函數是一種特殊的虛函數,它的一般格式如下:
class <類名>
{
virtual <類型><函數名>(<參數表>)=0;
…
};
在許多情況下,在基類中不能對虛函數給出有意義有實現,而把它說明為純虛函數,它的實現留給該基類的派生類去做。這就是純虛函數的作用。下面給出一個純虛函數的例子。
#include <iostream>
class point
{
public:
point(int i=0, int j=0) { x0=i; y0=j; }
virtual void set() = 0;
virtual void draw() = 0;
protected:
int x0, y0;
};
class line : public point
{
public:
line(int i=0, int j=0, int m=0, int n=0):point(i, j)
{
x1=m; y1=n;
}
void set() { std::cout<<"line::set() called.\n"; }
void draw() { std::cout<<"line::draw() called.\n"; }
protected:
int x1, y1;
};
class ellipse : public point
{
public:
ellipse(int i=0, int j=0, int p=0, int q=0):point(i, j)
{
x2=p; y2=q;
}
void set() { std::cout<<"ellipse::set() called.\n"; }
void draw() { std::cout<<"ellipse::draw() called.\n"; }
protected:
int x2, y2;
};
void drawobj(point *p)
{
p->draw();
}
void setobj(point *p)
{
p->set();
}
#include <iostream>
#include "test.h"
using namespace std;
int main()
{
line *lineobj = new line;
ellipse *elliobj = new ellipse;
drawobj(lineobj);
drawobj(elliobj);
cout<<endl;
setobj(lineobj);
setobj(elliobj);
cout<<"\nRedraw the object...\n";
drawobj(lineobj);
drawobj(elliobj);
return 0;
}
抽象類
帶有純虛函數的類稱為抽象類。抽象類是一種特殊的類,它是為了抽象和設計的目的而建立的,它處於繼承層次結構的較上層。抽象類是不能定義對象的,在實際中為了強調一個類是抽象類,可將該類的構造函數說明為保護的訪問控制權限。
抽象類的主要作用是將有關的組織在一個繼承層次結構中,由它來為它們提供一個公共的根,相關的子類是從這個根派生出來的。
抽象類刻畫了一組子類的操作接口的通用語義,這些語義也傳給子類。一般而言,抽象類只描述這組子類共同的操作接口,而完整的實現留給子類。
抽象類只能作為基類來使用,其純虛函數的實現由派生類給出。如果派生類沒有重新定義純虛函數,而派生類只是繼承基類的純虛函數,則這個派生類仍然還是一個抽象類。如果派生類中給出了基類純虛函數的實現,則該派生類就不再是抽象類了,它是一個可以建立對象的具體類了。