1. 程式人生 > >【重構C++知識體系】類 虛類 純虛類

【重構C++知識體系】類 虛類 純虛類

/*在C++中,我們通過類來定義自己的資料結構*/

類的作用(思想)

為了使C++遵守OOP基本法的資料抽象和封裝.

  • 資料抽象:就是將”介面”和”實現”實現分離的程式設計技術.
  • 封裝:隱藏物件的屬性和實現細節,僅對外公開介面.

    拓展:設計模式6大原則
    1.單一職責原則:一個類只負責一項職責
    2.里氏替換原則:子類可以擴充套件父類的功能,但不能改變父類原有的功能
    3.依賴倒置原則:高層模組不應該依賴低層模組,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象
    4.介面隔離原則:不多幹也不少幹(包含合成聚合複用原則)
    5.迪米特法則:一個物件應該對其他物件保持最少的瞭解(降低耦合)
    6.開閉原則:當軟體需要變化時,儘量通過擴充套件軟體實體的行為來實現變化,而不是通過修改已有的程式碼來實現變化

類的組成

/*包含成員函式和成員變數*/

只舉幾個特殊的例子來了解

成員變數

空類

類在例項化之後叫做一個例項.類是靜態的,不佔程序記憶體,而例項擁有動態記憶體.
現在考慮一個問題:如果空類的例項不佔用記憶體,如何唯一的表示?上程式碼

#include <bits/stdc++.h>
using namespace std;

class a{};

int main()
{
    a b;
    cout<<"sizeof(a) ="<<sizeof(a)<<endl;
    cout<<"sizeof(b) ="
<<sizeof(b)<<endl; return 0; }

output:
sizeof(a) =1
sizeof(b) =1
可以看到,空類的例項大小不是0,而是1.這是編譯器為空類隱式新增的一個字元.

static成員

#include <bits/stdc++.h>
using namespace std;

class a
{
public:
    static int v;
};

int main()
{
    a b;
    cout<<"sizeof(a) ="<<sizeof(a)<<endl;
    cout
<<"sizeof(b) ="<<sizeof(b)<<endl; cout<<"sizeof(a::v) ="<<sizeof(a::v)<<endl; return 0; }

output:
sizeof(a) =1
sizeof(b) =1
sizeof(a::v) =4
可以看到 static 的變數並沒有出現在類或者例項中.那麼他存在哪裡?
靜態資料成員被編輯器存放在全域性資料區中,而不是棧中.

拓展:程式執行時的記憶體分配:
1. 棧區(stack)
    由編譯器自動分配釋放,存放函式的引數值,區域性變數的值等,記憶體的分配是連續的.當我們宣告變數時,那麼編譯器會自動接著當前棧區的結尾來分配記憶體.
    思考:怎麼返回函式中的區域性變數.
2. 堆區(heap)
    一般由程式設計師分配釋放,若程式設計師不釋放,程式結束時可能由作業系統回收.在記憶體中的分佈不是連續的,它們是不同區域的記憶體塊通過指標連結起來的.一旦某一節點從鏈中斷開,我們要人為的把所斷開的節點從記憶體中釋放.
    拓展^2:垃圾回收機制
3. 全域性區(靜態區)(static)
    全域性變數和靜態變數的儲存是放在一塊的,初始化的全域性變數和靜態變數在一塊區域, 未初始化的全域性變數和未初始化的靜態變數在相鄰的另一塊區域.程式結束後由系統釋放
4. 文字常量區
    常量字串就是放在這裡的.程式結束後由系統釋放
思考:
    `char s[]="zyk";char *s1="zyk";`
    這兩者到底有什麼區別? 
5. 程式程式碼區
    存放函式體的二進位制程式碼.

成員函式

定義一個函式為虛擬函式,不代表函式為不被實現的函式。
定義他為虛擬函式是為了允許用基類的指標來呼叫子類的這個函式。
定義一個函式為純虛擬函式,才代表函式沒有被實現。
定義純虛擬函式是為了實現一個介面,起到一個規範的作用,規範繼承這個類的程式設計師必須實現這個函式。

虛擬函式

宣告定義時在前加 virtual 關鍵字的函式
參考此樣例:

class A  
{  
public:  
    virtual void foo()  
    {  
        cout<<"A::foo() is called"<<endl;  
    }  
};  
class B:public A  
{  
public:  
    void foo()  
    {  
        cout<<"B::foo() is called"<<endl;  
    }  
};  
int main(void)  
{  
    A *a = new B();  
    a->foo();   // 在這裡,a雖然是指向A的指標,但是被呼叫的函式(foo)卻是B的!  
    return 0;  
}  

這是 OOP 基本法中的多型.對於 類A 中的 函式foo ,在編譯時間不能被確定他是指向 基類的foo 還是 子類的foo .
也就是說虛就虛在所謂“推遲聯編”或者“動態聯編”上.

純虛擬函式

