1. 程式人生 > >C++類的設計與實現規範

C++類的設計與實現規範

規範是一種規定,遵守這種規定能夠帶來長遠的利益,而違反這種規定卻不會立即收到懲罰。程式設計的規範是人們在長期的程式設計實踐中總結出來的,深入理解這些規範需要認真的思考和大量的實踐 。不符合程式設計規範的程式碼也能通過編譯並執行,但是從長遠來看,程式碼存在可讀性差、安全性低、不易擴充套件、不易維護等問題。類是面向物件程式設計最主要的元素,遵循必要的規範,設計出效能優良的類,並以適當的方式實現,是編寫出高質量程式的關鍵。

1.規範一:將類的定義放在標頭檔案中實現

這樣可以保證通過引入標頭檔案時,使用的是同一個類,也有利於程式碼維護。比如我們有如下Student類:

//a.cpp
class Student
{
    uint64_t id;
    string
name; public: uint64_t getID(){return id;}; string getName(){return name;} }; //b.cpp //有相同的類Student定義 class Student { uint64_t id; string name; public: uint64_t getID(){return id;}; string getName(){return name;} };

假如根據專案的新需求,類Student需要新增年齡(age)私有資料成員,此時,如果更改了a.cpp中的Student定義而忘記更改b.cpp中的定義,則會出現類定義不一致的情況,容易導致編譯錯誤。即使記得每個原始檔都需要修改,如果幾十甚至上百個原始檔都定義了類Student,那麼我們豈不是要重複更改很多次,這種費力不討好的做法應該儘量避免。有沒有一勞永逸的做法,其實是有的,我們將類的定義放在標頭檔案中,在需要類的原始檔包含類定義所在標頭檔案即可,保證了類定義的一致性,並且修改效率高,程式碼易於維護。

2.規範二:儘量將資料成員申明為私有

資料成員表示了類物件的狀態,這些狀態對外界應該是不可見的。在設計一個類的時候,如果把它的資料成員訪問許可權設為public和protected,會帶來如下影響。
(1)會使類的封裝性遭到破壞。
(2)public資料成員,類的使用者直接以來資料成員,一旦資料成員的定義頻繁改變,類的所有客戶端程式碼都要修改,增加了程式碼模組間的耦合度。

考察如下示例程式:

#include <iostream>
#include <string>
using namespace std;

class Student
{
public
: uint64_t id; string name; public: Student() { id = 0; name = ""; }; void print() { cout<<"id:"<< id<<" name:"<<name<<endl; } uint64_t getID() { return id; }; string getName() { return name; } }; int main(int argc, char* argv[]) { Student s; s.id = 1; s.name = "C羅"; s.print(); }

程式輸出結果:

id:1 name:C

Student是一個學生類,我們希望使用者能夠正確的使用Student來建立學生物件,但是在上面的程式碼中,我們發現使用者給學生設定的名稱為“C羅”,然而中國目前姓名是不能以字母開頭的,所以這個名字是不合法的。產生這個錯誤的原因是Student類涉及存在缺陷,將資料成員id和name的訪問許可權設定為public,意味著有無數的函式可以不加限制地訪問學生物件的資料成員,這樣就無法保證每次對資料成員的設定是正確的。如果我們增加一個設定介面,例如成員函式int set(uint64_t id,const string& name){...},那麼能夠修改資料成員的介面只有一個,只要在修改介面中排除各種錯誤的輸入,就可以保證對Student物件的正確設定。這種對資料成員的直接訪問,是對類封裝性的一種破壞。

另外,從程式碼模組間的耦合度來看,將資料成員設定為共有,意味著所有使用者對類資料成員直接依賴,一旦資料成員的定義發生變化,類的所有客戶端程式碼均需要修改,降低了程式碼的可維護性。

同樣地,將資料成員宣告為protected,也破壞了類的封裝性,因為該類的所有子類均可以直接訪問protected資料成員,如果該類的子類數量龐大,一旦資料成員定義發生變化,所有的派生類都需要重寫。所以,應該儘量將所有的資料成員申明為私有(private)。

3.規範三:將成員函式放到類外定義

類成員函式既可以放在類體內定義,也可以放在類體外定義。如果將類成員函式定義在類體內,會有如下影響。
(1)類的成員函式定義在類的內部影響可讀性。一般來說,類的定義放在標頭檔案中,使用時被不同的原始檔包含,如果類成員函式定義在類體內,將會是程式碼體積增大,影響閱讀,不利於類的修改與維護。
(2)洩露類的實現細節,不利於保護設計者的合法權益。因為介面開放給外部使用時,需要給出原型,比如類的定義,如果將類成員函式定義放在類體內,則函式實現將被暴露。
(3)會存在潛在的風險,如果類的成員函式存在多重定義,由於類不具有外部連線特性,C++編譯器不能充分檢查出類定義的二義性。假設有一個類Student的定義放在兩個標頭檔案中,並且同名成員函式print()出現了二義性,考察如下程式:

/*test1.h*/
class Student
{
    string name;

public:
    Student()
    {
        name = "lvlv";
    };

    void print()
    {
        cout<<"name:"<<name<<endl;
    }
};
/*end test1.h*/

/*test1.cpp*/
#include "test1.h"

void useClass();

int main()
{
    Student s;
    s.print();
    useClass();
}
/*end test1.cpp*/

/*test2.h*/
class Student
{
    string name;

public:
    Student()
    {
        name = "jf";
    };

    void print()
    {
        cout<<"another name:"<<name<<endl;
    }
};
/*end test2.h*/

/*test2.cpp*/
#include "test2.h"

void useClass()
{
    Student s;
    s.print();
}
/*end test2.cpp*/

編譯執行上面的程式,輸出結果如下:

name:lvlv
name:lvlv

上面錯誤地將類Student成員函式print()放在類體內定義並且出現重定義,本希望編譯器在編譯時能夠幫助開發人員發現這種錯誤,但是由於編譯器採用分離編譯模式,各個原始檔中的函式在編譯時互不干涉,在連線時又由於類體內定義的函式為inline函式,不具有外部連線性,導致連線時也未發現重定義錯誤。如果將類成員函式放在類外定義,則編譯器可以發現這種重定義錯誤,所以在類的實現中,應該將類成員函式儘可能地放在類外定義,如果要定義行內函數,只需要在成員函式定義時顯示地使用inline關鍵字即可。

參考文獻

[1]陳剛.C++高階進階教程[M].武漢:武漢大學出版社,2008[4.10(P164-P167)]