C++的Class中的一些重點整理

class與struct有很明顯的的差別是,class可以定義member function,但struct不行。另外,class預設的member權限是private,而struct預設則是public。

以下是我看螞蟻書的重點整理。另外,也有參考這篇。

  • friend function: 簡單來說就是你在class裡面定義了一個friend function,這個function是在class的外面,同時這個function可以修改private data。

  • const function: 在function後面如果加了const的話,就表示它不會修改任何的menber data,所以,const object不能夠呼叫non const function。

以下講權限範圍,如果沒有定義的話,則預設表示private。其實跟java很類似,差別在於java沒有friend class,而且java的預設權限是package scope。

  • private: 只有自己或者是friend class看得到(其實就是可不可以直接存取的意思啦)。
  • protected: 只有自己,friend class或者子類別才可以看得到。
  • public: 任何可以看到這個class的地方都可以使用。

建構子跟解構子也很重要,以前不太清楚解構子可以幹嘛,只知道物件被回收的時候解構子會自動被呼叫。 以下這個例子我覺得很好,應該是說當物件內部有動態配置的時候,可以一起把動態

// example on constructors and destructors rect area: 12
#include <iostream.h> rectb area: 30
class CRectangle
{
    int* width, *height;
public:
    //overloading
    CRectangle();
    CRectangle(int, int);
    ~CRectangle();
    //實作內容定義在class以內的有機會被編譯器視為inline function,編譯器會自己作判斷
    //inline function會直接將程式碼展開來,與macro非常類似
    //程式碼很短,只有return statement,用inline較有效率
    int area(void)
    {
        return (*width * *height);
    }
};

CRectangle::CRectangle()
{
    width = new int;
    height = new int;
    *width = 5;
    *height = 6;
}

CRectangle::CRectangle(int a, int b)
{
    width = new int;
    height = new int;
    *width = a;
    *height = b;
}

CRectangle::~CRectangle()
{
    delete width;
    delete height;
}

int main()
{
    //若不傳參數,則不需要括弧,java則需要
    CRectangle rect(3, 4), rectb(5, 6), rectc;
    cout << "rect area: " << rect.area() << endl;
    cout << "rectb area: " << rectb.area() << endl;
    return 0;
}

實際上,當我們定義一個 class 而沒有定義建構子的時候,編譯器會自動假設兩 個重載的建構子 (預設建構子"default constructor" 和複製建構子"copy constructor"),又與java類似,只是在java中我沒聽過copy constructor。

class CExample
{
public:
    //沒有定義建構子
    int a, b, c;
    void multiply(int n, int m)
    {
        a = n;
        b = m;
        c = a * b;
    };
};

因此defalut constructor以及copy constructor會被自動定義,copy constructor是一個只有一個參數的建構子,該參數是這個 class 的一個物件,這個函數的功能是將被傳入的物件(object)的所有非靜態 (non-static)成員變數的值都複製給自身這個 object。

//default constructor
CExample::CExample() { };
//copy constructor
//一定要pass by reference,否則會導致遞迴呼叫copy constructor而形成無窮迴圈。
//若是有用到指標,則copy constructor預設只會複製指標的address,所以一定要視情況改寫
CExample::CExample(const CExample& rv)
{
    a = rv.a;
    b = rv.b;
    c = rv.c;
}

為什麼copy constructor一定要pass by reference?,這個問題我大一的時候有想過,去問了ptt的版友之後終於恍然大悟。 問題點出在pass by value的時候並不是直接用等號來作assignment,而是會先呼叫copy constructor。

假設有一個function foo長這樣

//pass by reference
foo(Bar bar)

當有人要呼叫foo()

Bar mybar;
foo(mybar)

進入 foo 之前, 因為是 pass by value, 所以要生成一個 myBar 的 copy. 要怎樣生成呢? compiler 會利用 copy constructor 生成一個 myBar 的複製品, 然後供 foo() 裡面使用。

要是Bar這個class的copy constructor 是 pass-by-value 的話, 比如

Bar myBar;
Bar barCopy(myBar);

生成 barCopy 的時候, 因為 myBar 是 pass by value, 所以 compiler 要生成一個 myBar 的複製品 (假設叫 tmpBarA) 才傳進去生成 barCopy.

但生成 tmpBarA, 也是利用 copy ctor, 即是 compiler 是在做 Bar tmpBarA(myBar); 由於是 pass-by-val, 那麼要生成 tmpBarA, 其實傳進去的是 myBar 的一個 複製品 (tmpBarB).

要生成 tmpBarB......(遞迴呼叫,無窮迴圈),而如果是copy by reference,則不會呼叫copy constructor。

繼承

繼承的語法如下。

//public表示子類別繼承過來的最低的權限
//例如如果把public改成protected,如果父類別有一個public member,則在子類別子類別會自動被改成protected
class derived_class_name: public base_class_name;

例如

class daughter: protected mother;

protected 將使得 daughter 從 mother 處繼承的成員其最低訪問限制為 protected。也就是說, 原來mother中的所有public成員到daughter中將會成為protected 成員。 最常用的繼承限制除了 public 外就是 private , 它被用來將父類別完全封裝起來, 因為在這種情況下,除了子類別自身外,其它任何的繼承子類別的子類別(孫子)都不能訪問那些從父類別(爺爺)繼承而來的成員。不過大多數情況下繼承都是使用 public。

子類別從父類別繼承了public及protected成員,但以下三個例外。

  • 建構子 Constructor 和解構子 destructor
  • operator=() 成員
  • friends

