1. 程式人生 > >C++基礎知識-Day8

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.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中的資料,所以說會出現問題

那麼我們應該怎麼來解決這個問題呢

需要解決的問題:

資料冗餘

訪問方便

由此引發了一個三角轉四角的問題

  1. 提取各父類中相同的成員,包括資料成員和函式成員,構成祖父類
  2. 讓各父類,繼承祖父類
  3. 虛繼承是一種繼承的擴充套件,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是不會發生改變的,只需要加一個新類就是可以的了,使用者端介面不會發生改變