走到這一步,咱們能夠進一步討論有關OOP在編程上的具體實現。
上一次,咱們提出來類的概念。其實類對應了OOP中的抽象這一律念。咱們將事物的共同點提取出來,抽象成了類。
這一次,爲了提升代碼的重用性,C++提出了繼承語法。很好理解,咱們將各類種類的羊抽象出來,寫出了class 羊。將各類牛特色提取出來,抽象成了 class 牛。若是咱們還有抽象出來 馬 這一動物。咱們發現了牛,羊,馬自己也都有共同點,就是它們動物,通常動物會吃喝跑,它們也都會。因此咱們抽象出動物這一特性,讓牛羊馬來繼承動物的特性。這樣能夠有效提升咱們代碼的效率。ios
經過以上的例子,咱們稱動物類爲父類(又稱基類),牛羊馬類爲繼承動物類的子類(又稱派生類)程序員
//父類 class Base { }; //子類繼承父類 class Son : public Base { };
繼承的格式:
class 派生類名 : 繼承方式 基類名
{
//派生類新增的成員變量或者成員函數
}編程
固然,這不意味這子類能夠啃老。子類還須要本身實現一些事情:數組
有關訪問權限(繼承方式)安全
在這裏有一件事須要重點關注:ide
派生類不能直接訪問基類的private函數和變量,可是能夠訪問protected函數和變量
即,對於外部世界來講,protected和private是同樣的;對於派生類來講,protected和public是同樣的
派生類不能直接訪問基類的私有成員,必須經過基類的方法來進行訪問(get和set函數)
這使得咱們想到構造函數,不過很遺憾派生類的構造函數不能直接設置繼承成員,而必須使用基類的公有方法來訪問私有基類成員。即,派生類構造函數必須使用基類構造函數函數
Son::Son(int a,int b,int c,int d):Base(_b,_c,_d) { this->a = a; }
上述代碼如何理解呢?
子類Son的構造函數其實只賦值了a這一成員變量。後面Base(_b,_c,_d)
咱們叫成員初始化列表。舉一個例子,若是咱們給子類Son實例化 Son son(1,2,3,4);
時Son的構造函數把實參 2, 3, 4賦值給形參_a,_b,_c而後將這些參數做爲實參賦值給父類Base構造函數,後者將嵌套一個Base對象,並將數據存入這個Base對象,而後進入Son的構造函數,完成對Son對象的建立,並將參數a賦值給this->a。
固然若是使用基類的拷貝構造函數也是能夠的:性能
Son::Son(int a,int b,int c,int d):Base(base) { this->a = a; }
這裏使用的是拷貝構造函數,這種方式咱們稱是隱式的,而上述方法是顯式的
若是說咱們後面什麼都不寫,編譯器默認調用默認構造函數,即:this
Son::Son(int a,int b,int c,int d) { this->a = a; }
就等於spa
Son::Son(int a,int b,int c,int d):Base() { this->a = a; }
咱們總結一下剛剛說過的要點
咱們在這裏並無討論析構函數,可是須要強調的是:析構函數的順序與構造函數是相反的,也就是先調用子類的析構,再調用父類的析構。
在使用子類的時候請記住要包含頭文件。
[注]:能夠和函數的初始化列表作一個聯繫
這個用法其實須要注意:
基類指針能夠在沒有進行顯示類型轉的狀況下指向派生類對象;同時基類的引用能夠在不進行顯示類型轉的狀況下引用派生類對象。
這樣作聽起來很美好,不過要注意的是:
基類的指針或者引用只用於調用基類的方法。
這一點是相當重要的,即
Son son1(1,2,3,4); Base & sn = son1; Base * psn = &son1; //這樣是容許的。可是使用sn 或者 *psn調用派生類(Son)的方法是不容許的!
其實,我到目前爲止一直在避免說起內存的問題,可是時至今日也應該慢慢開始C++的內存管理問題了。沒錯,這裏就是涉及一個內存的問題,請慢慢看下去:
首先,毋庸置疑的是子類的存儲空間確定比父類的存儲空間大。(父類有的子類都有,子類有的父類卻不必定有)
因此,一個指向父類的指針 的尋址範圍是否是比起子類的存儲空間要小。這時,你用這個指向父類的指針去尋子類方法的地址,頗有可能會超出這個指向父類的指針的尋址能力,這樣是會有很大的安全隱患,編譯器是不容許的。
固然引用也是這個道理。(咱們等等還會繼續說這個問題,目前先這樣。)
可是,反過來——指向派生類的指針或者引用能夠調用父類的方法嗎?
答案是能夠的,咱們把這種手法稱之爲「多態」。
C++中的繼承並不像Java中只能單繼承。C++的子類是能夠繼承多個父類。
可是,在多繼承中很容易引起二義性。這時請使用做用域運算符進行解決。
實際上,多繼承容易出現的問題並不只僅是命名問題,還有一個就是菱形繼承。
(這裏就不給UML圖了,本人仍是懶)
這時,咱們又要引入C++的一個解決辦法:虛基類
首先,具體怎麼作?在繼承方式前加 vitual 關鍵字
class Animal { public: int Age; }; class Sheep : virtual public Animal { };//虛基類 class Tuo : virtual public Animal { }; class SheepTuo: public Sheep,public Tuo { };
虛繼承能夠解決多種繼承前面提到的兩個問題:
虛繼承底層實現原理與編譯器相關,通常經過虛基類指針和虛基類表實現,每一個虛繼承的子類都有一個虛基類指針(佔用一個指針的存儲空間,4字節)和虛基類表(不佔用類對象的存儲空間)(須要強調的是,虛基類依舊會在子類裏面存在拷貝,只是僅僅最多存在一份而已,並非不在子類裏面了);當虛繼承的子類被當作父類繼承時,虛基類指針也會被繼承。
實際上,vbptr指的是虛基類表指針(virtual base table pointer),該指針指向了一個虛基類表(virtual table),虛表中記錄了虛基類與本類的偏移地址;經過偏移地址,這樣就找到了虛基類成員,而虛繼承也不用像普通多繼承那樣維持着公共基類(虛基類)的兩份一樣的拷貝,節省了存儲空間。
後面咱們會和虛函數(多態)進行比較。
同時這裏會有一個使用Visual Studio命令提示功能來查看內存分佈的技巧,就不展開說了。
其實,剛剛的描述已經解釋了什麼是多態了。用指向子類對象的指針或者引用去調用父類的函數。爲何要這麼作?咱們從常識來理解一下。好比:貓和魚均可以繼承動物這個類。若是動物這個類裏面有 move() 移動這個方法。可是,咱們都知道貓和魚的移動方式是不一樣的。因此咱們要利用多態,來使得咱們的程序更符合現實。
函數重載,從常識來考慮。好比:咱們經過一個函數來計算得某個結果。可是,給這個函數一個參數 函數能夠計算,給函數兩個參數 函數也能夠計算(計算的方法可能不同),或者給函數三個參數仍然能夠計算(可能計算出來的精確度進一步提高了)。這樣的話,咱們的函數名字同樣,可是參數卻不同。這樣的就是函數重載。固然沒有必要計算的意義也同樣。簡單的來講,函數重載就名字同樣,返回值類型同樣,就是參數不同了。
因此,咱們提煉出幾點:
[注]:當函數重載遇到默認參數時,要避免二義性。
void func(int a,int b = 10) { } void func(int a) { } void test() { func(10);//二義性,編譯器不知道使用哪一個func()了 }
簡單的說一下默認參數,其實很簡單。就是給函數參數設置一個默認值,在參數列表直接等於就好了,按照以上例子你能夠不給b值,默認是10;可是默認參數必須是最後面。不能插入沒有設定默認值的參數void test(int a = 10 ,int b);
這樣是不行的。
在面對func()時,編譯器會可能默認把名字改爲_func;當碰到func(int a)時,可能會默認改爲_func_int;當碰到func(int a,char b)編譯器可能會默認該成_func_int_char。這個「可能」意思時指,如何修飾函數名,編譯器並無一個統一的標準,因此不一樣編譯器會產生不一樣的內部名。
C++同時也容許給算符賦予新的意義
返回值 opertaor算符 (參數列表)
可是C++中並非全部算符均可以重載的:
如下是能夠重載的算符:
如下是不能夠重載的算符:
雖然在規則上是能夠重載 && 和 ||,可是在實際應用中最好很差重載這兩個運算符。其緣由是內置版本的&& ||首先計算左邊的表達式,若是能夠肯定結果,就無需計算右邊了,咱們已經習慣這種特性了,一旦重載便會失去這種特性。
因爲C++語言沒有自動內存回收機制,程序員每次new出來的內存都要手動delete。程序員忘記delete,流程太複雜,最終致使沒有delete,異常致使程序過早退出,沒有執行delete的狀況並不罕見。
因此,開發者能夠經過算符重載,從而達到智能管理內存的效果。
1.對於編譯器來講,智能指針其實是一個棧對象,並不是指針類型,在棧對象生命期即將結束時,智能指針經過析構函數釋放有它管理的堆內存。全部智能指針都重載了「operator->」操做符,直接返回對象的引用,用以操做對象。訪問智能指針原來的方法則使用「.」操做符。
2.所謂智能指針,是根據不一樣的場景來定製智能指針。如下給出一個最簡單的應用:
class Person { public: Person(int age) { this->Age = age; } ~Person() { } void showAge() { cout<<"年齡爲:"<<this->Age<<endl; } private: int Age; }; //智能指針,用來託管自定義類型的對象,讓對象自動釋放。 class smartPointer { public: smartPointer(Person * person) { this->person = person; } //重載->讓智能指針像Person *p同樣去使用 Person * operator->() { return this->person; } //重載* Person & operator*() { return * this->person; } ~smartPointer() { cout<<"智能指針析構了!"<<endl; if(this->person != NULL) { delete this->person; this->oerson = NULL; } } private: Person * person; } void test() { Person p(10);//自動析構 //至關於: //Person * p = new Person(10); //delete p; smartPointer sp(new Person(10));//開闢到棧上,自動析構 sp->showAge();//自己sp不支持這樣的調用,因此要重載-> (*sp).showAge();//一樣做爲智能指針,也要支持這樣的寫法。因此依舊重載* }
咱們上文中是經過算符重載來實現的智能指針,在C++11標準中引入了智能指針概念。
1.理解智能指針
C++和Java有一處最大的區別在於語義不一樣,在Java裏面下列代碼:
Animal a = new Animal(); Animal b = a; //你固然知道,這裏其實只生成了一個對象,a和b僅僅是把持對象的引用而已。但在C++中不是這樣, Animal a; Animal b = a; //這裏倒是就是生成了兩個對象。
2.智能指針的本質
智能指針是一個類對象,這樣在被調函數執行完,程序過時時,對象將會被刪除(對象的名字保存在棧變量中),
這樣不只對象會被刪除,它指向的內存也會被刪除的。
3.智能指針的使用
智能指針在C++11版本以後提供,包含在頭文件<memory>中,shared_ptr、unique_ptr、auto_ptr
這裏只給出建議(智能指針會涉及到不少知識,屬於C++的綜合題):
簡單來講,重寫就是返回值,參數,函數名都和圓臉同樣,以後函數體裏面的方法重寫了。
下面將詳細介紹。
程序調用函數時,編譯器將源代碼中的函數調用解釋爲特定函數代碼塊被稱爲函數名聯編(binding)。C語言中沒有重載,因此每一個函數名字都不一樣,因爲C++中有重載的概念,因此編譯器必須查看函數參數以及函數名才能肯定使用哪一個函數。C/C++編譯器能夠在編譯過程當中完成聯編。而在編譯過程實現的聯編稱靜態聯編(static binding)。所謂動態聯編(dynamic binding)是指聯編在程序運行時動態地進行,根據當時的狀況來肯定調用哪一個同名函數,其實是在運行時虛函數的實現。國內教材有的稱之爲束定。
經過動態聯編引出了虛函數:
語法上來講,虛函數的寫法是:在類成員函數聲明的時候添加 vitual關鍵字。
咱們繼續剛剛有關基類和派生類的特殊用法繼續說,
將派生類的引用或指針轉換成基類的引用和指針咱們稱之爲:向上強制轉換(upcasting)
相反,將基類的引用或指針轉換成派生類的引用和指針咱們稱之爲:向下強制轉換(downcasting)
咱們如今知道,向下轉型是不被容許的。
虛函數指針
虛函數指針 (virtual function pointer) 從本質上來講就只是一個指向函數的指針,與普通的指針並沒有區別。它指向用戶所定義的虛函數,具體是在子類裏的實現,當子類調用虛函數的時候,其實是經過調用該虛函數指針從而找到接口。
虛函數指針是確實存在的數據類型,在一個被實例化的對象中,它老是被存放在該對象的地址首位,這種作法的目的是爲了保證運行的快速性。與對象的成員不一樣,虛函數指針對外部是徹底不可見的,除非經過直接訪問地址的作法或者在DEBUG模式中,不然它是不可見的也不能被外界調用。
只有擁有虛函數的類纔會擁有虛函數指針,每個虛函數也都會對應一個虛函數指針。因此擁有虛函數的類的全部對象都會由於虛函數產生額外的開銷,而且也會在必定程度上下降程序速度。與JAVA不一樣,C++將是否使用虛函數這一權利交給了開發者,因此開發者應該謹慎的使用
虛函數表(如下解釋,來自於https://blog.csdn.net/haoel/a...。由於作圖太麻煩,因此直接選擇性的截取一點。)
在這個表中(V-Table),主是要一個類的虛函數的地址表,這張表解決了繼承、覆蓋的問題,保證其容真實反應實際的函數。這樣,在有虛函數的類的實例中這個表被分配在了這個實例的內存中,因此,當咱們用父類的指針來操做一個子類的時候,這張虛函數表就顯得由爲重要了,它就像一個地圖同樣,指明瞭實際所應該調用的函數。
編譯器應該是保證虛函數表的指針存在於對象實例中最前面的位置(這是爲了保證取到虛函數表的有最高的性能——若是有多層繼承或是多重繼承的狀況下)。 這意味着咱們經過對象實例的地址獲得這張虛函數表,而後就能夠遍歷其中函數指針,並調用相應的函數。
舉個例子:
class Base { public: virtual void f() { cout << "Base::f" << endl; } virtual void g() { cout << "Base::g" << endl; } virtual void h() { cout << "Base::h" << endl; } };
按照上面的說法,咱們經過把Base實例化,來得到虛函數表。
typedef void(*Fun)(void); Base b; Fun pFun = NULL; cout << "虛函數表地址:" << (int*)(&b) << endl; cout << "虛函數表 — 第一個函數地址:" << (int*)*(int*)(&b) << endl; // Invoke the first virtual function pFun = (Fun)*((int*)*(int*)(&b)); pFun();
實際結果以下:
虛函數表地址:0012FED4
虛函數表—第一個函數地址:0044F148
Base::f
經過這個示例,咱們能夠看到,咱們能夠經過強行把&b轉成int ,取得虛函數表的地址,而後,再次取址就能夠獲得第一個虛函數的地址了,也就是Base::f(),這在上面的程序中獲得了驗證(把int 強制轉成了函數指針)。經過這個示例,咱們就能夠知道若是要調用Base::g()和Base::h(),其代碼以下:
(Fun)*((int*)*(int*)(&b)+0); // Base::f() (Fun)*((int*)*(int*)(&b)+1); // Base::g() (Fun)*((int*)*(int*)(&b)+2); // Base::h()
在上面這個圖中,我在虛函數表的最後多加了一個結點,這是虛函數表的結束結點,就像字符串的結束符「/0」同樣,其標誌了虛函數表的結束。這個結束標誌的值在不一樣的編譯器下是不一樣的。(有多是NULL也有多是0)
同時,派生類是否對父類函數進行了覆蓋,虛函數表也是不同的,因此咱們分狀況來討論。
1.無覆蓋
定義以下的繼承關係:
對於實例而言,其虛函數表:
2.有覆蓋(這纔是通常狀況,由於虛函數不覆蓋便毫無心義)
咱們只重載了f()。因此其虛函數表:
Base \*b =newDerive(); b->f();
由b所指的內存中的虛函數表的f()的位置已經被Derive::f()函數地址所取代,因而在實際調用發生時,是Derive::f()被調用了。這就實現了多態。
3.有多個繼承可是無覆蓋
這是其虛函數表:
4.有多個繼承且有覆蓋
其虛函數表是:
三個父類虛函數表中的f()的位置被替換成了子類的函數指針。這樣,咱們就能夠任一靜態類型的父類來指向子類,並調用子類的f()了。
Derive d; Base1 *b1 = &d; Base2 *b2 = &d; Base3 *b3 = &d; b1->f(); //Derive::f() b2->f(); //Derive::f() b3->f(); //Derive::f() b1->g(); //Base1::g() b2->g(); //Base2::g() b3->g(); //Base3::g()
走到這一步,咱們就能夠總結一下了。
剛剛一直在說一個新的詞彙——覆蓋。但其實,可能不少人如今已經知道了,這裏的覆蓋就是重寫。
所謂靜態聯編就是函數重載,所謂動態聯編就是函數重寫
向下強制轉型不被編譯器容許。
同時,咱們利用虛函數表的特性仍能夠作非法的行爲:
訪問non-public的函數
若是父類的虛函數是private或是protected的,但這些非public的虛函數一樣會存在於虛函數表中,因此,咱們一樣可使用訪問虛函數表的方式來訪問這些non-public的虛函數,這是很容易作到的。
class Base { private: virtual void f() { cout << "Base::f" << endl; } }; class Derive : public Base{ }; typedef void(*Fun)(void); void main() { Derive d; Fun pFun = (Fun)*((int*)*(int*)(&d)+0); pFun(); }
咱們如今應該明白:編譯對虛函數使用動態聯編的意思了。
Q:爲何編譯默認是靜態聯編?
A:咱們除了功能之外始終不能忽視就是效率。由於根據上文的描述,咱們不難想到用一些方法來跟蹤基類指針或引用指向的類模型這件事自己其實增長咱們的開銷。所謂C++編譯器選擇了開銷更低的方式。咱們應該優先選擇效率更高的方式來開發程序。
當咱們知道了虛函數的原理的同時也必須知道虛函數到底增長哪些開銷:
class A { public: vitual void A(int a){...} }; class B:piblic A { public: vitual void A(){...} };
派生類中沒有參數的A把基類中有參數的A給隱藏了,並無重寫。有可能編譯器給你警告,也有可能不會。在《C++ Prime Plus》中將這樣的錯誤稱爲「返回類型協變(covariance of return type)」
抽象類(abstract base class ,ABC),這裏的抽象類其實就是Java中所說的接口。並不難理解。
這裏舉一個例子:羊類。咱們能夠寫出山羊類來繼承羊類,一樣也能夠寫綿羊類來繼承羊類,也許咱們還能寫出更不同的羊來繼承羊類。但別忘了,咱們必須給羊類的成員函數作出一個定義,即使羊類的成員函數里根本沒有有意義的代碼。那咱們與其寫沒有意義的代碼,倒不如干脆什麼都別寫。再具體一點: 羊會跑——void run() 同時 void run()中可能會用到羊類裏面的一個屬性——奔跑的速度。可是,不一樣種類的羊跑的速度又不同快。這是咱們會在void run()裏面什麼都不寫。直接一個{}就完事。等待子類重寫這個void run()。因此,這裏的run()雖然有定義,可是這倒是一個接口的思想。因此,咱們能夠把void run()寫出ABC的樣子:vitual void run() = 0;
這樣這個類就變成了抽象類,而run這個函數就成爲了純虛函數即,這個羊類純粹是爲了讓其餘類繼承重寫而出現的。這樣若是之後有新的需求能夠直接來實現這個羊的接口。
//"dma.h" #ifndef DMA_H_ #define DMA_H_ #include <iostream> class baseDMA { private: char * label; int rating; public: baseDMA(const char * l = "null", int r = 0); baseDMA(const baseDMA & rs); virtual ~baseDMA(); baseDMA & operator= (const baseDMA & rs); friend std::ostream & operator<<(std::ostream & os,const baseDMA & rs); }; class lacksDMA:public baseDMA { private: enum{COL_LEN = 40}; char color[COL_LEN]; public: lacksDMA(const char * c = "blank", const char * l = "null", int r = 0); lacksDMA(const char * c, const baseDMA & rs); friend std::ostream & operator<<(std::ostream & os,const lacksDMA & rs); }; class hasDMA:public baseDMA { private: char * style; public: hasDMA(const char * s = "none", const char * l = "null", int r = 0); hasDMA(cosnt char * s, const baseDMA & rs); ~hasDMA(); hasDMA & operator= (cosnt hasDMA & rs); friend std::ostream & operator<< (std::ostream & os,const hasDMA & rs); }; #ennif
#include "dam.h" #include <cstring> baseDMA::baseDMA(const char * l, int r) { label = new char[std::strlen(l) + 1]; std::strcpy(label, rs.label); rating = rs.rating; } baseDMA::~baseDMA() { delete [] label; } baseDMA & baseDMA::operator=(const baseDMA & rs) { if(this == &rs) reurn *this; delete [] label; label = new char[std::strlen(rs.label) + 1]; std::strcpy(label, rs.label); rating = rs.rating; return *this; } std::ostream & operator<<(std::ostream & os, const baseDMA & rs) { os<<"Label:"<< rs.label <<std::endl; os<<"Rating:"<< rs.rating << std::endl; return os; } lacksDMA::lacksDMA(const char * c, const char * l, int r):baseDMA(l,r) { std::strcpy(color, c, 39); color[39] = '\0'; } lacksDMA::lacksDMA(const char * c, const baseDMA & rs):baseDMA(rs) { std::strncpy{color, c, COL_LEN - 1}; color[COL_LEN - 1] = '\0'; } std::ostream & operator<<(std::ostream & os, const lacksDMA & ls) { os<< (const baseDMA &) ls; os<<"Color: "<< ls.color << std::endl; } hasDMA::hasDMA(cosnt char * s, const char * l, int r):baseDMA(l, r) { style = new char[std::strlen(s) + l]; std::strcpy(style,s); } hasDMA::hasDMA(const char * s, const baseDMA & rs):baseDMA(rs) { style = new char[std::strlen(s) + l]; std::strcpy(style,hs.style); } hasDMA::~hasDMA() { delete [] style; } hasDMA & hasDMA::operator=(const hasDMA & hs) { if(this == &hs) return *this; baseDMA::operator=(hs); style = new char[std::strlen(hs.style) + 1]; std::strcpy(style, hs.style); return *this; } std::ostream & operator<<(std::ostream & os, const hasDMA & hs) { os << (cosnt baseDMA & ) hs; os << "Style: " << hs.style << std::endl; return os; } //main.cpp #include <iostream> #include "dma.h" int main() { using std::cout; using std::endl; baseDMA shirt("Porablelly", 8); lacksDMA balloon("red", "Blimpo", 4); hasDMA map("Mercator", "Buffalo Keys", 5); cout << shirt << endl; cour << balloon << endl; lacksDMA balloon2(balloon); hasDMA map2; map2 = map; cout << balloon2 << endl; cout << map2 << endl; return 0; }
-----本文僅我的觀點,歡迎討論。