1. 程式人生 > 其它 >03C++複習.繼承/多型

03C++複習.繼承/多型

面向物件的三個基本特徵是:封裝、繼承、多型。其中,封裝可以隱藏實現細節,使得程式碼模組化;繼承可以擴充套件已存在的程式碼模組(類);它們的目的都是為了——程式碼重用。而多型則是為了實現另一個目的——介面重用!

封裝:將一類事物所共有的屬性和行為方法進行總結,封裝在一個類中。該類的形成過程稱為封裝,該封裝的類例項化的物件具有類的所有屬性和行為方法。
    封裝的類的內部按照訪問限定符可以分為:(public:)公有部分、(protected:)保護部分、(private:)私有部分。

一、繼承

1、簡介

面向物件程式設計中最重要的一個概念是繼承。繼承允許我們依據另一個類來定義一個類,這使得建立和維護一個應用程式變得更容易。這樣做,也達到了重用程式碼功能和提高執行時間的效果。

當建立一個類時,您不需要重新編寫新的資料成員和成員函式,只需指定新建的類繼承了一個已有的類的成員即可。這個已有的類稱為基類,新建的類稱為派生類。

2、分類

繼承分為:公有繼承 、 保護繼承 、 私有繼承。
(1)公有繼承: (格式: class Derived:public Base)
    父類的共有部分繼承到子類的公有部分,父類的保護部分繼承到子類的保護部分,父類的私有部分繼承到子類的私有部分(父類的私有部分在子類不可訪問)。
(2)保護繼承: (格式: class Derived:protected Base)
    父類的共有部分繼承到子類的保護部分,父類的保護部分繼承到子類的保護部分,父類的私有部分繼承到子類的私有部分(父類的私有部分在子類不可訪問)。
(3)私有繼承: (格式: class Derived:private Base)
    父類的所有都繼承到子類的私有部分,父類的私有部分在子類不可訪問。
注意:不管何種型別的繼承關係,父類私有成員到子類中都不能被訪問。

3、例項

(1)公開繼承

using namespace std; 
//基類A
class A
{ 
public: 
    int a; 
    int geta()
    { 
        a = 300; 
        return a; 
    } 
    /*保護型別成員在本類和子類中可以訪問*/ 
protected: 
    int b; 
private: 
    int c; 
}; 
//派生類B
class B:public A//公開繼承
{ 
public: 
    void getb()
    { 
        b = 200; 
    } 
    
    void show()
    { 
        cout << b << endl; 
    } 
    void showa()
    { 
        cout<< a<<endl; 
    } 
}; 
/*關鍵在於如何設定介面,成功合理的訪問到各種型別的資料*/ 
int main(){ 
    B pex; 
    /*公開繼承public成員依舊是public,所以可以類外訪問*/ 
    pex.a = 100; 
    /*b是保護型別成員,可以通過設定public介面來訪問*/ 
    pex.getb(); 
    pex.show(); 
    /*隱藏成員的問題,怎麼訪問到隱藏的成員*/ 
    pex.geta(); 
    pex.showa(); 
    //A a = pex;//子類型別賦給了父類型別 
    //a.geta(); 
    //cout << a.a << endl; 
}

(2)私有繼承

#include <iostream> 

using namespace std; 

class A
{ 
private: 
    void showa()
    { 
        cout << "this is showa()" << endl; 
    } 
protected: 
    void showb()
    { 
        cout << "this is showb" << endl; 
    } 
public: 
    void showc()
    { 
        cout << "this is showc" << endl; 
    } 
    void geta()
    {
        //設定合理的介面訪問A中的私有資料 
        showa(); 
    } 
}; 
class B:private A//私有繼承
{ 
public: 
    void show()
    { 
        showc(); 
        showb(); 
        geta(); 
    } 
}; 

int main()
{ 
    B b; 
    //A a = b;對比公開繼承,對比一下 
    b.show(); 
}

