《More Effective C++》讀書筆記


一、基礎議題(Basics)

1、仔細區別 pointers 和 references

當一定會指向某個對象,且不會改變指向時,就應該選擇 references,其它任何時候,應該選擇 pointers。 實現某一些操作符的時候,操作符由於語義要求使得指針不可行,這時就使用引用。

 

2、最好使用 C++ 轉型操作符

為解決 C 舊式轉型的缺點(允許將任何類型轉為任何類型,且難以辨識),C++ 導入 4 個新的轉型操作符(cast operators):

static_cast , const_cast , dynamic_cast , reinterpret_cast:分別是常規類型轉換,去常量轉換,繼承轉換,函數指針轉換

使用方式都是形如: static_cast<type>(expression)  , 如: int d = static_cast<int>(3.14);

#include <iostream>
using namespace std;

struct B
{
    virtual void print(){}//想要使用 dynamic_cast ,基類中必須有虛函數
};
struct D : B
{
    void print(){}
};

int fun(){}

int main()
{
    int i = static_cast<int>(3.14); //i == 3

    const int j = 10;
    int *pj = const_cast<int*>(&j);
    //int *pj = (int*)(&j);     //等同於上面
    *pj = 20;
    //雖然 *pj的地址和 j 的地址是一樣的,但是值卻不一樣。
    cout<<*pj<<endl;    //20
    cout<<j<<endl;      //10

    B *b;
    dynamic_cast<D*>(b);

    typedef void (*FunPtr)();
    reinterpret_cast<FunPtr>(&fun);     //盡量避免使用
}

const_cast :用於去除變量的const或者volatile屬性。但目的絕不是為了修改 const 變量的內容,而是因為無奈,比如說有一個const的值,想代入一個參數未設為const的函數

synamic_cast:用來針對一個繼承體系做向下的安全轉換,目標類型必須為指針或者引用。基類中要有虛函數,否則會編譯出錯;static_cast則沒有這個限制。原因是:存在虛函數,說明它有想要讓基類指針或引用指向派生類對象的情況,此時轉換才有意義。由於運行時類型檢查需要運行時類型信息,而這個信息存儲在類的虛函數表中,只有定義了虛函數的類才有虛函數表。必須保證源類型跟目標類型本來就是一致的,否則返回 null 指針。這個函數使用的是RTTI機制,所以編譯器必須打開這個選項才能編譯。

reinterpret_cast: 不具有移植性,最常用的用途是轉換函數指針類型,但是不建議使用它,除非迫不得已。

 

3、絕對不要以多態方式處理數組

#include <iostream>
using namespace std;

struct B
{
    virtual void print() const{cout<<"base print()"<<endl;}
};
struct D : B
{
    void print() const{cout<<"derived print()"<<endl;}
    int id;  //如果沒有此句,執行將正確,因為基類對象和子類對象長度相同  
};

int fun(const B array[],int size)
{
    for(int i = 0;i<size;++i)
    {
        array[i].print();
    }
}

int main()
{
    B barray[5];
    fun(barray,5);
    D darray[5];
    fun(darray,5);
}

array[i] 其實是一個指針算術表達式的簡寫,它代表的其實是 *(array+i),array是一個指向數組起始處的指針。在 for 里遍歷 array 時,必須要知道每個元素之間相差多少內存,而編譯器則根據傳入參數來計算得知為 sizeof(B),而如果傳入的是派生類數組對象,它依然認為是 sizeof(B),除非正好派生類大小正好與基類相同,否則運行時會出現錯誤。但是如果我們設計軟件的時候,不要讓具體類繼承具體類的話,就不太可能犯這種錯誤。(理由是,一個類的父類一般都會是一個抽象類,抽象類不存在數組)

 

4、避免無用的 default constructors

沒有缺省構造函數造成的問題:通常不可能建立對象數組,對於使用非堆數組,可以在定義時提供必要的參數。另一種方法是使用指針數組,但是必須刪除數組里的每個指針指向的對象,而且還增加了內存分配量。
提供無意義的缺省構造函數會影響類的工作效率,成員函數必須測試所有的部分是否都被正確的初始化。

 

二、操作符(Operators)

5、對定制的“類型轉換函數”保持警覺

定義類似功能的函數,而拋棄隱式類型轉換,使得類型轉換必須顯示調用。例如 String類沒有定義對Char*的隱式轉換,而是用c_str函數來實施這個轉換。擁有單個參數(或除第一個參數外都有默認值的多參數)構造函數的類,很容易被隱式類型轉換,最好加上 explicit 防止隱式類型轉換。

 

6、區別 increment/decrement 操作符的前置和后置形式

#include <iostream>
using namespace std;

class A
{
    public:
        A(int i):id(i){}
        A& operator++()
        {
            this->id += 1;
            return *this;
        }
        //返回值為 const ,以避免 a++++這種形式
        //因為第二個 operator++ 所改變的對象是第一個 operator++ 返回的對象
        //最終結果其實也只是累加了一次,a++++ 也還是相當於 a++,這是違反直覺的
        const A operator++(int)
        {
            A a = *this;
            this->id += 1;
            return a;
        }
        int id;
};
int main()
{
    A a(3);
    cout<<++a.id<<endl; //++++a;   也是允許的,但 a++++ 不允許。
    cout<<a.id<<endl;
    cout<<a++.id<<endl;
    cout<<a.id<<endl;
}

后置operator++(int) 的疊加是不允許的,原因有兩個:一是與內建類型行為不一致(內建類型支持前置疊加);二是其效果跟調用一次 operator++(int) 效果一樣,這是違反直覺的。另外,后置式操作符使用 operator++(int),參數的唯一目的只是為了區別前置式和后置式而已,當函數被調用時,編譯器傳遞一個0作為int參數的值傳遞給該函數

處置用戶定制類型時,盡可能使用前置式,因為后置式會產生一個臨時對象。

 

7、千萬不要重載 &&, || 和 , 操作符

int *pi = NULL;
if(pi != 0 && cout<<*pi<<endl) { }

上面的代碼不會報錯,雖然 pi 是空指針,但 && 符號采用"驟死式"評估方式,如果 pi == 0 的話,不會執行后面的語句。

不要重載這些操作符,是因為我們無法控制表達式的求解優先級,不能真正模仿這些運算符。操作符重載的目的是使程序更容易閱讀,書寫和理解,而不是來迷惑其他人。如果沒有一個好理由重載操作符,就不要重載。而對於&&,||和“,”,很難找到一個好理由。

 

8、了解各種不同意義的 new 和 delete

new 操作符的執行過程:
  (1). 調用operator new分配內存 ;  //這一步可以使用 operator new 或 placement new 重載。
  (2). 調用構造函數生成類對象;
  (3). 返回相應指針。

函數 operator new 通常聲明如下: 

void * operator new(size_t size);  //第一個參數必須為 size_t,表示需要分配多少內存。

返回值為void型指針,表示這個指針指向的內存中的數據的類型要由用戶來指定。比如內存分配函數malloc函數返回的指針就是void *型,用戶在使用這個指針的時候,要進行強制類型轉換,如(int *)malloc(1024)。任何類型的指針都可以直接賦給 void * 變量,而不必強制轉換。如果函數的參數可以為任意類型的指針,則可以聲明為 void * 了。

void 有兩個地方可以使用,第一是函數返回值,第二是作為無參函數的參數。(因為在C語言中,可以給無參函數傳任意類型的參數,而且C語言中,沒有指定函數返回值時,默認返回為 int 值)

#include <iostream>

using namespace std;
class User
{
    public:
    void * operator new(size_t size)
    {
        std::cout<<"size: "<<size<<std::endl;
    }
    void * operator new(size_t size,std::string str)
    {
        std::cout<<"size: "<<size <<"\nname: " << str<< std::endl;
    }
    int id;
};

int main()
{
    User* user1 = new User;
    User* user2 = new ("JIM")User;
    void *pi = operator new(sizeof(int));
    int i = 3;
    int *p = &i;
    pi = p;
    cout<<*(int*)pi<<endl;
}

 

三、異常(Exceptions)

9、利用 destructors 避免泄漏資源

#include <iostream>
#include <stdexcept>

void exception_fun()
{
    throw std::runtime_error("runtime_error");
}

void fun()
{
    int *pi = new int[10000];
    std::cout<<pi<<std::endl;
    try
    {
        exception_fun();    //如果此處拋出異常而未處理,則無法執行 delete 語句,造成內存泄漏。
    }
    catch(std::runtime_error& error)
    {
        delete pi;
        throw;
    }
    delete pi;
}

main()
{
    for(;;)
    {
        try { fun(); } catch(std::runtime_error& error) { }
    }
}

 一個函數在堆里申請內存到釋放內存的過程中,如果發生異常,如果自己不處理而只交給調用程序處理,則可能由於未調用 delete 導致內存泄漏。上面的方法可以解決這一問題,不過這樣的代碼使人看起來心煩且難於維護,而且必須寫雙份的 delete 語句。函數返回時局部對象總是釋放(調用其析構函數),無論函數是如何退出的。(僅有的一種例外是當調用 longjmp 時,而 longjmp 這個缺點也是C++最初支持異常處理的原因)

所以這里使用智能指針或類似於智能指針的對象是比較好的辦法:

#include <iostream>
#include <stdexcept>

void exception_fun()
{
    throw std::runtime_error("runtime_error");
}

void fun()
{
    int *pi = new int[10000];
    std::auto_ptr<int> ap(pi);    //用 auto_ptr 包裝一下
    std::cout<<pi<<std::endl;
    exception_fun();
}

main()
{
    for(;;)
    {
        try { fun(); } catch(std::runtime_error& error) { }
    }
}

上面的代碼看起來簡潔多了,因為 auto_ptr 會在離開作用域時調用其析構函數,析構函數中會做 delete 動作。

  

10、在 constructors 內阻止資源泄漏

這一條講得其實是捕獲構造函數里的異常的重要性。

堆棧輾轉開解(stack-unwinding):如果一個函數中出現異常,在函數內即通過 try..catch 捕捉的話,可以繼續往下執行;如果不捕捉就會拋出(或通過 throw 顯式拋出)到外層函數,則當前函數會終止運行,釋放當前函數內的局部對象(局部對象的析構函數就自然被調用了),外層函數如果也沒有捕捉到的話,會再次拋出到更外層的函數,該外層函數也會退出,釋放其局部對象……如此一直循環下去,直到找到匹配的 catch 子句,如果找到 main 函數中仍找不到,則退出程序。

#include <iostream>
#include <string>
#include <stdexcept>

class B
{
    public:
        B(const int userid_,const std::string& username_ = "",const std::string address_ = ""):
        userid(userid_),
        username(0),
        address(0)
        {
            username = new std::string(username_);
            throw std::runtime_error("runtime_error");  //構造函數里拋出異常的話,由於對象沒有構造完成,不會執行析構函數
            address = new std::string(address_);
        }
        ~B()    //此例中不會執行,會導致內存泄漏
        {
            delete username;
            delete address;
            std::cout<<"~B()"<<std::endl;
        }
    private:
        int userid;
        std::string* username;
        std::string* address;
};

main()
{
    try { B b(1); } catch(std::runtime_error& error) { }
}

 C++拒絕為沒有完成構造函數的對象調用析構函數,原因是避免開銷,因為只有在每個對象里加一些字節來記錄構造函數執行了多少步,它會使對象變大,且減慢析構函數的運行速度。

一般建議不要在構造函數里做過多的資源分配,而應該把這些操作放在一個類似於 init 的成員函數中去完成。這樣當 init 成員函數拋出異常時,如果對象是在棧上,析構函數仍會被調用(異常會自動銷毀局部對象,調用局部對象的析構函數,見下面),如果是在堆上,需要在捕獲異常之后 delete 對象來調用析構函數。

 

11、禁止異常流出 destructors 之外

這一條講得其實是捕獲析構函數里的異常的重要性。第一是防止程序調用 terminate 終止(這里有個名詞叫:堆棧輾轉開解 stack-unwinding);第二是析構函數內如果發生異常,則異常后面的代碼將不執行,無法確保我們完成我們想做的清理工作。

