關(guān)于網(wǎng)站建設(shè)的介紹鄭州官網(wǎng)網(wǎng)站推廣優(yōu)化
1. 多態(tài)的概念
多態(tài),顧名思義多種形態(tài);多態(tài)分為編譯時多態(tài)(靜態(tài)多態(tài))和運行時多態(tài)(動態(tài)多態(tài)),靜態(tài)多態(tài)就是就是我們前面講的函數(shù)重載和函數(shù)模板,可以通過傳不同類型,然后在編譯期間就確定好使用哪個的稱為靜態(tài)多態(tài);動態(tài)多態(tài)就是一般在程序運行時確定使用指定函數(shù)的就稱作動態(tài)多態(tài)。
動態(tài)多態(tài),舉個生活中的例子,例如鐵路12306中買票的時候,當(dāng)成年且沒有特殊情況的人是全價買票,學(xué)生買票有特定的學(xué)生價買票,軍人買票時可以優(yōu)先買票,而這個過程抽象來說就是傳入不同的對象就會完成不同的行為。
2. 多態(tài)的定義及使用
2.1 多態(tài)的構(gòu)成條件
多態(tài)是一個繼承關(guān)系下的類對象,去調(diào)用同一函數(shù),產(chǎn)生了不同行為。比如Student繼承了Person。Person對象買票全價,Student對象優(yōu)惠買票。
2.1.1 實現(xiàn)多態(tài)的兩個必要條件
1.?必須是基類的指針或引用調(diào)用函數(shù)。
2. 被調(diào)用的函數(shù)必須是虛函數(shù)(virtual修飾的)。
解釋一下為什么必須是基類的指針或引用調(diào)用函數(shù),因為只有是基類的指針或引用才能做到既接收基類的地址又接收派生類的地址;
第二派生類必須對基類的虛函數(shù)進行重寫/覆蓋,派生類對基類的虛函數(shù)重寫了才能做到傳入不同的類執(zhí)行不同的操作的效果,才能達到多態(tài)。
3.?虛函數(shù)
在繼承那一章我們也講到虛繼承,虛繼承是用來避免菱形繼承帶來代碼的冗余和二義性的問題,關(guān)鍵字是virtual;虛函數(shù)的關(guān)鍵字也是virtual,只需要在類成員函數(shù)前面加上virtual修飾,那么這個成員函數(shù)就被稱為虛函數(shù)。注意:如果不是成員函數(shù)是不能加virtual修飾的。如下代碼所示:
class Person
{
public:virtual void BuyTicket(){cout << "買票--全價" << endl;}
};
3.1 虛函數(shù)的重寫
派生類中有一個虛函數(shù)跟基類的虛函數(shù)完全一致的虛函數(shù)稱為虛函數(shù)的重寫。
完全一致指的是:函數(shù)名、參數(shù)列表、返回值類型。(注意!這里的參數(shù)列表指的是參數(shù)的類型,名字相同沒有關(guān)系);如下代碼所示:
class Person
{
public:virtual void BuyTicket(int x){cout << "買票--全價" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(int y){cout << "買票--半價" << endl;}
};
還有一點需要提一下的是:如果基類的虛函數(shù)加了virtual,那么派生類的虛函數(shù)可以不加virtual,這里我們可以直接按“在繼承的時候就把基類的虛函數(shù)的屬性繼承下來了”理解,這個不太建議這樣子用,但在筆試或者考試選擇題可能會考。
下面是虛函數(shù)、虛函數(shù)的重寫、實現(xiàn)多態(tài)的代碼使用:
#include<iostream>
using namespace std;class Person
{
public:virtual void BuyTicket(int x){cout << "買票--全價" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(int y){cout << "買票--半價" << endl;}
};void BuyTicket(Person* ptr)
{ptr->BuyTicket(6);
}int main()
{Person p1;Student s1;BuyTicket(&p1);BuyTicket(&s1);return 0;
}
👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇
運行結(jié)果為:
最后再補充一個重重重重要的知識:虛函數(shù)的重寫本質(zhì)上是重寫虛函數(shù)的實現(xiàn)!!!!下面就有一道相關(guān)的練習(xí)題。(畢竟人教人教不會,事教人一腳就懂)
3.2 多態(tài)場景下的選擇題
#include<iostream>
using namespace std;class A
{
public:virtual void func(int val = 1){std::cout << "A->" << val << std::endl; }virtual void test() {func(); }
};class B : public A
{
public:void func(int val = 0) {std::cout << "B->" << val << std::endl; }
};int main(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}
首先如果我們對重寫的知識理解的不夠全面的話,那么肯定會選擇D, 但我們上面說了重寫是重寫函數(shù)的實現(xiàn),函數(shù)的參數(shù)不會有變化,所以我們調(diào)用的func應(yīng)該如下圖所示:
所以正確的答案是B;
最后我們再來細(xì)致分析一下p->test()究竟怎么構(gòu)成多態(tài),我們都知道在類里面的成員函數(shù)會有一個隱藏的參數(shù)就是this指針,而test是在A里面的,所以完整的函數(shù)應(yīng)該為? ?virtual void test(A* this)這里就構(gòu)成重載了,基類做this指針的返回值,完全符合上面所說的2個多態(tài)的構(gòu)成條件。
3.3 虛函數(shù)重寫的一些問題
3.3.1 協(xié)變
協(xié)變:派生類重寫基類的虛函數(shù)的時候返回值不同。即基類虛函數(shù)返回基類對象的指針或引用,派生類虛函數(shù)返回派生類對象的指針或引用,這個就成為協(xié)變。協(xié)變用處不多,僅需了解一下即可;如下代碼所示:
#include<iostream>
using namespace std;
//基類
class A
{};
//派生類
class B :public A
{};class Person
{
public://基類做返回值virtual A* BuyTicket()//virtual Person* BuyTicket()也可以{cout << "買票-全價" << endl;return nullptr;}
};class Student :public Person
{
public://派生類做返回值virtual B* BuyTicket()//virtual Student* BuyTicket()也可以{cout << "買票-打折" << endl;return nullptr;}
};void Func(Person* ptr)
{ptr->BuyTicket();
}
3.3.2 析構(gòu)函數(shù)的重寫
基類的析構(gòu)函數(shù)只要定義為虛函數(shù)后,無論派生類的析構(gòu)函數(shù)寫不寫virtual,都會與基類的析構(gòu)函數(shù)構(gòu)成重寫。
前面繼承有提到一嘴,就是析構(gòu)函數(shù)最后都會被編譯器轉(zhuǎn)換為destructor(),那么這時候函數(shù)名(destructor)相同構(gòu)成隱藏(這個是繼承那邊的)。
所以只要基類的析構(gòu)加了virtual,派生類的就構(gòu)成了重寫;函數(shù)名一樣都是destructor、參數(shù)一樣都沒有、返回值類型都沒有。
接下來就談?wù)劄槭裁匆獙崿F(xiàn)析構(gòu)函數(shù)的重寫(重點!!!!!)
在思考為什么的時候我們就可以通過反推來證明即如果沒有實現(xiàn)會發(fā)生什么情況;我們先看一下下面的代碼:
#include<iostream>
using namespace std;class A
{
public:~A(){cout << "~A()" << endl;}
};class B :public A
{
public:~B(){cout << "~B()->delete:" << _p << endl;delete _p;}
protected:int* _p = new int[10];
};int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}
對代碼的解析:我們用基類A指針實例化了兩個對象p1接收基類A的地址、p2接收派生類B的地址,如果說我們沒有實現(xiàn)析構(gòu)函數(shù)的重寫,delete p1和delete p2 是直接走基類的析構(gòu)函數(shù),因為沒有構(gòu)成多態(tài),也就不會產(chǎn)生不同的行為而只走基類A的析構(gòu)函數(shù),如果派生類B里面有動態(tài)申請空間,那么就會出現(xiàn)內(nèi)存泄漏。如下圖:
但如果我們讓基類A的析構(gòu)函數(shù)定義為虛函數(shù),那么就構(gòu)成多態(tài),從而會根據(jù)傳入不同的類型調(diào)用不同對象的析構(gòu)函數(shù)。
這里為什么會還會調(diào)用一次基類A的析構(gòu)函數(shù)呢?前面繼承有提到,派生類的析構(gòu)函數(shù)里面其實包含了基類的析構(gòu)函數(shù),主要是為了達到先子后父的析構(gòu)順序。
最后總結(jié)一下:C++的這些實現(xiàn)其實都是有關(guān)聯(lián)的,他們?yōu)榱颂幚砩厦娴倪@種情況而實現(xiàn)了析構(gòu)函數(shù)的重寫,為了實現(xiàn)析構(gòu)函數(shù)的重寫而實現(xiàn)了讓析構(gòu)函數(shù)無論函數(shù)名為什么最后都會轉(zhuǎn)換成destructor。真的很妙。
3.3.3 override和final關(guān)鍵字
override是用來檢查你是否重寫錯誤;
final在繼承那里也提到過,如果想實現(xiàn)一個類不被繼承就用final,而這里的final是用來實現(xiàn)一個不能被重寫的虛函數(shù);
3.3.4 重載/重寫/隱藏的對比
3.3.5?純虛函數(shù)和抽象類
純虛函數(shù)只要在虛函數(shù)后面加個“=0”即可;純虛函數(shù)不需要定義實現(xiàn),沒有很大作用,但是在語法上是支持實現(xiàn)的 。而包含純虛函數(shù)的類稱作抽象類,抽象類不能實例化出對象,但是派生類可以繼承抽象類,如果派生類不重寫純虛函數(shù),那么派生類也稱為抽象類。純虛函數(shù)其實就是間接強制了派生類要重寫純虛函數(shù)。
干說有點難理解,舉個實際例子,狗和貓都是動物,狗的叫聲是“旺旺”,貓的叫聲是“喵喵”,狗和貓都是動物都有動物的特征所以可以繼承動物的屬性,但是動物是怎么叫的我們不知道,而動物就可以稱作是抽象類,“叫”這一個動作可以用純虛函數(shù)實現(xiàn),貓和狗則是動物的派生類,所以需要重寫這個純虛函數(shù),狗重寫“叫”函數(shù)為“旺旺”,貓則是“喵喵”。如下代碼所示:
#include<iostream>class Animal
{
public:virtual void talk() = 0;
};
class Dog : public Animal
{
public:virtual void talk() {std::cout << "汪汪" << std::endl;}
};class Cat : public Animal
{
public:virtual void talk() {std::cout << "(>^ω^<)喵" << std::endl;}
};
int main()
{Cat cat;Dog dog;cat.talk();dog.talk();return 0;
}
👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇
運行結(jié)果為:
4. 多態(tài)的原理
首先我們看一道題,用一道題引出下面的知識;
#include<iostream>
using namespace std;class Base {
public:virtual void Func1(){cout << "Func1()" << endl;}
protected:int _b = 1;char _ch = 'x';
};int main()
{Base b;cout << sizeof(b) << endl;return 0;
}
我們知道,計算類的大小是使用對齊的方式計算的,那么計算出來的結(jié)果是8,可能我們的答案就選擇了C,實則并不然,答案選的是D,首先我們通過調(diào)試看一下里面究竟多了什么玩意,如下圖所示:
我們發(fā)現(xiàn)里面除了成員變量_b和_char,還有一個指針_vfptr,這個指針我們稱作虛函數(shù)表指針(virtual function pointer);
4.1 虛函數(shù)表指針
每個含有虛函數(shù)的類中都會有一個虛函數(shù)表指針,這個指針是指向一塊存著虛函數(shù)地址的空間,虛函數(shù)表也稱作虛表。所以我們傳入不同類去調(diào)用該類的虛函數(shù)的時候就是通過這個指針去調(diào)用不同類中的虛函數(shù)的。
4.2 多態(tài)的實現(xiàn)
首先我們先來看一串代碼
#include<iostream>
using namespace std;
class Person {
public:virtual void BuyTicket() {cout << "買票-全價" << endl;}
protected:string _name;
};class Student : public Person
{
public:virtual void BuyTicket() { cout << "買票-打折" << endl;}protected:int _id;
};class Soldier : public Person
{
public:virtual void BuyTicket() { cout << "買票-優(yōu)先" << endl;}protected:string _codename; // 代號
};void Func(Person* ptr)
{// 這里可以看到雖然都是Person指針Ptr在調(diào)用BuyTicket// 但是跟ptr沒關(guān)系,而是由ptr指向的對象決定的。ptr->BuyTicket();
}int main()
{// 其次多態(tài)不僅僅發(fā)生在派生類對象之間,多個派生類繼承基類,重寫虛函數(shù)后// 多態(tài)也會發(fā)生在多個派生類之間。Student st;Soldier sr;Person pr;Func(&st);Func(&sr);Func(&pr);return 0;
}
我們可能會好奇,Func內(nèi)的ptr是如何做到傳入不同的類對象的地址然后走不同的虛函數(shù)執(zhí)行不同操作的,下面就來講解一下。
如下圖
通過上圖我們看到,滿足多態(tài)后,不再是編譯時通過調(diào)用對象確定函數(shù)的地址,而是運行時到指定的對象內(nèi)找到對象的__vfptr(虛表)然后確定對應(yīng)的虛函數(shù)的地址,然后執(zhí)行,這樣就實現(xiàn)了指針或引用指向基類就調(diào)用基類的虛函數(shù),指向派生類就調(diào)用派生類的虛函數(shù)。
4.3 動態(tài)綁定和靜態(tài)綁定
動態(tài)綁定則是滿足多態(tài)條件的函數(shù)調(diào)用是在運行時綁定,說簡單點就是在運行時去虛函數(shù)表內(nèi)找到函數(shù)地址然后調(diào)用。
靜態(tài)綁定則是在編譯期間確定函數(shù)的地址然后調(diào)用。
這個可以通過匯編層看一下。
動態(tài)綁定👇看著挺復(fù)雜的,挺多操作,因為他是在運行時先去找到__vptr這個指針,然后找到虛函數(shù)表,再通過虛函數(shù)表內(nèi)的地址找到虛函數(shù),再調(diào)用該函數(shù)。
靜態(tài)綁定👇編譯器直接確定函數(shù)地址然后直接調(diào)用。
4.4 虛函數(shù)表
1. 基類對象的虛函數(shù)表中存放基類所有虛函數(shù)的地址。如果同類型實例化對象的話則虛表共用,不同類型虛表各自獨立。
2. 派生類由兩部分構(gòu)成,一部分是派生類自己的成員,還有一部分是基類的成員,還有基類的虛表指針,繼承下來的基類中有虛表指針的話就不會自己再生成一個新的虛表指針,但繼承下來的和虛表指針和基類的虛表指針不是同一個,他們的地址不同。
3. 如果派生類中有虛函數(shù)的重寫,那派生類虛表中對應(yīng)的虛函數(shù)就會被重寫的虛函數(shù)覆蓋。
4. 派生類的虛表中包含基類的虛函數(shù)地址,派生類重寫的虛函數(shù)地址,還有派生類自己的虛函數(shù)地址。
5. 虛函數(shù)的本質(zhì)是一個存虛函數(shù)指針的指針數(shù)組,一般情況下這個數(shù)組后面會放一個0x00000000的標(biāo)記? ?(這個C++并沒有進?規(guī)定,各個編譯器自行定義的,vs系列編譯器會再后面放個0x00000000 標(biāo)記,g++系列編譯不會放)
6. 虛函數(shù)和普通函數(shù)一樣,編譯好后是一段指令,都是存在代碼段的,只是虛函數(shù)的地址存在虛表中。
7. 虛函數(shù)表的存在位置是看編譯器的,C++并沒有規(guī)定,下面將來看一下vs的在哪里;如下代碼所示:
#include<iostream>
using namespace std;class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
};class Derive : public Base
{
public:// 重寫基類的func1virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func1" << endl; }void func4() { cout << "Derive::func4" << endl; }protected:int b = 2;
};int main()
{int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("棧:%p\n", &i);printf("靜態(tài)區(qū):%p\n", &j);printf("堆:%p\n", p1);printf("常量區(qū):%p\n", p2);Base b;Derive d;Base* p3 = &b;Derive* p4 = &d;printf("Person虛表地址:%p\n", *(int*)p3);printf("Student虛表地址:%p\n", *(int*)p4);printf("虛函數(shù)地址:%p\n", &Base::func1);printf("普通函數(shù)地址:%p\n", &Base::func5);return 0;
}
👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇? 👇
運行結(jié)果為:
由此可見,虛表的地址與常量區(qū)的地址很接近,那就說明虛表是存在常量區(qū)的。
5. 額外補充的知識
首先我們先來看一個代碼。
#include<iostream>
using namespace std;class A
{
public: void test(float a) {cout << a; }
}; class B :public A
{
public: void test(int b) {cout << b; }
}; void main()
{A* a = new A;B* b = new B;a = b; a->test(1.1);
}
按照我們的思路,a賦值給b之后,那么A的test的隱式指針this就應(yīng)該轉(zhuǎn)換成B類的this,應(yīng)該調(diào)用的是cout出b的值為1。
但是編譯器不是這么做的,編譯器雖然通過指針的指向訪問成員變量,但是不能通過指針的指向來訪問成員函數(shù),而是通過指針的類型來訪問成員函數(shù)。也就是說無論a的指針指向了誰,都只能訪問到A類內(nèi)的成員函數(shù)。(引用也是一樣的!!!!!)
派生類有幾個父類,且那幾個父類都有虛函數(shù)的話,那么派生類就會有幾張?zhí)摫?。而如果在派生類中增加虛函?shù)的話只會放在第一張?zhí)摫淼淖詈蟆?/p>