1. 程式人生 > 其它 >C++筆記:關於面向物件

C++筆記:關於面向物件

技術標籤:C++c++多型封裝面向物件程式設計

面向物件vs基於物件

面向物件(Object-Oriented)基於物件(Object-Based) 其實是兩個不同的概念。筆者在閱讀《Essential C++》時發現作者把基於物件的程式設計風格面向物件的程式設計風格列為並列的兩章來講解。基於物件主要講解了類class的設計;而面向物件章節則主講面向物件三大特性。以下我會逐步辨析《Essential C++》中的num_sequence類的實現來幫助讀者領悟面向物件思維。

以下援引百度百科關於對於這兩種方式的論述:

基於物件(Object-Based),和麵向物件(Object-Oriented)不是一個概念,不提供抽象、繼承、過載等有關面嚮物件語言的功能。

基於物件的程式語言沒有提供象抽象、繼承、過載等有關面嚮物件語言的許多功能。而是把其它語言所建立的複雜物件統一起來,從而形成一個非常強大的物件系統,以供使用。

面向物件的三大特性

封裝(Package)、繼承(Inherit)、多型(Polymorphism) 是OO思想最明顯的體現。
我們現在打造一個num_seq類(數列類)作為基類,就有如下圖關係:

inherit inherit inherit inherit num_seq
Fibonacci Lucas Pell ...

num_seq代表數列類,Fibonacci等等都是特殊的數列,可以認為他們之間有繼承-派生關係。

class num_seq//抽象基類
{
public:
    int elem(int pos);//返回下標pos的元素值
    void gen_elems(int pos);//生成直到pos的元素
    string what_am_i()const;//返回數列型別
    ostream &print(ostream &
os = cout) const;//輸出數列內容 bool check_integrity(int pos);//檢查pos合法性 static unsigned int max_elems();//返回數列元素上限 };

封裝

這個抽象基類目前還非常不完善。我們還需要確定各個成員的訪問許可權,也即是執行封裝操作。

問:為什麼我們需要封裝操作?直接開放訪問許可權的話,我們可以省去許多介面函式的編寫,來讓我們的操作更加靈活。
·
答:放棄封裝操作確實可以提供更高的靈活性。但這樣做不利於拓展和維護。比如下面的一個容器類:

class Array
{
public:
	int size();//返回大小
	int getElem(int pos);
	int insert(int val, int pos);//插入
	//···省略更多操作
	
//到底需要嗎?
protected:
	int* _elems;
	int _size;
};

如果直接開放訪問許可權的話,將可以從外部不經過介面就改變成員的值。這樣就會有許多不可預料的情況發生。比如因為沒有同步修改_elems_size而導致_size不再與_elems指向陣列大小相等。
正是因為封裝,我們把對protected成員的可能發生的操作限定在了成員函式和友元這幾種操作方式。保證私有和保護成員不會遭到意料之外的修改
所以經過封裝之後的程式碼是:

class num_seq
{
public:
    int elem(int pos);
    string what_am_i()const;//不加上const,const修飾的物件就無法使用這個函式,所以請儘量加上。
    ostream& print(ostream &os = cout) const;
    static unsigned int max_elems()const;

    ~num_seq(){}//空白定義解構函式,因為基類並沒有需要析構的資料成員

protected://我們希望子類物件可以訪問這些成員
    void gen_elems(int pos);
    bool check_integrity(int pos)const;
    const static unsigned int _max_elems = 1024;
};
bool num_seq::check_integrity(int pos)const
{
    if(pos<=0||pos>_max_elem)
    {
        cerr << "Invaild position:" << pos << endl;
        return false;
    }
    return true;
}

check_integrity()_max_elems似乎與數列種類是什麼沒有關係,都是適用的,所以不妨在基類就實現它,直接由子類繼承。
這個基類還是不太完善,因為我們還需要繼續判定各個函式是否需要根據數列種類不同而重寫。

繼承

抽象基類已經提供了基本的介面,我們把不同的數列認為是其不同的子類,繼承基類之後,需要重寫一些基類函式,來讓他契合子類的特性。這裡以Fibonacci子類的實現為例子:

// 為了方便,我直接把函式定義寫在了類內
//該類的物件是Fibonacci數列的一個子列
class Fibonacci:public num_seq//繼承
{
public:
    Fibonacci(int len=1,int beg_pos=1)
    :_length(len),_beg_pos(beg_pos)
    {
        if(beg_pos+len>_elems.size())
            gen_elems(beg_pos + len);
    }

    int elem(int pos)
    {
        if(!check_integrity(pos))
            return 0;
        if(pos>_elems.size())
            Fibonacci::gen_elems(pos);//不加Fibonacci::也可以,不過我希望在這裡靜態繫結它。

        return _elems[pos - 1];
    }
    string what_am_i()const
    {
        return "Fibonacci";
    }
    ostream &print(ostream &os = cout) const
    {
        for (int i = _beg_pos; i < _beg_pos+_length; ++i)
        {
            os << _elems[i] << ' ';
        }
        return os;
    }
    static unsigned int max_elems()
    {
        return _max_elems;
    }

protected:
    void gen_elems(int pos)//斐波拉契數列生成方式
    {
        if(_elems.empty())
        {
            _elems.push_back(1);
            _elems.push_back(1);
        }
        if(_elems.size()<=pos)
        {
            int ix = _elems.size();
            int n2 = _elems[ix - 2];
            int n1 = _elems[ix - 1];

            for (; ix < pos; ++ix)
            {
                int new_one = n1 + n2;
                _elems.push_back(new_one);
                n2 = n1;
                n1 = new_one;
            }
        }
    }
    //bool check_integrity(int pos)const;//不需要這兩行
    //const static unsigned int _max_elems = 1024;
    int _length;
    int _beg_pos;
    static vector<int> _elems;
};
vector<int> Fibonacci::_elems;//別忘了這行

注意到我們並不需要重寫check_integrity(int pos)constconst static unsigned int _max_elems,可以直接從基類繼承過來。
這裡採用了靜態儲存斐波拉契數列的實體,所有物件共用一份實體,減少了浪費。
靜態物件成員必須在類外初始化,別忘了追加上面程式碼的最後一行。

到現在,你已經可以在主函式內測試一下了,也可以繼續追加其他數列的派生類。但目前為止,還是有一些問題沒有解決。

多型

多型(Polymorphism) 包含靜態多型和動態多型。顧名思義,多型就是多種形態。我們在過載時就實現了一種多型(同一個函式名、運算子具有多種形態)。過載是一種靜態多型,因為具體呼叫哪一個函式在編譯時就可以確定了,這也可以稱之為靜態繫結。看下面這個函式:

//顯示數列名稱以及前三個元素
void func(num_seq& ns)
{
    cout << ns.what_am_i() << ':'
         << ns.elem(1) << ' '
         << ns.elem(2) << ' '
         << ns.elem(3) << endl;
}

這個函式的神奇之處在哪裡?是num_seq&型別的形參也可以接受其子類物件作為實參。這樣的話好處多多。這個函式可以接受其所有的子類的物件,以後根據實際需要追加了新的子類,這個函式未經修改也可以很好的發揮功能。
我們的基類在這裡起到了框架的作用。非常方便程式以後拓展更多內容。

但如果你親自試驗過傳入子類物件,你就會發現這個函式是無法工作的。他會告訴你這一系列函式沒有定義。
這是因為函式名被動態繫結至父類,而父類裡面並沒有給出實現。
要讓他使用子類內定義的同名函式,就要對父類作如下修改:加上關鍵字virtual,因為我不打算在基類中給予定義,所以將其宣告為純虛擬函式

class num_seq
{
public:
    virtual int elem(int pos) = 0;//純虛擬函式
    virtual string what_am_i()const = 0;
    virtual ostream &print(ostream &os = cout) const = 0;
    static unsigned int max_elems() { return _max_elems; }

    virtual ~num_seq(){}//虛析構

protected:
    virtual void gen_elems(int pos) = 0;
    bool check_integrity(int pos)const;
    const static unsigned int _max_elems = 1024;
};

一旦有一個純虛擬函式,這樣的類將被認為是抽象類,因為成員函式沒有實現,所以不能宣告物件實體。
對解構函式使用virtual,可以避免一些問題,我現在做一個簡單的例子:

//演示案例,為構造析構都做了標記
class A
{
private:
    string s;

public:
    A() : s("") { cout << "Construct A" << endl; }
    ~A() { cout << "Destruct A" << endl; }//要不要加上virtual?
};

class B: public A
{
private:
    string s1;

public:
    B():s1(""){ cout << "Construct B" << endl; }
    ~B(){ cout << "Destruct B" << endl; }
};
int main()
{
    A *p = new B;//這是允許的
    delete p;
    return 0;
}

執行一下:
在這裡插入圖片描述
發現並沒有執行子類的析構。這會是個問題。如果採用虛析構:
在這裡插入圖片描述
如果採用了多重繼承的話,這樣的問題會變得極為複雜。所以一般不推薦多重繼承,多重繼承還需要虛繼承來避免多次繼承。這裡不做論述。

總結

面向物件基於三大特性,過程語言比如C語言雖然能模擬一些物件結構,但要實現這三大特性非常困難。
鑑於本人水平有限,所以有錯誤歡迎提出,敬請諒解。