之前我們知道,析構函數被調用,會發生在對象被刪除時,如棧對象超出作用域或堆對象被顯式 delete (還有繼承體系中,virtual 基類析構函數會在子類對象析構時調用)。除此之外,在異常傳遞的堆棧輾轉開解(stack-unwinding)過程中,異常處理系統也會刪除局部對象,從而調用局部對象的析構函數,而此時如果該析構函數也拋出異常,C++程序是無法同時處理兩個異常的,就會調用 terminate()終止程序(會立即終止,連局部對象也不釋放)。另外,如果異常被拋出,析構函數可能未執行完畢,導致一些清理工作不能完成。

所以不建議在析構函數中拋出異常,如果異常不可避免,則應在析構函數內捕獲,而不應當拋出。 場景再現如下:

#include <iostream>

struct T
{
    T()
    {
        pi = new int;
        std::cout<<"T()"<<std::endl;
    }
    void init(){throw("init() throw");}
    ~T()
    {
        std::cout<<"~T() begin"<<std::endl;
        throw("~T() throw");
        delete pi;
        std::cout<<"~T() end"<<std::endl;
    }
    int *pi;
};

void fun()
{
    try{
        T t;
        t.init();
    }catch(...){}

//下面也會引發 terminate
    /*
    try
    {
        int *p2 = new int[1000000000000L];
    }catch(std::bad_alloc&)
    {
        std::cout<<"bad_alloc"<<std::endl;
    }
    */
}

void terminate_handler()
{
    std::cout<<"my terminate_handler()"<<std::endl;
}

int main()
{
    std::set_terminate(terminate_handler);
    fun();
}

 

12、了解 "拋出一個 exception ”  與 “傳遞一個參數” 或 “調用一個虛函數”之間的差異

拋出異常對象,到 catch 中,有點類似函數調用,但是它有幾點特殊性:

 1 #include <iostream>
 2 
 3 void fun1(void)
 4 {
 5     int i = 3;
 6     throw i;
 7 }
 8 void fun2(void)
 9 {
10     static int i = 10;
11     int *pi = &i;
12     throw pi; //pi指向的對象是靜態的,所以才能拋出指針
13 }
14 
15 main()
16 {
17     try{
18         fun1();
19     }catch(int d)
20     {
21         std::cout<<d<<std::endl;
22     }
23     try{
24         fun2();
25     } catch(const void* v)
26     {
27         std::cout<<*(int*)v<<std::endl;
28     }
29 }

如果拋出的是 int 對象的異常,是不能用 double 類型接收的,這一點跟普通函數傳參不一樣。異常處理中,支持的類型轉換只有兩種,一種是上面例子中演示的從"有型指針"轉為"無型指針",所以用 const void* 可以捕捉任何指針類型的 exception。另一種是繼承體系中的類轉換,可見下一條款的例子。

另外,它跟虛擬函數有什么不同呢?異常處理可以出現多個 catch 子句,而匹配方式是按先后順序來匹配的(所以如 exception 異常一定要寫在 runtime_error異常的后面,如果反過來的話,runtime_error異常語句永遠不會執行),而虛函數則是根據虛函數表來的。

 

13、以 by reference 方式捕捉 exceptions

 1 #include <iostream>
 2 #include <stdexcept>
 3 
 4 class B
 5 {
 6     public:
 7         B(int id_):id(id_){}
 8         B(const B& b){id = b.id;std::cout<<"copy"<<std::endl;}
 9         int id;
10 };
11 
12 void fun(void)
13 {
14     static B b(3);  //這里是靜態對象
15     throw &b;   //只有該對象是靜態對象或全局對象時,才能以指針形式拋出
16 }
17 main()
18 {
19     try{
20         fun();
21     }catch(B* b)    //這里以指針形式接收
22     {
23         std::cout<<b->id<<std::endl;    //輸出3
24     }
25 }

用指針方式來捕捉異常,上面的例子效率很高,沒有產生臨時對象。但是這種方式只能運用於全局或靜態的對象(如果是 new 出來的堆中的對象也可以,但是該何時釋放呢?)身上,否則的話由於對象離開作用域被銷毀,catch中的指針指向不復存在的對象。接下來看看對象方式和指針方式:

#include <iostream>
#include <stdexcept>

class B
{
    public:
        B(){}
        B(const B& b){std::cout<<"B copy"<<std::endl;}
        virtual void print(void){std::cout<<"print():B"<<std::endl;}
};

class D : public B
{
    public:
        D():B(){}
        D(const D& d){std::cout<<"D copy"<<std::endl;}
        virtual void print(void){std::cout<<"print():D"<<std::endl;}
};

void fun(void)
{
    D d;
    throw d;
}
main()
{
    try{
        fun();
    }catch(B b) //注意這里
    {
        b.print();
    }
}

上面的例子會輸出:

可是如果把 catch(B b) 改成 catch(B& b) 的話,則會輸出:

該條款的目的就是告訴我們,請盡量使用引用方式來捕捉異常,它可以避免 new 對象的刪除問題,也可以正確處理繼承關系的多態問題,還可以減少異常對象的復制次數。 

 

14、明智運用 exception specifications

C++提供了一種異常規范,即在函數后面指定要拋出的異常類型,可以指定多個:

#include <iostream>

void fun(void) throw(int,double);    //必須這樣聲明,而不能是 void fun(void);

void fun(void) throw(int,double)    //說明可能拋出 int 和 double 異常
{
    int i = 3;
    throw i;
}

main()
{
    try{
        fun();
    }catch(int d)
    {
        std::cout<<d<<std::endl;
    }
}

 

15、了解異常處理的成本

 大致的意思是,異常的開銷還是比較大的,只有在確實需要用它的地方才去用。

 

四、效率(Efficiency)

16、謹記 80-20 法則

大致的意思是說,程序中80%的性能壓力可能會集中在20%左右的代碼處。那怎么找出這20%的代碼來進行優化呢?可以通過Profiler分析程序等工具來測試,而不要憑感覺或經驗來判斷。

 

17、考慮使用 lazy evaluation(緩式評估)

除非確實需要,否則不要為任何東西生成副本。當某些計算其實可以避免時,應該使用緩式評估。

 

18、分期攤還預期的計算成本

跟上一條款相對的,如果某些計算無可避免,且會多次出現時,可以使用急式評估。 

 

19、了解臨時對象的來源

C++真正所謂的臨時對象是不可見的——只要產生一個 non-heap object 而沒有為它命名,就產生了一個臨時對象。它一般產生於兩個地方:一是函數參數的隱式類型轉換,二是函數返回對象時。 任何時候,只要你看到一個 reference-to-const 參數,就極可能會有一個臨時對象被產生出來綁定至該參數上;任何時候,只要你看到函數返回一個對象,就會產生臨時對象(並於稍后銷毀)。

 

20、協助完成“返回值優化(RVO)”

不要在一個函數里返回一個局部對象的地址,因為它離開函數體后就析構了。不過在GCC下可以正常運行,無論是否打開優化;而在VS2010中如果關閉優化,就會看到效果。

這個條款想說的是:const Test fun(){ return Test(); } 比 const Test fun(){Test test; return test; }  好,更能使編譯器進行優化。

不過現在看來,在經過編譯器優化之后,這兩個好像已經沒有什么區別了。

 

21、利用重載技術避免隱式類型轉換

#include <iostream>

using namespace std;

struct B
{
    B(int id_):id(id_){}
    int id;
};

const B operator+(const B& b1,const B& b2)
{
    return B(b1.id + b2.id);
}

//const B operator+(const B& b1,int i)    //如果重載此方法,就不會產生臨時對象了
//{
//  return B(b1.id + i);
//}
int main()
{
    B b1(3),b2(7);
    B b3 = b1+ b2;
    B b4 = b1 + 6;    //會把 6 先轉換成B對象,產生臨時對象
}

 

22、考慮以操作符復合形式(op=)取代其獨身形式(op)

使用 operator+= 的實現來實現 operator= ,其它如果 operator*=、operator-= 等類似。

#include <iostream>

