面向對象程序設計——概述,定義基類和派生類,虛函數
一、OOP:概述
面向對象程序設計的核心思想是數據抽象、繼承和動態綁定。通過使用數據抽象,我們可以將類的接口和實現分離;使用繼承,可以定義相似的類型並對其相似關系建模;使用動態綁定,可以在一定程度上忽略相似類型的區別,而以統一的方式使用它們的對象。
1)繼承
通過繼承聯系在一起的類構成一種層次關系。通常在層次關系的根部有一個基類,其他類則直接或間接地從基類繼承而來,這些繼承得到的類稱為派生類。基類負責定義在層次關系中所有類共同擁有的成員,而每個派生類定義各自的成員。
在C++語言中,基類將類型相關的函數與派生類不做改變直接繼承的函數區分對待。對於某些函數,基類希望它的派生類各自定義適合自身的版本,此時基類就將這些函數聲明成虛函數。
1 class Quote { 2 public: 3 std::string isbn()const { return bookno; } 4 virtual double net_price(std::size_t n) const { 5 std::cout << "Quote::net_price" << std::endl; 6 return price; 7 } 8 public: 9 std::string bookno; 10 double price; 11 };
派生類必須通過使用類派生列表明確指出它是從哪個(哪些)基類繼承而來的。類派生列表的形式是:首先是一個冒號,後面緊跟以逗號分隔的基類列表,其中每個基類前面可以有訪問說明符。
1 class BulkQuote:public Quote{ 2 public: 3 virtual double net_price(std::size_t n) const override { 4 std::cout << "BulkQuote::net_price" << std::endl; 5 if (n > 10)View Code6 return 0.5 * price; 7 return price; 8 } 9 };
派生類必須在其內部對所有重新定義的虛函數進行聲明。派生類可以在這樣的函數之前加上virtual關鍵字,但是並不是非得這麽做。C++11新標準允許派生類顯式地註明它將使用哪個成員函數改寫基類的虛函數,具體措施是在該函數的形參列表之後增加一個override關鍵字。
2)動態綁定
通過使用動態綁定,我們可以用同一段代碼分別處理Quote和BulkQuote的對象。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class Quote { 9 public: 10 std::string isbn()const { return bookno; } 11 virtual double net_price(std::size_t n) const { 12 std::cout << "Quote::net_price" << std::endl; 13 return price; 14 } 15 public: 16 std::string bookno; 17 double price; 18 }; 19 20 class BulkQuote:public Quote{ 21 public: 22 virtual double net_price(std::size_t n) const override { 23 std::cout << "BulkQuote::net_price" << std::endl; 24 if (n > 10) 25 return 0.5 * price; 26 return price; 27 } 28 }; 29 30 void print_total(std::ostream &os, const Quote &item, std::size_t n) { 31 double price = item.net_price(n); 32 std::cout << "ISBN:" << item.isbn() << " 原價:" << item.price 33 << " 折後價:" << price << std::endl; 34 } 35 int main() 36 { 37 Quote q; 38 q.bookno = "233"; 39 q.price = 100; 40 BulkQuote bq; 41 bq.bookno = "123"; 42 bq.price = 100; 43 print_total(std::cout, q, 20); 44 std::cout << "------------------" << std::endl; 45 print_total(std::cout, bq, 20); 46 return 0; 47 }View Code
因為上述過程中函數的運行版本由實參決定,即在運行時選擇函數的版本,所以動態綁定有時又被稱為運行時綁定。
在C++語言中,當我們使用基類的引用或指針調用一個虛函數時將發送動態綁定。
二、定義基類和派生類
1、定義基類
基類通常都應該定義一個虛析構函數,即使該函數不執行任何實際操作也是如此。
1 class Quote { 2 public: 3 Quote() = default; 4 Quote(const std::string &_bookno, double _price) 5 :bookno(_bookno), price(_price) {} 6 std::string isbn()const { return bookno; } 7 virtual double net_price(std::size_t n) const { 8 std::cout << "Quote::net_price" << std::endl; 9 return n * price; 10 } 11 virtual ~Quote() = default; // 對析構函數進行動態綁定 12 private: 13 std::string bookno; // 書籍的ISBN編號 14 protected: 15 double price; // 代表普通狀態下不打折的價格 16 };View Code
1)成員與繼承
在C++語言中,基類必須將它的兩種成員函數區分開來:一種是基類希望其派生類進行覆蓋的函數;另一種是基類希望派生類直接繼承而不要改變的函數。對於前者,基類通常將其定義為虛函數。當我們使用指針或引用調用虛函數時,該調用將被動態綁定。根據引用或指針所綁定的對象類型不同,該調用可能執行基類的版本,也可能執行某個派生類的版本。
基類通過在其成員函數的聲明語句之前加上關鍵字virtual使得該函數執行動態綁定。任何構造函數之外的非靜態函數都可以是虛函數。關鍵字virtual只能出現在類內部的聲明語句之前而不能用於類外部的函數定義。如果基類把一個函數聲明成虛函數,則該函數在派生類中隱式地也是虛函數。
2)訪問控制與繼承
派生類可以繼承定義在基類中的成員,但是派生類的成員函數不一定有權訪問從基類繼承而來的成員。和其他使用基類的代碼一樣,派生類能訪問公有成員,而不能訪問私有成員。不過某些時候基類中還有這樣一種成員,基類希望它的派生類有權訪問該成員,同時禁止其他用戶訪問。我們用受保護的訪問運算符說明這樣的成員。
2、定義派生類
派生類必須通過使用類派生列表明確指出它是從哪個(哪些)基類繼承而來的。類派生列表的形式是:首先是一個冒號,後面緊跟以逗號分隔的基類列表,其中每個基類前面可以有以下三種訪問說明符中的一個:public、protected、private。
派生類必須將其繼承而來的成員函數中需要覆蓋的那些重新聲明。
1 class BulkQuote :public Quote { 2 public: 3 BulkQuote() = default; 4 BulkQuote(const std::string &_bookno, double _price, std::size_t qty, double disc) 5 :Quote(_bookno,_price),min_qty(qty),discount(disc){} 6 virtual double net_price(std::size_t n) const override; 7 private: 8 std::size_t min_qty; // 適用折扣的最低購買量 9 double discount; // 折扣值 10 }; 11 12 double BulkQuote::net_price(std::size_t n) const { 13 std::cout << "BulkQuote::net_price" << std::endl; 14 if (n >= min_qty) 15 return n * (1 - discount) * price; 16 return n * price; 17 }View Code
1)派生類中的虛函數
如果派生類沒有覆蓋其基類中的某個虛函數,則該虛函數的行為類似於其他的普通成員,派生類會直接繼承其在基類中的版本。
派生類可以在它覆蓋的函數前使用virtual關鍵字,但不是非得這樣做。C++11新標準允許派生類顯式地註明它使用某個成員函數的覆蓋了它繼承的虛函數。具體做法是在形參列表後面、或者在const成員函數的const關鍵字後面、或者在引用成員函數的引用限定符後面添加一個關鍵字override。
2)派生類對象及派生類向基類的類型轉換
一個派生類對象包含多個組成部分:一個派生類自己定義的非靜態成員的子對象,以及一個與該派生類繼承的基類對應的子對象,如果多個基類,那麽這樣的子對象也有多個。
C++標準並沒有明確規定派生類的對象在內存中如何分布。
因為在派生類對象中含有與其基類對應的組成部分,所以我們能把派生類的對象當成基類對象來使用,而且我們也能將基類的指針或引用綁定到該派生類對象中的基類部分上。這種轉換通常稱為派生類到基類的類型轉換。和其他類型轉換一樣,編譯器會隱式地執行派生類到基類的轉換。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class Quote { 9 public: 10 Quote() = default; 11 Quote(const std::string &_bookno, double _price) 12 :bookno(_bookno), price(_price) {} 13 std::string isbn()const { return bookno; } 14 virtual double net_price(std::size_t n) const { 15 std::cout << "Quote::net_price" << std::endl; 16 return n * price; 17 } 18 virtual ~Quote() = default; // 對析構函數進行動態綁定 19 private: 20 std::string bookno; // 書籍的ISBN編號 21 protected: 22 double price; // 代表普通狀態下不打折的價格 23 }; 24 25 class BulkQuote :public Quote { 26 public: 27 BulkQuote() = default; 28 BulkQuote(const std::string &_bookno, double _price, std::size_t qty, double disc) 29 :Quote(_bookno,_price),min_qty(qty),discount(disc){} 30 virtual double net_price(std::size_t n) const override; 31 private: 32 std::size_t min_qty; // 適用折扣的最低購買量 33 double discount; // 折扣值 34 }; 35 36 double BulkQuote::net_price(std::size_t n) const { 37 std::cout << "BulkQuote::net_price" << std::endl; 38 if (n >= min_qty) 39 return n * (1 - discount) * price; 40 return n * price; 41 } 42 43 int main() 44 { 45 Quote item; 46 BulkQuote bulk; 47 Quote *p = &item; // p指向Quote對象 48 p = &bulk; // p指向bulk的Quote部分 49 Quote &r = bulk; // r綁定到bulk的Quote對象 50 return 0; 51 }View Code
這種隱式特性意味著我們可以把派生類對象或者派生類對象的引用用在需要基類引用的地方;同樣的,我們也可以把派生類對象的指針用在需要基類指針的地方。
3)派生類構造函數
盡管派生類對象中含有從基類繼承而來的成員,但是派生類並不能直接初始化這些成員。和其他創建了基類對象的代碼一樣,派生類也必須使用基類的構造函數來初始化它的基類部分。
派生類對象的基類部分與派生類對象自己的數據成員都是在構造函數的初始化階段執行初始化操作的。類似我們初始化成員的過程,派生類構造函數同樣是通過構造函數初始化列表來將實參傳遞給基類的構造函數的。首先初始化基類的部分,然後按照聲明的順序依次初始化派生類的成員。
除非我們特別指出,否則派生類對象的基類部分會像數據成員一樣執行默認初始化。如果想使用其他的基類構造函數,我們需要以類名加圓括號內的實參列表的形式為構造函數提供初始值。
4)派生類使用基類的成員
派生類可以訪問基類的公有成員和受保護成員。
5)繼承與靜態成員
如果基類定義了一個靜態成員,則在整個繼承體系中只存在該成員的唯一定義。不論從基類中派生出來多少個派生類,對於每個靜態成員來說都只存在唯一實例。靜態成員遵循通用的訪問控制規則,如果基類中的成員是private的,則派生類無權訪問它。假設某靜態成員是可訪問的,則我們既能通過基類使用它也能通過派生類使用它。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class Base { 9 public: 10 static void statemem(); 11 }; 12 void Base::statemem() { 13 std::cout << __FUNCTION__ << std::endl; 14 } 15 class Derived :public Base { 16 public: 17 void f() { 18 Base::statemem(); // Base定義了statemem 19 Derived::statemem(); // Derived繼承了statemem 20 this->statemem(); // 通過this對象訪問 21 } 22 }; 23 int main() 24 { 25 Derived d; 26 d.f(); 27 return 0; 28 }View Code
6)派生類的聲明
派生類的聲明與其他類差別不大,聲明中包含類名但是不包含它的派生列表:
class BulkQuote : public Quote; // 錯誤:派生列表不能出現在這裏
class BulkQuote; // 正確
7)被用作基類的類
如果我們想將某個類用作基類,則該類必須已經定義而非僅聲明。這一規定的原因:派生類中包含並且可以使用它從基類繼承而來的成員,為了使用這些成員,派生類當然要知道它們是什麽。
8)防止繼承的發生
有時我們會定義這樣一種類,我們不希望其他類繼承它,或者不想考慮它是否適合作為一個基類。為了實現這一目的,C++11新標準提供了一種防止繼承發生的方法,即在類名後跟一個關鍵字final。
1 class Base final{ 2 public: 3 static void statemem(); 4 }; 5 void Base::statemem() { 6 std::cout << __FUNCTION__ << std::endl; 7 }View Code
3、類型轉換與繼承
可以將基類的指針或引用綁定到派生類對象上有一層極為重要的含義:當使用基類的引用(或指針)時,實際上我們並不清楚該引用(或指針)所綁定的對象的真實類型。該對象可能是指針,也可能是派生類的對象。
和內置指針一樣,智能指針類也支持派生類向基類的類型轉換,這意味著我們可以將一個派生類對象的指針存儲在一個基類的智能指針內。
1)靜態類型與動態類型
表達式的靜態類型在編譯時是已知的,它是變量聲明時的類型或表達式生成的類型;動態類型則是變量或表達式表示的內存中的對象的類型。動態類型直到運行時才可知。
如果表達式既不是引用也不是指針,則它的動態類型永遠與靜態類型一致。
2)不存在從基類向派生類的隱式轉換
之所以存在派生類向基類的類型轉換時因為每個派生類對象都包含一個基類部分,而基類的引用或指針可以綁定到該基類部分上。一個基類的對象既可以以獨立的形式存在,也可以作為派生類對象的一部分存在。如果基類對象不是派生類對象的一部分,則它只含有基類定義的成員,而不含有派生類定義的成員。
因為一個基類的對象可能是派生類對象的一部分,也可能不是,所以不存在從基類向派生類的自動類型轉換:
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class Quote { 9 public: 10 std::string isbn()const { return bookno; } 11 virtual double net_price(std::size_t n) const { 12 std::cout << "Quote::net_price" << std::endl; 13 return price; 14 } 15 virtual ~Quote() = default; 16 public: 17 std::string bookno; 18 double price; 19 }; 20 21 class BulkQuote :public Quote { 22 public: 23 virtual double net_price(std::size_t n) const override { 24 std::cout << "BulkQuote::net_price" << std::endl; 25 if (n > 10) 26 return 0.5 * price; 27 return price; 28 } 29 }; 30 31 int main() 32 { 33 Quote base; 34 BulkQuote *p = &base; //錯誤:不能將基類轉換成派生類 35 BulkQuote &r = base; // 錯誤:不能將基類轉換成派生類 36 return 0; 37 }View Code
有一種情況比較特別,即使一個基類指針或引用綁定在一個派生類對象上,我們也不能執行從基類向派生類的轉換:
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class Quote { 9 public: 10 std::string isbn()const { return bookno; } 11 virtual double net_price(std::size_t n) const { 12 std::cout << "Quote::net_price" << std::endl; 13 return price; 14 } 15 virtual ~Quote() = default; 16 public: 17 std::string bookno; 18 double price; 19 }; 20 21 class BulkQuote :public Quote { 22 public: 23 virtual double net_price(std::size_t n) const override { 24 std::cout << "BulkQuote::net_price" << std::endl; 25 if (n > 10) 26 return 0.5 * price; 27 return price; 28 } 29 }; 30 31 int main() 32 { 33 BulkQuote bulk; 34 Quote *p = &bulk; 35 BulkQuote *bulkp = p; // 錯誤 36 return 0; 37 }View Code
編譯器在編譯時無法確定某個特定的轉換在運行時是否安全,這是因為編譯器只能通過檢查指針或引用的靜態類型來推斷該轉換是否合法。如果在基類中含有一個或多個虛函數,我們可以使用dynamic_cast請求一個類型轉換,該轉換的安全檢查在運行時執行。同樣,如果我們已知某個基類向派生類的轉換是安全的,則我們可以使用static_cast來強制覆蓋編譯器的檢查工作。
3)在對象之間不存在類型轉換
當我們勇敢一個派生類對象為一個基類對象初始化或賦值時,只有派生類對象中的基類部分會被拷貝、移動或賦值,它的派生類部分會被忽略掉。
三、虛函數
通常情況下,如果我們不使用某個函數,則無須為該函數提供定義。但是我們必須為每一個虛函數都提供定義,而不管它是否被用到了,這是因為連編譯器也無法確定到底會使用哪個虛函數。
1)對虛函數的調用可能在運行時才解析
當某個虛函數通過指針或引用調用時,編譯器產生的代碼直到運行時才能確定應該調用哪個版本的函數。被調用的函數是與綁定到指針或引用上的對象的動態類型相匹配的那一個。
2)派生類中的虛函數
當我們在派生類中覆蓋了某個虛函數時,可以再一次使用virtual關鍵字指出該函數的性質。然而這麽做並非必須,因為一旦某個函數被聲明成虛函數,則在所有派生類中它都是虛函數。
一個派生類中的函數如果覆蓋了某個繼承而來的虛函數,則它的形參類型必須與它被覆蓋的基類函數完全一致。
同樣,派生類中虛函數的返回類型也必須與基類函數匹配。該規則有一個例外,當類的虛函數返回類型是類本身的指針或引用時,上述規則無效。也就是說,如果D由B派生得到,則基類的虛函數可以返回B*而派生類的對應函數可以返回D*,只不過這樣的返回類型要求從D到B的類型轉換是可訪問的。
3)final和override關鍵字
派生類如果定義了一個函數與基類中的虛函數名字相同但是形參列表不同,這仍然是合法的行為。編譯器會認為新定義的這個函數與基類中原有的函數是相互獨立的。這時,派生類的函數並沒有覆蓋掉基類中的版本。
在C++11新標準中我們可以使用override關鍵字來說明派生類中的虛函數。這麽做的好處是在使得程序員的意圖更加清晰的同時讓編譯器可以為我們發現一些錯誤,後者在編程實踐中顯得更加重要。如果我們使用override標記了某個函數,但該函數並沒有覆蓋已存在的虛函數,此時編譯器將報錯。
我們還能把某個函數指定為final,如果我們已經把函數定義成final了,則之後任何嘗試重新覆蓋該函數的操作都將引發錯誤。
1 class Quote { 2 public: 3 std::string isbn()const { return bookno; } 4 virtual double net_price(std::size_t n) const final{ 5 std::cout << "Quote::net_price" << std::endl; 6 return price; 7 } 8 virtual ~Quote() = default; 9 public: 10 std::string bookno; 11 double price; 12 };View Code
final和override關鍵字出現在形參列表(包括任何const或引用限定符)以及尾置返回類型之後。
4)虛函數與默認實參
虛函數也可以擁有默認實參。如果某次函數調用使用了默認實參,則該實參值由本次調用的靜態類型決定。換句話說,如果我們通過基類的引用或指針調用函數,則使用基類中定義的默認實參,即使實際運行的是派生類中的函數版本也是如此。此時,傳入派生類函數的將是基類函數定義的默認實參。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class A { 9 public: 10 virtual void func(int x = 1) { 11 std::cout << "A:"; 12 std::cout << x << std::endl; 13 } 14 }; 15 class B :public A { 16 public: 17 virtual void func(int x = 10) override { 18 std::cout << "B:"; 19 std::cout << x << std::endl; 20 } 21 }; 22 int main() 23 { 24 B b; 25 A &a = b; 26 a.func(); 27 return 0; 28 }View Code
5)回避虛函數的機制
在某些情況下,我們希望對虛函數的調用不要進行動態綁定,而是強迫其執行虛函數的某個特定版本。使用作用域運算符可以實現這一目的。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <algorithm> 5 #include <functional> 6 #include <map> 7 8 class A { 9 public: 10 virtual void func(int x = 1) { 11 std::cout << "A:"; 12 std::cout << x << std::endl; 13 } 14 }; 15 class B :public A { 16 public: 17 virtual void func(int x = 10) override { 18 std::cout << "B:"; 19 std::cout << x << std::endl; 20 } 21 }; 22 int main() 23 { 24 B b; 25 A &a = b; 26 a.A::func(); 27 return 0; 28 }View Code
面向對象程序設計——概述,定義基類和派生類,虛函數