雖然父類別的建構子和解構子沒有被繼承,但是當一個子類別的 object 被生成或銷毀的時候,其父類別的預設建構子 (即,沒有任何參數的建構子)和解構子總是被自動調用的。

如果父類別沒有預設建構子,或你希望當子類別生成新的 object 時,父類別的某個重載的建構子被調用,你需要在子類別的每一個建構子的定義中指定它:

class mother
{
public:
    mother()
    {
        cout << "mother: no parameters\n";
    }
    mother(int a)
    {
        cout << "mother: int parameter\n";
    }
}
class daughter : public mother
{
public:
    daughter(int a)
    //daughter建構子會自動呼叫mother()
    {
        cout << "daughter: int parameter\n\n";
    }
};
class son : public mother
{
public:
    //son建構子會呼叫mother(a)
    son(int a) : mother(a)
    {
        cout << "son: int parameter\n\n";
    }
};
int main()
{
    daughter cynthia(1);
    son daniel(1);
    return 0;
}

多型

多型的好處就是一個interface可以操控不同的物件,要達成多型的手段就是繼承,讓父類別的指標可以指到子類別的物件。

而一般來說,如果沒有用到virtual function,則父類別的指標只能夠呼叫那些被子類別所繼承的方法。

例如,

class CPolygon
{
protected:
    int width, height;
public:
    void set_values(int a, int b)
    {
        width = a;
        height = b;
    }
};
class CRectangle: public CPolygon
{
public:
    int area(void)
    {
        return (width * height);
    }
};
class CTriangle: public CPolygon
{
public:
    int area(void)
    {
        return (width * height / 2);
    }
};

int main()
{
    CRectangle rect;
    CTriangle trgl;
    //父類別指向子類別
    CPolygon* ppoly1 = &rect;
    CPolygon* ppoly2 = &trgl;
    //但只能夠呼叫父類別自己有定義的
    ppoly1->set_values(4, 5);
    ppoly2->set_values(4, 5);
    //例如父類別沒定義area(),就不能呼叫
    cout << rect.area() << endl;
    cout << trgl.area() << endl;
    return 0;
}

如果要讓父類別呼叫area,要怎麼做呢?方法就是在父類別中定義virtual function。

class CPolygon
{
protected:
    int width, height;
public:
    void set_values(int a, int b)
    {
        width = a;
        height = b;
    }
    virtual int area(void)
    {
        return (0);
    }
};

class CRectangle: public CPolygon
{
public:
    int area(void)
    {
        return (width * height);
    }
};
class CTriangle: public CPolygon
{
public:
    int area(void)
    {
        return (width * height / 2);
    }
};
int main()
{
    CRectangle rect;
    CTriangle trgl;
    CPolygon poly;
    CPolygon* ppoly1 = &rect;
    CPolygon* ppoly2 = &trgl;
    CPolygon* ppoly3 = &poly;
    ppoly1->set_values(4, 5);
    ppoly2->set_values(4, 5);
    ppoly3->set_values(4, 5);
    //呼叫的是rect的area()
    cout << ppoly1->area() << endl;
    //呼叫的是trgl的area()
    cout << ppoly2->area() << endl;
    //呼叫的是poly的area()
    cout << ppoly3->area() << endl;
    return 0;
}

因此,關鍵字 virtual 的作用就是在當使用父類別的指標的時候,使子類別中與父類別同名的成員在適當的時候被調用,如前面例子中所示。

這個又叫做dynamic binding。在執行階段決定要執行哪個實體的方法。

其實這邊跟java稍微不同,因為在java裡面只要子類別有覆寫父類別的方法的話,就會自動去呼叫子類別自己所定義的方法,不用再特別去寫virtual關鍵字。

如果我們的子類別覆寫了父類別的virtual function,但是名字卻打錯了,這樣就還是隻會呼叫父類別的方法。 要怎麼避免呢?答案就是採用pure virtual function,若子類別沒有正確override,則編譯失敗。 在java中,則是用abstract function來做到,所有包含abstract function的class又稱為interface。 例如,

// abstract class CPolygon
class CPolygon
{
protected:
    int width, height;
public:
    void set_values(int a, int b)
    {
        width = a;
        height = b;
    }
    virtual int area(void) = 0;
};

抽象父類別的最大不同是它不能夠有實例(物件),但我們可以定義指向它的指標。(這跟java的interface很像)

以下是一個多型的完整的例子,

class CPolygon
{
protected:
    int width, height;
public:
    void set_values(int a, int b)
    {
        width = a;
        height = b;
    }
    virtual int area(void) = 0;
    //我們可以寫一個 CPolygon 的成員函數,使得它可以將函數
    //area()的結果列印到螢幕上,而不必考慮具體是為哪一個子類別。
    //加上了printarea,記住,this 代表正在被程式執行的這一個物件的指標。
    void printarea(void)
    {
        cout << this->area() << endl;
    }
};
class CRectangle: public CPolygon
{
public:
    int area(void)
    {
        return (width * height);
    }
};
class CTriangle: public CPolygon
{
public:
    int area(void)
    {
        return (width * height / 2);
    }
};
int main()
{
    CRectangle rect;
    CTriangle trgl;
    CPolygon* ppoly1 = &rect;
    CPolygon* ppoly2 = &trgl;
    ppoly1->set_values(4, 5);
    ppoly2->set_values(4, 5);
    ppoly1->printarea();
    ppoly2->printarea();
    return 0;
}