《C++沉思錄》:類設計者的核查表——有關class的11問

來源:互聯網
上載者:User

標籤:c++   設計   

本文的11個問題提取自《C++沉思錄》第四章。所有問題的說明均為自己補充。

1 你的類需要一個建構函式嗎?

——正確的定義建構函式,把握好建構函式的職能範圍

  • 有些類太簡單,它們的結構就是它們的介面,所以不需要建構函式。
class print{    void print1(){cout<<"1"<<endl;}    void print2(){cout<<"2"<<endl;}    void print3(){cout<<"3"<<endl;}};
  • 有些類很複雜,構造過程就有很複雜的行為,所以必須設計一個建構函式,甚至多個重載建構函式。
class string{public:    string(){_buf_ptr = new char [15];}    string(const string &);    string(const char *);private:    char *_buf_ptr;};
  • 我常常困擾在這樣的問題上:
    “我應該把這部分操作放在建構函式中,還是另外聲明一個函數單獨執行?”
    例如:
class socket_connect{public:    socket_connect();    PowerOn();private:    int sock_fd;};socket_connect::socket_connect(){    sock_fd = socket(AF_INET,SOCK_STREAM,0);    init_sockaddr_in();}void socket_connect::PowerOn(){    int ret = bind(sock_fd,(const sockaddr*)&servaddr,sizeof(servaddr));    PERROR(ret,"bind failed!");    ret = listen(sock_fd,BACK_LOG);    PERROR(ret,"listen failed!");}

在這裡,根據我們的需求,socket本身在初始化的時候就應該開始監聽了吧?所以我們完全可以把bind()listen()放到建構函式裡面去嘛!

但是我還是單獨提出來了一個函數PowerOn(),這是有理由的。因為我傾向於以全域變數的方式建立socket,而且不希望在我完成一系列socket之外的初始化前,就有用戶端試圖串連。

嗯,聽起來都很有道理。第一種做法賦予了建構函式更全面的“構造”功能;第二種做法將操作細化,可以更好地控制類的行為。

  • 那麼。建構函式究竟該執行到哪一步呢?

    1. 一種說法是:PowerOn()或者單獨的init()這樣的函數容易被忘記調用,我們應該讓class在構造後就能愉快的跑起來!
    2. 另一種聲音說:太過複雜的建構函式是不可靠的!你不該過分的信任建構函式!如果你在調用建構函式時出現了問題,後果會很嚴重的!
  • 目前來看。我還沒有找到一個嚴格的限定。但是對於複雜的類來說,下面的兩條一定沒有錯!
    - 空的建構函式是愚蠢的行為!你至少應該把資料賦值成0嘛
    - 實現太多功能的建構函式同樣是愚蠢的行為!過於複雜的建構函式會讓class看起來很淩亂
    這額外的一條是未經過考究的:
    - 如果有init()這樣的一個函數作為建構函式的補充,起碼應該保證當我忘記調用init()時會通過某種方式給出警告或提醒!

2 你的資料成員是私人的嗎?

——pubicprivate 還是 protected?

在逐漸熟練的使用C++的過程中,我越來越傾向於將所有的資料成員都隱藏起來這樣的做法。看這樣一個例子:

class SOURCE{public:    void init(const char*,unsigned int, unsigned int,void*);    inline void increase() {source_amount++;};    inline int decrease(unsigned int val){source_amount -= val};    unsigned int get_sleeptime() const {return sleep_time;}    unsigned int get_amount() const {return source_amount;}    unsigned int get_speed() const {return source_speed; }    char* get_name() const { return name; }private:    Mutex source_lock    char name[20];    unsigned int source_amount;    unsigned int sleep_time;    unsigned int source_speed;};

看!我把所有的資料成員的存取權限都設定為private了!
如果你很懶。將資料成員直接暴露在public也並不是錯誤的做法,但是這樣的話有兩個致命缺點

  • 失去了對資料成員變化的完全控制
    . 我們不知道在何處,在哪裡,資料成員被修改了!暴露資料成員意味著會發生原本希望進行一次++操作,卻意外的被清零這樣的類似問題!
    . 所以說,這就相當於你把root許可權給了所有使用者,多可怕的一件事!

  • 不易於修改
    . 假設:今天我們的需求是: 每次把這個東西+1,明天需求可能就變成了: 每次把這個東西-1!
    . 難道說每一次你都要在所有可能出現修改類成員的地方都把++改為–嗎?這顯然是不現實的!
    . 如果我們把資料成員隱藏,僅僅提供一個提供者,那事情就變得簡單了!我只需要修改這個函數的行為就可以了!
    . 在上面的例子中,我可以在介面內進行mutex_lock()mutex_unlock()操作,修改成安全執行緒的自增操作!這很酷!

因此藏起來所有希望保護的資料成員可能是一個不錯的習慣!(儘管定義各種代替訪問行為的函數介面會增加工作量)

3 你的類需要一個無參的建構函式嗎?

這個問題和問題1相近,建構函式執行到什麼程度算好呢?
顯然這個問題沒有標準答案,決定因素是:你對這個class的設計意圖

class Example{    Example(int p,int q){cout<<p+q<<endl;}};class Example_2{    Expamle_2() {cout<<"nothing"<<endl;}};

如果只提供一個帶參數的constructor,會有哪些損失?

Example eg;Example_2 eg_2;
  • 顯然第一個式子是錯誤的。作為類的設計者,你瞭解關於這個類的一切,可能不會犯這樣的錯誤。但是如果別人需要使用你的類呢?這樣的建立對象顯然會帶來一些小問題。
  • 考慮Example a[100],同樣的道理,只提供一個有參建構函式有時候會帶來小麻煩。

好吧,到底該怎麼做呢?

  • 如果你的設計意圖很明確:我不希望編譯器給我建立預設建構函式,我希望嚴格控制這個class對象的構造行為
  • 那麼放心的僅僅提供有參建構函式,一定沒錯!(但是注意把這個要求傳達給使用者)
  • 如果你思考後,覺得提供一個無參建構函式並不被你的設計意圖所排斥,而且不會因為沒有賦給對象一個有意義的初值導致程式崩潰
  • 那麼提供一個無參建構函式會讓你的類使用起來更方便!

看看STL吧,string s; string s(const string&);
這是一種很好的參考模型。

  • 另外,提供預設值的有參建構函式也可以被用於無參的調用!這也不失為好的解決辦法
class A{public:    A(int a = 10) {}};...int main(){    A a(11);//合法    A b;//合法}
4 是不是建構函式應該初始化所有的資料成員?

看起來這個問題似乎有些奇怪,如果用下面的這種問法,應該會好回答一些:

是不是每個資料成員都應該被建構函式初始化?

顯然應該對每個出現的成員進行一個合理的初始化操作。不然出現未定義的行為會讓程式出現意料之外的錯誤。
幾個常用的例子有:
int data = 0;
char *ptr = NULL;
我想大多數人都知道像上面這樣做以避免“未初始化”這樣的錯誤!那麼,建構函式中也理應如此。

不過,也別做的那麼絕對。書中提到:

有時,類會有一些資料成員,它們只在它們的對象存在了一定時間後才有意義。

對於這種成員,要不要初始化?這就需要留到實際問題中去思考了!不過,養成把指標初始化成NULL的習慣一定不會有錯!

5 類需要解構函式嗎?

這個問題不難回答,我常在解構函式中做的就是使用delete來釋放new建立的對象。(同理mallocfree也是如此!)

保證一個new匹配一個delete,不要多也不要少。
有時候唯一需要注意的是,用delete還是delete []

6 類需要虛解構函式嗎?

這個問題比問題5有意義多了!
首先應該知道:

絕不會用作基類的類是不需要虛解構函式的!

那麼,為什麼解構函式有時候需要被聲明為virtual?
什麼時候應該聲明解構函式為virtual?

class B {};class D: public B {};int main(){    B *b_ptr = new D; //這是正確的    delete b_ptr; //可能會造成錯誤}

只要有人可能會對實際指向基類D對象的、但類型確實B*類型的指標執行delete運算式,就應該給B添加一個虛解構函式!

為什麼這樣做?這是我的解讀

  • 首先,你一定知道:衍生類別內部是存在一個完整的基類對象的,衍生類別做的一切變化都是在這個基類對象後面添加的。
  • 可以這樣理解:基類B1樓,衍生類別D1樓+2樓。現在我們的指標是B *b_ptr,它的作用範圍只有1樓;但是因為B *b_ptr = new D;,所以這個指標實際指向的對象擁有1樓+2樓
  • 如果我們delete b_ptr,很顯然,因為指標被編譯器限制作用範圍為1樓,所以只會是1樓被delete了!但是別忘了,我們的對象是有2樓的!
  • 那麼1樓被拆掉,2樓會怎樣呢?當然是直接懸空!不用懷疑,這是相當危險的!殘餘的2樓不僅沒有被安全釋放,還因為1樓已經消失,使得我們無法再通過1樓上到2樓。

因此,虛解構函式是有必要的!對於一個使用不恰當的指標(上面的b_ptr),我們一旦發現這個指標是不合理的,就通過動態綁定的方式,給他一個假的樓層讓它去析構。如此就避免了2樓還在1樓卻沒了的情況!

虛解構函式通常是空的

7 你的類需要一個賦值操作符嗎?

等號=太常用了,以至於我們經常忘記確定class是否有=操作就去使用它。
這的後果是什麼呢?

class A{public:    A(){name = new char[100];}    ~A(){delete [] name;}//注意這裡private:    char *name;};void function(const A& base){    A copy = base;}//function結束後調用解構函式int main(){    A base;    function(base);}//main結束後調用解構函式

上面的代碼中,function()函數內,建立了一個名為copy的對象,並使用了=操作。嘗試運行一下,會發生什嗎?

————————崩潰啦!!!

為什嗎?

  • 因為預設的=操作符只是簡單地賦值了一份記憶體中的。copybase的資料成員char *name值是相等的,指向同一片記憶體!
  • 這就造成,在function()調用結束後,析構copyname指向的記憶體已被刪除。
  • main函數結束後,還需要析構base一次。而此時name已經被釋放了!再次delete必然崩潰!

給我們的啟示:

  • 動態分配的資源,一定要考慮重載=
    這裡可以這樣修改:
A& A::operator=(const A&base){    char *temp = new char [100];    strcpy(temp,base.name);    name = temp;    //others = base.others;    return *this;}
  • 良好的指標習慣可以讓你在類設計不完善時避免程式崩潰!
    ~A(){delete [] name;name = NULL;}

——拒絕懸垂指標,從我做起!

上面的操作雖然沒有達到預期的目的,但是至少不會崩潰嘛!

TIPS:

通常operator=應該返回一個引用class&,並且由return *this結束以保證與內建的複製操作符一致。

8 你的類需要一個複製建構函式嗎?

如果需要一個複製建構函式,那麼你多半也需要定義一個=吧。即使你不需要一個=,它們的實現方法也是一致的。所以請參見問題7

(當然,如果你的複製建構函式(copy constructor)設計的足夠好的話,你完全可以用copy constructor來實現operator=而不是用=來實現copy constructor)

9 你的賦值操作符能正確的將對象賦給對象本身嗎?

——複製自己帶來的麻煩

記得《劍指offer》最開始的面試題嗎?

如下類型A的聲明,為該類添加一個賦值運算子函數。

class A{public:    A(char *pData = NULL);//pData一般用new動態分配    A(const A&str);    ~A();private:    char *pData;};

還記得問題7,8嗎?

我們的思路很明確:在賦值運算子函數內,先調用delete,釋放原來的字串,然後再new一個字串,用strcpy拷貝過來!(再次強調:delete後先賦值為NULL是好習慣

那麼想一想,下面的語句,會發生什麼呢?

A origin;origin = origin;

origin 先執行delete,然後拷貝自己。很顯然,origin玩脫了!複製自己的時候,已經delete過了,顯然無法完成正確的賦值!

核心問題就是這個啦——防止複製自身!

解決方案如下:

A& A::operator =(const A& origin){    if(this==&origin)        return *this;    delete [] pData;    pData = NULL;    pData = new char [strlen(origin.pData)+1];    strcpy(pData,origion.pData);    return *this;}

另一個可行,且較好的方案是,用temp臨時儲存起來origin的值。(調換語句順序)

A& A::operator =(const A& origin){    char *temp = new char[strlen(origion.pData)+1];    strcpy(temp,origin.pData);    delete [] pData;    pData = temp;    return *this;}

既然說到了這裡,同樣要提到copy constructor裡面的坑——下面哪個複製建構函式是正確的?

class A{public:    A(A origion);//1    A(A& origion);//2    A(const A& origion);//3};
  • 第一個通不過編譯!

    A的複製建構函式禁止帶有和A類型的參數!
    這是一個值得思考的問題。先有雞還是先有蛋呢。。。

  • 第二個可以通過編譯,但是不好,下個問題會詳細說明。

  • 第三個是推薦的!
10 const總是很重要!

問題9的末尾,已經提到了 在賦值運算子和複製建構函式中使用const限定符 有關的東西。

我們使用const是為了防止該變數被修改

原則上來講,凡是不希望改變資料成員的函數我們都給它聲明為const

那麼,當你真的使用const限定符後,對於某些成員函數,添加const同樣也是必須的!

假如一個class被限定為const,編譯器會判定一切非const的成員函數的調用為非法!
因為這些函數可能會擁有改變資料成員值的行為!
即使它實際上並沒有改變資料成員的想法。

這個例子是一個很好的說明:

class Vec{public:    int len_const() const {return len;}    int len(){return len;}    int len;};void use_Vec(const Vec& origin){    int ret1 = origin.len_const();//合法的    int ret2 = len();//error:不相容的類型限定符}

以上的問題常常被忽略,所幸IDE內建語法檢查,讓我們能夠及時發現這類錯誤!

11 刪除數組時你記住了用delete []嗎?

——new和delete的匹配

這條實在是簡單的很,但是實際應用起來卻不知忘記了多少次!

每一個new都要匹配一個delete。除非你確認程式馬上就會結束。
每一個new [ ] 都應該匹配一個delete[ ]。遵循對稱原則總沒有錯!

《C++沉思錄》:類設計者的核查表——有關class的11問

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.