<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
概念:通俗的來說就是多種形態,具體就是去完成某個行為,當不同型別的物件去完成同一件事時,產生的動作是不一樣的,結果也是不一樣的。
舉一個現實中的例子:買票這個行為,當普通人買票時是全價;學生是半價;軍人是不需要排隊。
多型也分為兩種:
這裡的靜態是指在編譯時實現多型的,而動態是在執行時完成的。
多型一定是建立在繼承上的,那麼除了繼承還要兩個條件:
概念:被virtual修飾的類成員函數稱為虛擬函式
class Person { public: virtual void BuyTicket() { cout<<"全價票"<<endl; } };
注意:
概念:派生類(子類)中有一個跟基礎類別(父類別)完全相同的虛擬函式(即派生類虛擬函式與基礎類別虛擬函式的返回值型別,函數名字,參數列完全相同),稱子類的虛擬函式重寫了基礎類別的虛擬函式。
例:
class Person { public: virtual void BuyTicket() { cout<<"全價票"<<endl; } }; class Student :public Person { public: //子類的虛擬函式重寫了父類別的虛擬函式 virtual void BuyTicket() { cout<<"半價票"<<endl; } }; class Soldier : public Person { public: //子類的虛擬函式重寫了父類別的虛擬函式 virtual void BuyTicket() { cout<<"優先買票"<<endl; } }; //多型的實現 void f(Person& p)//這塊的引數必須是參照或者指標 { p.BuyTicket(); } int main() { Person p; Student st; Soldier so; f(p); f(st); f(so); return 0; }
注意:這裡子函數的虛擬函式可以不加virtual,也算完成了重寫,但是父類別的虛擬函式必須要加,因為子類是先繼承父類別的虛擬函式,繼承下來後就有了virtual屬性了,子類只是重寫這個virtual函數;除了這個原因之外,還有一個原因,如果父類別的解構函式加了virtual,子類加不加都一定完成了重寫,就保證了delete時一定能實現多型的正確呼叫解構函式。
1、協變
概念:派生類重寫基礎類別虛擬函式時,與基礎類別虛擬函式返回值型別不同。即基礎類別虛擬函式返回基礎類別物件的指標或者參照,派生類虛擬函式返回派生類物件的指標或者參照時,稱為協變
例:
class A{}; class B : public A{}; class Person { public: virtual A* f() { return new A; } }; class Student : public Person { public: virtual B* f() //返回值不同但是構成虛擬函式重寫 { return new B; } };
2、解構函式的重寫
如果基礎類別的解構函式為虛擬函式,此時派生類解構函式只要定義,無論是否加virtual關鍵字,都與基礎類別的解構函式構成重寫,雖然基礎類別與派生類解構函式名字不同。雖然函數名不相同,看起來違背了重寫的規則,其實不然,這裡可以理解為編譯器對解構函式的名稱做了特殊處理,編譯後解構函式的名稱統一處理成destructor
例:
class Person { public: //建議把父類別解構函式定義為虛擬函式,這樣方便子類的虛擬函式重寫父類別的虛擬函式 virtual ~Person() {cout << "~Person()" << endl;} }; class Student : public Person { public: virtual ~Student() { cout << "~Student()" << endl; } }; // 只有派生類Student的解構函式重寫了Person的解構函式,下面的delete物件呼叫解構函式,才能構成多型,才能保證p1和p2指向的物件正確的呼叫解構函式。 int main() { Person* p1 = new Person; //這裡p2指向的子類物件,應該呼叫子類解構函式,如果沒有呼叫的話,就可能記憶體漏失 Person* p2 = new Student; //多型行為 delete p1; delete p2; //只有解構函式重寫了那麼這裡delete父類別指標呼叫解構函式才能實現多型。 return 0; }
C++11 override和finel
從上面可以看出,C++對函數重寫的要求比較嚴格,但是有些情況下由於疏忽,可能會導致函數名字母次序寫反而無法構成過載,而這種錯誤在編譯期間是不會報出的,只有在程式執行時沒有得到預期結果才來debug會得不償失,因此:C++11提供了override和final兩個關鍵字,可以幫助使用者檢測是否重寫
final:修飾虛擬函式,表示該虛擬函式不能再被重寫
class Car { public: virtual void Drive() final {} }; class Benz :public Car { public: //會在這塊報錯,因為基礎類別的虛擬函式已經被final修飾,不能被重寫了 virtual void Drive() {cout << "Benz-舒適" << endl;} };
override: 檢查派生類虛擬函式是否重寫了基礎類別某個虛擬函式,如果沒有重寫編譯報錯
class Car{ public: virtual void Drive(){} }; class Benz :public Car { public: virtual void Drive() override {cout << "Benz-舒適" << endl;} };
過載、覆蓋(重寫)、隱藏(重定義)的對比
純虛擬函式:在虛擬函式的後面加上=0就是純虛擬函式,有純虛擬函式的類就是抽象類,也叫介面類,抽象類無法範例化物件。抽象類的子類不重寫父類別的虛擬函式的話,也是一個抽象類。
//抽象類的定義 class Car { public: virtual void run()=0; //不用實現只寫介面就行。 };
純虛擬函式不寫函數體,並不意味著不能實現,只是我們不寫。因為寫出來也沒有人用。
虛擬函式的作用
普通函數的繼承就是實現繼承,虛擬函式的繼承就是介面繼承。子類繼承了函數的實現,可以直接使用。虛擬函式重寫後只會繼承介面,重寫實現。所以如果不用多型,就不要把函數寫為虛擬函式。
純虛擬函式就體現了介面函數。下面我們來實現一道題,展現一下介面繼承。
class A { public: virtual void fun(int val=0) { cout<<"A->val = "<<val <<endl; } void Fun() { fun(); } }; class B:public A { public: virtual void fun(int val=1) { cout<<"B->val"<<val<<endl; } }; int main() { B b; A* a=&b; a->Fun(); return 0; }
結果列印為 :B->val=0
子類物件切片給父類別指標,傳給Fun函數,滿足多型,會去呼叫子類的fun函數,但是子類的虛擬函式繼承了父類別的介面,所以val是父類別的0。
class A { public: virtual void fun() { } protected: int _a; };
sizeof(A)是多少?
列印出來是8。
我們定義了一個A型別的物件a,開啟偵錯視窗,發現a的內容如下
我們發現出了成員變數_a以外,還多了一個指標,這個指標是不準確的,實際上應該是 _vftptr(virtual function table pointer),虛擬函式表指標。在計算類大小的時候要加上這個指標的大小。虛表就是存放虛擬函式的地址地方,當我們去呼叫虛擬函式,編譯器就會通過虛表指標去虛表裡查詢。
class A { public: void fun1() { } virtual void fun2() {} }; int main() { A* a=nullptr; a->fun1();//呼叫函數,因為這是普通函數的呼叫 a->fun2();//呼叫失敗,虛擬函式需要對指標操作,無法操作空指標。 return 0; }
實現一個繼承
class A { public: virtual void fun1() {} virtual void fun2() {} }; class B : public A { public: virtual void fun1() {} virtual void fun2() {} }; int main() { A a; B b; return 0; }
子類與父類別一樣有一個虛表指標。
子類的虛擬函式表一部分繼承自父類別。如果重寫了虛擬函式,那麼子類的虛擬函式會在虛表上覆蓋父類別的虛擬函式。
本質上虛擬函式表是一個虛擬函式指標陣列,最後一個元素是nullptr,代表虛表的結束。所以,如果繼承了虛擬函式,那麼
虛擬函式表放在記憶體的那個區,虛擬函式又放在哪?
虛擬函式與虛擬函式表都放在程式碼段。
我們現在來看多型的原理
class person { public: virtual void fun() { cout<<"全價票"<<endl; } }; class student : public person { public: virtual void fun() { cout<<"半價票"<<endl; } }; void buyticket(person* p) { p->fun(); }
這樣就實現了不同物件去呼叫同一函數,展現出不同的形態。 滿足多型的函數呼叫是程式執行是去物件的虛表查詢的,而虛表是在編譯時確定的。 普通函數的呼叫是編譯時就確定的。
動態繫結與靜態繫結
1.靜態繫結又稱為前期繫結(早繫結),在程式編譯期間確定了程式的行為,也稱為靜態多型,比如:函數過載
2.動態繫結又稱後期繫結(晚繫結),是在程式執行期間,根據具體拿到的型別確定程式的具體行為,呼叫具體的函數,也稱為動態多型。我們說的多型一般是指動態多型。
這裡我附上一個有意思的問題:
就是在子類已經覆蓋了父類別的虛擬函式的情況下,為什麼子類還是可以呼叫“被覆蓋”的父類別的虛擬函式呢?
#include <iostream> using namespace std; class Base { public: virtual void func() { cout << "Base funcn"; } }; class Son : public Base { public: void func() { Base::func(); cout << "Son funcn"; } }; int main() { Son b; b.func(); return 0; }
輸出:
Base func
Son func
這是C++提供的一個迴避虛擬函式的機制
通過加作用域(正如你所嘗試的),使得函數在編譯時就係結。
到此這篇關於C++ 超全面講解多型的文章就介紹到這了,更多相關C++ 多型內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45