4、友元類

突破成員訪問許可權,可以設定合理的訪問介面,也可以使用友元類。下面我們看一下,友元類的使用:

#include <iostream> 
using namespace std; 

class A
{ 
private: 
    int x; 
    int y; 
public: 
    A():x(10),y(123){} 
    /*B,C宣告為A的友元類之後,可以訪問到父類的所有型別成員*/ 
    friend class B; 
    friend class C; 
}; 

class B:public A
{ 
public: 
    void show()
    { 
        cout << x << "---" << y << endl; 
    } 
}; 

class C
{ 
public: 
    void show()
    { 
        A a; c
        out <<a.x<< "---" << a.y << endl; 
    } 
}; 
int main()
{ 
    B b; 
    b.show(); 
    C c; 
    c.show(); 
}

5、繼承中的建構函式、解構函式、過載賦值運算子和拷貝建構函式

建構函式和解構函式是不能被繼承的,但是可以被呼叫。並且子類一定會呼叫父類的建構函式;

子類建立物件時,首先呼叫父類的建構函式,再呼叫子類自己的建構函式;子類建立的物件被釋放時,先呼叫子類自己的解構函式,再呼叫父類的解構函式

被繼承,但是可以被呼叫,而且子類肯定會呼叫父類的建構函式 
和解構函式。這種機制可以很自然的用於訪問父類的私有成員*/ 
#include <iostream> 
using namespace std; 

class A
{ 
private: 
    int x; 
public: 
    A(int x = 0):x(x)
    { 
        cout <<"A()構造"<<endl; 
        cout << x << endl;
    }
    
    ~A()
    {
        cout << "~A()" << endl;
    } 
    
    int _get()
    { 
        return x; 
    }
}; 
class B:public A
{ 
public: 
    /*在初始化引數列表中可以指定呼叫父類的建構函式,
    指定呼叫建構函式並且給 父類中的私有成員賦值*/ 
    /*注意:子類預設呼叫父類的無參構造,如果下面的程式碼沒有:A(100),
    則會呼叫無參構造,但是父類無參構造 被註釋掉,所以會出錯*/ 
    B():A(100)
    { //x = 200; //A(100); 
        cout << "B()" << endl; 
    } 
    //訪問有參構造的方式,理解這種方式的作用
    /*注意,這種機制下的建構函式所賦的值是賦到了子類中的資料x中, 
    而父類中的x仍然為0*/ 
    ~B()
    {
        cout << "~B()" << endl;
    } 
    int getbx()
    { 
        return _get(); 
    } 
}; 
int main()
{ 
    A a;//構建A物件,此時A類構造被呼叫,並打印出了值 
    B b;//B類為無參構造,首先呼叫了A的構造,在呼叫B的構造 
    
    //列印a物件中的x成員 
    cout <<a._get()<<endl;//a物件中的x為0 
    
    //列印b物件中的x 
    cout << b.getbx()<<endl;//是100 
    /*一層一層的退,先呼叫b的析構,在呼叫a的析構*/ 
}

拷貝建構函式和賦值運算子函式也不能被繼承:在子類不提供拷貝構造和賦值運算子時,子類預設呼叫父類的賦值運算子和拷貝建構函式。但子類一旦提供拷貝構造和賦值運算子函式則不再呼叫父類拷貝構造和賦值運算子函式。

using namespace std; 
/*系統一旦提供建構函式,系統預設的建構函式將被回收 
記住,拷貝構造也是建構函式*/ 
class A
{ 
    int arr; 
    public: A(){} 
    //A(int x = 0):arr(x){} 
    A(const A& a)
    { 
        cout << "父類拷貝構造" << endl; 
    } 
    
    void operator=(const A& a)
    { 
        cout << "父類賦值運算子函式" << endl; 
    } 
}; 
/*有指標型別的成員時,採用預設機制就麻煩了*/ 
class B:public A
{ 
    //int * pi; 
public: 
    B(){} 
    B(const B& b):A(b)
    { 
        //子類中提供了拷貝建構函式將不再呼叫父類的拷貝構造 
        cout << "子類拷貝構造" << endl; 
    } 
    void operator=(const B& b)
    { 
        A::operator=(b); //呼叫父類的拷貝建構函式的機制 
        cout << "子類賦值運算子函式"<< endl; 
    } 
}; 
int main()
{ 
    B a; 
    B b = a; 
    B c; 
    c = a; 
}

6、名字隱藏機制

名字隱藏機制:子類中如果定義了和父類中同名的資料,這些資料包括成員變數和成員函式。則會把父類中的資料隱藏掉。
    注意:只要名字相同,計算返回值或者形參列表不同,也會被隱藏。隱藏不代表就沒有了,可以通過類名作用域::訪問到被隱藏的成員。

using namespace std; 

class A
{ 
public: 
    int x; 
    int show()
    { 
        cout << "show A" << endl; 
        return 0; 
    } 
    A()
    {
        x=20;
    } 
    A(int x):x(x)
    {
        cout << "show A(int x)" << endl;
    } 
    void shouu()
    { 
        cout <<"shouu()"<<endl; 
    } 
}; 

class B:public A
{ 
public: 
    int x; 
    int y; 
    B()
    {
        cout << "B()" << endl;
    } 
    void show()
    { 
        cout << "show B" << endl; 
        //A::show(); 
    } 
}; 
int main()
{ 
    B b; 
    b.shouu(); 
    //cout << b.x << endl; 
    //cout << b.A::x << endl; //突破名字隱藏機制 
    //int c = b.show();被隱藏,無法訪問 
    //b.A::show(); 
}

7、多繼承和函式重寫

多繼承是c++特有的語法機制,表現為一個子類有多個直接的父類。

using namespace std; 
class phone
{ 
    double price; 
public: 
    //phone(); 
    phone(double price = 15):price(price)
    {
        cout << "phone" << endl;
    } 
    ~phone()
    {
        cout << "~phone" << endl;
    } 
    void call()
    { 
        cout << "use calling" << endl; 
    } 
    double getprice()
    { 
        return price; 
    } 
}; 
                        
class MP3
{ 
    double price; 
public: 
    MP3(double price = 20):price(price)
    {
        cout << "MP3" << endl;
    } 
    ~MP3()
    {
        cout << "~MP3" << endl;
    } 
    void play()
    { 
        cout << "use to listening music" << endl; 
    } 
    double getprice()
    { 
        return price; 
    } 
}; 

class vedio
{ 
    double price; 
public: 
    vedio(double price = 0):price(price)
    {
        cout << "vedio" << endl;
    } 
    ~vedio()
    {
        cout << "~vedio" << endl;
    } 
    void vcd()
    { 
        cout << "watch vedio" << endl; 
    } 
    double getprice()
    { 
        return price; 
    } 
}; 
/*多繼承*/ 
class iphone:public phone,public MP3,public vedio
{ 
public: 
    double getprice()
    { 
        return phone::getprice() + MP3::getprice() + vedio::getprice(); 
    } 
}; 

int main()
{ 
    iphone iphone6; 
    //cout << sizeof(iphone) << endl; 
    cout << iphone6.MP3::getprice() << endl; 
    cout << iphone6.phone::getprice() << endl; 
    //用名字隱藏機制解決多分資料同名衝突的問題 
    cout << iphone6.getprice() << endl; 
}

多繼承遇到的問題:上面的程式碼用sizeof就可以看到,子類在多繼承的時候會多次複製頂層資料,而我們期望的是price這個成員只需要複製一份就可以了,因為多餘的複製是無意義的。首先採用頂層抽象的方式,將三個父類抽象到更高的層面上。

using namespace std; 
/*抽象到更高層的類中*/ 
class product
{ 
    double price; 
public: 
    double getprice()
    { 
        return price; 
    } 
    product(double price = 0):price(price)
    {
        cout <<"product"<<endl;
    } 
}; 

class phone:public product{ 
public: 
    //phone(); 
    phone(double price = 15):product(price)
    {
        cout << "phone" << endl;
    } 
    ~phone()
    {
        cout << "~phone" << endl;
    } 
    void call()
    { 
        cout << "use calling" << endl; 
    } 
}; 

class MP3:public product
{ 
public: 
    MP3(double price = 20):product(price){cout << "MP3" << endl;} 
    ~MP3(){cout << "~MP3" << endl;} 
    void play(){ cout << "use to listening music" << endl; } 
}; 

class vedio:public product{ 
public: 
    vedio(double price = 0):product(price){cout << "vedio" << endl;} 
    ~vedio(){cout << "~vedio" << endl;} 
    void vcd(){ cout << "watch vedio" << endl; } 
}; 

class iphone:public phone,public MP3,public vedio
{ 

};

 int main()
 { 
     iphone iphone6; 
     //cout << iphone6.getprice() << endl;同樣會產生衝突的問題 
     //cout << sizeof(iphone) << endl; 
     cout << iphone6.MP3::getprice() << endl; 
     cout << iphone6.phone::getprice() << endl; 
     //直接呼叫產生衝突問題,編譯器不知道該呼叫哪一個
     //cout << iphone6.getprice() << endl;
}

上面的程式碼中,product的建構函式 被呼叫了三次,因為這種繼承是一級一級的來的,構造子類的時候找父類,發現父類還有父類,就去呼叫爺爺類的建構函式,三次繼承,三次呼叫。

這種繼承方式構成了一種菱形或者鑽石型的繼承,叫做菱形繼承或者鑽石繼承,但鑽石繼承並沒有實際解決資料多次複製的問題,為了解決菱形繼承,c++提出了虛繼承。虛繼承就是在繼承的時候加上virtual關鍵字修飾即可。虛繼承對於共同的成員父親類從爺爺類那裡繼承來的,這裡為double price,子類直接越級訪問,直接從爺爺類那裡繼承price。

#include <iostream> 
using namespace std; 

/*抽象到更高層的類中*/ 
class product
{ 
    int price; 
public: 
    int getprice(){ return price; } 
    product(double price = 0):price(price){cout << "product" << endl;
    } 
}; 
//C++ 虛繼承
class phone:virtual public product
{ 
public: 
    //phone(); 
    phone(double price = 15):product(price){cout << "phone" << endl;}
    ~phone(){cout << "~phone" << endl;} 
    void call(){ cout << "use calling" << endl; } 
}; 

class MP3:virtual public product
{ 
public: 
    MP3(double price = 20):product(price){cout << "MP3" << endl;} 
    ~MP3(){cout << "~MP3" << endl;} 
    void play(){ cout << "use to listening music" << endl; } 
}; 

class vedio:virtual public product
{ 
public: 
    vedio(double price = 0):product(price){cout << "vedio" << endl;} 
    ~vedio(){cout << "~vedio" << endl;} 
    void vcd(){ cout << "watch vedio" << endl; } 
}; 

class iphone:virtual public phone,virtual public MP3,virtual public vedio
{ 
public: 
    iphone(int m = 0,int v = 0,int p = 0):product(m + p + v){} 
};
 /*虛擬函式之後,product的建構函式只被呼叫了一次,孫子類直接越級訪問 了product類*/ 
 int main()
 { 
     iphone iphone6(1000,2041,3201); 
     //cout << iphone6.getprice() << endl;同樣會產生衝突的問題 
     //cout << sizeof(iphone) << endl; 
     //cout << iphone6.MP3::getprice() << endl; 
     //cout << iphone6.phone::getprice() << endl; 
     //cout << iphone6.getprice() << endl;直接呼叫產生衝突問題,編譯器不知道該呼叫哪一個 
     cout << sizeof(iphone) << endl; 
     cout << iphone6.getprice() << endl; 
}

這個程式碼中product的建構函式只調用了一次,說明子類直接越級訪問了爺爺類的資料。而對於父類特有的子類照常繼承,只是沒有通過父類去繼承爺爺類的資料成員,所以product的建構函式只被呼叫了一次。

8、虛擬函式

在函式前面加上virtual關鍵字修飾過的就是虛擬函式:

using namespace std;
class A{
	int x;
public:
	virtual void show(){}
	virtual void showa(){}
};
int main(){
	cout << sizeof(A) << endl;
}

虛擬函式的主要表現為會佔用四個位元組的空間,只要成員中出現虛擬函式,不管有多少個虛擬函式,都只用四個位元組來維護這個虛關係。虛擬函式會影響物件的大小。維護虛關係使用一個指標來維護的,所以是四個位元組。

9、函式重寫

在父類中出現一個虛擬函式,如果在子類中提供和父類同名的函式(注意區分名字隱藏),這就加函式重寫。

函式重寫要求必須有相同函式名,相同的引數列表,相同的返回值。

二、多型

1、簡介

多型:一個父型別的物件的指標或者引用指向或者是引用一個子類物件時,呼叫父型別中的虛擬函式,如果子類覆蓋了虛擬函式,則呼叫的表現是子類覆蓋之後的。

多型產生的必要條件:
    (1)繼承是構成多型的基礎;
    (2)虛擬函式是構成多型的關鍵;
    (3)函式覆蓋是構成多型的必備條件;
    (4)多型的應用:函式引數,函式返回值。

#include <iostream> 
using namespace std; 
class Animal
{ 
public: 
    virtual void run(){ cout <<"Ainimal run()"<<endl; } 
    void show(){ cout <<"Animal show()"<<endl; } 
}; 

class Dog:public Animal
{ 
public: 
    void run(){ cout <<"Dog run()"<<endl; } 
    void show(){ cout <<"dog show()"<<endl; } 
}; 
class Cat:public Animal
{ 
public: 
    void run(){ cout <<"cat run()"<<endl; } 
}; 
/*多型用作函式引數*/ 
void showAnimal(Animal * animal)
{ 
    animal->show(); animal->run(); 
} 
/*多型用作返回值*/ 
Animal * getAnimal(int x)
{ 
    if (1 == x) 
        return new Dog(); 
    if (2 == x) 
        return new Cat(); 
} 
int main()
{ 
    Cat cat; 
    showAnimal(&cat); 
    Dog dog; 
    showAnimal(&dog); 
}

2、多型的實現原理

多型的實現主要依賴於下面的三個東西:
虛擬函式:成員函式加了virtual修飾

虛擬函式表指標:一個型別有虛擬函式,則對這個型別提供一個指標,這個指標放在生成物件的前四個位元組。同類型的物件共享一張虛擬函式表。並且不同型別的虛擬函式表地址不同。

虛擬函式表:虛擬函式表中的每個元素都是虛擬函式的地址。

一個型別一旦出現虛擬函式,則會生成一張虛擬函式表,虛擬函式表中存放的就是虛擬函式的函式地址,通過這個函式地址可以到程式碼區中去執行對應的函式。虛擬函式表中只存放型別中的虛擬函式,不是虛擬函式的一概不管。在每個生成的型別物件的前四個位元組中存放的是虛擬函式表指標,通過這個指標可以訪問到虛擬函式表,從而訪問其中的函式。同種型別共享虛擬函式表,不同型別有自己獨立的虛擬函式表,繼承關係中的子類和父類屬於不同型別,所以有自己獨立的函式表。

#include <iostream> 
#include <cstring> 
using namespace std; 
class Animal
{ 
	int x; 
public: 
	virtual void fun(){ cout<< "Aniaml fun()"<<endl; } 
	virtual void run(){ cout <<"Animal run()"<<endl; } 
	void show(){ cout <<"Animal show()"<<endl; } 
}; 
class Dog:public Animal
{ 
public: 
	virtual void fun(){ cout << "dog run"<<endl; } 
	void run(){ cout <<"dog run"<<endl; } 
}; 
class Cat:public Animal
{ 
public: 
	void fun(){ cout <<"cat fun"<<endl; } 
}; 
int main()
{ 
	Animal a; 
	Animal b; /*取出虛擬函式表的地址並列印*/ 
	int * pi = (int*)&a; 
	cout <<showbase<< hex << *pi<<endl; 
	pi = reinterpret_cast<int*>(&b); 
	cout <<showbase<< hex << *pi<<endl; /*子類不會和父類共享虛擬函式表,地址不一樣*/ 
	Dog dog; 
	pi = reinterpret_cast<int*>(&dog); 
	cout <<showbase<< hex << *pi<<endl; 
	Animal * pcat = new Cat(); 
	pcat->run(); 
	pcat->fun(); /*更改dog的虛表的值,我們把dog的虛表地址 改成cat的虛表地址*/ 
	Animal * pdog = new Dog(); 
	pdog->run(); 
	pdog->fun(); /*更換dog的虛表地址,將cat的前四個位元組 移動到dog的前四個位元組*/ 
	memcpy(pdog,pcat,4); 
	pdog->run(); 
	pdog->fun(); /*上面的更改後,狗變成了貓的特性*/ 
}

虛表中存放的就是虛擬函式的函式指標,可以理解為函式指標的陣列,通過typedef將指標降級。

using namespace std; 

class Animal
{ 
public: 
	virtual void run(int x){ cout <<"run x="<<x<<endl; } 
	virtual void fun(int x){ cout <<"fun x="<<x<<endl; } 
	void show(){ cout <<"this is show()"<<endl; } 
}; 
int main() 
{ 
	/*去掉函式名就是函式指標的型別,指標簡化操作*/ 
	typedef void (*MFUN)(Animal* a,int x);/*MFUN就是虛表中的函式指標型別*/ 
	
	typedef MFUN* VTABLE;//MFUN*就是虛表型別 
	
	Animal animal; 
	VTABLE vt = *((VTABLE*)&animal); 
	/*虛擬函式表表現為函式指標陣列*/ 
	vt[0](&animal,100); 
	vt[1](&animal,123); 
	return 0; 
}

3、虛解構函式

virtual關鍵字只能修飾成員函式或者解構函式,其他的函式都不行。

當我們用new建立一個指向子類物件的父類指標時,例如Animal * animal = new Dog()時,其中Animal時父類,Dog是子類,並且delete animal時,其子類物件的解構函式不會被呼叫,只會呼叫父類的解構函式。所以就會遇到一個問題,如果子類物件有自己獨立的堆記憶體時,這部分記憶體就無法釋放。這時,我們只需要在父類的解構函式上用virtual修飾即可,子類解構函式就會被呼叫。

using namespace std; 

class Animal
{ 
public: 
	Animal(){ cout <<"Animal()"<<endl; } 
	virtual ~Animal(){ cout <<"~Animal()"<<endl; } 
}; 
class Dog:public Animal
{ 
public: 
	Dog(){ cout <<"Dog()"<<endl; } 
	~Dog(){ cout <<"~Dog()"<<endl; } 
}; 
int main()
{ 
	Animal * pa = new Dog(); 
	delete pa; 
	/*子類解構函式的呼叫必然引發父類析構*/ 
}

虛解構函式的應用場景:

(1)當父類中有虛擬函式時,應該把父類的解構函式定義成虛解構函式。

(2)子類和父類中都有自己的堆記憶體分配時。

轉自https://blog.csdn.net/sinat_33924041/article/details/83617501
------------恢復內容結束------------