2013年11月10日 星期日

const 的使用 (4) - 基礎測驗解答篇

上一篇: const 的使用 (3) - 基礎測驗篇
int a = 5, b = 6;
const int c = 7, d = 8;
int *             p1 = &a; // 編譯成功
int * const       p2 = &a; // 編譯成功
const int *       p3 = &a; // 編譯成功
const int * const p4 = &a; // 編譯成功
int *             p5 = &c; // 編譯失敗
int * const       p6 = &c; // 編譯失敗
const int *       p7 = &c; // 編譯成功
const int * const p8 = &c; // 編譯成功
int *             p9;      // 編譯成功
int * const       p10;     // 編譯失敗
const int *       p11;     // 編譯成功
const int * const p12;     // 編譯失敗
a = 15;                    // 編譯成功
c = 20;                    // 編譯失敗
p1 = &b;                   // 編譯成功
p2 = &b;                   // 編譯失敗 
p3 = &b;                   // 編譯成功
p4 = &b;                   // 編譯失敗
p1 = &d;                   // 編譯失敗
p2 = &d;                   // 編譯失敗 
p3 = &d;                   // 編譯成功
p4 = &d;                   // 編譯失敗
*p1 = a;                   // 編譯成功
*p2 = a;                   // 編譯成功
*p3 = a;                   // 編譯失敗
*p4 = a;                   // 編譯失敗
*p1 = c;                   // 編譯成功
*p2 = c;                   // 編譯成功 
*p3 = c;                   // 編譯失敗
*p4 = c;                   // 編譯失敗
int **p13;
int ** const              p14 = p13; // 編譯成功
int * const *             p15 = p13; // 編譯成功
const int **              p16 = p13; // 編譯失敗,為什麼?
int * const * const       p17 = p13; // 編譯成功
const int ** const        p18 = p13; // 編譯失敗,為什麼?
const int * const *       p19 = p13; // 編譯成功
const int * const * const p20 = p13; // 編譯成功

這裡面比較特別的應該是第 36 行跟 38 行為什麼會失敗?在這之前,我們要先想想其他的為什麼會成功:

第 34 行裡面,p14 跟 p13 之間的差異在於 p14 是一個本身為 const 的指標,而 p13 是一個本身不為 const 的指標。而不論 p14 本身是否是 const, 都可以在初始化的時候賦值,所以沒有問題。

第 35 行發生了一個型態轉換,轉換的是指標所指向的變數型態由原本 p13 指向的 int * 變成 p15 指向的 int * const。換句話說,原本指向的是一個非 const 的指標,現在被轉換成指向一個 const 的指標。這裡套用的規則就是指標篇提到的:『一個指向非 const 變數的指標可以轉型成一個指向 const 變數的指標』,只是這裡因為指向的變數也是個指標,所以同樣的規則可以描述成:『一個指向非 const 指標的指標可以轉型成一個指向 const 指標的指標』。很像繞口令吧?

第 36 行要做的事情乍看與第 35 行有些類似,指標指向的變數型態由原本 p13 指向的 int * 變成 p16 指向的 const int *。但是我們可以發現到這並不符合我們剛說的規則。因為指標指向的變數 (那個變數也是個指標) 都是非 const 的指標,跟規則所要描述的 const 性質轉換沒有關係。這裡的問題是這些被指向的指標分別又指向一個 int 和一個 const int。那我們可以跳過把 int * 變成 int * const 的步驟而直接將 int 變成 const int 嗎?很不幸地,允許這樣的轉換會造成我們可以透過這個指標實現一些不應該被允許的操作:

const int c = 5;
int *p21;
const int **p22 = &p21;  // (const int **) = (int **)
// 上面這裡應該是要編譯失敗的!但我們假設讓他成功看看會發生什麼事情!
*p22 = &c;               // (const int *)  = (const int *) 
// 上面這裡的語法沒有問題,但要注意到 *p22 就是 p21
// 這裡等同 p21 賦值為 &c 的效果 
// (注意到直接寫 p21 = &c; 會編譯失敗)
*p21 = 10;               // (int) = (const int)
// 上面這裡的語法沒有問題,但要注意到 *p21 就是 c
// 這裡等同把 c 賦值為 10 的效果
// (注意到直接寫 c = 10; 會編譯失敗)
// 結果我們讓具有 const 性質的 c 被重新賦值為 10 了! 

結論是如果允許這麼做的話,那會造成 const 的唯讀性質可能在不需要做任何強制轉型情況下被破壞!這對 const 語法的使用者來說是個大災難!

那反過來問,為什麼第 39 行又可以編譯成功呢?將 int ** 轉換成 const int * const * 是可以的,理由是因為如果同樣的 const 轉換規則連續套用在指標指向的變數上時,上面提到的問題就不會出現!我們先把規則套用在 int * 上,將 int ** 轉換成 int * const *,再連續套用在 int 上把 int * const * 轉換成 const int * const * 是可以的。

可以再想想下面的例子:

int ***p23;
int *** const         p24 = p23; // [編譯成功]
int ** const *        p25 = p23; // [編譯成功]
int * const **        p26 = p23; // [編譯失敗]
const int ***         p27 = p23; // [編譯失敗]
int ** const * const  p28 = p23; // [編譯成功]
int * const * const * p29 = p23; // [編譯成功]
const int * const **  p30 = p23; // [編譯失敗]

一個簡單的想法就是你必須要從右到左連續套用 const 轉換的規則去轉換指向的變數,而不能中斷!


[小結]一個指向非 const 變數的指標 (例如 int *) 可以隱性轉型成一個指向 const 變數的指標 (例如 const int *),且這個規則需要連續套用在指向的變數而不可中斷!


下一篇: const 的使用 (5) - 參考篇

沒有留言: