面向對象程序設計(Object-oriented programming)的核心思想是數據抽象,繼承,和動態綁定。
1. 繼承
在C++語言中,基類將類型相關的函數與派生類不做改變直接繼承的函數區分對待。對於某些函數,基類希望它的派生類各自定義適合自身的版本,此時基類就將這些函數聲明成虛函數(virtual function)。
1 class Quote { 2 public: 3 std::string isbn() const; 4 virtual double net_price(std::size_t n) const; 5 };
派生類必須通過使用派生列表(class derivation list)明確指出它是從哪個(哪些)基類繼承而來的:
如下:
1 class Bulk_quote : public Quote { 2 public: 3 double net_price(std::size_t) const override; 4 };
派生類必須在其內部對所有重新定義的虛函數進行聲明。派生類可以在這樣的函數之前加上virtual關鍵字,但是並不是非得這么做。並且,C++11新標准允許派生類顯式地注明它將使用哪個成員函數改寫基類的虛函數,具體措施是在改函數的形參列表之后增加一個override關鍵字。
1.1 訪問控制和繼承

1 #include<iostream> 2 class A { 3 public: 4 int p = 0; 5 virtual void test(); 6 private: int p2 = 1; 7 protected: int p3 = 2; 8 }; 9 10 void A::test() 11 { 12 std::cout << this->p << this->p2 << this->p3 << std::endl; 13 } 14 15 class B:public A { 16 public: 17 int b = 3; 18 void test() { 19 std::cout << this->b << this->b2 << this->b3 << std::endl; 20 } 21 22 void test2() { 23 std::cout << this->p3 << std::endl; // 派生類可以訪問基類的protect和public 24 } 25 26 friend void test3() { 27 std::cout << this-> << std::endl; 28 } 29 private: int b2 = 4; 30 protected: int b3 = 5; 31 }; 32 33 int main() 34 { 35 A a; 36 std::cout << a.p << std::endl;// 只能訪問自己的public 37 a.test(); 38 39 B b; 40 std::cout << b.b << b.p << std::endl;// 派生類 只能訪問自己的puiblic和基類的public 41 b.test(); 42 43 }
1.2 定義基類和派生類
1.定義基類。
Note:基類通常都應該定義一個虛析構函數,即使該函數不執行任何實際操作也是如此。
1 class Quote { 2 public: 3 Quote() = default; 4 Quote(const std::string &book, double sales_price): 5 bookNo(book), price(sales_price){} 6 std::string isbn()const {return bookNo;} 7 //返回給定數量的書籍的銷售總額 8 //派生類負責改寫並使用不同的折扣計算算法 9 virtual double net_price(std::size_t n) const 10 {return n*price;} 11 virtual ~Quote() = default; //對析構函數進行動態綁定 12 private: 13 std::string bookNo; 14 protected: 15 double price = 0.0; //代表普通狀態下不打折的價格 16 };
- 基類必須將它的兩種成員函數區分開來:一種是基類希望其派生類進行覆蓋的函數,基類通常將其定義為虛函數;另一種是基類希望派生類直接繼承而不要改變的函數。
- 任何構造函數之外的非靜態函數都可以是虛函數,關鍵字virtual只能出現在類內部的聲明語句之前而不能用於類外部的函數定義。
- 如果基類把一個函數聲明為虛函數,則該函數在派生類中隱式地也是虛函數。
- 成員函數如果沒被聲明為虛函數,則其解析過程發生在編譯時而非運行時。
- 派生類能訪問公有成員,而不能訪問私有成員。
- 不過在某些時候,基類中還有一種成員,基類希望它的派生類有權訪問該成員,同時禁止其他用戶訪問。我們用受保護的(protected)訪問運算符說明這樣的成員。
2. 定義派生類
1 class Bulk_quote : public Quote { 2 public: 3 Bulk_quote() = default; 4 Bulk_quote(const std::string&, double, std::size_t, double); 5 //覆蓋基類的函數版本以實現基於大量購買的折扣政策 6 double net_price(std::size_t) const override; 7 private: 8 std::size_t min_qty = 0; //使用折扣政策的最低購買量 9 double discount = 0.0; //以小數表示的折扣額 10 };
- 派生類經常(但不總是)覆蓋它繼承的虛函數。如果派生類沒有覆蓋其基類中的某個虛函數,則該虛函數的行為類似於其他的普通成員,派生類會直接繼承其在基類中的版本。
- C++新標准允許派生類顯式地注明它使用某個成員函數覆蓋了它繼承的虛函數。具體做法是在函數后面加上關鍵字override。
- 在派生類對象中含有與其基類對應的組成部分,所以我們能把派生類的對象當成基類對象來使用,而且我們也能將基類的指針或引用綁定到派生類對象中的基類部分上。如下:
1 Quote item; //基類對象 2 Bulk_quote bulk; //派生類對象 3 Quote *p = &item; //p指向Quote對象 4 p = &bulk; //p指向bulk的Quote部分 5 Quote &r = bulk; //r綁定到bulk的Quote部分
3. 派生類構造函數
派生類可以繼承基類的成員,但是不能直接初始化這些成員(每個類控制它自己的成員初始化過程)。如果沒有在子類中對父類進行初始化,則父類必須有默認構造函數。
1 Bulk_quote(const std::string &book, double p, std::size_t qty, 2 double disc):Quote(book, p), min_qty(qyt), discount(disc) { }
4.派生類使用基類的成員
派生類可以訪問基類的公有成員,和受保護成員
5.繼承與靜態成員
如果基類中有靜態變量,則不論派生出多少類,對每個靜態成員來說都只存在唯一實例。如果基類中靜態成員是private,則派生類無權訪問,假設派生類可以訪問,則我們既能通過基類也能通過派生類使用它。
6.防繼承
C++11新標准提供了一種防止繼承發生的方法,即在類名后跟一個關鍵字final
1 class NoDerived final { /* */}; //NoDerived不能作為基類
7.不存在從基類向派生類的隱式類型轉換
之所以存在派生類向基類的類型轉換是因為每個派生類都包含了基類的一部分,而基類引用或者指針可以綁定到該基類部分上。但是因為一個基類對象可能是派生類對象的一部分,也可能不是,所以不存在從基類向派生類的自動類型轉換。
1 Quote base; 2 Bulk_quote* blukP = &base; //不合法 3 Bulk_quote& blukRef = base; //不合法
下面這種情況也是不允許的
1 Bulk_quote bulk; 2 Quote* itemP = &bulk; //合法,基類綁定派生類 3 Bulk_quote* blukRef = itemP; //不合法
如果基類中含有一個或多個虛函數,我么可以使用 dynamic_cast 請求類型轉換。
8.對象之間不存在類型轉換
派生類向基類的自動類型轉換只對指針或引用類型有效,在派生類類型和基類類型之間不存在這樣的轉換。很多時候,我們確實希望將派生類對象轉換成基類類型,但是這種轉換往往與我們所期望的不一樣。
請注意,當我們初始化或賦值一個類類型的對象時,實際上實在調用某個函數。當執行初始化時,我們調用構造函數,而當執行賦值操作時,我們調用賦值運算符。
因為這些成員接受引用作為參數,多以派生類向基類的轉換允許我們給基類的拷貝和移動操作傳遞一個派生類的對象。這些操作不是虛函數,當我們給基類的構造函數傳遞一個派生類對象時,實際運行的構造函數是基類中定義的那個,顯然,該運算符只能處理基類自己的成員。
1 Bulk_quote bulk; // 派生類對象 2 Quote item(bulk);// 使用Quote::Quote(const Quote&)構造函數 3 item = bulk; // 調用Quote::opertator=(const Quote&)。同時忽略掉派生類中的部分成員
2. 動態綁定
在C++語言中,當我們使用基類的引用(或指針)調用一個虛函數時將發生動態綁定。明晰派生類調用到底調用誰的print方法。
比如:
1 #include<iostream> 2 class A { 3 public: 4 A() = default; 5 virtual void print() { 6 std::cout << "a" << std::endl; 7 } 8 virtual ~A() { 9 std::cout << "destroy A" << std::endl; 10 }; 11 }; 12 13 class B :public A { 14 public: 15 B() = default; 16 void print() { 17 std::cout << "b" << std::endl; 18 } 19 ~B() { 20 std::cout << "destroy B" << std::endl; 21 } 22 23 }; 24 25 int main() 26 { 27 A a; 28 B b; 29 a.print(); 30 b.print(); 31 32 /// 動態綁定.如果基類中 print 方法不是虛函數,則以下結果均為a 33 A *a2 = &a; 34 A *b2 = &b; 35 a2->print();// a 36 b2->print();// b 37 38 A &a3 = a; 39 A &b3 = b; 40 a3.print();// a 41 b3.print();// b 42 43 /// 強行調用基類 44 //b.A::print(); // a 45 //b2->A::print(); // a 46 //b3.A::print(); // a 47 48 }
3. 虛函數
如第二點所述,當我們使用基類的引用或指針調用一個虛函數時會執行動態綁定。因為我們直到運行時,才能知道到底調用了哪個版本的虛函數(上面的例子能看出,但是編譯器看不出),所以所有虛函數都必須定義。需要注意的,動態綁定必須通過指針,引用,調用虛函數才會發生。如果使用類類型【如:Quote base; Bulk_Quote derived; base=derived; base.net_price(21)。其中net_price是基類虛函數,派生類重寫了。這個情況下編譯時,會被解析成基類的net_price() 方法。】非虛函數在編譯時進行綁定。
一個派生類的函數如果覆蓋了某個繼承來的虛函數,則它的形參類型必須與被覆蓋的基類函數完全一致。
3.1 final和override說明符
派生類如果定義了一個和基類中名字相同但是形參列表不同,這也是合法的。編譯器會認為這是兩個函數與基類中原有的函數是相互獨立的。但是這可能不是我們想要的,我們想要覆蓋基類方法,但是編譯器不報錯。這是c++11 中新出了 overrride 關鍵字,這可以讓編譯器明白我們的用意,並為我們發現錯誤(形參是否寫錯了等)。
如果我們用 override 關鍵字標記了某個函數,但是該函數沒有覆蓋已存在的虛函數,此時編譯器會報錯。
如果使用 final 關鍵詞標記函數,則不允許后序其他類覆蓋該方法。
1 struct C { 2 virtual void f1() const; 3 }; 4 5 struct D:C 6 { 7 void f1() const final; // final 修飾虛函數 8 }; 9 void D::f1() const { 10 11 } 12 struct E:D 13 { 14 void f1() const ; // 錯誤 15 };
4.抽象基類
4.1 純虛函數
為什么要有純虛函數?因為在許多情況下,在基類中不能對虛函數給出有意義的實現(比如動物基類中“叫”的方法),而把它聲明為純虛函數,它的實現完全留給派生類去做。凡是含有純虛函數的類叫做抽象類,這種類不能聲明對象,只能作為基類為派生類服務。
1 virtual void f3()=0;
派生類構造函數只初始化它的直接基類。
5. 訪問控制與繼承
5.1 受保護的成員
類中的私有變量只能由該類的成員方法能訪問。一個類使用protected關鍵字來聲明哪些希望與派生類分享但是不想被其他公共訪問使用的成員,protected可以看成是private 和 public的中和產物:
- 和私有成員類似,受保護的成員對於類的用戶來說不可訪問
- 和公有成員類似,受保護的成員對於派生類的成員和友元來說是可訪問的
- 派生類的成員或友元只能通過派生類對象來訪問基類的受保護成員。派生類對於一個基類對象中的受保護成員沒有任何訪問權
1 class Base { 2 protected: 3 int prot_mem; //protected成員 4 }; 5 class Sneaky : public Base { 6 friend void clobber(Sneaky&); //能訪問Sneaky::prot_mem 7 friend void clobber(Base&); //不能訪問Base::prot_mem 8 int j; //j默認是private 9 }; 10 //正確:clobber能訪問Sneaky對象的private和protected成員 11 void clobber(Sneaky &s) { s.j = s.prot_mem = 0; } 12 //錯誤:clobber不能訪問Base的protected成員 13 void clobber(Base &b) { b.prot_mem = 0; }
- 在類的外部(比如main函數中),類的pirvate成員不管是對該類的對象還是該類派生類的對象,都是無訪問權限的
5.2 公有,私有和受保護繼承
1 #include<iostream> 2 using namespace std; 3 4 class base { 5 public: 6 int pub; 7 void pub_fun_base() { 8 cout << "base pub_fun" << endl; 9 } 10 private: 11 int pri; 12 void pri_fun_base() { 13 cout << "base pri_fun" << endl; 14 } 15 protected: 16 int pro; 17 void pro_fun_base() { 18 cout << "base pro_fun" << endl; 19 } 20 }; 21 22 struct pub_derived : public base 23 { 24 void f() { 25 cout << pub << pro << endl; // 不能訪問父類的私有變量 pri 26 pub_fun_base(); 27 pro_fun_base(); 28 } 29 30 }; 31 32 struct pri_derived : private base // 派生類說明符private同public看似訪問權限一樣,那該說明符何用?(在main函數中解釋) 33 { 34 void f() { 35 cout << pub << pro << endl; 36 pub_fun_base(); 37 pro_fun_base(); 38 } 39 }; 40 41 struct pro_derived : protected base 42 { 43 void f() { 44 cout << pub << pro << endl; 45 pub_fun_base(); 46 pro_fun_base(); 47 } 48 }; 49 50 int main() 51 { 52 base base1; 53 base1.pub; 54 base1.pub_fun_base(); // 說明基類對象能直接訪問public成員,protect/private不可。在成員方法中均可 55 56 pub_derived pub_d; 57 pri_derived pri_d; 58 pro_derived pro_d; 59 pub_d.f(); 60 pri_d.f(); 61 pro_d.f(); 62 63 pub_d.pub_fun_base(); // 派生類說明符作用在此,控制基類中繼承過來的公有成員是保持public還是改成private/public 64 //pri_d.pub_fun_base(); //非法 // 基類中的public成員通過private說明符繼承,則該方法不能由派生類對象直接訪問了,相當於變成了派生類中的private成員 65 //pro_d.pub_fun_base(); //非法 66 return 0; 67 }
5.3 虛析構函數
1 #include<iostream> 2 using namespace std; 3 4 struct base { 5 base() { cout << "base create" << endl; } 6 ~base(){ cout << "base destroy" << endl; } 7 }; 8 9 struct derived_base : public base { 10 derived_base() { cout << "derived_base create" << endl; } 11 ~derived_base() { cout << "derived_base destroy" << endl; } 12 }; 13 14 int main() 15 { 16 17 derived_base* db = new derived_base(); 18 delete db; 19 20 return 0; 21 }
下面更改下代碼如下:
1 #include<iostream> 2 using namespace std; 3 4 struct base { 5 base() { cout << "base create" << endl; } 6 virtual ~base(){ cout << "base destroy" << endl; } 7 }; 8 9 struct derived_base : public base { 10 derived_base() { cout << "derived_base create" << endl; } 11 ~derived_base() { cout << "derived_base destroy" << endl; } 12 }; 13 14 int main() 15 { 16 base* b = new derived_base(); // 使用動態綁定 17 delete b; 18 19 return 0; 20 }
如果沒有使用動態綁定,就是第一個例子。如果使用了動態綁定,但是如果沒有第二個例子第六行在基類中定義虛析構函數,則沒有截圖中紅色箭頭銷毀派生類的操作。為什么呢?因為我們知道第一個例子是靜態綁定,第二個例子是動態綁定,動態綁定在運行時才能確定執行基類or派生類方法,動態綁定的條件時:①使用指針或引用②使用虛函數。所以如果沒有在基類中定義虛函數,則析構函數沒有采用動態綁定,而是使用靜態綁定,則只會執行基類的析構,而不會執行派生類的虛構函數。