C++学习笔记:(五)继承 多态
目錄
7.組合、繼承與多態性
7.1 組合
7.2 繼承
7.3繼承和組合
7.4構造與析構順序
7.5派生類重載基類函數的訪問
7.6多態性&虛函數
7.7純虛函數和抽象基類
7.8 多重繼承
7.9派生類成員的標識與訪問
7.組合、繼承與多態性
面向對象設計的重要目的之一就是代碼重用,這也是C++重要性能之一。軟件的重用性鼓勵人們使用已有的、得到認可并經過測試的高質量代碼。多態性可以以常規方式書寫程序來訪問多種現有的且專門化了的相關類。繼承和多態是面向對象程序設計方法的兩個最主要的特征。繼承可以將一群相關的類組織起來,并共享期間的相同數據和操作行為;多態使得設計者在這些類上編程時,可以如同操作一個單一體,而非相互獨立的類,且給設計者更多靈活性來加入或刪除類的一些屬性或方法。
?
7.1 組合
對于比較簡單的類,其數據成員可能都是基本數據類型,但對于某些復雜的類來說,其某些數據成員可能又是另一些類類型,這就形成了類的組合。實際上,C語言中一直在使用組合,如C語言中的結構體就可以看成是不同類型數據的組合,前面提及的類也是不同類型數據及操作的組合,只不過是基本數據類型的組合而已。
#include <iostream> class Myclass {enum{NUM = 50};char cName[20];int iNum;Student stulist[Num];public:const char* Getclassname();const char* Getstuname(int ino); };Myclass::Myclass() {iNum = 0; }inline const char* Myclass::Getclassname() {return cName; }const char* Myclass::GetStuName(int iNo) {for(int i = 0; i < iNum; i++){if(stulist[i].Getno() == iNo)return stulist[i].GetName();}return NULL; }?
7.2 繼承
類的繼承就是新類從已有類哪里獲得已有的屬性和行為,或者說是基類派生了具有基類特征又有新特征的派生類。繼承是軟件可重用型的一種形式,新類通過繼承從現有類中吸取其屬性和行為,并對其進行覆蓋或改寫,產生新類所需要的功能。同樣,新類也可以派生出更新的類。創建新類,但不是從頭創建。可以使用其他人已經創建并調試過的類。關鍵是使用類,而不是更改已存在的代碼。
類繼承語法:
class 子類名:[public|private|protected] 父類名 {... }子類具有父類的所有屬性和行為,且可以增加新的行為和屬性。
C++提供了三種繼承的方式:公有繼承(public)、私有繼承(private)、保護繼承(protected)。
?
公有繼承
(1) 基類的private、public、protected成員的訪問屬性在派生類中保持不變。
(2)?派生類中繼承的成員函數可以直接訪問基類中所有成員,派生類中新增的成員函數只能訪問基類的public和protected成員,不能訪問基類的private成員。
(3)?通過派生類的對象只能訪問基類的public成員。
矩形移動:
#include <iostream> using namespace std; class Point {private:float x,y;public:void InitP(float xx = 0, float yy = 0){x = xx;y = yy;}void Move(float a, float b){x += a;y += b;}float Getx(){return x;}float Gety(){return y;} };class Rectangle:public Point {private:float w,h;public:void InitR(float x, float y, float w, float h){InitP(x, y);this->w = w;this->h = h;}float Geth(){return h;}float Getw(){return w;} };int main() {Rectangle rect;rect.InitR(2,3,20,10);//rect.h; //rect.w; //錯誤,父類私有成員rect.Move(3,2);cout << rect.Getx() << ',' <<rect.Gety() << ',' <<rect.Geth() << ',' <<rect.Getw() <<endl;return 0; }?
保護繼承
(1) 基類的public、protected成員都以protected身份出現在派生類中。
(2)?派生類中新增的成員函數可以直接訪問基類中的public和protected成員,但不能訪問基類的private成員。
(3)?通過派生類的對象不能訪問基類的任何成員。
?
私有繼承
(1) 基類的public、protected成員都以private身份出現在派生類中。
(2)?派生類中新增的成員函數可以直接訪問基類中的public和protected成員,但不能訪問基類的private成員。
(3)?通過派生類的對象不能訪問基類的任何成員。
例如:把上述程序的繼承改為私有繼承。
class Rectangle:private Point { private:float w,h; public:void InitR(float x, float y, float w, float h){InitP(x, y);this->w = w;this->h = h;}float geth(){retrun h;}float getw(){return w;} };?
當聲明的子對象調用父類的成員函數時,會發生錯誤:
Rectangle rect; Rect.InitP(); //錯誤,通過私有繼承,父類的InitP()在子類中變成私有的如果確實需要在子類里對私有繼承父類的公有成員變為公有的,只需要在子類中聲明為公有即可:
class Rectangle:private Point { ... public:Point::InitP;Point::getx; ... };注意:在派生類中聲明基類的函數時,只需要給出函數的名稱,函數的參數和返回值類型不應出現。
class Rectangle:private Point { ... public:Point::InitP;Point::getx;void Point::InitR(float x, float y, float w, float h); ???//錯誤Point::InitR(float x, float y, float w, float h); ?????? ?//錯誤Point::InitR(); ?????????????????????????????? ?//錯誤 ... };關于繼承,我將專門用一篇來介紹,這里只是先了解基本概念。
?
7.3繼承和組合
實際工作中往往需要在定義一個新類時這個新類的一部分內容時從已有類中繼承的,還有一部分內容則是需要由其他類組合的,這就需要把組合和繼承放在一起使用。例如,在定義Z類時它的一部分內容需要從X類繼承,另一部分內容則是Y類型的,具體定義:
class X {int i; }; class Y {float f; }; class Z:public X {double d;Y y; };組合通常在希望新類內部有已存在類性能時使用,而不希望已存在類作為它的接口。這就是說,嵌入一個計劃用于實現新類性能的對象,而新類的用戶看到的是新定義的接口,而不是來自父類的接口。
繼承是取一個已存在的類,并制作它的一個專門的版本。通常,這意味著取一個一般目的的類,并為了特殊的需要對它進行專門化。
建立班級類:
#include <iostream> #include <string.h> using namespace std;class Person {char strname[20];int iage;char csex;public:Person(const char* cpname = NULL, int age = 0, char sex = 'm');const char* Getname(){return strname;}int Getage(){return iage;}char Getsex(){return csex;}void Setname(const char *cpname);void Setage(int age){iage = age;}void Setsex(char sex){csex = sex;} };class Teacher:public Person {int wid;public:Teacher(const char* cpname = NULL, int age = 0, char sex = 'm', int no = 0):Person(cpname, age, sex),wid(no){}int Getwid(){return wid;}void Setwid(int no){wid = no;} };class Student:public Person {int ino;float score[5];float ave;public:Student(const char* cpname = NULL,int age = 0, char sex = 'm', int no = 0):Person(cpname, age, sex),ino(no){}int Getno(){return ino;}void Setno(int no){ino = no;}void Setscore(const float fdata[]);float Getavescore(){return ave;}void print(); };void Studentprint(Student &a) {cout << "Student's infomation:";cout << "Ino:" << a.Getno() << " " << "Age:" << a.Getage() << " " << "Name:" << a.Getname() << "Sex:" << a.Getsex() <<endl; }Person::Person(const char* cpname, int age, char sex) {csex = sex;iage = age;if(strlen(cpname) < 20){strcpy(strname, cpname);}else strname[0] = '\0'; }void Person::Setname(const char* cpname) {if(strlen(cpname) < 20)strcpy(strname, cpname);else strname[0] = '\0'; }void Student::Setscore(const float fdata[]) {float fsum = 0;for(int i = 0; i < 5; i++){score[i] = fdata[i];fsum += fdata[i];}ave = fsum/5; }int main() {Student a("Tyler", 23, 'm', 00001);Studentprint(a);return 0; }?
7.4構造與析構順序
當采用繼承方式創建子類對象時,首先從父類開始執行構造函數,父類的成員,然后再執行子類的構造函數,子類成員;當撤銷子類對象時,執行相反的順序,即首先撤銷子類的成員,執行子類的析構函數,在撤銷父類成員,執行父類的析構函數。
#include <iostream> using namespace std; class A {int a;public:A(int i = 0):a(i){cout << "A is constructed" <<endl;}~A(){cout << "A is destructed" <<endl;} };class B:public A {int b;public:B(int j = 0):b(j){cout << "B is constructed" <<endl;}~B(){cout << "B is destructed" <<endl;} };int main() {B b;return 0; }構造順序:
(1) 調用基類的構造函數。
(2) 根據類中聲明的順序構造組合對象。
(3) 派生類中構造函數的執行。
派生類中構造函數的格式:
class 派生類名:[public|private|protected]基類名 { public: 派生類名(參數列表1):基類名(參數列表2),組合對象列表{...} };組合類中構造函數與析構函數的調用順序:
#include <iostream> using namespace std;class X {public:X(){cout << "X is constructed" <<endl;}~X(){cout << "X is destructed" <<endl;} };class A {int a;X x;public:A(int i = 0){cout << "A is constructed" <<endl;}~A(){cout << "A is destructed" <<endl;} };class Y {int y;public:Y(int i = 0){y = i;cout << "Y is constructed" <<endl;}~Y(){cout << "Y is destructed" <<endl;} };class Z {int z;public:Z(int i = 0){z = i;cout << "Z is constructed" <<endl;}~Z(){cout << "Z is destructed" <<endl;} };class B:public A {int b;Y y;Z z;public:B(int j = 0):A(1),z(j),b(j),y(j){cout << "B is constructed" <<endl;}~B(){cout << "B is destructed" <<endl;} };int main() {B b;return 0; }如果基類的構造函數沒有參數,或者沒有顯示定義構造函數時,派生類構造時可以不向基類傳遞參數,甚至可以不定義構造函數。
注意:
(1)?派生類不能繼承基類的構造函數和析構函數。當基類帶有參數的構造函數時,派生類必須定義構造函數,以便把參數傳遞給基類構造函數。
(2)?當派生類也作為基類使用時,則各派生類只負責其直接的基類的構造。
(3) 因為析構函數不帶參數,派生類中析構函數的存在不依賴于基類,基類中析構函數的存在也不依賴于派生類。
繼承類中構造與析構函數的調用順序:
#include <iostream> using namespace std;class A {int a;public:A(int i = 0){cout << "A is constructed" <<endl;}~A(){cout << "A is destructed" <<endl;} };class Y {int y;public:Y(int i = 0){y = i;cout << "Y is constructed" <<endl;}~Y(){cout << "Y is destructed" <<endl;} };class B:public A {int b;Y y;public:B(int j = 0):b(j),y(j){cout << "B is constructed" <<endl;}~B(){cout << "B is destructed" <<endl;} };class C:public B {int c; public:C(int i):B(1),c(i){cout << "C is constructed" <<endl;}~C(){cout << "C is destructed" <<endl;} };int main() {C c(2);return 0; }?
7.5派生類重載基類函數的訪問
如果在基類中有一個函數名被重載幾次,在派生類中又重定義了這個函數名,則在派生類中會覆蓋這個函數的所有基類定義。也就是說,通過派生類來訪問該函數時,由于采用就近匹配的原則,只會調用在派生類中所定義的該函數,基類中所定義的函數都變得不再可用。
派生類中重載基類函數:
#include <iostream> using namespace std;class Number {public:void print(int i) {cout << i;}void print(char c) {cout << c;}void print(float f){cout << f;} };class Data:public Number {public:void print(){}void f(){//print(5); //錯誤,因為新定義的print()函數不帶參數} };int main() {Data data;data.print(); //正確data.print(1); //錯誤//data.print(12.3); //錯誤//data.print('a'); //錯誤return 0; }因為Data類中對print()函數做了重定義,所以沒有找到一個匹配的函數給Data類對象調用。如果要訪問基類中聲明的函數,則用以下方法:
(1)?使用作用域標識符限定。
Data.Number::print(5); ???//正確,被調用的函數是基類Number的print(int)(2)?避免名稱覆蓋。
class Number { public:void print(int i) ????{cout << i;}void print(char c) ??{cout << c;}void print(float f) ??{cout << f;} }; class Data:public Number { public:void print2(){}void f(){print(1);} };函數f()中調用基類函數print()的方法有稱為”向上映射”,即派生類向上調用基類的函數。向上映射是派生類中在不引用歧義的情形下才有,即沒有名稱覆蓋發生。
?
7.6多態性&虛函數
面向對象程序設計的真正優勢不僅僅在于繼承,還在于將派生類對象當基類對象一樣處理的功能。支持這種功能的機制就是多態和動態綁定。
多態是指同樣的消息被不同類型的對象接收時導致不同的行為。所謂消息是指對類的成員函數的調用,不同的行為是指不同的實現,也就是調用了不同的函數。最簡單的例子就是運算符,使用同樣的加號”+”,就可以實現整型數之間、浮點數之間、雙精度浮點數之間的加法,以及這幾種數據類型混合的加法運算。同樣的相加操作,被不同類型的對象接收后,不同類型的變量采用不同的方式進行加法運算。如果是不同類型的變量相加,例如浮點數和整型數,則要先將整型數轉換為浮點數,然后再進行加法運算,這就是典型的多態現象。
多態的類型:面向對象的多態性可以分為4類,重載多態、強制多態、包含多態和參數多態。前面兩種統稱為專用多態,而后面的兩種稱為通用多態。之前所學習的普通函數、類的成員函數的重載以及運算符重載都屬于重載多態。上述加法運算符在進行浮點數與整型數相加時,首先進行類型強制轉換,把整數變為浮點數再相加的情況,就是強制多態的實例。包含多態是類族定義與不同類中的同名成員函數的多態行為,主要是通過虛函數來實現的。參數多態與類模板相關聯,在使用時必須賦予實際的類型才可以實例化。這樣,由類模板實例化的各個類都具有相同的操作,而操作對象的類型卻各不相同。
多態性是面向對象程序設計的重要特征,重載和虛函數是體現多態性的兩個重要手段。虛函數體現了多態的靈活性,進一步減少冗余信息,顯著提高了軟件的可擴充性。通過學習函數重載與繼承方法后,經常會遇到以下問題:在派生類中存在對基類函數的重載,當通過派生類對象調用重載函數時卻調用了基類的原函數。
通過派生類對象間接調用重載函數:
#include <iostream> using namespace std;class Ins {public:void play() const{cout << "Ins::play" <<endl;} };class Piano:public Ins {public:void play() const{cout << "Piano::play" <<endl;} };void tune(Ins &i) //由參數類型決定 {i.play(); }int main() {Piano a;tune(a);return 0; }?
7.6.1靜態綁定與動態綁定
綁定,又稱聯編,是使一個計算機程序的不同部分彼此關聯的過程。根據進行綁定所處階段的不同,有兩種不同的綁定方法,靜態綁定和動態綁定。
(1)?靜態綁定在編譯階段完成,所有綁定過程都在程序開始之前完成。靜態綁定具有執行速度快的特點,因為在程序運行前,編譯程序能夠進行代碼優化。
函數重載(包括成員函數重載和派生類對基類函數的重載)就是靜態綁定。而上例中的問題根源在于:通過指針或引用引起的對普通成員函數的調用,由參數的類型決定,而在指針或引用實際指向的對象無關。這也是靜態綁定的限定性。
(2) 如果編譯器在編譯階段不確切地知道把發送到對象的消息和實現消息的哪段代碼具體聯系到一起,而是運行時才把函數調用與函數具體聯系在一起,就稱作動態綁定。相對于靜態綁定,動態綁定是在編譯后綁定,也稱晚綁定,又稱運行時識別。動態綁定具有靈活性好、更高級更自然的問題抽象、易于擴充和易于維護等特點。通過動態綁定,可以動態地根據指針或引用指向的對象實際類型來選擇調用的函數。
?
7.6.2虛函數
虛函數定義格式:
class 基類名 {...virtual 返回值類型 要在派生類中重載的函數名(參數列表); };用虛函數方法實現對派生類中重載函數的調用:
#include <iostream> using namespace std;class Ins {public:virtual void play() const{cout << "Ins::play" <<endl;} };class Piano:public Ins {public:void play() const{cout << "Piano::play" <<endl;} };class Newpiano:public Piano {public:void play() const{cout << "Newpiano::play" <<endl;} };void tune(Ins &i) //由參數類型決定 {i.play(); }int main() {Piano a;tune(a);Newpiano b;tune(b);Ins *p = &a;tune(*p);p = &b;tune(*p);Ins c;tune(c);return 0; }使用虛函數時需要注意:
(1)?必須在基類中聲明虛函數,即需要在派生類中重寫的函數,必須在基類中聲明為虛函數。
(2)?虛函數一經聲明,在派生類中重寫的基類中的函數即是虛函數,不需要再加virtual。
(3)?只有非靜態成員函數可以聲明為虛函數。靜態成員函數和全局函數不能聲明為虛函數。
(4)?編譯器把名稱相同、參數不同的函數看做不同的函數。基類和派生類中有相同名字但是參數列表不同的函數,不需要聲明為虛函數。
(5) 普通對象調用虛函數,系統仍然以靜態綁定方式調用函數。因為編譯器編譯時能確切知道對象的類型,能確切調用其成員函數。
Piano x; Newpiano y; X = y; tune(x);運行結果是Piano::play
?
7.6.3虛析構函數
通過學習虛函數,可以掌握虛函數在繼承和派生中的調用方式。
(1)?構造函數不能聲明為虛函數。因為構造函數有特殊的工作,它處在對象創建初期,首先調用基類構造函數,然后調用按照繼承順序派生的派生類的構造函數。
(2)?析構函數能夠且常常必須是虛函數。析構函數調用順序與構造函數完全相反,從最晚派生類開始,依次向上到基類。析構函數確切地知道它是從哪個類派生而來的。
虛析構函數聲明格式: virtual ~析構函數名稱(); 虛析構函數定義: class Ins { public:virtual void play(){cout << “Ins::play”?<<endl;}virtual ~Ins(); };虛函數的目的是讓派生類去做”自己想做的事”。所以應該在基類中聲明虛析構函數。當類中存在虛函數時,也應該使用虛析構函數。這樣保證類對象銷毀時得到”完整”空間。如果某個類不包含虛函數,一般是表示它將不作為一個基類來使用。當一個類不準備作為基類使用時,建議不要將析構函數聲明為虛函數,以保證程序執行的高效性。
?
7.7純虛函數和抽象基類
工作有時需要定義這么一個類,對這個類中的處理函數只需要說明函數的名稱、參數列表,以及返回值的類型,也就是只提供一個接口,以說明和規范其他程序對此服務的調用。至于這個函數如何實現,則根據具體需要在派生類中定義即可。通常把這樣的類稱為抽象基類,而把這樣的函數稱為純虛函數。純虛函數定義格式:
virtual 返回值類型 函數名稱(參數列表) = 0;當一個類中存在純虛函數時,這個類就是抽象類。
class Ins { public:virtual void play()const = 0; ??//純虛函數 };抽象類的主要作用是通過它為一個類建立一個公共的接口,使他們能夠更有效地發揮多態特性。使用抽象類時要注意:
(1)?抽象類只能用做其他類的基類,不能建立抽象類對象。抽象類處于繼承層次結構的較上層,抽象類自身無法實例化,只能通過繼承機制,生成抽象類的非抽象派生類,然后再實例化(抽象類提供了若干函數接口,當有其他類需要時,可以通過多重繼承來使用這些函數接口,而新派生類不是一個抽象類,所以可以實例化)。
(2)?抽象類不能用做參數類型、函數返回值或顯式轉換的類型。
(3)?可以聲明一個抽象類的指針和引用。通過指針或引用,可以指向并訪問派生類對象,以訪問派生類的成員。
(4)?抽象類派生出新的類之后,如果派生類給出所有純虛函數的函數實現,這個派生類就可以聲明自己的對象,因而不再是抽象類;反之,如果派生類沒有給出全部純虛函數的實現,這時的派生類仍然是一個抽象類。
用抽象類的方法:
#include <iostream> using namespace std;class Ins {public:virtual void play() const = 0; };class Piano:public Ins {public:void play() const{cout << "Piano::play" <<endl;} };class Newpiano:public Piano {public:void play() const{cout << "Newpiano::play" <<endl;} };void tune(Ins &i) //由參數類型決定 {i.play(); }int main() {Piano a;tune(a);Newpiano b;tune(b);Ins *p = &a;tune(*p);p = &b;tune(*p);//Ins c; //錯誤,Ins是抽象類,不能實例化return 0; }純虛函數非常有用,因為它使得類有明顯的抽象性,并告訴用戶和編譯器希望如何使用。在基類中,對純虛函數提供定義時有可能的,告訴編譯器不允許純抽象基類聲明對象,而且純虛函數在派生類中必須定義,以便于創建對象。然而,如果希望一塊代碼對于一些或所有派生類定義能共同使用,而不希望在每個函數中重復這段代碼,具體實現方法如下:
#include <iostream> using namespace std;class Ins {public:virtual void play() const = 0;virtual void showmsg() const{cout << "Ins::showmsg()" <<endl;} };class Piano:public Ins {public:void play() const{cout << "Piano::play" <<endl;}void showmsg()const{Ins::showmsg();} };class Newpiano:public Piano {public:void play() const{cout << "Newpiano::play" <<endl;}void showmsg()const{Ins::showmsg();} };void tune(Ins &i) //由參數類型決定 {i.play(); }int main() {Piano a;tune(a);a.showmsg();Newpiano b;tune(b);b.showmsg();return 0; }?
7.8 多重繼承
在派生類的聲明中,基類名可以有一個,也可以有多個。如果只有一個基類名,則這種繼承方式稱為單繼承;如果基類名有多個,則這種繼承方式稱為多繼承,這時的派生類同時得到了多個已有類的特征。
?
7.8.1多繼承語法
多繼承允許派生類有兩個或多個基類的能力,這是為了使多個類以這種方式組合起來,使得派生類對象的行為具有多個基類對象的特征。多繼承聲明語法:
class 派生類名:[繼承方式]基類名1,[繼承方式]基類名2,...[繼承方式]基類名n多繼承的使用:
#include <iostream> using namespace std;class A {int a;public:void SetA(int x){a = x;} };class B {int b;public:void SetB(int x){b = x;} };class C:public A, private B {int c;public:void SetC(int , int, int); };void C::SetC(int x, int y, int z) {SetA(x);SetB(y);c = z; }int main() {C obj;obj.SetA(5);obj.SetC(6, 7, 9);//obj.setB(6); //錯誤,不能訪問私有繼承的基類成員return 0; }?
7.8.2多繼承中的二義性
多繼承中的二義性例子:
#include <iostream> using namespace std;class A {int a;public:void f(int x){a = x;} };class B {int b;public:void f(int x){b = x;} };class C:public A, private B {int c;public:void SetC(int); }; void C::SetC(int x) {c = x; }int main() {C obj;//obj.f(5); //錯誤,不能訪問私有繼承的基類成員return 0; }對編譯器來說,不知道是調用基類A的f(),還是調用基類B的f()。解決的辦法是使用域名控制來解決:
obj.A::f(5); obj.B::f(5);增加作用域分辨符可以消除二義性,但降低了程序的可讀性。因此,盡量避免在基類中使用同名函數。
多繼承里還有這樣的情況:有相同基類帶來的二義性。例如:
class base { ... public:void show(); };class c1:public base { ... public:void show(); };class c2:public base { ... public:void show(); };class a:public c1,public c2 { ... public:void show(); };這種情況被稱為菱形繼承。類a的父類c1、c2繼承了基類base的成員函數show(),當然類a也繼承了成員函數show(),a調用show()則產生了二義性;類a繼承c1、c2的同時也包含了兩份基類base,菱形繼承使得子對象重疊,增加了額外的空間開銷。為了解決此類問題,C++引入了虛基類。
如把一個基類定義為虛基類,必須在派生子類時在父類的名字前加關鍵字virtual。
Class 派生類名:virtual 訪問權限修飾符 父類名{};
虛基類使用方法:
#include <iostream> using namespace std;class base {public:virtual char* show()const = 0; };class d1:virtual public base {public:char* show()const{return (char*)"d1";} }; class d2:virtual public base {public:char* show()const{return (char*)"d2";} }; class m:public d1,public d2 {public:char* show()const{return d2::show();} };int main() {m m1;cout << m1.show() <<endl;return 0; }?
7.8.3最終派生類
在上一個例子中,各類沒有構造函數,使用的是默認構造函數。如果類里有了構造函數,情形將有所不同。如在上面的base基類里增加構造函數:
Base(int){}則用派生類聲明對象時,編譯器報錯:沒有合適的構造函數調用。為解決此類問題,首先引入最終派生類的概念。
最終派生類,又稱為最晚輩派生類,指當前所在的類。例如,在基類base的構造函數里,base就是最終派生類;在派生類a里,a就稱為最終派生類;在類c1里,類c1就成為最終派生類。當使用虛基類時,尤其是帶有參數的構造函數的虛基類時,最終派生類的構造函數必須對虛基類初始化。不管派生類離虛基類多遠,都必須對虛基類初始化。
含虛基類的構造函數使用方法:
#include <iostream> using namespace std;class base {int i;public:base(int x):i(x){}int geti(){return i;}virtual char* show()const{return (char*)"base";} };class d1:virtual public base {int id1;public:d1(int x = 1):base(0),id1(x){}char* show()const{return (char*)"d1";} }; class d2:virtual public base {int id2;public:d2(int x = 2):base(1),id2(x){}char* show()const{return (char*)"d2";} }; class m:public d1,public d2 {int im;public:m(int x = 0):base(3),im(x){}char* show()const{return d2::show();} };int main() {m m1;cout << m1.show() <<endl;cout << m1.geti() <<endl;d1 d11;cout << d11.geti() <<endl;cout << d11.show() <<endl;return 0; }使用虛基類時要注意:
(1)?必須在派生類的構造函數中調用初始化虛基類的構造函數。
(2)?給虛基類安排默認構造函數。使得虛基類的使用者變得簡單易行。
?
7.8.4多繼承的構造順序
#include <iostream> using namespace std;class base {int i;public:base(int x):i(x){cout << "base is cnostructed" <<endl;}virtual ~base(){cout << "base is destructed" <<endl;}int geti(){return i;}virtual char* show()const = 0; };class d1:virtual public base {int id1;public:d1(int x = 1):base(0),id1(x){cout << "d1 is constructed" <<endl;}virtual ~d1(){cout << "d1 is destructed" <<endl;}char* show()const{return (char*)"d1";} }; class d2:virtual public base {int id2;public:d2(int x = 2):base(1),id2(x){cout << "d2 is constructed" <<endl;}virtual ~d2(){cout << "d2 is destructed" <<endl;}char* show()const{return (char*)"d2";} }; class m:public d1,public d2 {int im;public:m(int x = 0):base(3),im(x){cout << "m is constructed" <<endl;}virtual ~m(){cout << "m is destructed" <<endl;}char* show()const{return d2::show();} };int main() {m m1;cout << m1.show() <<endl;return 0; }多繼承構造順序與單繼承構造順序類似,從基類開始,沿著派生順序逐層向下,當同一層次派生同一個類時,按照聲明繼承的順序自左到右。例如,c1、c2派生a時,先構造c1,再構造c2。析構順序與構造順序相反。
?
7.9派生類成員的標識與訪問
經過類的派生,就形成了一個具有層次結構的類族。接下來就看看派生類使用過程中的一些問題,也就是標識和訪問派生類及其對象的成員問題。
派生類中,成員可以按訪問屬性劃分為以下4種:
(1)?不可訪問的成員。這是從基類私有成員繼承而來的,派生類或是建立派生類對象的模塊都沒有辦法訪問到它們,如果從派生類繼續派生新類,也是無法訪問的。
(2) 私有成員。包括從基類繼承過來的成員以及新增加的成員,在派生類內部可以訪問,但是建立派生類對象的模塊中無法訪問,繼續派生,就成為了新的派生類中的不可訪問成員。
(3) 保護成員。可能是新增也可能是從基類繼承過來的,派生類內部成員可以訪問,建立派生類對象的模塊無法訪問,進一步派生,在新的派生類中可以成為私有成員或保護成員。
(4) 公有成員。派生類、建立派生類的模塊都可以訪問,繼續派生,可能是新派生類中的私有、保護或者共有成員。
?
7.9.1作用域分辨符
作用域分辨符,就是我們經常見到的”::”,它可以用來限定要訪問的成員所在的類的名稱。一般使用形式是:
類名::成員名 ????????? //數據成員 類名::成員名(參數表) ???//函數成員對于在不同的作用域聲明的標識符,可見性原則是:如果存在兩個或多個具有包含關系的作用域,外層聲明了一個標識符,而內層沒有再次聲明同名標識符,那么外層標識符在內層仍然可見;如果在內層聲明了同名標識符,則外層標識符在內層不可見,這時稱內層標識符隱藏了外層同名標識符,這種現象稱為隱藏規則。
在類的派生層次結構中,基類的成員和派生類新增的成員都具有類作用域。二者的作用范圍不同,是相互包含的兩個層,派生類在內層。這是如果派生類聲明了一個和某個基類成員同名的新成員,派生的新成員就隱藏了外層同名成員,直接使用成員名只能訪問到派生類的成員。如果派生類中聲明了與基類成員函數同名的新函數,即使函數的參數表不同,從基類繼承的同名函數的所有重載形式也都會被隱藏。如果要訪問被隱藏的成員,就需要使用作用域分辨符和基類名來限定。
對于多繼承情況,首先要考慮各個基類之間有沒有任何繼承關系,同時考慮有沒有共同基類的情況。最經典的情況就是所有基類都沒有上級基類。如果某派生類的多個基類擁有同名的成員,同時,派生類有新增這樣的同名成員,在這種情況下,派生類成員將隱藏所有基類的同名成員。這時使用”對象名.成員名”或”對象指針->成員名”方式可以唯一標識和訪問派生類新增成員,基類的同名成員也可以使用基類名和作用域分辨符訪問。但是,如果派生類沒有聲明同名成員,”對象名.成員名”或”對象指針->成員名”方式就無法唯一標識成員。這時,從不同基類繼承過來的成員具有相同的名稱,同時具有相同的作用域,系統僅僅根據這些信息根本無法判斷到底是調用哪個基類的成員,這時就必須通過基類名和作用域分辨符來標識成員。
細節:如果子類中定義的函數與父類的函數同名但是具有不同的參數數量或參數類型,不屬于函數重載,這時子類中的函數將使父類中的函數隱藏,調用父類中的函數必須使用父類名稱來限定。只有在相同的作用域中定義的函數才可以重載。
#include <iostream> using namespace std; class Base1 { public:int var;void fun(){cout << “Member of Base1”?<<endl;} };class Base2 { public:int var;void fun(){cout << “Member of Base2”?<<endl} };class Derived:public Base1,public Base2 { public:int var;void fun(){cout << “Member of Derived”?<<endl;} };int main() {Derived d;Derived *p = &d;d.var = 1;d.fun();d.Base1::var = 2;d.Base2::fun();p->Base2::var = 3;p->Base2::fun();return 0; }在主函數中,創建了一個派生類的對象d,根據隱藏規則,如果通過成員名稱來訪問該類成員,就只能訪問到派生類新增加的兩個成員,從基類繼承過來的成員由于處于外層作用域而被隱藏。這時要想訪問從基類繼承來的成員,就必須使用類名和作用域分辨符。程序中后兩組語句就是分別訪問由基類Base1,Base2繼承來的成員。通過作用域分辨符,就明確地唯一標識了派生類中由基類所繼承來的成員,達到了訪問的目的,解決了成員被隱藏的問題。
如果將類Derived寫成這樣: class Derived:public Base1,public Base2{}; 此時在主函數中寫: d.var = 1; ??//錯誤,這樣會有二義性 d.fun(); ????//錯誤,這樣會有二義性如果希望d.var和d.fun()的用法不產生二義性,可以使用using關鍵字加以澄清。例如:
class Derived:public Base1,public Base2 { public:using Base1::var;using Base2::fun; };這樣,d.var和d.fun()都可以明確表示對Base1中相關成員的引用了。using的一般功能是將一個作用域中的名字引入到另一個作用域中,它還有一個非常有用的用法:將using用于基類中的函數名,這樣派生類中如果定義同名但參數不同的參數,基類的函數不會被隱藏,兩個重載的函數將會并存在派生類的作用域中。例如:
class Derived2:public Base1 { public:using Base1::fun;void fun(int i){...} };這時,使用Derived2的對象,既可以直接調用無參數的fun()函數,又可以直接調用帶int型參數的fun函數。
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結
以上是生活随笔為你收集整理的C++学习笔记:(五)继承 多态的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C++学习笔记:(三)静态与名字空间
- 下一篇: C++学习笔记:(六)public、pr