一、包含對象成員的類
接口和實現:使用公有繼承時,類可以繼承接口,可能還有實現(基類的純虛函數提供接口,但不提供實現)。獲得接口是is-a關系的組成部分。而使用組合,類可以獲得實現,但不能獲得接口。不繼承接口是has-a關系的組成部分。
1、初始化被包含的對象
對於繼承的對象,構造函數在成員初始化列表中使用類名來調用特定的基類構造函數。對於成員對象,構造函數則使用成員名。
C++要求在構建對象的其他部分之前,先構建繼承對象的所有成員對象。因此,如果省略初始化列表,C++將使用成員對象所屬類的默認構造函數。
初始化順序:當初始化列表包含多個項目時,這些項目被初始化的順序為它們被聲明的順序,而不是他們在初始化列表中的順序。但如果代碼使用一個成員的值作為另一個成員的初始化表達式的一部分時,初始化順序就非常重要了。
2、使用被包含對象的接口
被包含對象的接口不是公有的,但可以在類方法中使用它。
Student.h
1 #include <iostream> 2 #include <string> 3 #include <valarray> 4 5 class Student{ 6 private: 7 typedef std::valarray<double> ArrayDb; 8 std::string name_; 9 ArrayDb scores_; 10 std::ostream & arr_out(std::ostream & os)const; 11 public: 12 Student():name_("Null Student"),scores_(){}; 13 explicit Student(const std::string & s):name_(s),scores_(){}; 14 explicit Student(int n):name_("Nully"),scores_(n){}; 15 Student(const std::string & s, int n):name_(s),scores_(n){}; 16 Student(const std::string & s, const ArrayDb & a):name_(s),scores_(a){}; 17 Student(const std::string & s, double * pd, int n):name_(s), scores_(pd,n){}; 18 ~Student(){}; 19 double Average()const; 20 const std::string & Name()const; 21 double & operator[](int i); 22 double operator[](int i)const; 23 friend std::istream & operator>>(std::istream & is, Student & stu); 24 friend std::istream & getline(std::istream & is, Student & stu); 25 friend std::ostream & operator<<(std::ostream & os, const Student & stu); 26 };
Student.cpp
1 #include "Student.h" 2 double Student:: Average()const{ 3 if (scores_.size() > 0) { 4 return scores_.sum()/scores_.size(); 5 } 6 return 0; 7 } 8 const std::string & Student::Name()const{ 9 return name_; 10 } 11 double & Student::operator[](int i){ 12 return scores_[i]; 13 } 14 double Student::operator[](int i)const{ 15 return scores_[i]; 16 } 17 std::istream & operator>>(std::istream & is, Student & stu){ 18 is >> stu.name_; 19 return is; 20 } 21 std::istream & getline(std::istream & is, Student & stu){ 22 getline(is, stu.name_); 23 return is; 24 } 25 std::ostream & operator<<(std::ostream & os, const Student & stu){ 26 os<< "Scores for " << stu.name_ << ":\n"; 27 stu.arr_out(os); 28 return os; 29 } 30 std::ostream & Student::arr_out(std::ostream &os)const{ 31 int i; 32 size_t lim = scores_.size(); 33 if (lim > 0) { 34 for (i = 0; i < lim; i ++) { 35 os << scores_[i] << ","; 36 if (i%5 == 4) 37 os << std::endl; 38 } 39 if (i % 5 != 0) 40 os << std::endl; 41 } 42 else 43 os << "empty array "; 44 return os; 45 }
main.cpp
#include <iostream> #include "Student.h" using std::cin; using std::cout; using std::endl; void set(Student & sa, int n); const int pupils = 3; const int quizzes = 5; int main(int argc, const char * argv[]) { Student ada[pupils] = {Student(quizzes), Student(quizzes), Student(quizzes)}; int i; for (i = 0; i < pupils; i ++) { set(ada[i], quizzes); } cout << "學生名單:\n"; for (i = 0; i < pupils; i ++ ) { cout << ada[i].Name() << endl; } cout << "\n測試結果:"; for (i = 0; i < pupils; i++) { cout << endl << ada[i]; cout << "平均成績:" << ada[i].Average()<< endl; } cout << "完成\n"; return 0; } void set(Student & sa, int n){ cout << "請輸入學生的名字:\n"; getline(cin, sa); cout << "請輸入"<< n << "測試成績:\n"; for (int i = 0; i < n ; i++) cin >> sa[i]; while (cin.get() != '\n') continue; } 輸出結果: 請輸入學生的名字: xiaohong 請輸入5測試成績: 87.4 90 89 78 94 請輸入學生的名字: xiaolan 請輸入5測試成績: 78 45 91 23 46 請輸入學生的名字: huyu 請輸入5測試成績: 90 98 99 97 100 學生名單: xiaohong xiaolan huyu 測試結果: Scores for xiaohong: 87.4,90,89,78,94, 平均成績:87.68 Scores for xiaolan: 78,45,91,23,46, 平均成績:56.6 Scores for huyu: 90,98,99,97,100, 平均成績:96.8 完成
二、私有繼承
C++還有另外一種實現has-a關系的途徑——私有繼承。使用私有繼承,基類的公有成員和保護成員都將成為派生類的私有成員。這意味着基類方法將不會成為派生類對象公有接口的一部分,但可以在派生類的成員函數中使用它們。
使用公有繼承,基類的公有方法將成為派生類的公有方法。總之,派生類將繼承基類的接口;這是is-a關系的一部分。使用私有繼承,基類的公有方法將成為派生類的私有方法。總之,派生類不繼承基類的接口;這種不完全繼承是has-a關系的一部分。
使用私有繼承,類將繼承實現。
包含將對象作為一個命名的成員對象添加到類中,而私有繼承將對象作為一個未被命名的繼承對象添加到類中。我們使用術語子對象(subobject)來表示通過繼承或包含添加的對象。
因此私有繼承提供的特性與包含相同:獲得實現,但不獲得接口。所以,私有繼承也可以用來實現has-a關系。
1、Student類示例(新版本)
要進行私有繼承,請使用關鍵字private而不是public來定義類(實際上,private是默認值,因此省略訪問限定符也將導致私有繼承)。
Student類應從兩個類派生而來,因此聲明將列出這兩個類:
class Student:private std::string, private std::valarray<double>{
public:
...
};
使用多個基類的繼承被稱為多重繼承(multiple inheritance,MI)。
下面我們對Student類進行重新設計,新版本的名字叫做Student1,他使用私有繼承來實現包含關系。
(1)初始化基類組件
對於私有繼承類,構造函數將使用成員初始化列表語法,它使用類名而不是成員名來標示構造函數:
String1(const char * s, const double * pd, int n):std::string(s), ArrayDb(pd, n){}
私有繼承與包含唯一不同的地方在於:私有繼承省略了顯式對象名稱,並在內聯構造函數中使用了類名,而不是成員名。
Student1.h
1 #include <iostream> 2 #include <string> 3 #include <valarray> 4 5 class Student1:private std::string, private std::valarray<double>{ 6 private: 7 typedef std::valarray<double> ArrayDb; 8 std::ostream & arr_out(std::ostream & os) const; 9 public: 10 Student1():std::string("Null Student"),ArrayDb(){}; 11 explicit Student1(const std::string & s):std::string(s),ArrayDb(){}; 12 explicit Student1(int n):std::string("Nully"),ArrayDb(n){}; 13 Student1(const std::string & s, int n):std::string(s), ArrayDb(n){}; 14 Student1(const std::string & s, const ArrayDb & a):std::string(s), ArrayDb(a){}; 15 Student1(const char * s, const double * pd, int n):std::string(s), ArrayDb(pd, n){}; 16 ~Student1(){}; 17 double Average()const; 18 double & operator[](int n); 19 double operator[](int n)const; 20 const std::string & Name()const; 21 friend std::istream & operator>>(std::istream & is, Student1 & stu); 22 friend std::istream & getline(std::istream & is, Student1 & stu); 23 friend std::ostream & operator<<(std::ostream & os, const Student1 & stu); 24 };
(2)訪問基類的方法
使用私有繼承時,只能在派生類的方法中使用基類的方法。私有繼承能夠使用類名和作用域解析運算符來調用基類的方法。
總之,使用包含時將使用對象名來調用方法,而實用私有繼承的時候將使用類名和作用域解析運算符來調用方法。
(3)訪問基類對象
使用作用域解析運算符可以訪問基類方法,但如果要使用基類對象本身需要進行強制類型轉換,即將派生類強制轉換為基類對象,結果將為繼承而來的基類對象。
(4)訪問基類的友元函數
與公有繼承一樣,要想訪問基類的友元函數,需要對派生類進行強制類型轉換,才能正確匹配基類友元函數的原型。
有一點需要注意,在私有繼承中,未進行顯式轉換的派生類引用或指針,無法賦給基類的引用或指針。
Student1.cpp
1 #include "String1.h" 2 double Student1::Average()const{ 3 if (ArrayDb::size() > 0) { 4 return ArrayDb::sum()/ArrayDb::size(); 5 } 6 return 0; 7 } 8 double & Student1::operator[](int n){ 9 return ArrayDb::operator[](n); 10 } 11 double Student1::operator[](int n)const{ 12 return ArrayDb::operator[](n); 13 } 14 const std::string & Student1::Name()const{ 15 return (const std::string &) *this;//因為Student1是string的派生類,因此可以使用向上強制類型轉換來訪問string數據部分 16 } 17 std::istream & operator>>(std::istream & is, Student1 & stu){ 18 is >> (std::string &)stu; 19 return is; 20 } 21 std::istream & getline(std::istream & is, Student1 & stu){ 22 getline(is, (std::string & )stu); 23 return is; 24 } 25 std::ostream & operator<<(std::ostream & os, const Student1 & stu){ 26 os << "Scores for " << (const std::string &)stu << ": \n"; 27 stu.arr_out(os); 28 return os; 29 } 30 std::ostream & Student1::arr_out(std::ostream &os)const{ 31 int i; 32 size_t lim = ArrayDb::size(); 33 if (lim > 0) { 34 for (i = 0; i < lim; i++) { 35 os<<ArrayDb::operator[](i) << ","; 36 if (i % 5 == 4) { 37 os << std::endl; 38 } 39 } 40 if (i % 5 != 0) { 41 os << std::endl; 42 } 43 } 44 else 45 os << "empty array."; 46 return os; 47 }
2、使用包含還是私有繼承
大多數C++程序員傾向於使用包含。首先,它易於理解。類聲明中包含表示被包含類的顯式命名對象,代碼可以通過名稱引用這些對象,而使用繼承將使關系更抽象。其次,繼承會引起很多問題,尤其從多個基類繼承時,可能必須處理很多問題,如包含同名方法的獨立的基類或共享祖先的獨立基類。總之,實用包含不太可能遇到這樣的麻煩。另外,包含能夠包括多個同類的子對象,而繼承只能使用一個這樣的對象。
然而,私有繼承所提供的特性確實比包含多。例如,假設類包含保護成員(可以是數據成員,也可以是成員函數),則這樣的成員在派生類中是可用的,但在繼承層次結構外是不可用的。如果使用組合將這樣的類包含在另一個類中,則后者將不是派生類,而是位於繼承層次結構之外,因此不能訪問保護成員。但通過繼承得到的將是派生類,因此它能夠訪問保護成員。
另一種需要使用私有繼承的情況是需要重新定義虛函數。派生類可以重新定義虛函數,但包含類不能。使用私有繼承,重新定義的函數將只能在類中使用,而不是公有的。
提示:通常,應使用包含來建立has-a關系;如果新類需要訪問原有類的保護成員,或需要重新定義虛函數,則應使用私有繼承。
3、保護繼承
保護繼承是私有繼承的變體。保護繼承在列出基類時使用關鍵protected:
class Student:protected std::string, protected std::valarray<double>{...};
使用保護繼承時,基類的公有成員和保護成員都將成為派生類的保護成員。和私有私有繼承一樣,基類的接口在派生類中也是可用的,但在繼承層次結構之外是不可用的。當從派生類派生出另一個類時,私有繼承和保護繼承之間的主要區別便呈現出來了。使用私有繼承時,第三代類將不能使用基類的接口,這是因為基類的公有方法在派生類中將變成私有方法:使用保護繼承時,基類的公有方法在第二代中將變成受保護的,因此第三代派生類可以使用它們。
下面的表總結了公有、私有和保護繼承。隱式向上轉換意味着無需進行顯式類型轉換,就可以將基類的指針或引用指向派生類對象。
| 特征 | 公有繼承 | 保護繼承 | 私有繼承 |
| 公有成員變量 | 派生類的公有成員 | 派生類的保護成員 | 派生類的私有成員 |
| 保護成員變量 | 派生類的保護成員 | 派生類的保護成員 | 派生類的私有成員 |
| 私有成員變量 | 只能通過基類接口訪問 | 只能通過基類接口訪問 | 只能通過基類接口訪問 |
| 能否隱式向上轉換 | 是 | 是(但只能在派生類中) | 否 |
4、使用using重新定義訪問權限
使用保護派生和私有派生時,基類的公有成員將會成為保護成員或私有成員。如果要讓基類的方法在派生類外面可用,方法之一是定義一個使用該基類方法的派生類方法。
另一種方法是,將函數調用包裝在另一個函數調用中,即使用一個using聲明(就像名稱空間那樣)來指出派生類可以使用特定的基類成員,即使采用的是私有派生。例如,假設希望通過Student類能夠使用valarray的方法min()和max(),可以在Student的聲明的公有部分加入如下using聲明:
class Student:private std::string, private std::valarray<double>{
....
public:
using std::valarray<double>::min;
using std::valarray<double>::max;
...
};
上述using聲明使得valarray<double>::min()和valarray<double>::max()可用,就像他們是Student的公有方法一樣。
注意,using聲明只使用成員名——沒有圓括號、函數特征標和返回類型。例如,為使Student類可以使用valarray的operator[]()方法,只需要在Student類聲明的公有部分包含下面的using聲明:
using std::valarray<double>::operator[]();
這將使兩個版本(const和非const)都可用。這樣,便可以刪除Student::operator[]()的原型和定義。
注意,using聲明只適用於繼承,而不適用於包含。
三、多重繼承
MI描述的是有多個直接基類的類。與單繼承一樣,公有MI表示的也是is-a關系。注意,私有MI和保護MI可以表示has-a關系。
MI可能會帶來很多新問題。其中兩個主要的問題是:從兩個不同的基類繼承同名方法;從兩個或更多相關基類那里繼承同一個類的多個實例。
現在有下面的繼承關系:

上面的關系,表示成代碼如下:
1 #include <iostream> 2 3 class A{ 4 int a; 5 public: 6 A(int n = 0):a(n) {}; 7 virtual~A(){}; 8 virtual void show(){std::cout << "a:" << a;}; 9 }; 10 class B:public A{ 11 int b; 12 public: 13 B(int a = 0, int n = 0):A(a),b(n){}; 14 virtual ~B(){}; 15 virtual void show(){A::show();std::cout << ", b:" << b;}; 16 }; 17 class C:public A{ 18 int c; 19 public: 20 C(int a = 0, int n = 0):A(a),c(n){}; 21 virtual ~C(){}; 22 virtual void show(){A::show();std::cout << ", c:" << c;}; 23 }; 24 class D:public B, public C{ 25 int d; 26 public: 27 D(int b1 = 0, int b2 = 0,int c1 = 0, int c2 = 0, int n = 0):B(b1,b2),C(c1,c2),d(n){}; 28 void show(){B::show();std::cout << ", ";C::show();std::cout << ", ";std::cout << " d:" << d ; }; 29 };
1、在D中有多少個A?
因為B和C都繼承了一個A組件,因此D將包含兩個A組件。
` 正如預期的,這將會引起問題。通常可以將派生類對象的地址賦給基類指針,但現在將出現二義性:
D d;
A * pa = &d;//發生二義性
通常,這種賦值將把基類指針設置為派生類對象中基類對象的地址,但是d中包含兩個A對象,有兩個地址可供選擇,所有應使用類型轉換來指定對象:
A * pa = (B *)&d;
A * pa2 = (C *)&d;

這將使得使用基類指針來引用不同的對象(多態性)復雜化。
包含兩個A對象拷貝還會導致其他的問題。然而,真正的問題是:為什么需要A對象的兩個拷貝?因為D公有繼承自B和C,而B和C又都公有繼承自A,即D既是B也是C,同時B、C、D又都是A,他們之間是is-a關系;那么D對象中包含兩個A子對象將很奇怪,D對象中應該只有一個A子對象而不是兩個或多個。C++引入多繼承的同時,引入了一種新技術——虛基類(virtual base class),使MI成為可能。
(1)虛基類
虛基類使得從多個類(它們的基類相同)派生出的對象只繼承一個基類對象,這種技術是通過在類聲明中加入關鍵字virtual來實現的(virtual和public的順序無關緊要):
class B:public virtual A{....};
class C:virtual public A{....};
然后,可以將D類定義為:
class D:public B, public C{....};
現在D對象中將只包含A對象的一個副本。從本質上說,繼承的B和C對象共享一個A對象,而不是各自引入自己的A對象副本。

(2)新的構造函數
使用虛基類時,需要對類的構造函數采用一種新的方法。對於非虛基類,唯一可以出現在初始化列表中的構造函數是即時基類構造函數。但這些構造函數可能需要將信息傳遞給基類。例如,可能有下面的構造函數:
class A{
int a;
public:
A(int n = 0):a(n){};
.....
};
class B:public A{
int b;
public:
B(int a = 0, int n = 0):A(a), b(n){};
...
};
class E: public B{
int e;
public:
E(int a = 0, int b = 0, int n = 0):B(a,b), e(n){};
.....
};
在這里,E類的構造函數只能調用B類的構造函數,而B類的構造函數只能調用A類的構造函數。這里,E類的構造函數將使用n值,並將a和b傳遞給B類的構造函數;而B類的構造函數使用值b,並將a傳遞給A類的構造函數。
但是,如果A是虛基類:
class A{
int a;
public:
A(int n = 0):a(n){};
...
};
class F:virtual public A{
int f;
public:
F(int a = 0, int n = 0):A(a),f(n){};
....
};
class G:public virtual A{
int g;
public:
G(int a = 0, int n = 0):A(a), f(n){};
....
};
則這種信息自動傳遞將不起作用。例如:
class H:public F, public G{
int h;
public:
H(int a = 0, int f = 0, int g = 0, int n = 0):F(a,f),G(a,g),h(n){};
.....
};
存在的問題是,自動傳遞信息時,將通過2條不同的途徑(F和G)將a傳遞給A對象。未避免這種沖突,C++在基類是虛的時,禁止信息通過中間類自動傳遞給基類。因此,上面的構造函數將初始化成員f和g,但a參數中的信息將不會傳遞給子對象A。然而,編譯器必須在構造派生對象前構造基類對象組件:在上述情況下,編譯器將使用A的默認構造函數。
如果不希望默認構造函數來構造虛基類對象,則需要顯式地調用所需的基類構造函數。因此,構造函數應該這樣:
class H:public F, public G{
int h;
public:
H(int a = 0, int f = 0, int g = 0, int n = 0):A(a),F(a,f),G(a,g),h(n){};
...
};
上述代碼顯式調用構造函數A(int )。請注意,這種用法是合法的,對於虛基類,必須這樣做;但對於非虛基類,則是非法的。
警告:如果類有間接虛基類,則除非只需要使用該虛基類的默認構造函數,否則必須顯式調用該虛基類的某個構造函數。
2、哪個方法
假設有如下幾個類A、B、C、D,它們之間的關系如下:
1 #include <iostream> 2 3 class A{ 4 int a; 5 public: 6 A(int n = 0):a(n) {}; 7 virtual~A(){}; 8 virtual void show(){std::cout << "a:" << a;}; 9 }; 10 class B:virtual public A{ 11 int b; 12 public: 13 B(int a = 0, int n = 0):A(a),b(n){}; 14 virtual ~B(){}; 15 virtual void show(){A::show();std::cout << ", b:" << b;}; 16 }; 17 class C:virtual public A{ 18 int c; 19 public: 20 C(int a = 0, int n = 0):A(a),c(n){}; 21 virtual ~C(){}; 22 virtual void show(){A::show();std::cout << ", c:" << c;}; 23 }; 24 class D:public B, public C{ 25 int d; 26 public: 27 D(int a = 0, int b = 0,int c = 0, int n = 0):A(a),B(a,b),C(a,c),d(n){};28 };
現在有如下問題,我們試圖用D對象調用繼承的show()方法:
D d(12,33,45,14,53);
d.show();
對於單繼承,如果沒有重新定義show(),則將使用最近祖先中的定義。而在多繼承中,每個直接祖先都有一個show()函數,這使得上述調用是二義性的。
警告:多重繼承可能導致函數調用的二義性。
可以使用作用域解析運算符來澄清意圖:
D d1(90,123,435,223,55);
d1.B::show();
d1.C::show();
然而,更好的方法是在D中重新定義show()方法,並指出要使用哪個show()。例如,如果希望使用D對象使用B版本的show(),則可以這樣做:
void D::show(){
B::show();
}
對於單繼承,可以讓派生類方法調用基類方法顯示基類信息,並添加自己的信息,即遞增的方式來顯示派生類對象的信息。但是這種方法對於多繼承來說是有缺陷的。例如,在D類的show()方法中同時調用基類B和基類C的show()方法,並加上自己的信息:
void D::show(){
B::show();
C::show();
std::cout << d;
}
然而,像上面的方法將會顯示虛基類對象的信息兩次,即會顯示兩次a的信息。因為B::show()和C::show()都調用了A::show()。
解決這種問題的一種方法是使用模塊化,而不是遞增方式,即提供一個只顯示A組件的方法和一個只顯示B組件或C組件(而不是B和C組件)的方法。然后,在D::show()方法中將組件合起來。例如,可以像下面這樣做:
void A::Data()const{
std::cout << "a:" << a;
}
void B::Data()const{
std::cout << "b:" << b;
}
void C::Data()const{
std::cout << "c:" << c;
}
void D::Data()const{
B::Data();
std::cout << ", ";
C::Data();
}
void D::show()const{
A::Data();
std::cout << ", ";
Data();
}
與此相似,其他show()方法可以組合適當的Data()組件。
采用這種方法,對象仍可使用show()方法。而Data()方法只在類內部可用,作為協助公有接口的輔助方法。然而,使Data()方法成為私有的將阻止B中的代碼使用C::Data(),這正是保護訪問類的用武之地。如果Data()方法是保護的,則只能在繼承層次結構中的類使用它,在其他地方則不能使用。
另一種方法是將所有的數據組件都設置為保護的,而不是私有的,不過使用保護方法(而不是保護數據)將可以更嚴格地控制對數據的訪問。
總之,在祖先相同時,使用MI必須引入虛基類,並修改構造函數初始化列表的規則。另外,如果在編寫這些類時沒有考慮到MI,則還可能需要重新編寫他們。
下面是對A、B、C、D的完整代碼:
1 #include <iostream>
2 3 class A{ 4 private: 5 int a; 6 protected: 7 void Data()const{ 8 std::cout << "a:" << a; 9 } 10 public: 11 A(int n = 0):a(n) {}; 12 virtual~A(){}; 13 virtual void show(){ 14 Data(); 15 } 16 }; 17 class B:virtual public A{ 18 private: 19 int b; 20 protected: 21 void Data()const{ 22 std::cout << "b:" << b; 23 }; 24 public: 25 B(int a = 0, int n = 0):A(a),b(n){}; 26 virtual ~B(){}; 27 virtual void show(){ 28 A::Data(); 29 std::cout << ", "; 30 Data(); 31 } 32 }; 33 class C:virtual public A{ 34 private: 35 int c; 36 protected: 37 void Data()const{ 38 std::cout << "c:" << c; 39 } 40 public: 41 C(int a = 0, int n = 0):A(a),c(n){}; 42 virtual ~C(){}; 43 virtual void show(){ 44 A::Data(); 45 std::cout << ", "; 46 Data(); 47 } 48 }; 49 class D:public B, public C{ 50 private: 51 int d; 52 protected: 53 void Data()const{ 54 std::cout << "d:" << d; 55 } 56 public: 57 D(int a = 0, int b = 0,int c = 0, int n = 0):A(a).B(a,b),C(a,c),d(n){}; 58 void show(){ 59 A::Data(); 60 std::cout << ", "; 61 B::Data(); 62 std::cout << ", "; 63 C::Data(); 64 std::cout << ", "; 65 Data(); 66 } 67 };
main.cpp
1 #include <iostream> 2 #include "ABCD.h" 3 4 using namespace std; 5 6 int main(int argc, const char * argv[]) { 7 D d{12,33,45,22}; 8 d.show(); 9 10 return 0; 11 }
輸出結構:
a:12, b:33, c:45, d:22
(1)混合使用虛基類和非虛基類
通過多種途徑繼承一個基類的派生類的情況:
*如果基類是虛基類,派生類將包含基類的一個子對象;
*如果基類不是虛基類,派生類將包含多個子對象;
*當類通過多條虛途徑和非虛途徑繼承某個特定的基類時,該類將包含一個表示所有的虛途徑的基類子對象和分別表示各條非虛途徑的多個基類子對象。
(2)虛基類和支配
使用虛基類將改變C++解析二義性的方式。
使用非虛基類時,規則很簡單。如果類從不同的類那里繼承了兩個或更多的同名成員(數據或方法),則使用該成員名時,如果沒有使用類名將導致二義性。
但如果使用的是虛基類,則這樣做不一定會導致二義性。在這種情況下,如果某個名稱優先於其他所有名稱,則使用它,即便不使用限定符,也不會導致二義性。
派生類中的名稱優先於直接或間接祖先類中的相同名稱。
3、MI小結
(1)不使用虛基類的MI
*這種形式的MI不會引入新的規則。然而,如果一個類從兩個不同類那里繼承了兩個同名的成員,則需要在派生類中使用類限定符來區分它們。
*如果一個類通過多種途徑繼承了一個非虛基類,則該類從每種途徑分別繼承非虛基類的一個實例。在某些情況下,這可能正是需要的,但通常情況下,多個基類實例都是問題。
(2)使用虛基類的MI
當派生類使用關鍵字virtual來指示派生時,基類就稱為虛基類。主要變化(同時也是使用虛基類的原因)是,從虛基類的一個或多個實例派生而來的類將只繼承一個基類對象。為實現這種特性,必須滿足其他要求:
*有間接虛基類的派生類包含直接調用間接基類構造函數的構造函數,這對於間接非虛基類來說是非法的。
*通過優先規則解決名稱二義性。
MI會增加編程的復雜度。然而,這種復雜性主要是由於派生類通過多條途徑繼承同一個基類引起的。避免這種情況后,唯一需要注意的是,在必要時對繼承的名稱進行限定。
四、模板類
1、定義類模板
模板提供參數化類型,即能夠將類型名作為參數傳遞給接收方來建立類或函數。
和模板函數一樣,模板類以下面這樣的代碼開頭:
template <class Type>
關鍵字template告訴編譯器,將要定義一個模板。尖括號中的內容相當於函數的參數列表。可以把關鍵字class看作是變量的類型名,該變量接受類型作為其值,把Type看作是該變量的名稱。
這里使用class並不意味着Type必須是一個類;而只是表明Type是一個通用的類型說明符,在使用模板時,將使用實際的類型替換他。較新的C++實現允許在這種情況下使用不太容易混淆的關鍵字typename來代替class:
template<typename Type>//新選擇
可以使用自己的范型名代替Type,其命名規則與其他標識符相同。當前流行的選項包括T和Type。當模板被調用時,Type將被具體的類型值取代。
下面,我們對前面的棧Stack用模板類來實現:
Stack.h
1 #include <iostream> 2 3 template <class Type> 4 class Stack{ 5 private: 6 enum {MAX = 10}; 7 Type items[MAX]; 8 int top; 9 public: 10 Stack(); 11 bool isempty(); 12 bool isfull(); 13 bool push(const Type & item); 14 bool pop(Type & item); 15 }; 16 template<class Type> 17 Stack<Type>::Stack(){ 18 top = 0; 19 } 20 template<class Type> 21 bool Stack<Type>::isempty(){ 22 return top == 0; 23 } 24 template<class Type> 25 bool Stack<Type>::isfull(){ 26 return top == MAX; 27 } 28 template<class Type> 29 bool Stack<Type>::push(const Type &item){ 30 if (top < MAX) { 31 items[top ++] = item; 32 return true; 33 } 34 return false; 35 } 36 template<class Type> 37 bool Stack<Type>::pop(Type &item){ 38 if (top > 0) { 39 item = items[--top]; 40 return true; 41 } 42 return false; 43 }
注意,類模板和成員函數模板不是類和成員函數的定義。它們是C++編譯器指令,說明了如何生成類和成員函數定義。模板的具體實現被稱為實例化或具體化。不能將模板成員函數放在獨立的實現文件中。由於模板不是函數,他不能單獨編譯。模板必須與特定的模板實例化請求一起使用。為此,最簡單的方法是將所有模板信息放在一個頭文件中,並在要使用這些模板的文件中包含該頭文件。
2、使用模板
僅僅在程序包含模板並不能生成模板類,而必須請求實例化。為此,需要聲明一個類型為模板類的對象,方法是使用所需的具體類型替換范型名。例如,下面的代碼創建兩個棧,一個用於存儲int,另一個用於存儲string對象:
Stack<int> stack_int; Stack<string> stack_str;
范型標識符——例如這里的Type——稱為類型參數,這意味着他們類似於變量,但賦給他們的不能是數字,而只能是類型。注意,在使用類模板的時候必須現實提供所需的類型。
1 #include <iostream> 2 #include "Stack.h" 3 4 using namespace std; 5 6 int main(int argc, const char * argv[]) { 7 Stack<int> stack_int; 8 int num = 5; 9 while (!stack_int.isfull()) { 10 cout << num << ","; 11 stack_int.push(num); 12 num +=20; 13 } 14 cout << endl; 15 while (!stack_int.isempty()) { 16 stack_int.pop(num); 17 cout << num << ","; 18 } 19 return 0; 20 } 21 22 輸出結果: 23 5,25,45,65,85,105,125,145,165,185, 24 185,165,145,125,105,85,65,45,25,5,
3、深入討論模板類
對於上面的Stack模板類的功能不是很完善,比如不能制定模板大小等等。下面我們對Stack模板進行重新設計,使其能根據實際需要設定棧長度,那我們將會采用動態內存分配,因此,還需要顯式定義復制構造函數和賦值運算符。
#include <iostream> template <class Type> class Stack{ private: enum {MAX = 10}; int stacksize; Type *items; int top; public: explicit Stack(int size = MAX); Stack(const Stack &); bool isempty(); bool isfull(); bool push(const Type & item); bool pop(Type & item); Stack & operator=(const Stack & st); }; template<class Type> Stack<Type>::Stack(int size){ stacksize = size; items = new Type[stacksize]; top = 0; } template<class Type> Stack<Type>::Stack(const Stack & st){ stacksize = st.stacksize; top = st.top; delete [] items; items = new Type[stacksize]; for (int i = 0; i < stacksize; i++) { items[i] = st.items[i]; } } template<class Type> bool Stack<Type>::isempty(){ return top == 0; } template<class Type> bool Stack<Type>::isfull(){ return top == MAX; } template<class Type> bool Stack<Type>::push(const Type &item){ if (top < MAX) { items[top ++] = item; return true; } return false; } template<class Type> bool Stack<Type>::pop(Type &item){ if (top > 0) { item = items[--top]; return true; } return false; } template<class Type> Stack<Type> & Stack<Type>:: operator=(const Stack<Type> & st){ if (this == &st) { return *this; } delete [] items; stacksize = st.stacksize; top = st.top; items = new Type[stacksize]; for (int i = 0; i < stacksize; i ++ ) { items[i] = st.items[i]; } return *this; }
原型將賦值運算符的返回值聲明為Stack引用,而實際的模板函數定義需要將類型定義為Stack<Type>。前者是后者的縮寫,但只能在類中使用。
下面我們用上面的模板類設計一個小程序:
1 #include <iostream> 2 #include "Stack.h" 3 #include <cstdlib> 4 #include <ctime> 5 6 const int Num = 10; 7 using namespace std; 8 9 int main(int argc, const char * argv[]) { 10 srand(time(0)); 11 cout << "請輸入棧的長度:"; 12 int stacksize; 13 cin >> stacksize; 14 Stack<const char *> st(stacksize); 15 16 const char * in[Num] = { 17 "1:小紅","2:小藍","3:小余","4:小黃","5:小明","6:小樣","7:小平","8:小曲","9:小波","10:小萌" 18 }; 19 const char *out[Num]; 20 int processed = 0; 21 int nextin = 0; 22 while (processed < Num) { 23 if (st.isempty()) 24 st.push(in[nextin++]); 25 else if(st.isfull()) 26 st.pop(out[processed++]); 27 else if(rand() % 2 && nextin < Num) 28 st.push(in[nextin++]); 29 else 30 st.pop(out[processed ++]); 31 } 32 for (int i = 0; i < Num; i ++) { 33 cout << out[i] << endl; 34 } 35 cout << "完成"; 36 return 0; 37 } 38 39 兩次輸出結果: 40 請輸入棧的長度:5 41 1:小紅 42 2:小藍 43 3:小余 44 5:小明 45 4:小黃 46 7:小平 47 8:小曲 48 10:小萌 49 9:小波 50 6:小樣 51 完成 52 53 請輸入棧的長度:5 54 1:小紅 55 2:小藍 56 3:小余 57 5:小明 58 4:小黃 59 6:小樣 60 10:小萌 61 9:小波 62 8:小曲 63 7:小平 64 完成
4、數組模板示例和非類型參數
模板常用做容器類,這是因為類型參數的概念非常適合於將相同的存儲方案用於不同的類型。確實,為容器類提供可重用代碼是引入模板的主要動機,所以我們來看另外一個例子,深入討論模板設計和使用的其他幾個方面。具體地說,將探討一些非類型(或表達式)參數以及如何使用數組來處理繼承族。
下面我們介紹一種使用模板參數來提供常規數組的大小:
1 #include <stdio.h> 2 #include <iostream> 3 4 template<class Type, int n>//提供非類型參數 5 class ArrayTP{ 6 private: 7 Type ar[n]; 8 public: 9 ArrayTP(){}; 10 explicit ArrayTP(const Type & v); 11 virtual Type & operator[](int i); 12 virtual Type operator[](int i)const; 13 }; 14 template<class T , int n> 15 ArrayTP<T,n>::ArrayTP(const T & v){ 16 for (int i = 0; i < n; i ++ ) { 17 ar[i] = v; 18 } 19 } 20 template <class T, int n> 21 T & ArrayTP<T, n>::operator[](int i){ 22 if (i < 0 || i >= n) { 23 std::cout << "輸入的數字超出了范圍\n"; 24 std::exit(EXIT_FAILURE); 25 } 26 return ar[i]; 27 } 28 template<class T, int n> 29 T ArrayTP<T, n>::operator[](int i)const{ 30 if (i < 0 || i >= n) { 31 std::cout << "超出了范圍。\n"; 32 std::exit(EXIT_FAILURE); 33 } 34 return ar[i]; 35 }
template<class Type, int n>
關鍵字class(或在這種上下文中等價的關鍵字typename)指出T為類型參數,int指出n的類型為int。這種參數(指定特殊的類型而不是用作范型名)稱為非類型(non-type)或表達式(expression)參數。假設有下面的聲明:
ArrayTP<double, 12>arr;
上面的聲明將導致編譯器定義名為ArrayTP<double, 12>的類,,並創建一個類型為ArrayTP<double,12>的arr對象。定義類時,編譯器將使用double替換T,使用12替換n。
表達式參數有一些限制。表達式參數可以是整型、枚舉、引用或指針。另外,模板代碼不能修改參數的值,也不能使用參數的地址。另外,實例化模板時,用作表達式參數的值必須是常量表達式。
與Stack中使用的構造函數方法相比,這種改變數組大小的方法有一個優點。構造函數方法使用的是new和delete管理的對內存,而表達式參數方法使用的是為自動變量維護的內存棧。這樣執行速度更快,尤其是在使用很多小型數組時。
表達式參數的主要缺點是,沒種數組大小都將生成自己的模板。也就是說,下面的聲明將生成兩個獨立的類聲明:
ArrayTP<double, 12> arr1;
ArrayTP<double, 13> arr2;
但下面的聲明只生成一個類聲明,並將數組大小信息傳遞給類的構造函數:
Stack<int> stack1;
Stack<int> Stack2;
另一個區別是,構造函數方法更通用,這是因為數組大小是作為類成員(而不是硬編碼)存儲在定義中的。這樣可以將一種尺寸的數組賦給另一種尺寸的數組,也可以創建允許數組大小可變的類。
5、模板多功能性
可以將用於常規類的技術用於模板類。模板類可用作基類,也可用做組件類,還可用作其他模板的類型參數。例如,例如下面用數組模板來實現棧模板:
#include <iostream> #include "Stack.h" #include "ArrayTP.h" using namespace std; int main(int argc, const char * argv[]) { Stack<ArrayTP<int, 12> > stack{12};//在這條語句中,C++98要求使用至少一個空白字符將兩個>符號分開,以免與>>混淆。C++11中不要求這樣做。 int num = 4; while (!stack.isfull()) { ArrayTP<int, 12> arr{num}; stack.push(arr); num += 20; } return 0; }
在第一條語句中,C++98要求使用至少一個空白符將兩個>符號分開,以免與運算符>>混淆。C++11不要求這樣做。
(1)遞歸使用模板
另一個模板多功能性的例子是,可以遞歸使用模板。例如,可以這樣使用ArrayTP模板:
ArrayTP<ArrayTP<int, 5>, 10> twodee;
這使得twodee是一個包含10個元素的數組,每個元素都是一個包含5個int元素的數組。與之等價的常規數組聲明為:
int twodee[10][5];
請注意,在模板語法中,維de順序與等價的二維數組相反。
(2)使用多個類型參數
模板可以包含多個類型參數。例如,可以定義個一個模板類Item接受兩個類型參數:
template<class T1, class T2>
class Item{
private:
T1 item1;
T2 item2;
public:
......
};
(3)默認類型模板參數
類模板的另一項特性是,可以為類型參數提供默認值:
template<class T1, class T2 = int> class Topo{...};
這樣,如果省略T2的值,編譯器將會使用默認類型int:
Topo<double, double> m1;
Topo<double> m2;
6、模板的具體化
類模板與函數模板很相似,因為可以隱式實例化、顯式實例化和顯式具體化,它們統稱為具體化。模板以范型的方式描述類,而具體化是使用具體的類型生成類聲明。
(1)隱式實例化
到目前為止,所有的模板示例使用的都是隱式實例化,即它們聲明一個或多個對象,指出所需的類型,而編譯器使用通用模板提供的處方生成具體的類定義。
(2)顯式實例化
當使用關鍵字template並指出所需類型來聲明類時,編譯器將生成類聲明的顯式實例化。聲明必須位於模板定義所在的名稱空間中。例如,下面的聲明將ArrayTP<string,100>聲明為一個類:
template class ArrayTP<string, 100>;
在這種情況下,雖然沒有創建或提及對象,編譯器也將生成類聲明(包括方法定義)。和隱式實例化一樣,也將根據通用模板來生成具體化。
(3)顯式具體化
顯式具體化是特定類型(用於替換模板中的范型)的定義。有時候,可能需要在為特殊類型實例化時,對模板進行修改,使其行為不同。在這種情況下,可以創建顯式具體化。
具體化類模板定義的格式如下:
template<> class Classname<specialized-type-name>{...};
早期的編譯器可能只能識別早期的格式,這種格式不包括前綴template<>:
class Classname<specialized-type-name>{....};
(4)部分具體化
C++還允許部分具體化,即部分限制模板的通用性。例如,部分具體化可以給類型參數之一指定具體的類型:
//通用模板
template<class T1, class T2> class Pair{...};
//將T2部分具體化為int
template<class T1> class Pair<T1,int>{...};
關鍵字template后面的<>聲明的是沒有被具體化的類型參數。因此,上面的第二個聲明將T2具體化為int,但T1保持不變。注意,如果指定所有的類型,則<>內將為空,這將導致顯式具體化:
template<> class Pair<int, int>{...};
如果有多個模板可供選擇,編譯器將使用具體化最高的模板。給定上述三個模板,情況如下:
Pair<double,double> p1;//使用通用模板
Pair<double,int> p2;//使用Pair<T1,int>部分顯式具體化模板
Pair<int,int> p3;//使用Pair<int,int>顯式具體化模板
也可以通過為指針提供特殊版本來部分具體化現有的模板:
template<class T>//通用模板
class Feed{...};
template<class T*>//指針部分具體化模板
class Feed{....};//定義代碼
如果提供的類型不是指針,編譯器將使用通用版本;如果提供的是指針,則編譯器使用指針具體化版本:
Feed<char> fb1;//使用通用模板,T是char
Feed<char *> fb2;//使用T*部分具體化,T是char
如果沒有進行部分具體化,則第二個聲明將使用通用模板,將T轉換為char *類型。如果進行了部分具體化,則第二個聲明將使用具體化模板,將T轉換為char。
部分具體化特性使得能夠設置各種限制。例如,可以這樣做:
//通用模板
template<class T1, class T2, class T3>class Trio{...};
//用T2部分具體化T3
template<class T1, class T2> class Trio<T1,T2,T2>{...};
//用T1*部分顯式具體化T2和T3
template<class T1> class Trio<T1, T1 *, T1 *>{...};
給定上述聲明,編譯器將作出如下選擇:
Trio<int, short, char *> t1;//使用通用模板
Trio<int, char> t2;//使用Trio<T1,T2,T2>
Trio<int,int *, int *> t3;//使用Trio<T1, T1 *, T1 *>
7、成員模板
模板可用作結構、類和模板類的成員。要完成STL的設計,必須使用這項特性。下面的程序是一個簡單的模板類示例,該模板類將另一個模板類和和模板函數作為其成員。
1 #include <stdio.h> 2 #include <iostream> 3 using std::cout; 4 using std::endl; 5 template<class T> 6 class Beta{ 7 private: 8 template<typename V> 9 class Hold{ 10 private: 11 V val; 12 public: 13 Hold(V v = 0):val(v){}; 14 void show()const{cout << val << endl;} 15 V Value()const {return val;}; 16 }; 17 Hold<T> q; 18 Hold<int> n; 19 public: 20 Beta(T t, int i):q(t),n(i){}; 21 template<typename U> 22 U blab(U u, T t){return (n.Value() + q.Value())* u / t;} 23 void Show()const{q.show();n.show();} 24 };
#include <iostream> #include "Beta.h" using namespace std; int main(int argc, const char * argv[]) { Beta<double> guy{3.5,3}; guy.Show(); cout << guy.blab(10, 2.3) << endl; cout << guy.blab(10.0, 2.3) << endl; return 0; } 輸出結果: 3.5 3 28 28.2609
8、將模板用作參數
模板可以包含類型參數和非類型參數。模板還可以包含本身就是模板的參數,這種參數是模板新增的特性,用於實現STL。
9、模板類和友元
模板類聲明也可以有友元。模板的友元分3類:
*非模板友元
*約束(bound)模板友元,即友元的類型取決於類被實例化時的類型;
*非約束(unbound)模板友元,即友元的所有具體化都是類的每一個具體化的友元。
(1)模板類的非模板友元函數
在模板類中將一個常規函數聲明為友元:
template<class T>
class HasFriend{
public:
friend void counts();
....
};
上面的聲明使counts()函數成為模板所有實例化的友元。
counts()函數不是通過對象調用的(它是友元,不是成員函數),也沒有對象參數,它可以訪問全局對象;可以使用全局指針訪問非全局對象;可以創建自己的對象;可以訪問獨立於對象的模板類的靜態數據成員。
如果要為友元函數提供模板類參數,必須指明具體化。例如:
template<class T>
class HasFriedn
{
public:
friend void report(HasFriedn<T> &);
};
注意:report()本身並不是模板函數,而只是使用一個模板作為參數。這意味着必須為要使用的友元定義顯式具體化,像上面的report()可以給他定義其他類型的顯式具體化模板:
friend void report(HasFriend<int> &);
(2)模板類的約束模板友元函數
可以修改前面的例子,使友元函數本身成為模板。具體地說,為約束模板友元作准備,要使類的每一個具體化都獲得與友元匹配的具體化。這比非模板友元復雜些,包含以下3步。
首先,在類定義的前面聲明每個模板函數。
template<typename T> void counts();
template<typename T>void report(T &);
然后,在函數中再次將模板聲明為友元。這些語句根據類模板參數的類型聲明具體化。
template<class T1>
class HasFriend{
public:
friend void counts<T1>();
friend void report<>(HasFriend<T1> &);
};
聲明中的<>指出這是模板具體化。對於report(),<>可以為空,因為可以從函數參數推斷出如下模板類型參數:
HasFriend<T1>
然而,也可以使用:
friend void report<HasFriedn<T1> >(HasFriend<T1> &);
最后,為友元提供模板定義。
(3)模板類的非約束模板友元函數
通過在類內部聲明模板,可以創建非約束友元函數,即每個函數具體化都是每個類具體化的友元。
對於非約束友元,友元模板類型參數與模板類類型參數是不同的:
template<typename T>
class ManyFriend{
....
template<class T1, class T2> friend void show(T1 &, T2 &);
};
10、模板別名
可以使用typedef為模板具體化指定別名:
typedef ArrayTP<doub,12> arrd;
typedef ArrayTP<int, 13> arre;
C++11新增了一項功能——使用模板提供一系列別名,如下面所示:
templaty<class T>
using arrtype = std::array<T,12>;
這樣將arrtype定義為一個模板別名,可使用它來指定類型,如下所示:
arrtype<double> arr;
這將會使用arrtype<T>表示類型std::array<T,12>;
C++11允許將語法using=用於非模板。用於非模板時,這種語法與常規typedef等價:
typedef const char * pc1;
using pc2 = const char *;