C++ 中引入純虛擬函式是遵守 OOP基本法 的介面.首先含有純虛擬函式的類稱為抽象類,不能被例項化,也就是和 Java 中的介面類似.
編譯器要求在派生類中必須予以重寫.
定義方法 virtual ReturnType FunctionName() = 0;

虛擬函式表

虛擬函式表是為了讓虛擬函式執行時能方便找到聯編的位置而設定的. C++ standard 中並沒有提到過, 所以每個編譯器都可能不一樣, 比如 gcc 和 微軟編譯器 都是將 vptr(虛擬函式指標,指向虛擬函式表) 放在物件記憶體佈局最前面. 但是虛擬函式表的位置卻放在不同位置上.

#include <bits/stdc++.h>
using namespace std;

class A
{
    virtual void f1()
    {
        cout<<"af1"<<endl;
    }
    virtual void f2()
    {
        cout<<"af2"<<endl;
    }
public:
    int v;
};
class B
{
    virtual void f1()
    {
        cout<<"bf1"<<endl;
    }
    virtual void f2()
    {
        cout<<"bf2"<<endl;
    }
};

typedef void (*PTR)();

int main(void)
{
    A a;
    cout<<"sizeof(A)\t\t\t\t="<<sizeof(A)<<endl;
    cout<<"sizeof(B)\t\t\t\t="<<sizeof(B)<<endl;
    cout<<"&a\t\t\t\t\t="<<&a<<endl;
    cout<<"*(int*)(&a) /*VFT的位置*/\t\t="<<(int*)(&a)<<endl;
    cout<<"(int*)*(int*)(&a)/*第一個函式的位置*/\t="<<(int*)*(int*)(&a)<<endl;
    PTR foo = (PTR)*((int*)*(int*)(&a));
    foo();
    foo = (PTR)*(((int*)*(int*)(&a))+1);
    foo();
    return 0;
}

output:
sizeof(A) =8
sizeof(B) =4
&a =0x68fee4
(int)(&a) /VFT的位置/ =0x68fee4
(int*)(int)(&a)/第一個函式的位置/ =0x473618
af1
af2

抽象類

抽象類是一種特殊的類,它是為了抽象和設計的目的為建立的,它處於繼承層次結構的較上層(聯絡:設計原則)。

抽象類的定義

帶有純虛擬函式的類為抽象類。

抽象類的作用:

抽象類的主要作用是將有關的操作作為結果介面組織在一個繼承層次結構中,由它來為派生類提供一個公共的根,派生類將具體實現在其基類中作為介面的操作。所以派生類實際上刻畫了一組子類的操作介面的通用語義,這些語義也傳給子類,子類可以具體實現這些語義,也可以再將這些語義傳給自己的子類。

特點

  • 抽象類只能作為基類來使用,其純虛擬函式的實現由派生類給出。如果派生類中沒有重新定義純虛擬函式,而只是繼承基類的純虛擬函式,則這個派生類仍然還是一個抽象類。如果派生類中給出了基類純虛擬函式的實現,則該派生類就不再是抽象類了,它是一個可以建立物件的具體的類。
  • 抽象類是不能定義物件的。

類的繼承

宣告:

class 派生類名:繼承方式 基類名1, 繼承方式 基類名2...,繼承方式 基類名n
{
    派生類成員宣告;
};
  • 公有繼承
    當類的繼承方式為公有繼承時,基類的公有和保護成員的訪問屬性在派生類中不變,而基類的私有成員不可訪問。即基類的公有成員和保護成員被繼承到派生類中仍作為派生類的公有成員和保護成員。派生類的其他成員可以直接訪問它們。無論派生類的成員還是派生類的物件都無法訪問基類的私有成員。

  • 私有繼承
    當類的繼承方式為私有繼承時,基類中的公有成員和保護成員都以私有成員身份出現在派生類中,而基類的私有成員在派生類中不可訪問。基類的公有成員和保護成員被繼承後作為派生類的私有成員,派生類的其他成員可以直接訪問它們,但是在類外部通過派生類的物件無法訪問。無論是派生類的成員還是通過派生類的物件,都無法訪問從基類繼承的私有成員。通過多次私有繼承後,對於基類的成員都會成為不可訪問。因此私有繼承比較少用。

  • 保護繼承
    保護繼承中,基類的公有成員和私有成員都以保護成員的身份出現在派生類中,而基類的私有成員不可訪問。派生類的其他成員可以直接訪問從基類繼承來的公有和保護成員,但是類外部通過派生類的物件無法訪問它們,無論派生類的成員還是派生類的物件,都無法訪問基類的私有成員。

建構函式和解構函式

派生類建構函式的語法:

派生類名::派生類名(引數總表):基類名1(引數表1),基類名(引數名2)....基類名n(引數名n),內嵌子物件1(引數表1),內嵌子物件2(引數表2)....內嵌子物件n(引數表n{
    派生類新增成員的初始化語句;
}

建構函式的初始化順序並不以上面的順序進行,而是根據宣告的順序初始化。