class B
{
    public:
        B(int id_):id(id_){}
        B& operator+=(const B& b)
        {
            id +=  b.id;
            return *this;
        }
        int print_id(){std::cout<<id<<std::endl;}
    private:
        int id;
};

B operator+(const B& b1,const B& b2)    //不用聲明為 B 的 friend 函數,而且只需要維護 operator+= 即可。
{
    return const_cast<B&>(b1) += b2;    //這里要去掉b1的const屬性,才能帶入operator+= 中的 this 中
}

int main()
{
    B b1(3),b2(7),b3(100);
    (b1+b2).print_id(); //10    這里進行 operator+ 操作,會改變 b1 的值,這個不應該吧
    b1.print_id();      //10
    b3+=b1;
    b3.print_id();      //110
}

 

23、考慮使用其它程序庫

提供類似功能的程序庫,可能在效率、擴充性、移植性和類型安全方面有着不同的表現。比如說 iostream 和 stdio 庫,所以選用不同的庫可能會大幅改善程序性能。

 

24、了解 virtual functions、multiple inheritance、virtual base classes、runtime type identification 的成本

在使用虛函數時,大部分編譯器會使用所謂的 virtual tables 和 virtual table pointers ,通常簡寫為 vtbls 和 vptrs 。vtbl 通常是由 "函數指針" 架構而成的數組,每一個聲明(或繼承)虛函數的類都有一個 vtbl ,而其中的條目就是該 class 的各個虛函數實現體的指針。

虛函數的第一個成本:必須為每個擁有虛函數的類耗費一個 vtbl 空間,其大小視虛函數的個數(包括繼承而來的)而定。不過,一個類只會有一個 vtbl 空間,所以一般占用空間不是很大。

不要將虛函數聲明為 inline ,因為虛函數是運行時綁定的,而 inline 是編譯時展開的,即使你對虛函數使用 inline ,編譯器也通常會忽略。

虛函數的第二個成本:必須為每個擁有虛函數的類的對象,付出一個指針的代價,即 vptr ,它是一個隱藏的 data member,用來指向所屬類的 vtbl。

調用一個虛函數的成本,基本上和通過一個函數指針調用函數相同,虛函數本身並不構成性能上的瓶頸。

虛函數的第三個成本:事實上等於放棄了 inline。(如果虛函數是通過對象被調用,倒是可以 inline,不過一般都是通過對象的指針或引用調用的)

#include <iostream>

struct B1 { virtual void fun1(){} int id;};
struct B2 { virtual void fun2(){} };
struct B3 { virtual void fun3(){} };
struct D : virtual B1, virtual B2, virtual B3 {virtual void fun(){}  void fun1(){}  void fun2(){}   void fun3(){}};

int main()
{
    std::cout<<sizeof(B1)<<std::endl;   //8
    std::cout<<sizeof(B2)<<std::endl;   //4
    std::cout<<sizeof(B3)<<std::endl;   //4
    std::cout<<sizeof(D)<<std::endl;    //16
}

//D 中只包含了三個 vptr ,D和B1共享了一個。

 

五、技術(Techniques,Idioms,Patterns)

25、將 constructor 和 non-member functions 虛化

這里所謂的虛擬構造函數,並不是真的指在構造函數前面加上 virtual 修飾符,而是指能夠根據傳入不同的參數建立不同繼承關系類型的對象。 

被派生類重定義的虛函數可以與基類的虛函數具有不同的返回類型。所以所謂的虛擬復制構造函數,可以在基類里聲明一個 virtual B* clone() const = 0 的純虛函數,在子類中實現 virtual D* clone() const {return new D(*this);}

同樣的,非成員函數虛化,這里也並不是指使用 virtual 來修飾非成員函數。比如下面這個輸出 list 中多態對象的屬性:

#include <iostream>
#include <list>
#include <string>

using namespace std;

class B
{
    public:
        B(string str):value(str){}
        virtual ostream& print(ostream& s) const = 0;
    protected:
        string value;
};

