1. 程式人生 > >c++:多型的詳解(理解)

c++:多型的詳解(理解)

目錄

3抽象類

6物件模型

2:單繼承

3:多繼承

1多型概念:

通俗來講,就是多種形態,同一事物在不同場景下表現出的不同狀態。

2多型實現:

c++的多型是在繼承的基礎上,增加了虛擬函式,並且讓派生類對基類的虛擬函式進行重寫

2.1虛擬函式

在類的成員函式前新增virtual關鍵字即可,稱該函式為虛擬函式

class Person
{
public:
      virtual void BuyTicket()
     {
     cout << "買票全價" << endl;
     }
};

總結:

  1. 派生類重寫基類的虛擬函式實現多型,要求函式名,引數列表,返回值完全相同(協變除外)
  2. 基類中定義虛擬函式,派生類中該函式始終保持虛擬函式特性。
  3. 只有類的成員函式才能被定義成虛擬函式。
  4. 靜態成員函式不能定義成虛擬函式。
  5. 如果在類外定義虛擬函式,只能在宣告是加上virtual,類外定義時不能加virtual。
  6. 建構函式不能定義為虛擬函式,雖然可以將operator=定義為虛擬函式,但最好不要,容易引起混淆
  7. 不要在構造或者解構函式中呼叫虛擬函式,在建構函式和解構函式中物件是不完整的,可能會發生未定義的行為
  8. 最好把基類的解構函式宣告為虛擬函式(這樣我們可以通過基類的指標或引用去釋放子類的資源,防止記憶體洩漏)

2.2重寫(覆蓋)

重寫是針對基類中虛擬函式的,在派生類中實現一個與基類虛擬函式原型(返回值型別、函式名字、引數列表)相同的虛 函式,即派生類與基類中虛擬函式的原型完全相同,才稱之為對基類虛擬函式的重寫。

class Person 
{
public:
	virtual void BuyTicket()
	{
		cout << "買票全價" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "買票半價" << endl;
	}
};

但是有兩個類外:

 1 協變:基類中虛擬函式返回基類物件的指標或引用,派生類域基類同名虛擬函式返回派生類的指標或引用,這種情況也構成重寫,                 但是此時派生類與基類返回值型別不同

 2 解構函式:基類中的解構函式如果是虛擬函式,只要派生類的解構函式顯示提供,就構成重寫(此種情況派生類與基類虛擬函式名不同)

2.3過載,覆蓋(重寫),隱藏(重定義)的區別

 2.4多型的構成條件

1基類中一定要構成虛擬函式,並且派生類中一定要對基類的虛擬函式進行重寫

2通過基類物件的指標或引用呼叫虛擬函式

class Person 
{
public:
	virtual void BuyTicket()
	{
		cout << "買票全價" << endl;
	}
};
class Student : public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "買票半價" << endl;
	}
};

void Func(Person &p)
{
	p.BuyTicket();
}

void test()
{
	Person a;
	Func(a);
	Student b;
	Func(b);
}

普通呼叫跟物件型別有關,多型呼叫跟具體物件有關

2.5動態繫結與靜態繫結

靜態繫結:又稱為前期繫結,在程式編譯期間確定了程式的行為(編譯時確定函式地址),也稱為靜態多型

靜態繫結一定發生在編譯期間,編譯器根據所傳遞引數的型別確定具體應該呼叫那個函式,如果有對應函式就呼叫,否則可能會發生隱式型別轉換或者報錯。

 動態繫結:又稱後期繫結,在程式執行期間(執行時到虛表中尋找呼叫函式的地址),根據具體拿到的型別確定程式的具體行為(與物件有關),呼叫具體函式,也稱為動態多型   

(通常:虛擬函式是動態繫結,非虛擬函式是靜態繫結,預設引數值也是靜態繫結)

3抽象類

在成員函式(必須是虛擬函式)的形參列表後面寫上=0,則成員函式為純虛擬函式,包含純虛擬函式的類叫做抽象類(也叫做介面類),抽象類不能例項化出物件,純虛擬函式在派生類中重新定義以後,派生類才能例項化出物件。(一種強制子類重寫虛擬函式的機制)

【面試題】

哪些函式不能被定義為虛擬函式?

1:不能被繼承的函式2:不能被重寫的函式3:普通函式,友元函式,建構函式,內聯成員函式,靜態成員函式。

【注意點】

  1. 虛擬函式重寫:函式必須完全相同,返回值可以不同,但必須是父子類的指標或引用
  2. 子類重寫虛擬函式可以不加virtual(c++語法坑)
  3. 普通函式不能定義為虛擬函式(只有成員函式可以)
  4. 靜態成員不能定義為虛擬函式(呼叫不需要物件,沒有this指標)
  5. inline行內函數不能定義虛擬函式,(呼叫直接展開,可以理解為沒有地址,沒有指標)
  6. 建構函式,拷貝建構函式不能定義為虛擬函式,operator=(可以,但無意義,也不構成重寫)
  7. 基類定義了static成員,無論派生出多少子類,都只有一個static例項

4帶有虛擬函式物件模型剖析

1: 包含有虛擬函式的類物件與普通類物件的區別

class B1
{
public:
    void TestFunc()
    {}
    int _b;
};
class B2
{
public:
    virtual void TestFunc()
    {}
    int _b;
};
void TestVirtualFunc()
{
    cout<<"sizeof(B1) = "<<sizeof(B1)<<endl;
    cout<<"sizeof(B2) = "<<sizeof(B2)<<endl;
}

一個類物件中實際只包括了該類中非靜態的成員變數,所以B1和B2大小應該都是4位元組,

實際B2增加了虛擬函式,導致B2多了4個位元組

2:虛擬函式表

帶有虛擬函式的類物件模型中多了4個位元組,前4個位元組中放置了一個特殊資料。

B2類物件中前4位元組所放地址指向的位置中存放的是虛擬函式的地址。一般將存放 虛擬函式的位置稱作為虛擬函式表,簡稱虛表將指物件前4個位元組指向虛表的指標稱為虛表指標。

3:列印虛表

typedef void (*PVFT)();
//PVFT型別的指標,指向了返回值和引數的為空的函式指標
void PrintVFT(B2& b, const string& str)
{
cout<<str<<endl;
//&b:從物件前4位元組取出虛表地址
//(int*)&b:強制轉換為int型別的指標,利用int型別和指標都是4位元組長度,獲取虛擬函式指標
//(*(int*)&b):解引用取出虛擬函式
//(PVFT*)(*(int*)&b):強制型別轉換為PVFT型別
PVFT* pVFT = (PVFT*)(*(int*)&b);
while(*pVFT)
{
// 呼叫該虛擬函式
(*pVFT)();
++pVFT;
}
cout<<endl;
}

結論:基類虛擬函式在虛表中的次序與其在類中生命次序一致。(按順序連續存放最後空地址)

5基類與派生類虛表中內容

先將基類中的虛表內容拷貝一份到派生類虛表中。

如果派生類重寫了基類中某個虛擬函式,用派生類自己的虛擬函式替換虛表中基類的虛擬函式。

派生類自己新增加的虛擬函式按其在派生類中的生命次序增加到派生類虛表的最後。

6物件模型

1:虛擬函式呼叫過程

2:單繼承

3:多繼承

多繼承:子類的虛擬函式放在第一個繼承類的虛擬函式表中