C++基礎知識-Day8
2.類的作用域運算子
shadow
在我們之前講的內容中,我們會發現一種情況,就是在我們在不同類中的列印函式我們都是儘量讓其名字不同,那麼為什麼會有這種情況呢?首先我們來看一個函式
void func() { cout<<"B::void func()"<<endl; func(); }
執行程式會發現這是一個死迴圈,因為其存在自己呼叫自己的情況,那麼放在類中會是什麼樣子的呢
#include <iostream> using namespace std; classA { public: void foo() { cout<<"A::void foo()"<<endl; } }; class B:public A { public: void foo() { cout<<"B::void foo()"<<endl; foo();//實際上這裡是有一個this指標指向foo的 } }; int main() { B b; b.func(); return 0; }
這樣呼叫還是會出現死迴圈的情況,雖然其本意是在類B中的foo呼叫類A中的foo,但是由於this指標指向foo並且由於類中的兩個函式重名,因此會出現死迴圈,為了解決這個問題,引入類的作用域運算子,將類B中的foo函式寫成如下形式
void foo() { cout<<"B::void foo()"<<endl; A::foo(); }
shadow產生機理
(1) 在父子類中出現重名的識別符號(函式成員和資料成員),就會構成shadow,如果想訪問被shadow的成員,加上父類的名稱空間
(2) shadow在父子類中的識別符號只有一個,就是重名,不論返回值,引數不同什麼
3. 繼承的方式詳解
繼承的方式有三種:public,protected和private,但是我們一般都用public
所有的繼承必須是public的,如果想私有繼承的話,應該採用將基類例項作為成員的方式作為替代
一般情況下,在一個類中,public常用於介面,protected常用於資料,private常用於隱私
那麼為什麼public是用的最多的呢
如果多級派生中,均採用public,直到最後一級,派生類中均可訪問基類的public,protected,很好的做到了介面的傳承,保護資料以及隱私的保護
protected:封殺了對外的介面,保護資料成員,隱私保護
public:傳承介面,間接地傳承了資料(protected)
protected:傳承資料,間接封殺了對外介面(public)
private:統殺了資料和介面
4. 類的作用域運算子
shadow產生機理
(1) 在父子類中出現重名的識別符號(函式成員和資料成員),就會構成shadow,如果想訪問被shadow的成員,加上父類的名稱空間
(2) shadow在父子類中的識別符號只有一個,就是重名,不論返回值,引數不同什麼
5. 多重繼承
從繼承類別來說,繼承可以分為單繼承和多繼承
多繼承的意義:
俗話講,魚和熊掌不可兼得,而在計算機中可以實現,生成一種新的物件,叫熊掌魚,多繼承自魚和熊掌即可
繼承語法:
派生類名:public 基類名1,public 基類名2,…,protected 基類名n
構造器格式
派生類名:派生類名(總參列表)
:基類名1(引數表1),基類名2(引數名2),…基類名n(引數名n),
內嵌子物件1(引數表1),內嵌子物件2(引數表2)…內嵌子物件n(引數表n)
{
派生類新增成員的初始化語句
}
多繼承可能存在的問題
(1) 三角問題
多個父類中重名的成員,繼承到子類中後,為了避免衝突,攜帶了各父類的作用域資訊,子類中要訪問繼承下來的重名成員,則會產生二義性,為了避免衝突,訪問時需要提供父類的作用域資訊
構造器問題
下面我們用一個實際的例子來對其進行講解
1 #include <iostream> 2 3 using namespace std; 4 5 class X 6 { 7 public: 8 X(int d) 9 { 10 cout<<"X()"<<endl; 11 } 12 protected: 13 int _data; 14 }; 15 16 class Y 17 { 18 public: 19 Y(int d) 20 { 21 cout<<"Y()"<<endl; 22 } 23 protected: 24 int _data; 25 }; 26 27 class Z:public X,public Y 28 { 29 public: 30 Z() 31 :X(1),Y(2) 32 { 33 34 } 35 void dis() 36 { 37 cout<<Y_data<<endl;39 } 40 }; 41 42 int main() 43 { 44 Z z; 45 z.dis(); 46 return 0; 47 }
直接這樣的話會報錯,因為_data會產生二義性,為了解決這個問題,我們可以在資料之前加上其父類作用域
1 void dis() 2 { 3 cout<<Y::_data<<endl; 4 cout<<X::_data<<endl; 5 }
下面我們看一個有趣的情況
#include <iostream> using namespace std; class X { public: X(int d) { cout<<"X()"<<endl; _data=d; } void setData(int d) { _data=d; } protected: int _data; }; class Y { public: Y(int d) { cout<<"Y()"<<endl; _data=d; } int getData() { return _data; } protected: int _data; }; class Z:public X,public Y { public: Z(int i,int j) :X(i),Y(j) { } void dis() { cout<<X::_data<<endl; cout<<Y::_data<<endl; } }; int main() { Z z(100,200); z.dis(); cout<<"================="<<endl; z.setData(1000000); cout<<z.getData()<<endl; cout<<"================="<<endl; z.dis(); return 0; }
在這裡我們getData得到的資料仍然是200,並不是setData的1000000,原因如下
剛開始的時候,在類X和類Y中,都有一個_data,
當其繼承在類Z中後
由於是重名的問題,setData設定的是類X中的資料,但是getData得到的是類Y中的資料,所以說會出現問題
那麼我們應該怎麼來解決這個問題呢
需要解決的問題:
資料冗餘
訪問方便
由此引發了一個三角轉四角的問題
- 提取各父類中相同的成員,包括資料成員和函式成員,構成祖父類
- 讓各父類,繼承祖父類
- 虛繼承是一種繼承的擴充套件,virtual
首先解決初始化問題,
祖父類的好處是,祖父類是預設的構造器,因此在父類中,並不需要顯示地呼叫,按道理說,Z中有類X,Y,只需要管X,Y的初始化就可以了
#include <iostream> using namespace std; //祖父類 class A { protected: int _data; }; //父類繼承祖父類 class X:virtual public A { public: X(int d) { cout<<"X()"<<endl; _data=d; } void setData(int d) { _data=d; } }; //各父類繼承祖父類 class Y:virtual public A //虛繼承 { public: Y(int d) { cout<<"Y()"<<endl; _data=d; } int getData() { return _data; } }; class Z:public X,public Y { public: Z(int i,int j) :X(i),Y(j) { } void dis() { cout<<_data<<endl; } }; int main() { Z z(100,200); z.dis(); cout<<"================="<<endl; z.setData(1000000); cout<<z.getData()<<endl; cout<<"================="<<endl; z.dis(); return 0; }
這樣就帶來了兩個好處,解決了資料冗餘的問題,並且為訪問帶來了便利,虛繼承也是一種設計的結果,被抽象上來的類叫做虛基類。也可以說成:被虛繼承的類稱為虛基類
虛基類:被抽象上來的類叫做虛基類
虛繼承:是一種對繼承的擴充套件
那麼虛繼承就有幾個問題需要我們來注意了,首先是初始化的順序問題,為了測試初始化的順序問題,因為上述都是構造器的預設情況,但是實際情況中,可能都會帶引數,甚至是虛繼承的祖父類也會帶引數,那麼構造器順序又將是如何的呢?我們利用如下程式碼進行測試
1 #include <iostream> 2 3 using namespace std; 4 5 class A 6 { 7 public: 8 A(int i) 9 { 10 _data=i; 11 cout<<"A(int i)"<<endl; 12 } 13 protected: 14 int _data; 15 }; 16 class B:virtual public A 17 { 18 public: 19 B(int i) 20 :A(i) 21 { 22 _data=i; 23 cout<<"B(int i)"<<endl; 24 } 25 }; 26 27 class C:virtual public A 28 { 29 public: 30 C(int i) 31 :A(i) 32 { 33 _data=i; 34 cout<<"C(int i)"<<endl; 35 } 36 }; 37 38 class D:public C,B 39 { 40 public: 41 D() 42 :C(1),B(1),A(1) 43 { 44 cout<<"D(int i)"<<endl; 45 } 46 void dis() 47 { 48 cout<<_data<<endl; 49 } 50 }; 51 int main() 52 { 53 D d; 54 d.dis(); 55 return 0; 56 }
執行程式碼後我們可以得知,構造的順序是從祖父類的構造器開始,按照順序執行下來,最後到孫子類的構造器為止的
當然,上述只是一個測試,因為在實際過程中,祖父類是由父類抽象起來的,因此一般不會用祖父類生成物件
在實際過程中,在父類的構造器中我們常帶預設引數,這樣我們就可以不使得派生類的構造器如此複雜
實際例子,沙發床,除了上述之外,我們還需要增加顏色和重量,除此之外,我們還需要用descript函式來對其進行描述
#include <iostream> using namespace std; class Furniture { public: void descript() { cout<<"_weight:"<<_weight<<endl; cout<<"_color :"<<_color<<endl; } protected: float _weight; int _color; }; class Sofa:virtual public Furniture { public: Sofa(float w=0,int c=1) { _weight=w; _color=c; } void sit() { cout<<"take a sit and have a rest"<<endl; } }; class Bed:virtual public Furniture { public: Bed(float w=0,int c=1) { _weight=w; _color=c; } void sleep() { cout<<"have a sleep ......."<<endl; } }; class SofaBed:public Sofa,public Bed { public: SofaBed(float w,int c) { _weight=w; _color=c; } }; int main() { SofaBed sb(1000,2); sb.sit(); sb.sleep(); sb.descript(); return 0; } int main1() { Sofa sf; sf.sit(); Bed bd; bd.sleep(); return 0; }
6. 多型
(1) 生活中的多型
如果有幾個相似而不完全相同的物件,有時人們要求在向他們發出同一個訊息時,他們的反應各不相同,分別執行不同的操作,這種情況就是多型現象
(2) C++ 中的多型
C++ 中的多型是指,由繼承而產生的相關的不同的類,其對同一訊息會做出不同的響應
比如,Mspaint中的單擊不同圖形,執行同一拖動動作而繪製不同的圖形,就是典型的多型應用
多型性是面向物件程式設計的一個重要特徵,能增加程式的靈活性,可以減輕系統的升級,維護,除錯的工作量和複雜度
(3) 賦值相容
賦值相容是指,在需要基類物件的任何地方,都可以使用共有派生的物件來替代
只有在共有派生類中才有賦值相容,賦值相容是一種預設行為,不需要任何的顯示的轉化步驟
賦值相容總結起來有以下三種特點
派生類的物件可以賦值給基類物件 |
派生類的物件可以初始化基類的引用 |
派生類物件的地址可以賦給指向基類的指標 |
下面我們將分別對其進行說明
- 派生類的物件可以賦值給基類物件
觀察下面程式碼
1 #include <iostream> 2 3 using namespace std; 4 5 class Shape 6 { 7 public: 8 Shape(int x=0,int y=0) 9 :_x(x),_y(y){} 10 void draw() 11 { 12 cout<<"draw shape from"<<"("<<_x<<","<<_y<<")"<<endl; 13 } 14 protected: 15 int _x; 16 int _y; 17 }; 18 class Circle:public Shape 19 { 20 public: 21 Circle(int x=0,int y=0,int r=1) 22 :Shape(x,y),_radius(r){} 23 void draw() 24 { 25 cout<<"draw shape from"<<"("<<_x<<","<<_y<<")"<<"radius:"<<_radius<<endl; 26 } 27 protected: 28 int _radius; 29 }; 30 int main() 31 { 32 Shape s(1,2); 33 s.draw(); 34 Circle c(4,5,6); 35 c.draw(); 36 s=c;//派生類物件可以賦值給基類物件 37 s.draw(); 38 return 0; 39 }
有上述例子可以看出,派生類的物件是可以複製給基類物件的
- 派生類的物件可以初始化基類的引用
1 int main() 2 { 3 Shape s(1,2); 4 s.draw(); 5 Circle c(4,5,6); 6 Shape &rs=c; 7 rs.draw(); 8 return 0; 9 }
- 派生類的物件的地址可以賦給指向基類的指標
1 int main() 2 { 3 Shape s(1,2); 4 s.draw(); 5 Circle c(4,5,6); 6 Shape *ps=&c; 7 ps->draw(); 8 return 0; 9 }
在這三種情況中,使用的最多的是第三種,即派生類物件的地址可以賦給指向基類的指標
就如圖示一樣,假設左邊的類是父類,右邊的類是子類,,左邊的指標是派生類的物件的地址賦給指向派生類的指標,那麼其可訪問的範圍就是整個派生類,右邊的指標是派生類的物件的地址賦給指向基類的指標,那麼其訪問範圍就只有基類的那一部分
7. 多型
多型分為靜多型和動多型
靜多型,就是我們說的函式過載,表面上,是由過載規則來限定的,內部實現卻是Namemangling,此種行為,發生在編譯期,故稱為靜多型
(動)多型,不是在編譯階段決定,而是在執行階段決定,故稱動多型,動多型的形成條件如下
多型實現的條件
父類中有虛擬函式(加virtual,是一個宣告型關鍵字,即只能在宣告中有,在實現中沒有),即公用介面 |
子類override(覆寫)父類中的虛擬函式
|
通過已被子類物件賦值的父類指標,呼叫共有介面 |
下面分別對這些條件進行講解
- 父類中有虛擬函式(加virtual,是一個宣告型關鍵字,即只能在宣告中有,在實現中沒有),即公用介面
virtual函式是一個宣告型關鍵字,只能在宣告中有,在實現中沒有
class A { public: A(){}; virtual void draw(); private: int _x; } void A::draw() { cout<<_x<<endl; }
假設在實現的過程中也加入virtual關鍵字,即
virtual void A::draw() { cout<<_x<<endl; }
系統即會開始報錯
- 子類覆寫父類中的虛擬函式,子類中同名同參同函式,才能構成覆寫
- 通過已被子類物件賦值的父類指標,呼叫虛擬函式,形成多型
1 #include <iostream> 2 #include <typeinfo> 3 using namespace std; 4 5 class Shape 6 { 7 public: 8 Shape(int x=0,int y=0) 9 :_x(x),_y(y) 10 { 11 cout<<"shape->this"<<this<<endl; 12 cout<<typeid(this).name()<<endl; 13 } 14 virtual void draw() 15 { 16 cout<<"draw shape from"<<"("<<_x<<","<<_y<<")"<<endl; 17 } 18 protected: 19 int _x; 20 int _y; 21 }; 22 class Circle:public Shape 23 { 24 public: 25 Circle(int x=0,int y=0,int r=1) 26 :Shape(x,y),_radius(r) 27 { 28 cout<<"shape->this"<<this<<endl; 29 cout<<typeid(this).name()<<endl; 30 } 31 void draw() 32 { 33 cout<<"draw shape from"<<"("<<_x<<","<<_y<<")"<<"radius:"<<_radius<<endl; 34 } 35 protected: 36 int _radius; 37 }; 38 39 40 class Rect:public Shape 41 { 42 public: 43 Rect(int x=0,int y=0,int w=0,int l=0) 44 :Shape(x,y),_width(w),_lenth(l){} 45 virtual void draw() 46 { 47 cout<<"draw Circle from"<<"("<<_x<<","<<_y<<")" 48 <<"width:"<<_width<<"lenth:"<<_lenth<<endl; 49 } 50 protected: 51 52 int _width; 53 int _lenth; 54 }; 55 56 57 int main() 58 { 59 Circle c(3,4,5); 60 Shape *ps=&c;//父類指標指向子類的物件 61 ps->draw(); 62 63 Rect r(6,7,8,9); 64 ps=&r; 65 ps->draw(); 66 return 0; 67 }
可以看出,利用virtual,可以實現多型
通過父類的指標呼叫父類的介面指向其本來應該指向的內容
1 int main() 2 { 3 Circle c(3,4,5); 4 Shape *ps=&c;//父類指標指向子類的物件 5 ps->draw(); 6 7 Rect r(6,7,8,9); 8 ps=&r; 9 ps->draw(); 10 while(1) 11 { 12 int choice; 13 cin>>choice; 14 switch(choice) 15 { 16 case 1: 17 ps=&c; 18 break; 19 case 2: 20 ps=&r; 21 break; 22 } 23 ps->draw(); 24 } 25 return 0; 26 }
一個介面呈現出不同的行為,其中virtual是一個宣告型關鍵字,用來宣告一個虛擬函式,子類覆寫了的函式,也是virtual
虛擬函式在子函式中的訪問屬性並不影響多型,要看子類
虛擬函式和多型總結
(1)virtual是宣告函式的關鍵字,他是一個宣告型關鍵字
(2)override構成的條件,發生在父子類的繼承關係中,同名,同參,同返回
(3)虛擬函式在派生類中仍然為虛擬函式,若發生覆寫,最好顯示的標註virtual
(4)子類中覆寫的函式,可以為任意的訪問型別,依子類需求決定
8. pure virtual function
純虛擬函式,指的是virtual修飾的函式,沒有實現體,被初始化為0,被高度抽象化的具有純介面類才配有純虛擬函式,含有純虛擬函式的類稱為抽象基類
抽象基類不能例項化(不能生成物件),純粹用來提供介面用的
子類中若無覆寫,則依然為純虛,依然不能例項化
9. 總結
(1)純虛擬函式只有宣告,沒有實現,被“初始化”為0
(2)含有純虛擬函式的類,稱為Abstract Base Class(抽象基類),不能例項化,即不能創造物件,存在的意義就是被繼承,而在派生類中沒有該函式的意義
(3)如果一箇中聲明瞭純虛擬函式,而在派生類中沒有該函式的定義,則該虛擬函式在派生類中仍然為虛擬函式,派生類仍然為純虛基類
10. 解構函式
含有虛擬函式的類,解構函式也應該宣告為虛擬函式
對比棧物件和對物件在多型中銷燬的不同
首先我們來看位於棧上的物件
在這裡,我們生成了幾個類,一個是抽象基類,一個是Dog類,一個是Cat類,我們分別在class中去構造這幾個類
首先生成Animal類
其.h檔案的內容如下
1 #ifndef ANIMAL_H 2 #define ANIMAL_H 3 class Animal 4 { 5 public: 6 Animal(); 7 ~Animal(); 8 virtual void voice()=0; 9 }; 10 #endif // ANIMAL_H
其.cpp檔案中的內容如下
1 #include "animal.h" 2 #include <iostream> 3 using namespace std; 4 Animal::Animal() 5 { 6 cout<<"Animal::Animal()"<<endl; 7 } 8 9 Animal::~Animal() 10 { 11 cout<<"Animal::~Animal()"<<endl; 12 }
然後我們再生成Dog的.h檔案
1 #ifndef DOG_H 2 #define DOG_H 3 #include "animal.h" 4 class Animal; 5 class Dog : public Animal 6 { 7 public: 8 Dog(); 9 ~Dog(); 10 11 virtual void voice(); 12 }; 13 #endif // DOG_H
然後我們再生成Dog的.cpp檔案
1 #include "dog.h" 2 #include "animal.h" 3 #include <iostream> 4 using namespace std; 5 Dog::Dog() 6 { 7 cout<<"Dog::Dog()"<<endl; 8 } 9 10 Dog::~Dog() 11 { 12 cout<<"Dog::~Dog()"<<endl; 13 } 14 15 void Dog::voice() 16 { 17 cout<<"wang wang wang"<<endl; 18 }
然後我們生成Cat類
首先生成Cat的.h檔案
1 #ifndef CAT_H 2 #define CAT_H 3 #include "animal.h" 4 class Cat : public Animal 5 { 6 public: 7 Cat(); 8 ~Cat(); 9 10 virtual void voice(); 11 }; 12 #endif // CAT_H
然後再生成cat的.cpp檔案
1 #include "cat.h" 2 #include "animal.h" 3 #include <iostream> 4 using namespace std; 5 Cat::Cat() 6 { 7 cout<<"Cat::Cat()"<<endl; 8 } 9 Cat::~Cat() 10 { 11 cout<<"Cat::~Cat()"<<endl; 12 } 13 void Cat::voice() 14 { 15 cout<<"miao miao miao"<<endl; 16 }
最後,main函式如下
1 #include <iostream> 2 #include "animal.h" 3 #include "cat.h" 4 #include "dog.h" 5 using namespace std; 6 7 int main() 8 { 9 Cat c; 10 Dog d; 11 Animal *pa=&c; 12 pa->voice(); 13 return 0; 14 }
生成的結果為
可以看出其是析構完全了的
但是若為棧上的物件,即主函式改寫為如下
1 #include <iostream> 2 #include "animal.h" 3 #include "cat.h" 4 #include "dog.h" 5 using namespace std; 6 7 int main() 8 { 9 Animal *pa=new Dog; 10 pa->voice(); 11 delete pa; 12 return 0; 13 }
得出的結果為
可以看出其是沒有析構完全的,生成的Dog是沒有析構的,因此對於堆上的物件,其是析構器有問題的
我們只需要解決如下
但凡類中含有虛擬函式(包括純虛擬函式),將其虛構函式置為virtual ,這樣即可以實現完整虛構
12. 以一個例子來進行舉例,用母親給給孩子講故事來進行舉例
原本母親給孩子講故事是依賴於故事書上的內容,因此對於母親給孩子講故事我們可以寫成如下程式碼
1 //Mother 依賴於 Book 依賴->耦合 -->低耦合 2 class Book 3 { 4 public: 5 string getContents() 6 { 7 return "從前有座山,山裡有座廟,廟裡有個小和尚." 8 "聽老和尚講故事,從前有座山"; 9 } 10 }; 11 class Mother 12 { 13 public: 14 void tellStory(Book &b) 15 { 16 cout<<b.getContents()<<endl; 17 } 18 };
在這裡,母親和書的關係是一種強耦合關係
即只要書的內容發生改變,Book,Mother等都需要發生改變,這樣是很麻煩的
但是實際上,這種強耦合關係是我們所不希望的,為了解決這種強耦合關係,我們引入一箇中間層
1 #include <iostream> 2 3 using namespace std; 4 5 //Mother 依賴於 Book 依賴->耦合 -->低耦合 6 7 class IReader 8 { 9 public: 10 virtual string getContents()=0; 11 }; 12 13 class Book:public IReader 14 { 15 public: 16 string getContents() 17 { 18 return "從前有座山,山裡有座廟,廟裡有個小和尚." 19 "聽老和尚講故事,從前有座山"; 20 } 21 }; 22 23 class NewsPaper:public IReader 24 { 25 public: 26 string getContents() 27 { 28 return "Trump 要在黑西哥邊境建一座牆"; 29 } 30 }; 31 class Mother 32 { 33 public: 34 void tellStory(IReader *pi) 35 { 36 cout<<pi->getContents()<<endl; 37 } 38 }; 39 int main() 40 { 41 Mother m; 42 Book b; 43 NewsPaper n; 44 m.tellStory(&b); 45 m.tellStory(&n); 46 return 0; 47 }
這樣的話,書改變時,Mother是不會發生改變的,只需要加一個新類就是可以的了,使用者端介面不會發生改變