C++基础知识-Day8
?
2.類的作用域運算符
shadow
在我們之前講的內容中,我們會發現一種情況,就是在我們在不同類中的打印函數我們都是盡量讓其名字不同,那么為什么會有這種情況呢?首先我們來看一個函數
void func(){cout<<"B::void func()"<<endl;func();}運行程序會發現這是一個死循環,因為其存在自己調用自己的情況,那么放在類中會是什么樣子的呢
#include <iostream>using namespace std; class A { 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.foo();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中的數據,所以說會出現問題
那么我們應該怎么來解決這個問題呢
需要解決的問題:
數據冗余
訪問方便
由此引發了一個三角轉四角的問題
首先解決初始化問題,
祖父類的好處是,祖父類是默認的構造器,因此在父類中,并不需要顯示地調用,按道理說,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 }?有上述例子可以看出,派生類的對象是可以復制給基類對象的
- 派生類的對象可以初始化基類的引用
- 派生類的對象的地址可以賦給指向基類的指針
?在這三種情況中,使用的最多的是第三種,即派生類對象的地址可以賦給指向基類的指針
就如圖示一樣,假設左邊的類是父類,右邊的類是子類,,左邊的指針是派生類的對象的地址賦給指向派生類的指針,那么其可訪問的范圍就是整個派生類,右邊的指針是派生類的對象的地址賦給指向基類的指針,那么其訪問范圍就只有基類的那一部分
?
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; }系統即會開始報錯
- ?子類覆寫父類中的虛函數,子類中同名同參同函數,才能構成覆寫
- 通過已被子類對象賦值的父類指針,調用虛函數,形成多態
可以看出,利用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. 析構函數
含有虛函數的類,析構函數也應該聲明為虛函數
這是為了保證對象析構的完整性,具體的情況就是父類的指針指向子類的堆對象,此時通過父類指針去析構子類堆對象時就會虛構不完整,為了保證析構的完整性,含有虛函數的類將其析構函數也聲明為虛函數(virtual)
對比棧對象和對對象在多態中銷毀的不同
首先我們來看位于棧上的對象
在這里,我們生成了幾個類,一個是抽象基類,一個是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.設計模式的原則:依賴倒置原則-核心思想:面向接口編程
傳統的過程式設計傾向于使高層次的模塊依賴于低層次的模塊(自頂向下,逐步細化),而依據DIP的設計原則,將中間層抽象為抽象層,讓高層模塊和底層模塊依賴于中間層
以一個例子來進行舉例,用母親給給孩子講故事來進行舉例
原本母親給孩子講故事是依賴于故事書上的內容,因此對于母親給孩子講故事我們可以寫成如下代碼
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是不會發生改變的,只需要加一個新類就是可以的了,用戶端接口不會發生改變
虛繼承和虛函數總結
虛繼承解決了多個父類中重名冗余的成員(包括數據成員和函數成員)
虛函數解決了多態的問題
被虛繼承的類稱為虛基類,含有純虛函數的類稱為抽象基類
?
轉載于:https://www.cnblogs.com/Cucucudeblog/p/10148671.html
總結
以上是生活随笔為你收集整理的C++基础知识-Day8的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python-函数递归调用
- 下一篇: Mvc项目解决方案分析