2011-04-07

[Tip]const-correctness(常數正確性)

因為以前筆者寫C/C++程式時,中了const的應用陷阱太多次了,所以特地整理了這篇,幫助廣大的華人C/C++初學者了解const的行為。

首先,我們來看,當const關鍵字與pointer、reference變數的宣告一起使用時,會出現什麼情形:
int* ptr; // pointer本身與其指向的記憶區塊皆可覆寫。
int const* ptr; // pointer本身可覆寫,但指向的記憶區塊不可覆寫。
const int* ptr; // 同上。此為常見的寫法。
int*const ptr; // pointer本身不可覆寫,但指向的記憶區塊可覆寫。
int const*const ptr; // pointer本身與其指向的記憶區塊皆不可覆寫。
int const& ref; // reference指向的記憶區塊不可覆寫。附帶一提,reference本身一定不可覆寫。
int&const ref; // reference本身一定不可覆寫,所以沒有這種寫法。

看到這邊大家應該就了解了。在"*"的右邊有沒有const,決定這個pointer本身是不是唯讀;而"*"或"&"的左邊有沒有const,則決定這個pointer或reference所指向的記憶區塊是不是const。

以此類推,在雙層指標的宣告中使用const關鍵字,就會像這個樣子:
int const**const constPtrToPtrToConstInt; // pointer本身不可覆寫;pointer指向的記憶區塊可覆寫;pointer向的記憶區塊中存放的位址,其指向的記憶區塊不可覆寫。
int *const* ptrToConstPtrToInt; // pointer本身可覆寫;pointer指向的記憶區塊不可覆寫;pointer指向的記憶區塊中存放的位址,其指向的記憶區塊可覆寫。

在function的pointer參數中使用const關鍵字,也有相同的效果(摘自Wikipedia的const-correctness條目):
void Foo( int       *       ptr,
          int const *       ptrToConst,
          int       * const constPtr,
          int const * const constPtrToConst )
{
    *ptr = 0; // OK: modifies the pointee (可覆寫pointer指向的記憶區塊)
    ptr  = 0; // OK: modifies the pointer (可覆寫pointer本身)
 
    *ptrToConst = 0; // Error! Cannot modify the pointee (不可覆寫pointer指向的記憶區塊)
    ptrToConst  = 0; // OK: modifies the pointer (可覆寫pointer本身)
 
    *constPtr = 0; // OK: modifies the pointee (可覆寫pointer指向的記憶區塊)
    constPtr  = 0; // Error! Cannot modify the pointer (不可覆寫pointer本身)
 
    *constPtrToConst = 0; // Error! Cannot modify the pointee (不可覆寫pointer指向的記憶區塊)
    constPtrToConst  = 0; // Error! Cannot modify the pointer (不可覆寫pointer本身)
}

const關鍵字除了可以用在pointer與reference的宣告,還能夠用在物件本身。
在定義與宣告class C的method時,method參數之後若沒有const關鍵字:
class C
{
    int i;
    void set(int j){
        i=j;
    }
};
則執行此method時,this的型態為:
C *const this;
也就是說,物件本身的內容可以覆寫。
而在定義與宣告class C的method時,在method參數之後若加上const關鍵字:
class C
{
    int i;
    int get() const{
        return i;
    }
};
則執行此method時,this的型態會變成:
C const*const this;
也就是說,物件本身的內容不可以覆寫。

以下的C++程式碼可以印證上述的特性(摘自Wikipedia的const-correctness條目):
class C
{
    int i;
public:
    int Get() const // Note the "const" tag
      { return i; }
    void Set(int j) // Note the lack of "const"
      { i = j; }
};
 
void Foo(C& nonConstC, const C& constC)
{
    int y = nonConstC.Get(); // Ok
    int x = constC.Get();    // Ok: Get() is const
 
    nonConstC.Set(10); // Ok: nonConstC is modifiable
    constC.Set(10);    // Error! Set() is a non-const method and constC is a const-qualified object
}

最近C++ template使用的場合越來越多,所以就有人寫了這樣的template function:
template<class T>
void func(const T str){
    str[0]='m';
    printf("%s\n",str);
}

func<char*>(testStr);
奇怪的是,編譯結果顯示,str指向的記憶區塊竟然可以覆寫!這是為什麼呢?
因為在這個場合,"const T"等同於"T const",也就是單純表示T本身不可覆寫,而不影響T所指向的記憶區塊是否可以覆寫。所以,此時str的型態其實是:
char *const str;
所以說,程式能夠覆寫str指向的記憶區塊,也不是什麼奇怪的事。
若要讓str指向的記憶區塊也不可覆寫,那就要改成這樣:
func<char const*>(testStr);
也因此,在實作C++ template class與function時,一般不建議把template變數宣告成pointer。而reference變數加上const時,一律表示reference指向的記憶區塊不可覆寫,自然就不會有上述的問題。所以把template變數宣告成reference,才是C++領域的權威們建議的用法。

總之,希望看過這篇的人,不要再掉入const的應用陷阱了。

沒有留言:

張貼留言