C++物件常用功能

列出設計一新 class 時需要注意的問題

  • 真的需要一個新 class 嗎?(我覺得這最重要)
    • 使用既有 class 是否能達成需求?
  • 如何產生及銷毀新 class 的 object?
    • 與 constructor、destructor、new、new[]、delete、delete[] 有關
  • 物件的 initialization 跟 assignment 有何區別?
    • 與 constructor、assignment operator 的行為有關
  • 如果以 pass by value 的方式傳遞新 class 的 object 時是什麼意思?
    • 與 copy constructor 有關
  • 新 class 的合法值為何?
    • 需維護的條件、member function(如 constructor、assignment operator、setter)所做的錯誤檢查、exception、exception specification
  • 新 class 是否繼承某個繼承架構?
    • base class 的 non-virtual 及 virtual function
    • virtual distructor
  • 新 class 需要那些轉型?
    • 轉型 function
  • 那些 operator、function 對此 class 來說是合法的?
    • 牽涉到要宣告那些 function?是否為 member?
  • 不想使用哪些 compiler 會自動生成的 function(如 constructor)?
    • 如果不使用,將它宣告成 private。
  • 誰可以使用新 class 的 memeber?
    • public、protected、private、friend
  • 新 class 的 undeclared interface 是?
  • 這個 class 需要多 general?
    • class template

class功能

  • 類別成員可以是資料 (data) 、函數 (function) 與建構子 (constructor)

    • 資料其實就是專屬於類別的變數 (variable) ,C++ 的習慣稱之為成員變數 (member variable)
    • 函數也是專屬於類別的,稱之為成員函數 (member function)
    • 建構子屬於特別的成員函數,用來建立該類別物件的專屬函數, 因為建構子用來建立物件,所以建構子沒有回傳值,或著可以這麼想像,建構子預設回傳物件自己本身,因此無須宣告回傳值 (return value) 。
  • class預設內部為private,而struct預設內部為public,除了此因素外,class與struct在c++有完全相同的功能與設定。

    • public關鍵字,它表示以下所定義的成員可以使用物件名稱直接被呼叫,也稱之為「公用成員」或「公開成員」
    • private關鍵字下的則是「私用成員」或「私有成員」,不可以透過物件名稱直接呼叫。
    • protected關鍵字的意思表示存取它有條件限制以保護該成員,當您將類別成員宣告為受保護的成員之後,繼承它的類別就可以直接使用這些成員,但這些成員 仍然受到類別的保護,不可被物件直接呼叫使用。
    • 在類別封裝時,有一個基本原則是:資訊的最小化公開。如果屬性可以不公開就不公開,如果要取得或設定物件的某些屬性,也是儘量透過方法成員來進行。
#include <string>
using namespace std;

class Ball { 
public: 
    // 3 constructors
    Ball();     //default constructor
    Ball(double, const char*); 
    Ball(double, string&); 

    double radius();
    string& name(); 

    void radius(double); 
    void name(const char*); 
    void name(string&); 

    double volumn(); 

private:
    double _radius; // 半徑 
    string _name; // 名稱 
};

// 實作(implementation)
/ 預設建構函式
Ball::Ball() {
    _radius = 0.0; 
    _name = "noname ball"; 
}

Ball::Ball(double radius, const char *name) { 
    _radius = radius; 
    _name = name;
}

Ball::Ball(double radius, string &name) { 
    _radius = radius; 
    _name = name;
}

double Ball::radius() { 
    return _radius; 
} 

double Ball::volumn() { 
    return (4 / 3 * 3.14159 * _radius * _radius * _radius); 
} 

string& Ball::name() { 
    return _name; 
} 

void Ball::radius(double radius) { 
    _radius = radius; 
} 

void Ball::name(string &name) { 
    _name = name; 
}

void Ball::name(const char *name) { 
    _name = name; 
}

// 定義好類別之後,您就可使用這個類別來建立物件,例如: 
Ball ball1; 
Ball ball2(5.0, "black ball");
string name("yellow ball");
Ball ball3(10.0, name);
  • 使用類別建立的變數稱其為「物件」(Object)或「實例」(Instance)。
  • 對於簡單的成員函式,您可以將之實作於類別定義中,在類別定義中即實作的函式會自動成為inline函式,例如:
include <string>
using namespace std;

class Ball { 
public: 
    Ball(); 
    Ball(double, const char*); 
    Ball(double, string&); 

