一.派生類的概念
1.爲什麼要使用繼承
繼承性也是程序設計中的一個非常有用的、有力的特性, 它可以讓程序員在既有類的基礎上,通過增加少量代碼或修改少量代碼的方法得到新的類, 從而較好地解決了代碼重用的問題。
2.派生類的說明
在類名 employee 的冒號之後, 跟着關鍵字 public 與類名 person,這就意味着類 employee 將繼承類 person 的全部特性。
關鍵字 public 指出派生的方式,告訴編譯程序派生類employee從基類 person 公有派生。
// 定義一個基類( person 類)
class person
{
private :
char name [10] ;
int age;
char sex;
public:
// …
} ;
// 定義一個派生類
class employee∶public person
{
char department[20] ;
float salary;
public:
// …
} ;
聲明一個派生類的一般格式爲:
class 派生類名∶派生方式 基類名
{
// 派生類新增的數據成員和成員函數
} ;
派生方式”可以是關鍵字 private 或 public。如果使用了 private, 則稱派生類從基類私有派生; 如果使用了 public,則稱派生類從基類公有派生。派生方式可以缺省, 這時派生方式默認爲private ,即私有派生。
1. 公有派生
class employee∶public person
{
// …
};
2. 私有派生
class employee∶private person
{
// …
} ;
這兩種派生方式的特點如下:
- 無論哪種派生方式, 基類中的私有成員既不允許外部函數訪問, 也不允許派生類中的成員函數訪問,但是可以通過基類提供的公有成員函數訪問。
- 公有派生與私有派生的不同點在於基類中的公有成員在派生類中的訪問屬性。
公有派生時, 基類中的所有公有成員在派生類中也都是公有的。
私有派生時, 基類中的所有公有成員只能成爲派生類中的私有成員。
下面我們分別討論私有派生和公有派生的一些特性。
- 私有派生
(1 ) 私有派生類對基類成員的訪問
由私有派生得到的派生類, 對它的基類的公有成員只能是私有繼承。也就是說基類的所有公有成員都只能成爲私有派生類的私有成員, 這些私有成員能夠被派生類的成員函數訪問,但是基類私有成員不能被派生類成員函數訪問。
# include <iostream>
using namespace std;
class base // 聲明一個基類
{
int x;
public:
void setx(int n)
{
x = n;
}
void showx ()
{
cout << x << endl;
}
} ;
class derived: private base // 聲明一個私有派生類
{
int y;
public:
void setxy(int n, int m)
{
setx( n) ;
y = m;
}
void showxy()
{
cout<< x << y << endl; // 非法
}
};
int main()
{
derived obj;
obj.setxy( 10,20) ;
obj.showxy() ;
return 0 ;
}
例中首先定義了一個類 base , 它有一個私有數據 x 和兩個公有成員函數 setx ( ) 和showx( ) 。將類 base 作爲基類,派生出一個類 derived。派生類 derived 除繼承了基類的成員外,還有隻屬於自己的成員: 私有數據成員 y、公有函數成員 setxy( )和 showxy( )。派生方式關鍵字是 private, 所以這是一個私有派生。
類 derived 私有繼承了 base 的所有成員, 但它的成員函數並不能直接使用 base 的私有數據 x, 只能使用兩個公有成員函數。所以在 derived 的成員函數 setxy ( ) 中引用 bas的公有成員 setx( )是合法的, 但在成員函數 showxy( ) 中直接引用 base 的私有成員 x 是法的。
如果將例中函數 showxy( )改成如下形式:
void showxy( ) {showx( ) ; cout < < y < < endl; }
重新編譯,程序將順利通過。可見基類中的私有成員既不能被外部函數訪問, 也不能被派生類成員函數訪問,只能被基類自己的成員函數訪問。因此, 我們在設計基類時, 總要爲它的私有數據成員提供公有成員函數,以使派生類或外部函數可以間接使用這些數據成員。
(2 ) 外部函數對私有派生類繼承來的成員的訪問
私有派生時,基類的公有成員在派生類中都成爲私有成員, 外部函數不能訪問。下面的例子將驗證外部函數對私有派生類繼承來的成員的訪問性。
#include <iostream>
using namespace std;
class base
{
// 聲明一個基類
int x;
public:
void setx(int n)
{
x = n;
}
void showx ()
{
cout << x << endl;
}
};
class derived: private base
{
// 聲明一個私有派生類
int y;
public:
void sety(int n)
{
y = n;
}
void showy()
{
cout << y << endl;
}
};
int main()
{
derived obj;
obj .setx(10) ; // 非法
obj .sety(20) ; // 合法
obj .showx() ; // 非法
obj .showy() ; // 合法
return 0 ;
}
由於是私有派生, 所以基類 base 的公有成員 setx ( ) 和 showx ( ) 被 derived 私有繼 承後, 成爲 derived 的私 有成員, 只能 被derived的成員函數訪問, 不能被外界函數訪問。在 main ( )函數中, 定義了派生類 derived的對象 obj, 由於 sety ( ) 和 showy ( ) 在類 derived 中是 公有函 數, 所 以對 obj .sety ( ) 和obj .showy( )的調用是沒有問題的, 但是對 obj .setx ( )和obj .showx( ) 的調用是非法的, 因爲這兩個函數在類 derived 中已成爲私有成員。
需要注意的是:無論 setx( ) 和 showx( )如何被一些派生類繼承, 它們仍然是 base 的公有成員,因此以下的調用是合法的:
base base-obj;
base-obj.setx (2);
- 公有派生
在公有派生中,基類中的私有成員不允許外部函數和派生類中的成員函數直接訪問,但是可以通過基類提供的公有成員函數訪問。基類中的公有成員在派生類中仍是公有成員,外部函數和派生類中的成員函數可直接訪問。
#include <iostream>
using namespace std;
class base
{
// 聲明一個基類
int x;
public:
void setx(int n)
{
x = n;
}
void showx()
{
cout << x << endl;
}
};
class derived: public base
{
// 聲明一個公有派生類
int y;
public:
void sety(int n)
{
y = n;
}
void showy()
{
cout << y << endl;
}
} ;
int main ()
{
derived obj;
obj.setx(10); // 合法
obj.sety(20); // 合法
obj.showx(); // 合法
obj.showy(); // 合法
return 0 ;
}
例中類 derived 從類 base 中公有派生, 所以類 base 中的兩個公有成員函數 setx ( ) 和showx( )在派生類中仍是公有成員。因此, 它們可以被程序的其它部分訪問。特別是它們可以合法地在 main( )中被調用。
說明:
- 派生類以公有派生的方式繼承了基類, 並不意味着派生類可以訪問基類的私有成員。
class base
{
int x;
public:
void setx (int n )
{
x = n;
}
void showx()
{
cout << x << endl;
}
} ;
class derived∶public base
{
int y;
public:
void sety(int n)
{
y = n;
}
void show-sum()
{
cout << x + y << endl; // 非法
}
void showy()
{
cout << y << endl;
}
} ;
派生類 derived 企圖訪問基類 base 的私有成員 x, 但是這種企圖是非法的,因爲基類無論怎樣被繼承, 它的私有成員都針對該基類保持私有性。
- 在派生類中聲明的名字支配基類中聲明的同名的名字, 即如果在派生類的成員函數中直接使用該名字的話,則表示使用派生類中聲明的名字
class X
{
public:
int f() ;
};
class Y∶public X
{
public:
int f();
int g() ;
};
void Y∷g()
{
f() ; // 表示被調用的函數是 Y∷f( ), 而不是 X∷f( )
}
對於派生類的對象的引用,也有相同的結論, 例如:
Y obj;
obj .f( ) ; / / 被調用的函數是 Y∷f( )
如果要使用基類中聲明的名字,則應使用作用域運算符限定, 例如:
obj .X∷f( ) ; / / 被調用的函數是 X∷f( )
保護成員的作用
protected 說明符可以放在類聲明的任何地方,通常將它放在私有成員聲明之後, 公有成員聲明之前。類聲明的一般格式如下所示:
class 類名
{
[private:]
私有數據成員和成員函數
protected:
保護數據成員和成員函數
public:
公有數據成員和成員函數
};
保護成員可以被派生類的成員函數訪問,但是對於外界是隱藏起來的, 外部函數不能訪問它。因此,爲了便於派生類的訪問, 可以將基類私有成員中需要提供給派生類訪問的成員定義爲保護成員。
下面的程序說明類的私有成員、公有成員與保護成員是如何被訪問的。
# include < iostream .h >
class samp
{
int a;
protected:
int b; // 定義變量 b 爲保護成員
public:
int c;
samp(int n, int m)
{
a = n;
b = m;
}
int geta()
{
return a ;
}
int getb()
{
return b;
}
};
int main()
{
samp obj(20, 30);
obj.b = 99; // 非法,類的保護成員不能被外部函數訪問
obj .c = 50; // 合法,類的公有成員能被外部函數訪問
cout << obj .geta() <""; // 合法
cout << obj .getb() <<""<< obj .c << endl; // 合法
return 0 ;
}
對象 obj 的數據成員b 不能被訪問,這是因爲 b 是保護成員, 而類的保護成員是不允許外部函數訪問的。
C + + 規定, 派生類對於保護成員的繼承與公有成員的繼承很相似, 也分爲兩種情況: 若爲公有派生, 則基類中的保護成員在派生類中也爲保護成員;若爲私有派生, 則基類中的保護成員在派生類中成爲私有成員。
下面的程序說明保護成員以公有方式被繼承後的訪問特性。
#include <iostream>
using namespace std;
class base
{
protected:
int a, b;
public:
void setab(int n, int m)
{
a = n;
b = m;
}
};
class derive : public base
{
int c;
public:
void setc(int n)
{
c = n;
}
void showabc()
{
cout << a <<" "<< b <<" "<< c << endl;
} // 允許派生類成員函數
// 訪問保護成員 a 和 b
} ;
int main ()
{
derive obj;
obj .setab(2, 4);
obj .setc(3) ;
obj .showabc() ;
return 0 ;
}
由於 a 和 b 是基類 base 的保護成員, 而且被派生類以公有方式繼承,所以它們可以被派生類的成員函數訪問。但是基類的保護成員 a 與 b 不能被外部函數訪問。
下面程序說明保護成員以私有方式被繼承後的訪問特性。
#include <iostream>
using namespace std;
class base
{
protected:
int a;
public:
void seta (int sa)
{
a = sa;
}
} ;
class derive1: private base
{
protected:
int b;
public:
void setb(int sb)
{
b = sb;
}
} ;
class derive2: public derive1
{
int c;
public:
void setc(int sc)
{
c = sc ;
}
void show( )
{
cout << "a = "<< a << endl; // 非法
cout << "b = "<< b << endl; // 合法
cout << "c = "<< c << endl; // 合法
}
} ;
int main()
{
base op1;
op1.seta(1);
derive1 op2;
op2.setb(2);
derive2 op3;
op3.setc(3);
op3.show();
}
基類 base 中的保護成員 a 被其派生類 derive1 私有繼承後成爲私有成員, 所以不能被 derive1 的派生類 derive2中的成員函數訪問。derive1 類中的保護成員 b,被其派生類 derive2 公有繼承後仍是保護成員,所以可以被 derive2 中的成員函數 show( )訪問。
派生方式 | 基類中的訪問權限 | 派生類中的訪問權限 |
---|---|---|
public(公有派生) | public、protect、private | public、protect、private |
protect(私有派生) | public、protect、private | private、private、private |
在公有派生情況下, 基類中所有成員的訪問特性在派生類中維持不變;在私有派生情況下, 基類中所有成員在派生類中成爲私有成員。
二.派生類的構造函數和析構函數
1.派生類構造函數和析構函數的執行順序
通常情況下,當創建派生類對象時, 首先執行基類的構造函數, 隨後再執行派生類的構造函數; 當撤消派生類對象時, 則先執行派生類的析構函數, 隨後再執行基類的析構函數。
下列程序的運行結果,反映了基類和派生類的構造函數及析構函數的執行順序。
#include<iostream>
using namespace std;
class base
{
public:
base()
{
cout<< "Constructing base class \n";
} // 基類的構造函數
~base()
{
cout <<"Destructing baes class \n";
} // 基類的析構函數
} ;
class derive : public base
{
public:
derive() // 派生類的構造函數
{
cout << "Constructing derived class \n";
}
~derive() // 派生類的析構函數
{
cout<< "Destructing derived class \n";
}
};
int main()
{
derive op;
return 0 ;
}
程序運行結果如下:
Constructing base class
Constructing derived class
Destructing derived class
Destructing base class
構造函數的調用嚴格地按照先調用基類的構造函數, 後調用派生類的構造函數的順序執行。析構函數的調用順序與構造函數的調用順序正好相反,先調用派生類的析構函數, 後調用基類的析構函數。
2.派生類構造函數和析構函數的構造規則
當基類的構造函數沒有參數,或沒有顯式定義構造函數時, 派生類可以不向基類傳遞參數,甚至可以不定義構造函數。
派生類不能繼承基類中的構造函數和析構函數。當基類含有帶參數的構造函數時,派生類必須定義構造函數,以提供把參數傳遞給基類構造函數的途徑。
在 C + + 中,派生類構造函數的一般格式爲:
派生類構造函數名(參數表) :基類構造函數名( 參數表)
{
/ / …
}
其中基類構造函數的參數,通常來源於派生類構造函數的參數表, 也可以用常數值。
下面的程序說明如何傳遞一個參數給派生類的構造函數和傳遞一個參數給基類的構造函數。
#include<iostream>
using namespace std;
class base
{
int i;
public:
base(int n) // 基類的構造函數
{
cout << "Constructing base class \n";
i = n;
}
~base () // 基類的析構函數
{
cout << "Destructing base class \n";
}
void showi()
{
cout << i << endl;
}
};
class derive : public base
{
int j;
public:
derive(int n, int m) : base (m) // 定義派生類構造函數時,
{
// 綴上基類的構造函數
cout << "Constructing derived class"<< endl;
j = n;
}
~derive() // 派生類的析構函數
{
cout << "Destructing derived class"<< endl;
}
void showj()
{
cout << j << endl;
}
};
int main()
{
derive obj(30, 40) ;
obj.showi();
obj.showj();
return 0 ;
}
程序運行結果爲:
Constructing base class
Constructing derived class
40
30
Destructing derived class
Destructing base class
當派生類中含有對象成員時,其構造函數的一般形式爲:
派生類構造函數名(參數表) :基類構造函數名( 參數表),對象成員名 1 (參數表), …,對象成員名 n (參數表)
{
// …
}
在定義派生類對象時,構造函數的執行順序如下:
- 基類的構造函數
- 對象成員的構造函數
- 派生類的構造函數
撤消對象時,析構函數的調用順序與構造函數的調用順序正好相反。
下面這個程序說明派生類構造函數和析構函數的執行順序。
#include<iostream>
using namespace std;
class base
{
int x;
public:
base(int i) // 基類的構造函數
{
x = i;
cout << "Constructing base class \n";
}
~base() // 基類的析構函數
{
cout << "Destructing base class \n";
}
void show()
{
cout << "x = "<< x << endl;
}
} ;
class derived: public base
{
base d;
// d 爲基類對象,作爲派生類的對象成員
public:
derived(int i) : base (i), d(i) // 派生類的構造函數, 綴上基類構造函數和
{
// 對象成員構造函數
cout
<< "Constructing derived class \n";
}
~derived() // 派生類的析構函數
{ cout << "Destructing derived class \n"; }
} ;
int main ()
{
derived obj(5 );
obj.show();
return 0 ;
}
程序執行結果如下:
Constructing base class
Constructing base class
Constructing derived class
x = 5
Destructing derived class
Destructing base class
Destructing base class
說明:
- 當基類構造函數不帶參數時, 派生類不一定需要定義構造函數, 然而當基類的構造函數哪怕只帶有一個參數,它所有的派生類都必須定義構造函數, 甚至所定義的派生類構造函數的函數體可能爲空,僅僅起參數的傳遞作用。
- 若基類使用缺省構造函數或不帶參數的構造函數, 則在派生類中定義構造函數時可略去“∶基類構造函數名(參數表)”; 此時若派生類也不需要構造函數, 則可不定義構造函數。
- 如果派生類的基類也是一個派生類, 則每個派生類只需負責其直接基類的構造,依次上溯。
- 由於析構函數是不帶參數的, 在派生類中是否要定義析構函數與它所屬的基類無關,故基類的析構函數不會因爲派生類沒有析構函數而得不到執行, 它們各 自是獨立的。
三.多重繼承
前面我們介紹的派生類只有一個基類, 這種派生方法稱爲單基派生或單一繼承。當一個派生類具有多個基類時,這種派生方法稱爲多基派生或多重繼承。
1.多重繼承的聲明
一般形式如下:
class 派生類名: 派生方式 1 基類名 1, …,派生方式 n 基類名 n
{
// 派生類新增的數據成員和成員函數
} ;
冒號後面的部分稱基類表,各基類之間用逗號分隔, 其中“派生方式 i”( i = 1, 2, …, n )規定了派生類從基類中按什麼方式繼承: private 或 public,缺省的派生方式是 private
class z: public x, y
{
// 類 z 公有繼承了類 x,私有繼承了類 y
// …
};
class z: x, public y
{
// 類 z 私有繼承了類 x,公有繼承了類 y
// …
};
class z: public x, public y
{
// 類 z 公有繼承了類 x 和類 y
// …
};
在多重繼承中,公有派生和私有派生對於基類成員在派生類中的可訪問性與單繼承的規則相同。
#include <iostream>
using namespace std;
class X
{
int a ;
public:
void setX(int x)
{
a = x;
}
void showX()
{
cout << "a = "<< a << endl;
}
};
class Y
{
int b;
public:
void setY(int x)
{
b = x;
}
void showY()
{
cout << "b = "<< b << endl;
}
};
class Z: public X, private Y
{
int c;
public:
void setZ(int x, int y)
{
c = x;
setY(y) ;
}
void showZ()
{
showY();
cout << "c = "<< c << endl;
}
};
int main()
{
Z obj;
obj .setX(3);
obj .showX();
//obj .setY(4); // 錯誤
// obj .showY(); // 錯誤
obj .setZ(6, 8 );
obj .showZ();
}
根據派生的有關規則,類 X 的公有成員在 Z 中仍是公有成員, 類 Y 的公有成員在 Z 中成爲私有成員。所以, 在主函數中對類 X 的公有成員函數的引用是正確的, 因爲在 Z 中它們仍是公有成員; 對 Y的成員函數的引用是錯誤的,因爲 Y 的成員函數在 Z 中已成爲私有成員,不能直接引用。
刪去標有錯誤的兩條語句,程序運行結果如下:
a = 3
b = 8
c = 6
說明:對基類成員的訪問必須是無二義的, 例如下列程序段對基類成員的訪問是二義的,必須想法消除二義性。
class X
{
public:
int f();
};
class Y
{
public:
int f();
int g();
};
class Z∶public X, public Y
{
public:
int g();
int h();
};
假如定義類 Z 的對象 obj: Z obj;
則以下對函數 f( )的訪問是二義的:
obj .f( ) ;
- / / 二義性錯誤,不知調用的是類 X 的 f( ) ,還是類 Y 的 f( )
使用成員名限定可以消除二義性,例如:
obj .X∷f( ) ;
- / / 調用類 X 的 f( )
obj .Y∷f( ) ;
- / / 調用類 Y 的 f( )
2.多重繼承的構造函數與析構函數
多重繼承構造函數的定義形式與單繼承構造函數的定義形式相似, 只是 n 個基類的構造函數之間用“,”分隔。多重繼承構造函數定義的一般形式如下:
派生類構造函數名(參數表) :基類 1 構造函數名 ( 參數表), 基類 2 構造函數名 (參數表), …,基類 n 構造函數名(參數表)
{
// …
}
例如,由一個硬件類 Hard 和一個軟件類 Soft ,它們共同派生出系統類 System, 聲明如下:
class Hard
{
protected:
char bodyname[20];
public:
Hard(char * bdnm ); // 基類 Hard 的構造函數
// …
};
class Soft
{
protected:
char os[10];
char Lang[15];
public:
Soft( char * o, char * lg);// 基類 Soft 的構造函數
// …
} ;
class System: public Hard, public Soft
{
private:
char owner[10] ;
public:
System( char * ow, char * bn, char * o, char * lg) // 派生類 System 的構造函數
∶Hard( bn), Soft(o, lg);
// 綴上了基類 Hard 和 Soft 的構造函數
// …
};
注意:在定義派生類 System 的構造函數時,綴上了 Hard 和 Soft 的構造函數。
再如,現有一個窗口類 window 和一個滾動條類 scrollbar, 它們可以共同派生出一個帶有滾動條的窗口,聲明如下:
class window
{
// 定義窗口類 window
// …
public:
window(int top, int left, int bottom, int right);
~window();
// …
} ;
class scrollbar
{
// 定義滾動條類 scrollbar
// …
public:
scrollbar(int top, int left, int bottom, int right);
~scrollbar();
// …
};
class scrollbarwind∶window,scrollbar
{
/ / 定義派生類
/ / …
public:
scrollbarwind(int top, int left, int bottom, int right);
~scrollbarwind();
// …
};
scrollbarwind∷scrollbarwind(int top, int left, int bottom, int right)∶window( top, left,bot tom, right),scrollbar(top, right - 20, bottom, right)
{
// …
}
在這個例子中, 定義派生類 scrollbarwind 的構造函數時, 也綴上了基類 window 和scrollbar 的構造函數。
下面我們再看一個程序,其中類 X 和類 Y 是基類, 類 Z 是類 X 和類 Y 共同派生出來的,請注意類 Z 的構造函數的定義方法。
#include<iostream>
using namespace std;
class X
{
int a;
public:
X(int sa ) // 基類 X 的構造函數
{
a = sa;
}
int getX()
{
return a ;
}
};
class Y
{
int b;
public:
Y(int sb) // 基類 Y 的構造函數
{
b = sb;
}
int getY()
{
return b;
}
} ;
class Z: public X, private Y
{
// 類 Z 爲基類 X 和基類 Y 共同的派生類
int c;
public:
Z(int sa, int sb, int sc ) :X(sa), Y(sb) // 派生類 Z 的構造函數,綴上
{
c = sc ;
} // 基類 X 和 Y 的構造函數
int getZ()
{
return c;
}
int getY()
{
return Y::getY();
}
};
int main()
{
Z obj( 2, 4, 6) ;
int ma = obj.getX();
cout << "a = "<< ma << endl;
int mb = obj .getY();
cout << "b = "<< mb << endl;
int mc = obj .getZ();
cout << "c = "<< mc << endl;
return 0 ;
}
上述程序運行的結果如下:
a = 2
b = 4
c = 6
由於派生類 Z 是 X 公有派生出來的, 所以類 X 中的公有成員函數 getX( )在類 Z 中仍是公有的, 在 main ( ) 中可以直接引用, 把成員 a 的值賦給 main ( ) 中的變量 ma ,並顯示在屏幕上。類 Z 是從類 Y 私有派生出來的, 所以類 Y 中的公有成員函數 getY( ) 在類 Z 中成爲私有的,在 main( ) 中不能直接引用。爲了能取出 b 的值,在 Z 中另外定義了一個公有成員函數 Z∷getY( ) ,它通過調用 Y∷getY( ) 取出 b 的值。主函數 main( )中的語句:int mb = obj .getY( ) ;調用的是派生類 Z 的成員函數 getY( ) , 而不是基類 Y 的成員函數 getY( )。由於類 Z 中的成員函數 getZ( ) 是公有成員,所以在 main( ) 中可以直接調用取出 c 的值。
總結:
多重繼承的構造函數的執行順序與單繼承構造函數的執行順序相同, 也是遵循先執行基類的構造函數,再執行對象成員的構造函數, 最後執行派生類構造函數的原則。在多個基類之間, 則嚴格按照派生類聲明時從左到右的順序來排列先後。而析構函數的執行順序則剛好與構造函數的執行順序相反。
3.虛基類
爲什麼要引入虛基類
當引用派生類的成員時, 首先在派生類自身的作用域中尋找這個成員, 如果沒有找到,則到它的基類中尋找。如果一個派生類是從多個基類派生出來的, 而這些基類又有一個共同的基類,則在這個派生類中訪問這個共同的基類中的成員時, 可能會產生二義性。
#include <iostream>
using namespace std;
class base
{
protected:
int a;
public:
base()
{
a = 5;
}
};
class base1:public base
{
public:
base1()
{
cout << "base1 a = "<< a << endl;
}
};
class base2:public base
{
public:
base2()
{
cout << "base2 a = "<< a << endl;
}
};
class derived: public base1, public base2
{
public:
derived()
{
cout << "derived a = "<< a << endl;
}
};
int main()
{
derived obj;
return 0 ;
}
上述程序中,類 base 是一個基類, 從類 base 派生出類 base1 和類 base2, 這是兩個單一繼承;從類 base1 和類 base2 共同派生出類 derived, 這是一個多重繼承。
這是一個存在問題的程序, 問題出在派生類 derived 的構造函數的定義上, 它試圖輸出一個它有權訪問的變量 a, 表面上看來這是合理的,但實際上它對 a 的訪問存在二義性,即函數中的變量 a 的值可能是從 base1 的派生路徑上來的, 也有可能是從類 base2 的派生路徑上來的,這裏沒有明確的說明。
雖然 base1 和 base2 是從同一個基類 base 派生而來的, 但它們所對應的是基類 base的不同拷貝。類 derived 是 base1 和 base2 的派生類,因此類 base 是類 derived 的間接基類,它有兩個拷貝與類 derived 相對應,一個是 base1 派生路徑上的拷貝,另一個是 base2 派生路徑上的拷貝。當類 derived 要訪問這個間接基類 base 時, 必須指定要 訪問的是哪個路徑上 的base 拷貝。
爲了解決這種二義性, C + + 引入了虛基類的概念。
2.虛基類的概念
不難理解,如果在上例中類 base 只存在一個拷貝, 那麼對 a的引用就不會產生二義性。在 C + + 中,如果想使這個公共的基類只產生一個拷貝,則可以將這個基類說明爲虛基類。這就要求從類 base 派生新類時, 使用關鍵字 virtual 將類base 說明爲虛基類。
#include <iostream>
using namespace std;
class base
{
protected:
int a;
public:
base()
{
a = 5;
}
};
class base1:virtual public base
{
public:
base1()
{
cout << "base1 a = "<< a << endl;
}
};
class base2:virtual public base
{
public:
base2()
{
cout << "base2 a = "<< a << endl;
}
};
class derived:public base1, public base2
{
public:
derived()
{
cout << "derived a = "<< a << endl;
}
};
int main ()
{
derived obj;
return 0 ;
}
在上述程序中,從類 base 派生出類 base1 和類 base2 時,使用了關鍵字 virtual ,把類base 聲明爲 base1 和 base2 的虛基類。這樣, 從 base1 和base2 派生出的類 derived 只有一個基類 base, 從而可以消除二義性。
3.虛基類的初始化
虛基類的初始化與一般的多重繼承的初始化在語法上是一樣的,但構造函數的調用順序不同。虛基類構造函數的調用順序是這樣規定的:
- 若同一層次中包含多個虛基類, 這些虛基類的構造函數按對它們說明的先後次序調用。
- 若虛基類由非虛基類派生而來, 則仍然先調用基類構造函數, 再調用派生類的構造函數。
- 若同一層次中同時包含虛基類和非虛基類, 應先調用虛基類的構造函數, 再調用非虛基類的構造函數,最後調用派生類構造函數, 例如:
class X∶public Y, virtual public Z
{
// …
};
X one;
定義類 X 的對象 one 時,將產生如下的調用次序:
Z( ) ;
Y( ) ;
X( ) ;
#include<iostream>
using namespace std;
class base
{
int a ;
public:
base (int sa)
{
a = sa;
cout << "Constructing base"<< endl;
}
};
class base1: virtual public base
{
int b;
public:
base1 (int sa, int sb) : base(sa)
{
b = sb;
cout << "Constructing baes1"<< endl;
}
};
class base2: virtual public base
{
int c;
public:
base2 (int sa, int sc) : base (sa)
{
c = sc ;
cout << "Constructing baes2"<< endl;
}
};
class derived: public base1, public base2
{
int d;
public:
derived(int sa, int sb, int sc, int sd ) :
base(sa ), base1 (sa,sb), base2(sa, sc )
{
d = sd;
cout << "Constructing derived"<< endl;
}
};
int main()
{
derived obj(2, 4, 6, 8 ) ;
return 0 ;
}
在上述程序中, base 是一個虛基類,它只有一個帶參數的構造函數, 因此要求在派生類 base1、base2 和 derived 的構造函數的初始化表中,都必須帶有對 base 構造函數的調用。
如果 base 不是虛基類,在派生類 derived 的構造函數的初始化表中調用 base 的構造函數是錯誤的,但是當 base 是虛基類且只有帶參數的構造函數時, 就必須在類 derived 構造函數的初始化表中調用類 base 的構造函數。因此, 在 derived 構造函數的初始化表中,不僅含有對 base1 和 base2 構造函數的調用, 還有對虛基類 base 構造函數的調用。上述程序運行的結果爲:
Constructing base
Constructing base1
Constructing base2
Constructing derived
不難看出,上述程序中虛基類 base 的構造函數只執行了一次。顯然, 當 derived 的構造函數調用了虛基類 base 的構造函數之後, 類 base1 和類 base2 對 base 構造函數的調用被忽略了。這也是初始化虛基類和初始化非虛基類不同的地方。
說明:
- 關鍵字 virtual 與派生方式關鍵字 ( public 或 private ) 的先後順序無關緊要, 它只說明是“虛擬派生”。例如以下兩個虛擬派生的聲明是等價的。
class derived: virtual public base{
// …
} ;
class derived: public virtual base{
// …
} ;
- 一個基類在作爲某些派生類虛基類的同時, 又作爲另一些派生類的非虛基類, 這種情況是允許存在的,例如:
class B{
// …
} ;
class X∶virtual public B{
// …
} ;
class Y∶virtual public B{
// …
} ;
class Z∶public B{
// …
} ;
class AA∶public X, public Y , public Z{
// …
} ;
此例中,派生類 AA 由類 X、類 Y 和類 Z 派生而來。AA 與它的間接基類 B 之間的對應關係是:類 B 既是 X、Y 繼承路徑上的一個虛基類, 也是 Z 繼承路徑上的一個非虛基類。