class D1 : public B
{
    public:
        D1(int id_):B("protect value"),id(id_){}    //子類構造函數中,要先調用基類構造函數初始化基類
        ostream& print(ostream& s) const{cout<<value<<"\t"<<id;;return s;}  //如果基類虛函數是 const 方法,則這里也必須使用 const 修飾
    private:
        int id;
};

class D2 : public B
{
    public:
        D2(int id_):B("protect value"),id(id_){}    //子類構造函數中,要先調用基類構造函數初始化基類
        ostream& print(ostream& s) const{cout<<value<<"\t"<<id;return s;}
    private:
        int id;
};

ostream& operator<<(ostream& s,const B& b)
{
    return b.print(s);
}

int main()
{
    list<B*> lt;
    D1 d1(1);
    D2 d2(2);
    lt.push_back(&d1);
    lt.push_back(&d2);

    list<B*>::iterator it = lt.begin();
    while(it != lt.end())
    {
        cout<<*(*it)<<endl;     //D1   D2
        it++;
    }
}

在這里,即使給每一個繼承類單獨實現友元的 operator<< 方法,也不能實現動態綁定,只會調用基類的方法。那么,在基類里寫 operator<< 用 virtual 修飾不就行了嗎?遺憾的,虛函數不能是友元。

 

26、限制某個 class 所能產生的對象數量

類中的靜態成員總是被構造,即使不使用,而且你無法確定它什么時候初始化;而函數中的靜態成員,只有在第一次使用時才會建立,但你也得為此付出代價,每次調用函數時都得檢查一下是否需要建立對象。(另外該函數不能聲明為內聯,非成員內聯函數在鏈接的時候在目標文件中會產生多個副本,可能造成程序的靜態對象拷貝超過一個。)這個已經由標准委員會在1996年把 inline 的默認連接由內部改為外部,所以問題已經不存在了,了解一下即可。 限制對象個數:建立一個基類,構造函數和復制構造函數中計數加一,若超過最大值則拋出異常;析構函數中計數減一。

 

27、要求(或禁止)對象產生於 heap 中

析構函數私有,有一個致命問題:妨礙了繼承和組合(內含)。

#include <iostream>
#include <string>

using namespace std;

class B1    //禁止對象產生於 heap 中
{
    public:
        B1(){cout<<"B1"<<endl;};
    private:
        void* operator new(size_t size);
        void* operator new[](size_t size);
        void operator delete(void* ptr);
        void operator delete[](void* ptr);
};

class B2    //要求對象產生於 heap 中
{
    public:
        B2(){cout<<"B2"<<endl;};
        void destroy(){delete this;}  //模擬的析構函數 private:
        ~B2(){}
};
int main()
{
    //B1* b1  = new B1; //Error!
    B1 b1;
    //B2 b2;    //Error
    B2* b2 = new B2;
    b2->destroy();
}

 

28、Smart Pointer(智能指針)

可以參考 auto_ptr 和 share_ptr(源於boost,已被收錄進c++11標准)源碼。 

 

29、Reference counting(引用計數)

同上。

 

30、Proxy classes(替身類、代理類)

參考《可復用面向對象軟件基礎》結構型模式之代理模式。

 

31、讓函數根據一個以上的對象類型來決定如何虛化

 

 

六、雜項討論(Miscellany)

32、在未來時態下發展程序

要用語言提供的特性來強迫程序符合設計,而不要指望使用者去遵守約定。比如禁止繼承,禁止復制,要求類的實例只能創建在堆中等等。處理每個類的賦值和拷貝構造函數,如果這些函數是難以實現的,則聲明它們為私有。

所提供的類的操作和函數有自然的語法和直觀的語義,和內建類型(如 int)的行為保持一致。

盡可能寫可移植性的代碼,只有在性能極其重要時不可移植的結構才是可取的。

多為未來的需求考慮,盡可能完善類的設計。

 

33、將非尾端類設計為抽象類

只要不是最根本的實體類(不需要進一步被繼承的類),都設計成抽象類。

 

34、如何在同一個程序中結合 C++ 和 C

等有時間看看 C語言的經典書籍后再說。

 

35、讓自己習慣於標准 C++ 語言

可以參考《C++標准程序庫》,另外可以使用最新編譯器,嘗試c++11新特性。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM