發新話題

C++ Gossip - 物件基礎《封裝》簡介類別(Class)

C++ Gossip - 物件基礎《封裝》簡介類別(Class)

class是C++中用來封裝資料的關鍵字,當您使用類別來定義一個物件(Object)時,您考慮這個物件可能擁有的「屬性」(Property)與「方法」(Method),屬性是物件的靜態描述,而方法是可施加於物件上的動態操作,您使用類別定義出這個物件的規格書,之後就可依這個規格書製作出一個個的物件實例,並在製作過程中設定個別物件的專屬特性資料。

舉個例子來說,您可以定義一個「球」的模子,考慮球有各種不同的顏色(或名稱),以及球最基本的球半徑資訊,您想到這些資訊應該可以取得,並可以進一步取得球的體積,當您在C++中要包裝這些資訊時,您可以如下進行定義:
  • Ball.h
#include <string>
using namespace std;

class Ball {
public:
    Ball();
    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;  // 名稱
};
您在表頭檔案中定義類別,表頭檔案的名稱建議與類別名稱同名,一個定義良好的類別,即使在不看程式碼實作的情況下,也可以從定義中看出這個類別的大致功能;class是C++中用來定義類別的關鍵字,Ball是我們取得類別名稱,記得一個類別的定義是這麼作的:
class Ball {
    // 成員定義
};


最重要的是別忘了在最後加上分號,初學C++的新手很常犯這個錯誤;接下來定義類別的成員,注意到public這個關鍵字,它表示以下所定義的成員可以使用物件名稱直接被呼叫,也稱之為「公用成員」或「公開成員」,private關鍵字下的則是「私用成員」或「私有成員」,不可以透過物件名稱直接呼叫。

在類別封裝時,有一個基本原則是:資訊的最小化公開。如果屬性可以不公開就不公開,如果要取得或設定物件的某些屬性,也是儘量透過方法成員來進行。

資訊的最小化公開原則是基於安全性的考量,避免程式設計人員隨意操作屬性成員而造成程式的錯誤,您可以在日後的程式設計中慢慢來體會;在稍後的實作中,您將可以看到,我將不會radius與name兩個私用成員直接進行存取,而是透過公開的方法來進行設定。

接下來實作類別的內容:
  • Ball.cpp
#include <string>
#include "Ball.h"
using namespace std;

// 預設建構函式
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.cpp,與類別名稱同名的方法稱之為「建構函式」(Constructor),也有人稱之為「建構子」,它沒有傳回值,建構函式的作用在於物件生成時自動初始一些必要的資訊,它可以被過載,以滿足物件生成時不同的設定條件。

「::」稱之為類別範圍解析(Class scope resolution)運算子,在實作類別方法時,在::之前指明您要實作的是哪一個類別的方法,您在實作中過載了建構函式,在不指定引數的情況下,會將 radius設定為0,而name設定為"noname ball",另兩個建構函式則可以指定引數,無參數數建構函式是預設建構函式,如果您沒有定義預設建構函式,則編譯器會自動幫您產生一個無實作內容的預設建構函式。

定義好類別之後,您就可使用這個類別來建立物件,例如:
Ball ball1;
Ball ball2(5.0, "black ball");
string name("yellow ball");
Ball ball3(10.0, name);


也可以這麼建立物件:
Ball ball1 = Ball(5.0, "black ball");


這有些類似宣告變數,使用類別建立的變數稱其為「物件」(Object)或「實例」(Instant),在上例中就是ball1、ball2與ball3 三個物件,ball1物件在建立時並不指定任何參數,所以根據之前實例建構函式的內容,b1的radius將設定為0.0,name設定為"noname ball";ball2則給定兩個參數,所以ball2的radius設定為5.0,而ball2的name設定為"black ball";ball3則是給定radius引數為10.0,第二個參數則給定string實例。

您可以透過公開成員來操作物件或取得物件資訊,方法是使用物件名稱加上「.」運算子,例如:
ball1.name("green ball");

cout << ball1.name() << endl;


以下是使用Ball類別的一個實際例子:
  • main.cpp
#include <iostream>
#include "Ball.h"
using namespace std;

int main() {
    Ball ball1;
    cout << ball1.name() << "\t"
         << ball1.volumn()
         << endl;
   
    ball1.name("green ball");
    ball1.radius(2.5);
    cout << ball1.name() << "\t"
         << ball1.volumn()
         << endl;

    Ball ball2(5.0, "black ball");
    cout << ball2.name() << "\t"
         << ball2.volumn()
         << endl;   
            
    string name("yellow ball");
    Ball ball3(10.0, name);
   
    cout << ball3.name() << "\t"
         << ball3.volumn()
         << endl;
         
    return 0;
}

執行結果:
noname ball     0
green ball         49.0873
black ball          392.699
yellow ball       3141.59

對於簡單的成員函式,您可以將之實作於類別定義中,在類別定義中即實作的函式會自動成為inline函式,例如:
  • Ball.h
#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;  // 名稱
};
  • Ball.cpp
#include <string>
#include "Ball.h"
using namespace std;

// 預設建構函式
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;
}
在定義類別時,如果您只是需要使用到某個類別來宣告指標或是參考,但不涉及類別的生成或操作等訊息,則您可以作該類別的前置宣告(Forward declaration),而不用含入該類別的定義,例如:
  • Test.h
class Ball;

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

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

在定義Test類別時,您尚未真正使用Ball來建構物件進行操作,您只是用它來宣告一些名稱,則您只要使用前置宣告就可以了,實際實作類別時再含入 Ball.h表頭檔即可,例如:
  • 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;     
}

如果您的類別定義有單一參數的建構函式(或除了第一個參數之外,其它參數都有預設值的建構函式),則預設會有自動轉換的作用,例如:
class Ball {
public:
    Ball(const char*);
   ...
};


則您可以使用以下的方式來建構物件並初始化:
Ball ball = "Green ball";


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

TOP

發新話題

本站所有圖文均屬網友發表,僅代表作者的觀點與本站無關,如有侵權請通知版主會盡快刪除。