    // 實作於類別定義中的函式會自動inline
    double radius() {
        return _radius;
    }

    string& name() {
        return _name; 
    }

    void radius(double radius) {
        _radius = radius;
    } 

    void name(const char *name) {
        _name = name;
    }

    void name(string& name) {
        _name = name;
    }

    double volumn() {
        return (4 / 3 * 3.14159 * _radius * _radius * _radius); 
    }

private:
    double _radius; // 半徑 
    string _name; // 名稱 
};
  • 在定義類別時,如果您只是需要使用到某個類別來宣告指標或是參考,但不涉及類別的生成或操作等訊息,則您可以作該類別的前置宣告(Forward declaration),而不用含入該類別的定義.
    • 在定義Test類別時,您尚未真正使用Ball來建構物件進行操作,您只是用它來宣告一些名稱,則您只要使用前置宣告就可以了,實際實作類別時再含入 Ball.h表頭檔即可
// test.h
class Ball;

class Test { 
public:
    Test();
    Test(Ball*); 

    Ball* ball(); 
    void ball(Ball*);

private:
    Ball *_ball; // 名稱 
};

// test.cpp
#include "Test.h"
#include "Ball.h"

Test::Test() { 
    _ball = new Ball;
}

Test::Test(Ball *ball) {
    _ball = ball;
}

Ball* Test::ball() {
    return _ball;
}

void Test::ball(Ball *ball) {
    _ball = ball; 
}
  • 預設的轉換行為是由編譯器施行的,但有時是有危險的,如果您不希望編譯器自作主張,則您可以使用explicit修飾,告訴編譯器不要自作主張
class Ball { 
public: 
    explicit Ball(const char*); 
   ...
};

編編器自動生成的函數

  • 在 C++03 的時候,如果程式開發者自己定義一個新的類別的話,就算在什麼都沒有寫的情況下,編譯器也會自動產生一些預設的函式,這些函式包括了
    • 預設建構函式(default constructor) sampleClass()
    • 複製建構函式(copy constructor): sampleClass( const sampleClass& )
    • 複製指派運算子(copy assignment operator) sampleClass& operator= ( const sampleClass& )
    • 解構函式(destructor) : ~sampleClass()
    • 而在一般狀況下,除非自己去實作同類型的函式,否則這些預設的函式,都是會被編譯器自動產生的。所以,我們定義的類別才可以很方便地直接被建構、刪除、複製。
class Empty{};

// 等價於:

class Empty {
public:
    Empty() { ... }                            // default constructor
    Empty(const Empty& rhs) { ... }            // copy constructor
    ~Empty() { ... }                           // destructor — see below
                                               // for whether it's virtual
    Empty& operator=(const Empty& rhs) { ... } // copy assignment operator
};

建構函式(constructor)、解構函式(destructor)

  • 您可以使用建構函式(Constructor)來進行物件的初始化,而在物件釋放資源之前,您也可以使用「解構函式」 (Destructor)來進行一些善後的工作,例如清除動態配置的記憶體,或像是檔案的儲存、記錄檔的撰寫等等。
class SafeArray { 
public: 
    // 建構函式 
    SafeArray(int); 
    // 解構函式 
    ~SafeArray();

    int get(int); 
    void set(int, int);

    int length; 
private:
    int *_array; 

    bool isSafe(int i); 
};

#include "SafeArray.h"

// 動態配置陣列
SafeArray::SafeArray(int len) {
    length = len;
    _array = new int[length];
}

// 測試是否超出陣列長度
bool SafeArray::isSafe(int i) {
    if(i >= length || i < 0) {
        return false;
    } 
    else {
        return true;
    }
}

// 取得陣列元素值
int SafeArray::get(int i) {
    if(isSafe(i)) {
        return _array[i];
    }

    return 0;
}

// 設定陣列元素值
void SafeArray::set(int i, int value) {
    if(isSafe(i)) {
        _array[i] = value;
    }
}

// 刪除動態配置的資源
SafeArray::~SafeArray() {
    delete [] _array;
}

default constructor

  • 我們知道若不寫default constructor,compiler會幫我們產生一個synthesized default constructor。但是若寫了constructor,compiler就不會生成constructor。

  • 哪些地方會用到default constructor呢?

    • 若建立物件時,沒有提供參數將無法建立物件,因為class中有一個constructor後,compiler就不在自動產生synthesized default constructor了,也就是不能用Foo foo這種寫法,但這違背一般人寫程式的習慣。
    • 靜態建立array時,需使用default constructor,如Foo fooa[3],除非改成Foo fooa[] = {1, 2, 3};寫法。
    • 動態建立array時,需使用default construcor,如Foo *pfoo = new Foo[5];,若無default constructor,則以上寫法無法執行。
    • 建立container時,若vector foovec(3);寫法,一開始就得建立3個element,若無default constructor則無法執行。
    • 基於以上理由,compiler會強制我們一定要寫default constructor。
#include <iostream>
include <string>
using namespace std;

class Foo {
public:
    // 自訂的constructor
    Foo(int i=0) : i(i) {}

public:
    int getI() { return i; }
    string getS(){ return s; }

private:
    int i;
    string s;
};

int main() {
    Foo foo;
    cout << foo.getI() << endl;
    cout << foo.getS() << endl;
}

### 初始化列表(initialization list)
* 在建構函式的初始化設定語法中,您還可以使用成員初始化列表(Member initialization list),兩者的差異是原本的方法是設值(assignment),而此方法是給定初始值,兩者的行為不相同。

* 如果沒有定義解構函式時,程式如何結束物件?答案是程式會自動建立一個沒有實作內容的解構函式並自動於適當的時機執行。
```c
SafeArray::SafeArray(int len) : length(len) {
    _array = new int[length];
}

copy constructor

  • 假設我們有個 Demo 類別 (class) ,先建立 Demo 型態的變數 (variable) d1 ,然後宣告同是 Demo 型態的變數 d2 ,並且直接把 d1 指派給 d2 ,此時d2是使用物件d1做為初始化。
    • d1 複製給 d2 的過程當中會啟動 copy 建構函數 (constructor) ,利用 copy 建構函數完成整個過程,而且就算我們沒有自己寫出 copy 建構函數的話,編譯器 (compiler) 也會主動幫我們加上一個。
    • 可是當 Demo 有成員 (member) 是指標 (pointer) 的話會出現一些問題,當我們改變 d2 的指標成員,使用預設的 copy 建構函數會連帶改變 d1 的指標成員 ,這就不會是我們希望的結果了。
    • 當類別中有資料型別為指標時。因為預設的複雜建構子進行進行物件的成員複製時,僅會複製指標型態屬性位址值,而不是指標所指向的值。因此會造成兩個物件的指標會指向同一個內容。
    • 當類別沒有指標,而所複製物件的動作只需要把物件的所有屬性複製一次,您並不需要自行複製建構子,直接利用系統自動產生(內定)的複製建構子即可
Demo d1(p1);
Demo d2 = d1;
#include <iostream>
#include <string>
using namespace std;

class Demo {
public:
    Demo(std::string s) {
        cout << "constructor called.." << endl;
        aPtr = new string;
        *aPtr = s;
    }

    Demo(const Demo &obj) {
        cout << "copy constructor called.." << endl;
        aPtr = new string;
        *aPtr = *obj.aPtr;
    }

    void setA(std::string s) {
        *aPtr = s;
    }

    void do_something() {
        std::cout << *aPtr << std::endl;
    }

private:
    std::string *aPtr;

};

int main(void) {
    Demo d1("There is no spoon.");
    d1.do_something();
    Demo d2 = d1;
    d2.do_something();

    d1.setA("What's truth?");
    d1.do_something();
    d2.do_something();

    return 0;
}

assignment operator

  • 而assignment operator使用時機如下
Foo foo1;
Foo foo2;
foo2 = foo1;
  • 簡單的說,copy constructor和assignment operator都在做copy的動作,當資料是pointer,也就是動態資料時,就必須重新改寫,否則只會copy pointer,而不是copy data。
class Solution {
public:
    char *m_pData;

    // default constructor
    Solution() {
        this->m_pData = NULL;
    }

    // copy constructor
    Solution(char *pData) {
        if (pData) {
            m_pData = new char[strlen(pData) + 1];
            strcpy(m_pData, pData);
        } else {
            m_pData = NULL;
        }
    }

    // Implement an assignment operator
    Solution operator=(const Solution &object) {
        if (this == &object) {
            return *this;
        }

        if (object.m_pData) {
            char *temp = m_pData;
            try {
                m_pData = new char[strlen(object.m_pData)+1];
                strcpy(m_pData, object.m_pData);
                if (temp)
                    delete[] temp;
            } catch (bad_alloc& e) {
                m_pData = temp;
            }
        } else {
            m_pData = NULL;
        }
        return *this;
    }
};

virtual member function

  • 函式與運算子的重載(Overload),重載可以使用一個函式名稱來執行不同的實作,這是一種「編譯時期」就需決定的方式,這是「早期繫 結」(Early binding)、「靜態繫結」(Static binding),因為在編譯時就可以決定函式的呼叫對象,它們的呼叫位址在編譯時就可以得知。

  • 「虛擬函式」(Virtual function)可以實現「執行時期」的多型支援,是一個「晚期繫結」(Late binding)、「動態繫結」(Dynamic binding),也就是指必須在執行時期才會得知所要調用的物件或其上的公開介面。

  • 在談虛擬函式之前必須先知道,一個基底類別的物件指標,可以用來指向其衍生類別物件而不會發生錯誤,例如若基底類別是Base,而衍生類別是Derived,則 下面這個指定是可以接受的。

    • 由於fptr仍是Base類型的指標,它只能存取Base中有定義 的成員,目前來說也只能操作Foo1中的成員。
    • 將衍生類別型態的指標指向基底類別的物件基本是不可行的(雖然可以使用型態轉換的方式來勉強達成,但並不鼓勵),衍生類別的指標並不能存取基底類別的成員。
      • 虛擬函式是一種成員函式,它在基底類別中使用關鍵字"virtual"宣告(定義),並在衍生類別中重新定義虛擬函式,這將成員函式的操作決議 (Resolution)推遲至執行時期再決定。
Base *fptr;
Derived f2;
fptr = &f2;
/* 在此多型範例中,如果Base的show()函式沒有宣告virtual時,ptr->show()會印出base show();
   而若宣告為virtual,則ptr->show()會正常解讀得到derived show();
 */

#include <cstdio>
using namespace std;

class Base {
public:
    virtual void show() { puts("base show()");};
};

class Derived : public Base {
public:
    virtual void show() { puts("derived show()");};
};

int main() {
    Base f1;
    Derived f2;
    Base* ptr = &f2;

    ptr->show();
    return 0;
}

virtual destructor

  • 如果base class的destructor沒有宣告為virtual時,在多型時不會去呼叫derived class的destructor,因此若在derivated中有動態配置記憶體時,會造成memory leak。

  • 只要是有繼承的class 他的base class最好都使用virtual destructor這是一個好習慣。

    • 不使用 virtual constructor 也不一定錯,只要能保證不須使用destructor不會造成memory leak即可。
#include <cstdio>
using namespace std;

class Base {
public:
    Base() { printf("Base constructor\n"); };
    virtual ~Base() { printf("Base destructor\n"); };
};
class Derived: public Base {
public:
    Derived() { printf("Derived constructor\n"); };
    ~Derived() { printf("Derived destructor\n"); };
};

int main() {

    /* 若base的destrutor沒有宣告virtual,則不會呼叫derived的destructor
       即~Base()時,會印出:
        Base constructor
        Derived constructor
        Base destructor

       若為 virtual ~Base()時,會印出:
        Base constructor
        Derived constructor
        Derived destructor
        Base destructor
     */

    Base *p = new Derived;
    delete p;
    return 0;
}

abstract function, abstact class

  • C++預設函式成員都不是虛擬函式,如果要將某個函式成員宣告為虛擬函式,則要加上"virtual"關鍵字,然而C++提供一種語法定義「純虛擬函式」 (Pure virtual function),指明某個函式只是提供一個介面,要求繼承的子類別必須重新定義該函式,定義純虛擬函式除了使用關鍵字"virtual"之外,要在函 式定義之後緊跟著'='並加上一個0.
class Some {
public:
    // 純虛擬函式
    virtual void someFunction() = 0;
    ....
};
  • 一個類別中如果含有純虛擬函式,則該類別為一「抽象類別」(Abstract class),該類別只能被繼承,而不能用來直接生成實例,如果試圖使用一個抽象類別來生成實例,則會發生編譯錯誤。
    • AbstractCircle是個抽象類別,它只能被繼承,繼承了AbstractCircle的類別 必須實作render()函式.
#ifndef ABSTRACTCIRCLE
#define ABSTRACTCIRCLE

class AbstractCircle {
public:
    void radius(double radius) {
        _radius = radius;
    }
    double radius() {
        return _radius;
    }
    // 宣告虛擬函式
    virtual void render() = 0;

protected:
    double _radius;
};

#endif

參考資料

results matching ""

    No results matching ""