OOP2(虛函數/抽象基類/訪問控制與繼承)
通常情況下,如果我們不適用某個函數,則無需為該函數提供定義。但我們必須為每個虛函數都提供定義而不管它是否被用到了,這因為連編譯器也無法確定到底會適用哪個虛函數
對虛函數的調用可能在運行時才被解析:
當某個虛函數通過指針或引用調用時,編譯器產生的代碼直到運行時才能確定應該調用哪個版本的函數。被調用的函數是與之綁定到指針或引用上的對象的動態類型相匹配的那一個
註意:動態綁定只有當我們通過指針或引用調用虛函數時才會發生。當我們通過一個具有普通類型(非引用非指針)的表達式調用虛函數時,在編譯時就會將調用的版本確定下來:
1 Quote base; 2 Bulk_quote derived; 3View Codebase = derived;//將derived的Quote部分拷貝給base 4 base.net_price(20);//調用Quote::net_prive
註意:對非虛函數的調用在編譯時進行綁定。通過對象進行的函數(虛函數或非虛函數)調用也在編譯時綁定。
當且僅當對通過指針或引用調用虛函數時,才會在運行時解析該調用,也只有在這種情況下對象的動態類型才有可能與靜態類型不同
派生類中的虛函數:
基類中的虛函數在派生類中隱式地也是一個虛函數。當派生類覆蓋了某個虛函數時,該函數在基類中的形參必須與派生類中的形參嚴格匹配(包括 this 參數)。同樣,派生類中虛函數的返回類型也必須與基類函數匹配。
final 和 override 說明符:
派生類如果定義一個函數與基類中函數的名字相同但是形參列表不同,這仍然是合法的行為。編譯器將認為新定義的這個函數與基類中原有的函數是相互獨立的。這時,派生類的函數沒有覆蓋掉基類中的版本。就實際的編程習慣而言,這種聲明往往是錯誤的,因為我們可能原本希望派生類能覆蓋掉基類的虛函數,但是一不小心把形參列表寫錯了
我們可以通過 override 關鍵字來發現這種錯誤。如果我們使用 override 標記了某個函數,但該函數並沒有覆蓋已存在的虛函數,此時編譯器將報錯:
1 #include <iostream> 2 using namespace std; 3 4 struct B{ 5 virtual void f1(int) const; 6 virtual void f2(); 7 void f3(); 8 }; 9 10 struct D : B{ 11 void f1(int) const override;//正確,f1與基類中的f1匹配 12 // void f1(int) override;//錯誤,this參數應該是const的 13 // void f2(int) override;//錯誤,f2沒有形如f2(int)的函數 14 // void f3() override;//錯誤,f3不是虛函數 15 // void f4() override;//錯誤,B沒有名為f4的函數 16 }; 17 18 int main(void){ 19 20 }View Code
final 關鍵字作用和 override 恰好相反,如果我們已經把函數定義成 final 了,則之後任何嘗試覆蓋該函數的操作都將引發錯誤:
1 #include <iostream> 2 using namespace std; 3 4 struct B{ 5 virtual void f1(int) const; 6 virtual void f2(); 7 void f3(); 8 }; 9 10 struct D1 : B{ 11 //從B繼承f2(),f3(),覆蓋f1(int) 12 void f1(int) const final;//不允許後繼的其它類覆蓋f1(int) 13 }; 14 15 struct D2 : D1{ 16 void f2();//正確,覆蓋從間接基類B繼承而來的f2 17 // void f1(int) const;//錯誤,D1已經將f1聲明成final的 18 }; 19 20 int main(void){ 21 22 }View Code
虛函數與默認實參:
和其它函數一樣,虛函數也可以擁有默認實參。如果某次函數調用使用了默認實參,則該實參值由本次調用的靜態類型決定。如果虛函數使用默認實參,則基類和派生類中定義的默認實參最好一致
回避虛函數的機制:
某些情況下,我們希望對虛函數的調用不要進行動態綁定,而是強迫其執行虛函數的某個特定版本。使用作用域運算符可以實現這一目的:
強制調用基類中定義的函數版本而不管 baseP 的動態類型到底是什麽
double undiscounted = baseP->Quote::net_price(42);//該調用在編譯時完成解析
註意:通常情況下,只有成員函數(或友元)中的代碼才需要使用作用域運算符來回避虛函數的機制
通常當一個派生類的虛函數調用它覆蓋的基類的虛函數版本時才需要回避虛函數的默認機制
如果一個派生類虛函數需要調用它的基類版本,但是沒有使用作用域運算符,則在運行時該調用將被析構為派生類版本自身的調用,從而導致無限遞歸:
1 #include <iostream> 2 using namespace std; 3 4 class A{ 5 protected: 6 int x; 7 8 public: 9 // A(); 10 // ~A(); 11 virtual ostream& f(ostream &os) const { 12 os << x; 13 return os; 14 } 15 }; 16 17 class B : public A{ 18 private: 19 int y; 20 21 public: 22 // B(); 23 // ~B(); 24 ostream& f(ostream &os) const { 25 // return f(os) << " " << y;//錯誤,調用該函數時會無限遞歸 26 return A::f(os) << " " << y; 27 } 28 }; 29 30 int main(void) { 31 B *b = new B; 32 A *a = b; 33 a->f(cout);//動態類型為B,調用ostream& B::f(ostream&) const 34 delete b; 35 36 return 0; 37 }View Code
抽象基類:
純虛函數:
我們通過在聲明語句的分號之前加 =0 可以將一個虛函數聲明成純虛函數:
1 class Disc_quote : public Quote { 2 public: 3 Disc_quote() = default; 4 Disc_quote(const std::string &book, double price, std::size_t qty, double disc) : 5 Quote(book, price), quantity(qty), discount(disc) {} 6 7 double net_price(std::size_t) const = 0;//純虛函數 8 9 protected: 10 std::size_t quantity = 0; 11 double discount = 0.0; 12 }; 13 14 double Disc_quote::net_price(std::size_t sz) const { 15 //純虛函數可以提供定義,但函數體必須定義在類的外部 16 }View Code
註意:純虛函數可以提供定義,但函數體必須定義在類的外部
含有純虛函數的類是抽象基類。抽象基類負責定義接口,後繼的其他類可以覆蓋該接口。我們不能直接創建一個抽象基類對象,我們可以定義抽象基類的派生類對象,前提是這些派生類覆蓋了抽象基類中的純虛函數。
抽象基類的派生類必須覆蓋抽象基類中的純虛函數,否則派生類將仍然是抽象基類,不能創建對象
雖然抽象基類不能創建對象,但是我們仍然需要定義抽象基類的構造函數,因為抽象基類的派生類將會使用抽象基類的構造函數來構造派生類中的抽象基類部分數據成員:
1 #include <iostream> 2 using namespace std; 3 4 class Quote { 5 public: 6 Quote() = default; 7 Quote(const std::string &book, double sales_price) : 8 bookNo(book), price(sales_price) {} 9 10 std::string isbn() const { 11 return bookNo; 12 } 13 14 virtual double net_price(std::size_t n) const {//定義成虛函數,運行2時進行動態綁定 15 return n * price; 16 } 17 18 virtual ~Quote() = default;//基類通常都應該定義一個虛析構函數,即使該函數不執行任何實際操作 19 20 private: 21 std::string bookNo;//書籍的isbn編號 22 23 protected://可被派生類訪問 24 double price = 0.0;//代表普通狀態下不打折的價格 25 }; 26 27 class Disc_quote : public Quote { 28 public: 29 Disc_quote() = default; 30 Disc_quote(const std::string &book, double price, std::size_t qty, double disc) : 31 Quote(book, price), quantity(qty), discount(disc) {} 32 33 double net_price(std::size_t) const = 0;//純虛函數 34 35 protected: 36 std::size_t quantity = 0; 37 double discount = 0.0; 38 }; 39 40 double Disc_quote::net_price(std::size_t sz) const { 41 //純虛函數可以提供定義,但函數體必須定義在類的外部 42 } 43 44 class Bulk_quote : public Disc_quote { 45 public: 46 Bulk_quote() = default; 47 Bulk_quote(const std::string&, double, std::size_t, double); 48 49 double net_price(std::size_t) const override;//override顯式註明該成員函數覆蓋它繼承的虛函數 50 51 // ~Bulk_quote(); 52 53 private: 54 std::size_t min_qty = 0;//適用折扣政策的最低購買量 55 double discount = 0.0;//以小數表示的折扣額 56 57 }; 58 59 Bulk_quote::Bulk_quote(const std::string &book, double price, std::size_t qty, double disc) : 60 //調用抽象基類的構造函數來構造派生類中的抽象基類部分數據成員 61 Disc_quote(book, price, qty, disc) {} 62 63 double Bulk_quote::net_price(size_t cnt) const { 64 if(cnt >= min_qty) return cnt * (1 - discount) * price; 65 return cnt * price; 66 } 67 68 69 int main(void){ 70 // Disc_quote discount;//錯誤,不能創建抽象類基類的對象 71 Bulk_quote bqt;//正確,該派生類中覆蓋了抽象基類中的純虛函數,可以創建對象 72 return 0; 73 }View Code
訪問控制與繼承:
受保護的成員:
和私有成員類似,受保護的成員對於類的用戶來說是不可訪問的
和公有成員類似,受保護的成員對於派生類成員和友元來說是可訪問的
派生類的友元只能通過派生類對象來訪問基類的受保護成員:
1 #include <iostream> 2 using namespace std; 3 4 class Base{ 5 public: 6 Base(int a = 0) : prot_mem(a) {} 7 // ~Base(); 8 9 protected: 10 int prot_mem; 11 12 }; 13 14 class Sneaky : public Base{ 15 friend void clobber(Sneaky&); 16 friend void clobber(Base&); 17 int j; 18 19 public: 20 //如果使用了默認實參,則派生類和基類的默認實參應該保持一致 21 Sneaky(int a = 0, int b = 0) : Base(a), j(b) {}//調用基類的構造函數來構造派生類對象中的基類部分 22 ostream& print(ostream&) const; 23 24 }; 25 26 ostream& Sneaky::print(ostream &os) const { 27 os << prot_mem << j;//派生類成員中可以直接使用基類中的受保護數據成員 28 return os; 29 } 30 31 // 註意:clobber(Sneaky&)和clobber(Base&)是派生類的友元,但不是基類的友元,因此我們可以通過派生類對象來訪問基類中的受保護數據成員, 32 // 但不能直接通過基類對象來訪問基類中的受保護成員。該函數相對於基類僅僅是一個用戶,只能直接訪問基類的公共成員 33 void clobber(Sneaky &s) { 34 s.j = s.prot_mem = 0; 35 } 36 37 void clobber(Base &b) { 38 // b.prot_mem = 0;//錯誤,該函數不是Base類的友元,只能訪問Base類中的公共成員 39 } 40 41 int main(void){ 42 Sneaky s(0); 43 s.print(cout) << endl; 44 45 return 0; 46 }View Code
公有、私有和受保護繼承:
某個類對其繼承而來的成員的訪問權限受兩個因素影響:一是基類中該成員的訪問說明符,二是在派生類的派生類列表中的訪問說明符。與其派生訪問說明符無關。派生類訪問說明符的目的是控制派生類用戶(包括派生類的派生類在內)對於基類成員的訪問權限:
1 #include <iostream> 2 using namespace std; 3 4 class Base { 5 public: 6 void pub_mem(); 7 8 protected: 9 int prot_mem; 10 11 private: 12 char priv_mem; 13 }; 14 15 void Base::pub_mem() { 16 17 } 18 19 struct Pub_Derv : public Base {//公有繼承,用戶代碼(包括Pub_Derv類的派生類)可以訪問基類(如果基類的成員本身可以被訪問的話) 20 int f() { 21 return prot_mem;//正確,派生類能訪問protected成員 22 } 23 24 char g() { 25 // return priv_mem;//錯誤,派生類不能訪問private成員 26 } 27 }; 28 29 struct Priv_Derv : private Base{//私有繼承,用戶代碼(包括Priv_Derv類的派生類)可以訪問基類 30 int f1() const { 31 return prot_mem;//正確,private不影響派生類的訪問權限 32 } 33 }; 34 35 //派生類訪問說明符還可以控制繼承自派生類的新類的訪問權限 36 struct Derived_from_public : public Pub_Derv { 37 int use_base() { 38 return prot_mem; 39 } 40 }; 41 42 struct Derived_from_private : public Priv_Derv { 43 int use_base() { 44 // return prot_mem;//Priv_Derv中繼承自Base的成員都變成private了,不能被派生類調用 45 } 46 }; 47 48 int main(void) { 49 Pub_Derv d1;//繼承自Base的成員是public的 50 Priv_Derv d2;//繼承自Base的成員是private的 51 d1.pub_mem();//正確,pub_mem在派生類中是public的 52 // d2.pub_mem();//錯誤,pub_mem在派生類中是private的 53 54 // Base *b = &d2;//只有公有繼承才能在用戶代碼中使用派生類像基類轉換 55 // b->pub_mem(); 56 57 return 0; 58 }View Code
註意:
1) public 繼承:基類成員保持自己的訪問級別
2) protected 繼承:基類的 public 和 protected 成員在派生類中為 protected 成員
3) private 繼承:基類所有成員在派生類中為 private 成員
派生類向基類轉換的可訪問性:
派生類向基類的轉換是否可訪問由使用該轉換的代碼決定,同時派生類的派生訪問說明符也會有影響。假定 D 繼承自 B:
只有當 D 公有地繼承 B 時,用戶代碼才能使用派生類向基類轉換;如果 D 繼承 B 的方式是受保護的或私有的,則用戶代碼不能使用該轉換
不論 D 以什麽方式繼承 B,D 的成員函數和友元函數都能使用派生類向基類的轉換
如果 D 繼承 B 的方式是公有的或者受保護的,則 D 的派生類的成員和友元可以使用 D 向 B 的類型轉換,反之則不行
友元與繼承:
不能繼承友元關系,每個類負責控制各自成員的訪問權限
改變個別成員的可訪問性:
我們可以通過 using 聲明改變派生類繼承的某個名字的訪問級別:
1 class Base{ 2 public: 3 // Base(); 4 // ~Base(); 5 std::size_t size() const { 6 return n; 7 } 8 9 protected: 10 std::size_t n; 11 }; 12 13 class Derived : private Base{ 14 public: 15 // Derived(); 16 // ~Derived(); 17 using Base::size;//該成員被標記為public的 18 19 protected: 20 using Base::n;//該成員被標記為protected的 21 22 };View Code
註意:using 聲明語句中名字的訪問權限由該 using 聲明語句之前的訪問說明符決定
派生類只能為那些它可以訪問的名字提供 using 聲明。即不能對基類中的 private 成員提供 using 聲明
默認的繼承保護級別:
默認情況下,使用 class 關鍵字定義的派生類是私有繼承的;而使用 strcut 關鍵字定義的派生類是公有繼承的:
1 class Base {}; 2 struct D1 : Base {};//默認public繼承 3 class D2 : Base {};//默認private繼承View Code
OOP2(虛函數/抽象基類/訪問控制與繼承)