繼承、多態知識概括
繼承
所謂繼承,也就是在已有類的基礎上創建新類的過程,適當的使用繼承可以節省代碼量,優化整個程序的結構。
派生類的生成過程
派生類的生成過程經歷了三個步驟:
●吸收基類成員(全部吸收(構造、析構除外),但不一定可見)
在C++的繼承機制中,派生類吸收基類中除構造函數和析構函數之外的全部成員。但是不論種方式繼承基類,派生類都不能直接使用基類的私有成員 。
公有繼承中,派生類對基類的所有成員進行復制(僅把數據的“型”複製,數據的“值”之間沒有關係),在此基礎上在添加新的成員。
注意:基類和派生類的同名成員一共保留兩份副本,訪問時通過類名 :: 成員
方式訪問基類的成員
基類定義的靜態成員,將被所有派生類共享(基類和派生類共享基類中的靜態成員)(共用同一空間,數據同步修改) 派生類中訪問靜態成員,顯式說明類名訪問類名 :: 成員
或通過對象訪問 對象名 . 成員
●改造基類成員
通過在派生類中定義同名成員(包括成員函數和數據成員)來屏蔽(隱藏)在派生類中不起作用的部分基類成員。
派生類定義了與基類同名的成員,在派生類中訪問同名成員時屏蔽(hide)了基類的同名成員
在派生類中使用基類的同名成員,需顯式地使用類名限定符:
類名 :: 成員
●添加派生類新成員
定義派生類對象時,只是複製基類的空間但是沒有賦值
基類的初始化:
派生類構造函數 ( 變元表 ) : 基類 ( 變元表 ) , 對象成員1( 變元表 )… 對象成員n ( 變元表 )
{
// 派生類新增成員的初始化語句
}
構造函數執行順序:基類 ->對象成員->派生類
基類成員調用基類構造函數對其進行初始化
(1)當派生類中不含對象成員時
●在創建派生類對象時,構造函數的執行順序是:基類的構造函數→派生類的構造函數;
●在撤消派生類對象時,析構函數的執行順序是:派生類的析構函數→基類的析構函數。
(2)當派生類中含有對象成員時
●在定義派生類對象時,構造函數的執行順序:基類的構造函數→對象成員的構造函數→派生類的構造函數;
●在撤消派生類對象時,析構函數的執行順序:派生類的析構函數→對象成員的析構函數→基類的析構函數。
析構函數的執行順序與構造函數的執行順序完全相反
多繼承
class 派生類名(參數總表) : 訪問控制 基類名1 (參數表1), 訪問控制 基類名2 (參數表2), … , 訪問控制 基類名n (參數表n)
{
數據成員和成員函數聲明
// 派生類新增成員的初始化語句
};
多繼承方式下構造函數的執行順序:
處於同一層次的各基類構造函數的執行順序取決於定義派生類時所指定的基類順序,與派生類構造函數中所定義的成員初始化列表順序沒有關係。
●先執行所有基類的構造函數
●再執行對象成員的構造函數
●最後執行派生類的構造函數
二義性
如果一個派生類從多個基類派生,而這些基類又有一個共同的基類,則在對該基類中聲明的名字進行訪問時,可能產生二義性。也就是如果在多條繼承路徑上有一個公共的基類,那麼在繼承路徑的某處匯合點,這個公共基類就會在派生類的對象中產生多個基類子對象。
要使這個公共基類在派生類中只產生一個子對象,必須對這個基類聲明爲虛繼承,使這個基類成爲虛基類,虛繼承聲明使用關鍵字virtual
賦值兼容規則
在程序中需要使用基類對象的任何地方,都可以用公有派生類的對象來替代。也就是基礎數據類型中形如double->int轉型,強制類型轉換。
賦值兼容規則中所指的替代包括以下的情況:
a 派生類的對象可以賦給基類對象
b 派生類的對象可以初始化基類的引用
c 派生類的對象的地址可以賦給基類類型的指針
在替代之後,派生類對象就可以作爲基類的對象使用,但只能使用從基類繼承的成員。
如想用派生類指針引用基類對象,派生類指針只有經過強制類型轉換之後,才能引用基類對象 。
基類指針和派生類指針與基類對象和派生類對象4種可能匹配:
- 直接用基類指針引用基類對象;
- 直接用派生類指針引用派生類對象;
- 用基類指針引用一個派生類對象;
- 用派生類指針引用一個基類對象。
多態
多態性(Polymorphism)是指一個名字,多種語義;或界面相同,多種實現。
多態性的實現和聯編這一概念有關。所謂聯編(Binding,綁定)就是把函數名與函數體的程序代碼連接(聯繫)在一起的過程。聯編分成兩大類:靜態聯編和動態聯編。
靜態聯編優點:調用速度快,效率高,但缺乏靈活性;動態聯編優點:運行效率低,但增強了程序靈活性。
C++爲了兼容C語言仍然是編譯型的,採用靜態聯編。爲了實現多態性,利用虛函數機制,可部分地採用動態聯編。
多態從實現的角度來講可以劃分爲兩類:編譯時的多態和運行時的多態。
編譯時的多態是通過靜態聯編來實現的。靜態聯編就是在編譯階段完成的聯編。編譯時多態性主要是通過函數重載和運算符重載實現的,是程序的匹配、連接在編譯階段實現(如函數重載)
運行時的多態是用動態聯編實現的。動態聯編是運行階段完成的聯編。運行時多態性主要是通過虛函數來實現的,程序聯編推遲到運行時進行(如switch、if語句)
靜態聯編
普通函數重載的兩種方式:
1、在一個類說明中重載
2、基類的成員函數在派生類重載
(1)根據參數的特徵加以區分
(2)使用“ :: ”加以區分
(3)根據類對象加以區分
動態聯編
根據賦值兼容,用基類類型的指針指向派生類,就可以通過這個指針來使用類(基類或派生類)的成員函數。
如果這個函數是普通的成員函數,通過基類類型的指針訪問到的只能是基類的同名成員。
而如果將它設置爲虛函數,則可以使用基類類型的指針訪問到指針正在指向的派生類的同名函數。從而實現運行過程的多態。
實現動態聯編方式的前提:
●先要聲明虛函數
●類之間滿足賦值兼容規則
●通過指針與引用來調用虛函數。
實現多態的兩個條件:
1、類的定義時存在繼承關係,存在虛機制,派生類重寫虛函數
2、運行時,以基類對象的指針或引用,將派生類對象綁定給基類對象或指針,並運行虛函數
有關虛函數:
- 一個虛函數,在派生類層界面相同的重載函數都保持虛特性
- 虛函數必須是類的成員函數
- 不能將友元說明爲虛函數,但虛函數可以是另一個類的友元
- 析構函數可以是虛函數,但構造函數不能是虛函數
關於重載和覆蓋:
- 重載是函數名稱相同,參數個數、類型不同,返回值相同(返回值不能區分函數的重載,如果返回值類型不同會被c++認爲錯誤重載)
- 覆蓋是函數名、返回類型、參數個數、參數類型和順序完全相同,如果函數原型不同,僅函數名相同,丟失虛特性。
純虛函數和抽象類
- 純虛函數是一個在基類中說明的虛函數,在基類中沒有定義, 要求任何派生類都定義自己的版本,爲各派生類提供一個公共界面。
- 純虛函數說明形式:
virtual 類型 函數名(參數表)= 0 ;
- 一個具有純虛函數的基類稱爲抽象類。
- 定義抽象類不能直接生成對象,但是可以生成指針和引用
- 派生抽象類時,一定要重寫純虛函數
實際使用
在我們實際使用過程中,首先根據需求設計出類,分析這些類,看一下這些類裏面是否有重複的部分,如果有,那麼就將重複的部分抽象出來形成一個基類,在需要使用這些重複功能的子類中繼承基類,從而節省了代碼量。
以圖書管理系統爲例,在上一個版本中,實現了管理員端,在管理員端中有需要用到查找的地方,比如管理員對書、記錄等的查找,而在現在的版本中實現了客戶端,在用戶使用的時候也會對書籍、記錄等信息進行查詢,這兩部分代碼的查找部分是重複的, 所以將這兩部分抽象出一個新的基類,然後管理員、客戶端分別繼承這個基類,從而實現查找的部分功能,在繼承查找功能後,再對某些功能進行修改改造,比如在客戶端中,查找讀者的借閱記錄只能查詢本人的,那麼需要在派生類中對改函數進行重寫覆蓋;另外客戶端和管理員端本身還有其他的功能,比如管理員的對書和讀者的增刪查改啊,客戶端的借、續、還操作啊,都是需要在派生類中新添加的。
class Find
{
protected:
vector<Book> book;
vector<Record> record;
multimap<string, int>bookname;
multimap<string, int>bookid;
multimap<Time, int>booktime;
multimap<string, int>bookpub;
multimap<string, int>bookwriter;
multimap<string, int>bmap;
multimap<string, int>rmap;
public:
Find();
~Find();
void getBRecord(string id);
void getRRecord(string id);
void findBookByName(string name);
void findBookById(string id);
void findBookByPub(string pub);
void findBookByPDate(Time date);
void fuzzyFindBname(string str);
void multifind1(string str1, string str2);//str1是作者,str2是書名
void multifind2(string str1, string str2);//str1是出版社,str2是作者
void findBookByTime(Time t1, Time t2);
};
Find::Find() {}
Find ::~Find() {}
void Find::multifind1(string str1, string str2)
{
//實現過程略
}
void Find::multifind2(string str1, string str2)
{
//實現過程略
}
void Find::fuzzyFindBname(string str)
{
//實現過程略
}
void Find::findBookByTime(Time t1, Time t2)
{
//實現過程略
}
void Find::findBookByName(string name)
{
//實現過程略
}
void Find::findBookByPub(string name)
{
//實現過程略
}
void Find::findBookByPDate(Time str)
{
//實現過程略
}
void Find::findBookById(string id)
{
//實現過程略
}
void Find::getBRecord(string id)
{
//實現過程略
}
void Find::getRRecord(string id)
{
//實現過程略
}
class Operation :public Find
{
private:
Reader reader;
Time time;
public:
Operation(Reader r, Time t)
{
loadBook();
loadRecord();
reader = r;
time = t;
}
~Operation()
{
saveBook();
saveRecord();
}
void loadBook();
void loadRecord();
void saveBook();
void saveRecord();
void getRRecord();
void borrowBook(string str);
void renewBook(string str);
void returnBook(string str);
//void bookDisplay();
};
void Operation::loadBook()
{
//實現過程略
}
void Operation::saveBook()
{
//實現過程略
}
void Operation::loadRecord()
{
//實現過程略
}
void Operation::saveRecord()
{
//實現過程略
}
void Operation::getRRecord()//重寫函數
{
Find::getRRecord(this->reader.getSno());
}
void Operation::borrowBook(string str)
{
//實現過程略
}
void Operation::renewBook(string str)
{
//實現過程略
}
void Operation::returnBook(string str)
{
//實現過程略
}
在使用過程中注意,定義派生類對象時,只是複製基類的空間但是沒有賦值,所以在使用派生類對象時,操作的都是派生類本身生成的對象空間,和基類無關。函數也是如此,僅僅是將基類的函數代碼複製到派生類中。
當然,作爲面向對象中三大特性中的繼承和多態,其作用遠遠不止節省代碼這麼簡單,在設計模式中,其主要思想就是利用繼承和多態,實現更優的軟件框架,從而具有更好的可擴展、可複用、可維護性,進而將編程的過程視爲“工程”,提高代碼的規範性,最終提高效率。
下面以C++實現抽象工廠模式爲例,簡單說明繼承和多態的實際應用
#include<iostream>
using namespace std;
//真正的車
class AbstractRealCar
{
public:
virtual void run() = 0;
};
//玩具車
class AbstractToyCar
{
public:
virtual void run() = 0;
};
//BMW
class RealBMW : public AbstractRealCar
{
public:
virtual void run()
{
cout << "real BMW run!" << endl;
}
};
//BenZ
class RealBenZ : public AbstractRealCar
{
public:
virtual void run()
{
cout << "real BenZ run!" << endl;
}
};
//BMW
class ToyBMW : public AbstractToyCar
{
public:
virtual void run()
{
cout << "toy BMW run!" << endl;
}
};
//BenZ
class ToyBenZ : public AbstractToyCar
{
public:
virtual void run()
{
cout << "toy BenZ run!" << endl;
}
};
//抽象工廠
class AbstractFactory
{
public:
virtual AbstractRealCar* CreateRealCar() = 0;
virtual AbstractToyCar* CreateToyCar() = 0;
};
class BMWFactory : public AbstractFactory
{
public:
virtual AbstractRealCar* CreateRealCar()
{
return new RealBMW();
}
virtual AbstractToyCar* CreateToyCar()
{
return new ToyBMW();
}
};
class BenZFactory : public AbstractFactory
{
public:
virtual AbstractRealCar* CreateRealCar()
{
return new RealBenZ();
}
virtual AbstractToyCar* CreateToyCar()
{
return new ToyBenZ();
}
};
void test01()
{
AbstractFactory* factory = NULL;
factory = new BMWFactory;
AbstractRealCar* real_car = factory->CreateRealCar();
real_car->run();
delete real_car;
real_car = NULL;
AbstractToyCar* toy_car = factory->CreateToyCar();
toy_car->run();
delete toy_car;
toy_car = NULL;
delete factory;
factory = NULL;
factory = new BenZFactory;
real_car = factory->CreateRealCar();
real_car->run();
delete real_car;
real_car = NULL;
toy_car = factory->CreateToyCar();
toy_car->run();
delete toy_car;
toy_car = NULL;
delete factory;
factory = NULL;
}
int main()
{
test01();
return 0;
}
其UML類圖
所以,在適當的時候利用繼承和多態可以更好的發揮出面向對象編程的優點
學習感想
至此,本學期的C++理論課程已接近尾聲,雖然短短的一學期學到的可能也只是C++的一部分,但是學到現在也可以站到整個語言的角度來和其他語言做對比,在學C++繼承和多態的過程中,由於上個學期學過了java語言,又零零散散地接觸了一些其他的語言,C++語言給我的印象是“功能全面但過於臃腫”。C++是基於C語言改編的,所以在很多場景都少不了C語言的影子,而C++本身又是一門面向對象的語言,所以更少不了面向對象的功能,更何況C++考慮的還很全面,照顧到了各種情況,許多java等純面向對象的語言沒有的功能C++都提供了,這樣一組合造成了C++的臃腫,所以有一些功能其實雖然有,但是並不好用。比如前面的友元就很雞肋,破壞了好不容易纔封裝的一個類,所以只有重載運算符等特殊情況纔會用,還有繼承這裏的多繼承,java是沒有多繼承的,只有對多接口的實現,很好的避免了二義性的問題,而C++提供了完整的多繼承方案,十分強大,雖然會產生二義性,但是還特地提供了虛基類來解決二義性問題,那麼強大的功能爲啥不好呢?因爲其在使用過程中非常的麻煩,而且多繼承的情況很少,不太實用。綜上,我個人現階段認爲C++的特點是“功能強大,使用複雜”。所以,我認爲C++作爲初學編程的小白的敲門磚是很合適的,因爲可以通過C++全面的瞭解編程的整體思路,熟悉各類功能的具體用法,畢竟雖然語言的語法不同,但是功能基本相通,學好了C++以後再學什麼編程語言我相信都是“輕車熟路”。
下面談一下近期的系統開發收穫,最近系統開發這方面最近有些放鬆,但是學到繼承多態這裏發現,繼承和多態對一個系統來說十分重要。像上面介紹其應用時所說,一方面可以節省代碼量,另一方面還可以優化代碼的結構,使得我們的代碼可擴展、可複用、可維護。所以,在今後的編程中,一定不要忘記多態的合理使用。對於這種的系統開發,應該首先根據需求功能實現出來幾個類,然後再觀察這些類是否有重複的部分,如果有則將其共有的部分抽象出來,形成一個新類做父類,在需要使用這些重複功能的子類中繼承父類,從而節省了代碼量。也就是說,我們在開發系統的過程中,需要從下向上的進行,而不能從父類向下寫,那樣有可能出現考慮不周的問題。在完成類的抽象後,再分析類與類之間的關係,如果類之間功能相近,可以考慮構造抽象類、虛函數,從而實現多態。
最後反思一下最近的學習狀態,開學的時間突然公佈,這是我意想不到的,前兩天我還估計山東省開學一定會在全國兩會之後,接着沒多久就打臉了,雖然最終到底能不能返校尚不清楚,但是瞬間感到了壓力。因爲一回到學校,生活可能會有各種不便,另一方面是估計一返校就要迎接不少考試,還有已經拖了很久的工作上的問題,所以一返校估計也是忙的不可開交。這就導致最近學習有點小浮躁,總是關注各種羣,各種小道消息,但很多消息之間是相互矛盾的,可信度也沒有保證,這兩天一想,其實這麼關注開學也沒有用,開不開學也不是我能決定的,該啥時候開學我不想去也得去,關注這些小道消息耗費精力沒有一點用處,還不如現在專心學習,啥時候通知返校了啥時候回去就是,我唯一能做的是儘量提前做一些準備工作,如果返校的話也不至於太忙。另外,現在已經13周了,一部分課程已經結課了,有考試的壓力,還有幾篇論文壓着沒寫,另外課外的東西好長時間沒學了,也想抽個時間練練手,20年建模下通知了,也要暑假前練一下,往年按理說現在應該有不少的建模模擬比賽,今年到底什麼樣還都不明朗。所以雖然課程少了,但壓力沒少多少,還是需要保持一個緊張的學習狀態。今年的疫情打亂了所有的安排,但是我相信今年雖然比賽少了,但是在這個時間不爲了拿獎,安心學習知識,提高能力,以後可能拿到的獎含金量更高。到現在感覺時間是真快啊,時間纔是最寶貴的東西,如果不返校的話,我們回去的時候就是大三的老學長了,說的嚴峻點也就是還有一年的大學生活,所以還是希望我自己不要忘記初心,不要鬆懈,一方面把課內的知識學好,另一方面不要把自己的理想放鬆,珍惜寶貴的時間,多學點知識,加油!