C++數(shù)據(jù)結(jié)構(gòu)分析多態(tài)的實現(xiàn)與原理及抽象類
??上一篇博客我和大家聊了聊關(guān)于繼承的內(nèi)容,繼承是C++的三大特性之一,今天要和大家聊一聊有關(guān)C++的三大特性中的最后一個——多態(tài)。 ??博客代碼已上傳至gitee:https://gitee.com/byte-binxin/cpp-class-code
??多態(tài)的
??概念
多態(tài): 從字面意思來看,就是事物的多種形態(tài)。用C++的語言說就是不同的對象去完成同一個行為會產(chǎn)生不同的效果。
??虛函數(shù)
虛函數(shù): 被virtual關(guān)鍵字修飾的類成員函數(shù)叫做虛函數(shù)。
實例演示: 看一下代碼,其中BuyTicket成員函數(shù)被virtual關(guān)鍵字修飾
class Person
{
public:
// 虛函數(shù)
virtual void BuyTicket()
{
cout << "買票全價" << endl;
}
};
??多態(tài)構(gòu)成的條件
多態(tài)是在不同繼承關(guān)系的類對象,去調(diào)用同一函數(shù),產(chǎn)生了不同的行為。
繼承中構(gòu)成多態(tài)有兩個條件:
- 必須有基類的指針或引用調(diào)用
- 被調(diào)用的函數(shù)必須是虛函數(shù),其派生類必須對基類的虛函數(shù)進(jìn)行重寫
虛函數(shù)的重寫是什么?
虛函數(shù)的重寫(覆蓋): 派生類中有一個跟基類完全相同的虛函數(shù)(即派生類虛函數(shù)與基類虛函數(shù)的返回值類型、函數(shù)名字、參數(shù)列表完全相同),稱子類的虛函數(shù)重寫了基類的虛函數(shù)。(重寫是對函數(shù)體進(jìn)行重寫)
實例演示:
class Person
{
public:
virtual void BuyTicket()
{
cout << "買票全價" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket() // 這里也可以不寫virtual,因為基類的虛函數(shù)屬性已經(jīng)被保留下來了,這里只是完成虛函數(shù)的重寫
{
cout << "買票半價" << endl;
}
};
虛函數(shù)重寫的兩個例外:
1.協(xié)變:基類和派生類的虛函數(shù)的返回類型不同
派生類重寫基類虛函數(shù)時,與基類虛函數(shù)返回值類型不同。即基類虛函數(shù)返回基類對象的指針或者引用,派生類虛函數(shù)返回派生類對象的指針或者引用時,稱為協(xié)變。(也就是基類虛函數(shù)的返回類型和派生類的虛函數(shù)的返回類型是父子類型的指針或引用)
// 協(xié)變 返回值類型不同,但它們之間是父子或父父關(guān)系 返回類型是指針或者引用
// 基類虛函數(shù) 返回類型 是 基類的指針或者引用
// 派生類虛函數(shù) 返回類型 是 基類或派生類的返回類型是基類的指針或引用
class A {};
class B : public A {};
class Person {
public:
virtual A* f() { return new A; }
};
class Student : public Person {
public:
virtual A* f() { return new B; }
};
2.析構(gòu)函數(shù)的重寫 基類與派生類的析構(gòu)函數(shù)的函數(shù)名不同
我在上一篇博客中說到過,基類和派生類的析構(gòu)函數(shù)的函數(shù)名會被編譯器統(tǒng)一處理成destructor,所以只要基類的析構(gòu)函數(shù)加了關(guān)鍵字virtual,就會和派生類的析構(gòu)函數(shù)構(gòu)成重寫。
我們再回到多態(tài)構(gòu)成的兩個條件中,完成基類虛函數(shù)的重寫我已經(jīng)介紹了,還有一個必須由基類的指針或引用調(diào)用的條件,這個應(yīng)該很好理解吧。下面舉個例子: 實例演示:
class Person
{
public:
virtual void BuyTicket()
{
cout << "買票全價" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket() // 這里也可以不寫virtual,因為基類的虛函數(shù)屬性已經(jīng)被保留下來了,這里只是完成虛函數(shù)的重寫
{
cout << "買票半價" << endl;
}
};
void Func1(Person& p) { p.BuyTicket(); }
void Func2(Person* p) { p->BuyTicket(); }
void Func3(Person p) { p.BuyTicket(); }
int main()
{
Person p;
Student s;
// 滿足多態(tài)的條件:與類型無關(guān),父類指針指向的是誰就調(diào)用誰的成員函數(shù)
// 不滿足多態(tài)的條件:與類型有關(guān),類型是誰就調(diào)用誰的成員函數(shù)
cout << "基類的引用調(diào)用:" << endl;
Func1(p);
Func1(s);
cout << "基類的指針調(diào)用:" << endl;
Func2(&p);
Func2(&s);
cout << "基類的對象調(diào)用:" << endl;
Func3(p);
Func3(s);
return 0;
}
代碼運行結(jié)果:

總結(jié):
- 滿足多態(tài)的條件:成員函數(shù)調(diào)用與對象類型無關(guān),指向那個對象就調(diào)用哪個的虛函數(shù)
- 不滿足多態(tài)的條件:成員函數(shù)的調(diào)用與對象類型有關(guān),是哪個對象類型就調(diào)用哪個對象的虛函數(shù)。
思考: 析構(gòu)函數(shù)是否要加virtual? 答案是需要的。先給大家看一個例子:
class Person
{
public:
/*virtual*/ ~Person()
{
cout << "~Person()" << endl;
}
};
class Student: public Person
{
public:
~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Person* p = new Person;
Person* ps = new Student;// 不加virtual,不構(gòu)成多態(tài),父類指針只會根據(jù)類型去調(diào)用對于的析構(gòu)函數(shù)
// 加了virtual,構(gòu)成多態(tài),父類指針會根據(jù)指向的對象去調(diào)用他的析構(gòu)函數(shù)
delete p;
delete ps;
return 0;
}
下面分別是基類析構(gòu)函數(shù)不加virtual和加virtual的代碼運行結(jié)果:


可以看出,不加virtual關(guān)鍵字時,第二個對象delete時沒有調(diào)用子類的析構(gòu)函數(shù)清理釋放空間。為什么呢?因為不加virtual關(guān)鍵字時,兩個析構(gòu)函數(shù)不構(gòu)成多態(tài),所以調(diào)用析構(gòu)函數(shù)時是與類型有關(guān)的,因為都是都是父類類型,所以只會調(diào)用父類的析構(gòu)函數(shù)。加了virtual關(guān)鍵字時,因為兩個析構(gòu)函數(shù)被編譯器處理成同名函數(shù)了,所以完成了虛函數(shù)的重寫,且是父類指針調(diào)用,所以此時兩個析構(gòu)函數(shù)構(gòu)成多態(tài),所以調(diào)用析構(gòu)函數(shù)時是與類型無關(guān)的,因為父類指針指向的是子類對象,所以會調(diào)用子類的析構(gòu)函數(shù),子類調(diào)用完自己的析構(gòu)函數(shù)又會自動調(diào)用父類的析構(gòu)函數(shù)來完成對父類資源的清理。 所以總的來看,基類的析構(gòu)函數(shù)是要加virtual的。
??C++11override和final
final: 修飾虛函數(shù),表示該虛函數(shù)不可以被重寫(還可以修飾類,表示該類不可以被繼承)
實例演示:
class Car
{
public:
// final 表示該虛函數(shù)不能被重寫 也可以修飾類,表示該類不可以被繼承
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz-舒適" << endl; }
};
編譯器檢查結(jié)果: 由于dirve字母編寫錯誤,所以編譯器檢查出沒有重寫基類的虛函數(shù)

2.overide: 檢查派生類虛函數(shù)是否重寫了基類的某個虛函數(shù) 實例演示:
class Car
{
public:
// final 表示該虛函數(shù)不能被重寫 也可以修飾類,表示該類不可以被繼承
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() { cout << "Benz-舒適" << endl; }
};
編譯器檢查結(jié)果:

??重載、重寫和重定義(隱藏)
| 名稱 | 作用域 | 函數(shù)名 | 其他 |
|---|---|---|---|
| 重載 | 兩個函數(shù)在同一作用域 | 相同 | 參數(shù)類型不同 |
| 重寫 | 兩個函數(shù)分別再基類和派生類的作用域 | 相同 | 函數(shù)返回類型和參數(shù)類型一樣 |
| 重定義(隱藏) | 兩個函數(shù)分別再基類和派生類的作用域 | 相同 | 兩個基類和派生類的同名函數(shù)不是構(gòu)成重寫就是重定義 |
??抽象類
概念: 在虛函數(shù)的后面寫上 =0 ,則這個函數(shù)為純虛函數(shù)。包含純虛函數(shù)的類叫做抽象類(也叫接口類),抽象類不能實例化出對象。派生類繼承后也不能實例化出對象,只有重寫純虛函數(shù),派生類才能實例化象純虛函數(shù)規(guī)范了派生類必須重寫,另外純虛函數(shù)更體現(xiàn)出了接口繼承。
總結(jié)出幾個特點:
- 虛函數(shù)后面加上=0
- 不能實例化出對象
- 派生類如果不重寫基類的純虛函數(shù)那么它也是抽象類,不能實例化出對象
- 抽象類嚴(yán)格限制派生類必須重寫基類的純虛函數(shù)
- 體現(xiàn)了接口繼承
實例演示:
class Car
{
public:
virtual void Drive() = 0;
};
class Benz : public Car
{
public:
virtual void Drive()
{
cout << "Benz" << endl;
}
};
class BMW : public Car
{
public:
virtual void Drive () override
{
cout << "BMW" << endl;
}
};
int main()
{
Car* pBenZ = new Benz;
pBenZ->Drive();
Car* pBMW = new BMW;
pBMW->Drive();
delete pBenZ;
delete pBMW;
return 0;
}
代碼運行結(jié)果:

抽象類的意義?
- 強制子類完成父類虛函數(shù)的重寫
- 表示該類是抽象類,沒有實體(例如:花、車和人等)
接口繼承和實現(xiàn)繼承
普通函數(shù)的繼承是一種實現(xiàn)繼承,派生類繼承了基類函數(shù),可以使用函數(shù),繼承的是函數(shù)的實現(xiàn)。虛函數(shù)的繼承是一種接口繼承,派生類繼承的是基類虛函數(shù)的接口,目的是為了重寫,達(dá)成多態(tài),繼承的是接口。所以如果不實現(xiàn)多態(tài),不要把函數(shù)定義成虛函數(shù)。
??多態(tài)的原理
??虛函數(shù)表
概念: 一個含有虛函數(shù)的類中至少有一個虛函數(shù)指針,這個指針指向了一張表——虛函數(shù)表(簡稱虛表),這張表中存放了這個類中所有的虛函數(shù)的地址。
計算一下下面這個類的大小:
class Base
{
public:
virtual void func1() {}
virtual void func2() {}
public:
int _a;
};
int main()
{
cout << sizeof(Base) << endl;
return 0;
}
代碼運行結(jié)果如下:

這個類中存放了一個虛表指針和一個成員變量,所以總大小就是8。給大家看一下它的類對象模型:

實例演示:
class Person
{
public:
virtual void BuyTicket()
{
cout << "買票全價" << endl;
}
virtual void func()
{
cout << "func()" << endl;
}
int _p = 1;
};
class Student : public Person
{
public:
virtual void BuyTicket() // 這里也可以不寫virtual,因為基類的虛函數(shù)屬性已經(jīng)被保留下來了,這里只是完成虛函數(shù)的重寫
{
cout << "買票半價" << endl;
}
int _s = 1;
};
int main()
{
Person p;
Student s;
return 0;
}
類對象模型如下:

可以看出,兩個虛函數(shù)地址是不一樣的,其實子類會先把父類的虛表拷貝一份下來,如果子類重寫了虛函數(shù),那么子類的虛函數(shù)的地址將會覆蓋虛表中的地址,如果沒有重寫,那么將不覆蓋。
總結(jié)幾點:
- 子類對象由兩部分構(gòu)成,一部分是父類繼承下來的成員,虛表指針指向的虛表有父類的虛函數(shù),也有子類新增的虛函數(shù)
- 子類完成父類虛函數(shù)的重寫其實是對繼承下來的虛表的中重寫了的虛函數(shù)進(jìn)行覆蓋,把地址更換了,語法層是稱為覆蓋
- 虛函數(shù)表本質(zhì)是一個存虛函數(shù)指針的指針數(shù)組,一般情況這個數(shù)組最后面放了一個nullptr
- 虛表生成的過程:先將基類中的虛表內(nèi)容拷貝一份到派生類虛表中 b.如果派生類重寫了基類中某個虛函數(shù),用派生類自己的虛函數(shù)覆蓋虛表中基類的虛函數(shù) c.派生類自己新增加的虛函數(shù)按其在派生類中的聲明次序增加到派生類虛表的最后
下面我們來討論一下虛表存放的位置和虛表指針存放的位置
虛表指針肯定是存在類中的,從上面的類對象模型中可以看出。其次虛表存放的是虛函數(shù)的地址,這些虛函數(shù)和普通函數(shù)一樣,都會被編譯器編譯成指令,然后放進(jìn)代碼段。虛表也是存在代碼段的,因為同類型的對象共用一張?zhí)摫?。下面帶大家驗證一下(環(huán)境:vs2019)
驗證代碼:
class Base
{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
virtual void func3() { cout << "Base::func3" << endl; }
void func() {}
int b = 0;
};
class Derive :public Base
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func2() { cout << "Derive::func2" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
virtual void func5() { cout << "Derive::func5" << endl; }
int d = 0;
};
void func() {}
int globalVar = 10;
int main()
{
Base b;
Derive d;
const char* pChar = "hello";
int c = 1;
static int s = 20;
int* p = new int;
const int i = 10;
printf("棧變量:%p\n", &c);
printf("虛表指針:%p\n", (int*)&b);
printf("對象成員:%p\n", ((int*)&b + 1));
printf("堆變量:%p\n", p);
printf("代碼段常量:%p\n", pChar);
printf("普通函數(shù)地址:%p\n", func);
printf("成員函數(shù)地址:%p\n", &Base::func);
printf("虛函數(shù):%p\n", &Base::func1);
printf("虛函數(shù)表:%p\n", *(int*)&b);
printf("數(shù)據(jù)段:%p\n", &s);
printf("數(shù)據(jù)段:%p\n", &globalVar);
delete p;
return 0;
}
代碼運行結(jié)果如下:

容易看出,代碼段常量存放的地址和虛表存放的地址很接近,和數(shù)據(jù)段的地址也很接近,所以可以猜測虛表存放在數(shù)據(jù)段或代碼段,更可能是在代碼段。
??原理
多態(tài)是在運行時到指向的對象中的虛表中查找要調(diào)用的虛函數(shù)的地址,然后進(jìn)行調(diào)用。
總結(jié):
- 多態(tài)滿足的兩個條件:一個是虛函數(shù)的覆蓋,一個是對象的指針和引用調(diào)用
- 滿足多態(tài)后,函數(shù)的調(diào)用不是編譯時確認(rèn)的,而是在運行時確認(rèn)的。

動態(tài)綁定和靜態(tài)綁定
- 靜態(tài)綁定: 發(fā)生在編譯時,也就是早期綁定,就是我們之前說過的函數(shù)重載就是屬于靜態(tài)綁定,也稱靜態(tài)多態(tài)。
- 動圖綁定: 發(fā)生在運行時,也就是后期綁定,多態(tài)就是發(fā)生在運行時,也稱動態(tài)多態(tài)。
??單繼承和多繼承的虛表
??單繼承的虛表
先看下面的代碼(單繼承)
class Base
{
public:
virtual void func1() { cout << "Base::func1" << endl; }
virtual void func2() { cout << "Base::func2" << endl; }
virtual void func3() { cout << "Base::func3" << endl; }
void func() {}
int b = 0;
};
class Derive :public Base
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func2() { cout << "Derive::func2" << endl; }
virtual void func4() { cout << "Derive::func4" << endl; }
virtual void func5() { cout << "Derive::func5" << endl; }
int d = 0;
};
觀察它的類對象模型:

在上面的類對象模型中,派生類中只可以看見func1和func2,后面兩個函數(shù)看不見,這是因為編譯器把這兩個新增的虛函數(shù)給隱藏了,為了我們能夠更好的觀察,我們可以通過寫代碼來看。 先定義一個函數(shù)指針:
typedef void(*VF_PTR)(); // 給函數(shù)指針typedef
下面是打印虛表的代碼:
void PrintVFTable(VF_PTR* pTable)
{
for (size_t i = 0; pTable[i] != nullptr; ++i)
{
printf("vfTable[%d]:%p->", i, pTable[i]);
VF_PTR f = pTable[i];
f();// 通過函數(shù)地址調(diào)用函數(shù)
}
cout << endl;
}
下面我們只需要通過傳虛表地址的方式來調(diào)用函數(shù)打印虛表,虛表地址如何獲取呢?從上面的類對象模型可以知道,類對象的前四個地址存放的是虛表指針,虛表指針也就是虛表的指針,所以我們要獲取類對象的前四個字節(jié)。下面是獲取方法:
(VF_PTR*)*(int*)&b;
先將類對象的地址取出,然后強轉(zhuǎn)為整形,解引用就會按照四個字節(jié)來獲取內(nèi)容,這四個字節(jié)的內(nèi)容是虛表指針,其實也是虛表的地址,我們可以把這個整形強轉(zhuǎn)為函數(shù)地址的類型就可以了。
打印虛表:
int main()
{
Base b;
Derive d;
PrintVFTable((VF_PTR*)*(int*)&b);
PrintVFTable((VF_PTR*)*(int*)&d);
return 0;
}
打印結(jié)果如下:

可以看出派生類對象中新增的虛函數(shù)會按照虛函數(shù)函數(shù)次序聲明放在虛表的最后。
??多繼承的虛函數(shù)表
看下面代碼(多繼承)
class Base1
{
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2
{
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2 = 1;
};
class Derive : public Base1 , public Base2
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1 = 1;
};
類對象模型如下:

為了更好地觀察,我們還是通過打印虛表來觀察:
int main()
{
Derive d;
cout << sizeof(Derive) << endl;
cout << "Base1的虛表:" << endl;
PrintVFTable((VF_PTR*)*(int*)&d);
cout << "Base2的虛表:" << endl;
PrintVFTable((VF_PTR*)*(int*)((char*)&d+sizeof(Base1)));
cout << "Derive的成員變量d:" << endl;
//PrintVFTable((VF_PTR*)*(int*)((char*)&d + sizeof(Base1) + sizeof(Base2)));
cout << *(int*)((char*)&d + sizeof(Base1) + sizeof(Base2)) << endl;
return 0;
}
打印結(jié)果如下:

可以看出,派生類新增的虛函數(shù)放在了第一個繼承的對象的虛表中最后了。
??幾個值得思考的問題
- 內(nèi)斂函數(shù)可以是虛函數(shù)嗎?
答:不可以,內(nèi)聯(lián)(inline)函數(shù)沒有地址,因為虛函數(shù)要把地址放到虛表中去。
- 構(gòu)造函數(shù)可以是虛函數(shù)嗎?
答:不可以,因為對象中虛函數(shù)指針是在構(gòu)造函數(shù)初始化列表階段才初始化的。
- 析構(gòu)函數(shù)可以是虛函數(shù)嗎?
答:可以,且建議設(shè)計成虛函數(shù),具體原因前面說了。
- 對象訪問普通函數(shù)快還是虛函數(shù)更快?
答:首先如果是普通對象,是一樣快的。如果是指針對象或者是引用對象,則調(diào)用的普通函數(shù)快,因為構(gòu)成多態(tài),運行時調(diào)用虛函數(shù)需要到虛函數(shù)表中去查找。
- 虛函數(shù)表是在什么階段生成的?
答:在編譯階段生成的,存在于代碼段。
- 什么是抽象類?有什么意義?
答:前面介紹過了,可以參考前文。
??總結(jié)
多態(tài)也是C++的三大特性之一,之前也介紹過兩個,就是封裝和繼承。多態(tài)也是十分的重要,我們要理清其中一些改了,更好地理解這些。今天的內(nèi)容就到這里了,喜歡的話,歡迎點贊支持和關(guān)注~

到此這篇關(guān)于C++數(shù)據(jù)結(jié)構(gòu)分析多態(tài)的實現(xiàn)與原理及抽象類的文章就介紹到這了,更多相關(guān)C++ 多態(tài)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解C語言中index()函數(shù)和rindex()函數(shù)的用法
vscode和cmake編譯多個C++文件的實現(xiàn)方法

