==========================================================================
day11 面向對象程序設計
==========================================================================ios
1.面向對象程序設計的核心思想是數據抽象、繼承、動態綁定(也叫封裝、繼承、多態)數組
數據抽象:將類的接口和實現分離。
繼 承:能夠定義類似的類型並對其類似關係建模。
動態綁定:能夠在必定程度上忽略類似類型的區別,而以統一的方式使用它們的對象。安全
2.基類將它的成員函數分爲兩類,一種是但願派生類直接繼承而不須要改變的函數;另外一種是但願派生類進行覆蓋的函數。
對於後者,基類一般將其定義爲虛函數(virtual)。任何構造函數以外的非靜態函數均可以是虛函數。
當咱們使用指針或引用調用虛函數時,該調用將被動態綁定,根據指針或引用所綁定的對象的不一樣,該調用可能執行基類版本,也可能執行某個派生類版本。多線程
3.若是基類把一個函數聲明爲virtual,則該函數在派生類中隱式地也是虛函數。ide
4.派生類能訪問基類的共有成員,但不能訪問私有成員。可是某些時候,基類中還有這樣一些成員,基類但願它的派生類能夠訪問,同時禁止其餘用戶訪問。因而產生了protected成員。函數
5.【派生類繼承基類全部成員,除了構造函數和拷貝控制成員。】this
6.派生列表中用到的訪問說明符的做用是,控制派生類從基類繼承來的成員是否對派生類的用戶可見。
class Father{
...
};線程
class Son : private Father{ // 私有繼承,繼承來的基類成員對派生類的用戶不可見
...
};設計
7.咱們能夠將基類的指針或引用綁定到派生類對象上。 但若是基類的對象既不是指針,又不是引用就不能夠。
編譯器也會隱式地執行派生類到基類的轉換。這種隱式特性意味着咱們能夠在須要基類指針或引用的地方使用派生類來代替。指針
【須要記住的一點是,即便基類的引用或指針知道本身其實是一個子類,也不能調用基類中未定義的子類方法或者成員。】
【這是由於,當調用非虛函數時,不會發生動態綁定,實際調用的函數版本由指針或引用的靜態類型決定!】
eg:
class Super{
public:
Super();
virtual void someMethod();
...
};
class Sub : public Super{
public:
Sub();
virtual void someMethod(); // 覆蓋了基類的該方法
virtual void someOtherMethod(); // 子類新增的方法
};
考慮以下:
Sub mySub;
Super &ref = mySub;
mySub.someOtherMethod(); // 正確
ref.someOtherMethod(); // 錯誤 當調用非虛函數時,不會發生動態綁定,實際調用的函數版本由指針或引用的靜態類型決定
// 引用的靜態類型是基類類型,但基類中沒有此方法,因此錯誤!
若是非引用、非指針的對象,則不具有這個特性。
eg: Sub mySub;
Super obj = mySub; // 將派生類對象直接賦值或強制轉換給基類,然而這樣一來,對象就丟失了子類的一些知識。也就是致使了切割(slicing)
// 切割也就是覆蓋的方法和子類數據的丟失
obj.someMethod(); // 調用的是基類版本的此方法,而非派生類版本
不存在基類向派生類的隱式轉換。(但能夠強制轉換,使用dynamic_cast能夠將基類指針/引用安全地轉換爲派生類的指針/引用)
更特殊的是:
Bulk_quote bulk;
Quote *itemp = &bulk; // 正確,基類對象指針指向子類對象
Bulk_quote *bulkp = itemp; // 錯誤,不能將基類轉換爲派生類,即便基類實際上指向的是派生類也不行
8.派生類繼承了基類後,它必須用基類的構造函數來初始化它的基類部分。
【這是C++中的一個關鍵概念:每一個類負責定義各自的接口。要想與類的對象交互必須使用該類的接口,即便這個對象是派生類的基類部分。】
class Quote{
public:
...
Quote(const string &book,double sales_price):bookNo(book),price(sales_price){}
..
private:
string bookNo;
protected:
double price = 0.0;
};
class Bulk_quote : public Quote{ //Bulk_quote繼承了Quote
public:
...
Bulk_quote(const string &,double,size_t,double);
...
private:
size_t min_qty = 0;
double discount = 0.0;
};
eg: Bulk_quote(const string &book,double p,size_t qty,d disc):
Quote(book,p),min_qty(qty),discount(disc){} // 使用基類構造函數初始化它的基類部分
9.防止繼承 (C++新標準)
有時候咱們不但願一個類被其餘類繼承。爲了實現這個目的,在類名後面加關鍵字 final
eg: class NoDerived final {...}
==================================================================================
10.dynamic_cast運算符。用於將基類的指針或引用安全地轉換爲派生類的指針或引用
特別適用於:咱們想使用基類的指針或引用執行某個派生類的操做而且該操做並非虛函數。
dynamic_cast<type*>(e);
dynamic_cast<type&>(e);
e必須知足:e必須是目標type的公有派生類或公有基類或自己就是type類型。
若是符合條件,則轉換成功,不然轉換失敗,要轉換的類型爲指針則返回0,爲引用則拋出bad_cast異常
用法:
對於指針:
if(Derived *d = dynamic_cast<Derived*>(b)){
// 使用d指向的Derived對象
}else{ // 轉換失敗
// 使用b指向的Base對象
}
對於引用:
void f(const Base &b){
try{
const Derived &d = dynamic_cast<Derived&>(b);
// 使用b引用的Derived對象
}catch(bad_cast){
//處理類型轉換失敗的狀況
}
}
typeid運算符。typeid(e),e能夠是任何表達式或類型的名字,返回的是一個常量對象的引用。(type_info類型的)
注意點:typeid忽略頂層const,typeid一個數組,返回數組類型,而不是指針類型。
typeid通常用來比較兩個表達式的類型是否同樣:if(typeid(*b) == typeid(*d)) ... // typeid應該做用於對象,所以咱們使用*b而不是b
【使用RTTI】 p734
某些狀況下,RTTI很是有用,好比當咱們想爲具備繼承關係的類實現相等運算符時。
11. 一般狀況下,若是咱們不使用某個函數,就無須爲該函數提供定義。
但咱們必須爲虛函數提供定義,無論它有沒有被使用到。
12.一個派生類的函數若是覆蓋了某個繼承來的虛函數,則它的形參類型必須與被它覆蓋的基類函數徹底一致。
返回類型也必須一致。但有一個例外:當類的虛函數返回類型是類自己的指針或引用時。
也就是說若是D由B派生獲得,則基類的虛函數能夠返回B*而派生類的對應函數能夠返回D*,只不過這樣的返回類型【要求從D到B的類型轉換是可訪問的】。
13.override說明符的使用。
實際上,派生類若是定義了一個函數與基類中虛函數名字相同但形參列表不一樣,這仍然是合法的行爲。編譯器會認爲這兩個函數是獨立的,也就是派生類的
函數並無覆蓋掉基類中的版本。 這種聲明通常意味着咱們本來想覆蓋基類中的虛函數,但一不當心形參列表弄錯了。而編譯器並不會發現這樣的錯誤,
因而override產生了,它被用來防止這種錯誤。若是咱們使用override標記了某個函數,但該函數並無覆蓋虛函數,此時編譯器就會報錯。
14.final的使用
除了阻止類被繼承以外,咱們還能夠經過在形參列表後面加final來阻止函數被覆蓋。
15.若是虛函數使用默認實參,則基類和派生類中定義的默認實參最好一致。
這是由於:若是虛函數調用使用了默認實參,則該實參值由本次調用的靜態類型決定。
也就是說,若是咱們經過基類的指針或引用調用虛函數,即便實際運行的是派生類中的函數版本,也依舊使用的是基類中定義的默認實參。
16.派生類能夠訪問基類中的protected成員,但有一點必須明確:派生類的成員或友元只能經過派生類對象(而不能經過基類對象)來訪問基類的受保護成員。 p543
17.類的對象不能訪問類的私有成員,只能經過類成員和友元函數訪問。
類的對象也不能訪問類的保護成員,只能經過類的成員函數或派生類的成員函數訪問。之前理解錯了。。。
18.派生類向基類轉換的可訪問性:
1)若是D公有繼承B,【用戶代碼】才能使用派生類向基類的轉換。若是是protected或private則不行。 用戶代碼我理解爲函數體外的地方
2)不論D以什麼方式繼承B,【D的成員函數和友元】均可以使用派生類向基類的轉換。
3)若是D繼承B的方式是公有的或保護的,則【D的派生類的成員和友元】可使用派生類向基類的轉換。若是是私有的則不行。
19.默認繼承
和struct成員默認爲公有,class成員默認爲私有同樣,默認繼承也是如此:
struct D1 : Base{...} // 默認爲公有繼承
class D2 : Base{...} // 默認爲私有繼承
20.改變派生類繼承的某個名字的訪問級別
class Base{
public:
size_t size() const {return n;}
protected:
size_t n;
};
class Derived : private Base{
public:
using Base::size();
protected:
using Base::n;
};
考慮以上代碼,Derived是私有繼承,它繼承來的成員對它的用戶來講是私有。但咱們經過using聲明改變了這些成員的訪問級別,它們再也不是私有的了。
改變以後,Derived的用戶可使用size()成員,而Derived的派生類可使用n
以上須要注意的一點是:派生類只能爲那些它能夠訪問的基類成員提供using聲明。 也就是基類中的private成員不能夠被改變訪問級別
21.派生類的成員將隱藏同名的基類成員,可使用做用域運算符來使用被隱藏的基類成員。
struct Base{
Base():mem(0){}
protected:
int mem;
};
struct Derived:Base{
Derived(int i):mem(i){}
int get_Dmem(){return mem;}
int get_Bmem(){return Base::mem;} // 0
protected:
int mem; // 隱藏了基類中的同名成員
};
Derived d(42);
cout<<d.get_Dmem()<<endl; // 42
22.經過函數調用的解析過程來理解C++繼承:
假定咱們調用p->mem()
1)首先肯定p的靜態類型,因爲咱們調用的是一個成員,p必須是類類型。
2)在p的靜態類型對應的類中查找mem。若是找不到,依次在直接基類中不斷查找直到繼承鏈的頂部。還找不到,則報錯。
3)一旦找到mem,進行常規類型檢查,看調用是否合法。
4)若是合法,則編譯器根據調用的是不是虛函數產生不一樣的代碼。即動態調用仍是普通調用
23.派生類的做用域是嵌套在基類的做用域的,也能夠說基類做用域是外層做用域,派生類是內層做用域。
【在C++中,名字查找發生在類型檢查以前。】 重載是根據函數名、參數的個數、參數的類型
在不一樣的做用域,咱們沒法重載函數名。若是咱們在內層做用域聲明名字,它將隱藏外層做用域中聲明的同名實體。
24.虛析構函數
當咱們delete一個動態分配的對象的指針時,將執行析構函數。
若是咱們delelte一個基類的指針,而該指針實際上指向的是一個派生類對象,就會產生未定義的行爲。
這時應該將基類的析構函數聲明爲虛函數,動態地決定調用基類版本的析構函數仍是派生類版本的。
25.若是構造函數或析構函數調用了某個虛函數,則咱們應該執行與構造函數或析構函數所屬類型相對應的虛函數版本。 p556
26.多重繼承是指從多個直接基類中產生派生類。多重繼承的派生類繼承了全部父類的屬性。
多重繼承的二義性問題
當一個派生類同時繼承了兩個基類,而兩個基類中又包含了同名的成員,則派生類調用該成員時會出現二義性問題。
咱們知道,在C++中名字查找先於類型檢查。因此即便派生類從兩個基類繼承的兩個同名函數的形參列表不一樣,也會致使二義性錯誤。此外,
即便這個同名函數在一個基類中是私有的,在另外一個基類中是公有或保護的,一樣會產生錯誤。
「倒三角」 和 「恐怖菱形」
倒三角:當一個派生類同時繼承了兩個基類,而兩個基類中又包含了同名的成員。
能夠經過做用域運算符::來規避這種二義性錯誤。
要避免二義性錯誤,最好的辦法是:在派生類中爲該函數定義一個新版本。
FA FB
\ /
C
恐怖菱形:以下圖所示,iostream類(間接)繼承了base_io兩次。
base_io
/ \
istream ostream
\ /
iostream
C++中咱們經過虛繼承的機制來解決這個問題。虛繼承的目的是令某個類作出聲明,承諾願意共享它的基類。
在這種機制下,無論虛基類在繼承體系中出現多少次,在派生類中都只包含惟一一個共享的虛基類子對象。
假定類B定義了一個名爲x的成員,D1和D2都是從B虛繼承獲得的,而D又繼承了D1和D2. 若是咱們經過D對象來使用x,有三種可能:
1)若是x在D1和D2中都沒有定義,由於虛繼承的緣故,不存在二義性問題。
2)若是x是B的成員,同時是D1和D2中某一個的成員,則一樣不存在二義性。派生類的x比虛基類B的x優先級更高。
3)若是D1和D2中都有x的定義,則直接訪問x將產生二義性問題。 最好的解決辦法是:在派生類中爲成員定義新的實例。
27.調用虛函數的指針也能夠是this指針,當經過子類對象調用基類中的成員函數時,該函數裏面的this指針將是一個指向子類對象的基類指針,這時再經過this去調用虛函數也能夠表現多態的語法特性.
eg:QT多線程實現 class QThread{//QT官方寫好的類 public: void start(void){ this->run(); } protected: virtual void run(void){線程入口函數} }; class myThread:public QThread{//本身寫的類 protected: //重寫線程入口函數 void run(void){須要放在線程中執行的代碼..} }; myThread thread; //子類重寫的線程入口函數將被執行 thread.start();