2013年12月10日 星期二

如何用 cout 印出字元陣列的位址 ?

Q: 給一個整數陣列 ia, 使用 cout 會印出其開頭的記憶體位址。但給一個字元陣列 ca, 使用 cout 卻是印出陣列內容。我們該如何印出其記憶體位址呢?

int ia[3] = {0, 1, 2};
cout << ia << endl;    // 這會印出 ia 陣列的開頭位址,也就是 &ia[0]
char ca[3] = "Hi";
cout << ca << endl;    // 這會印出 ca 陣列的內容,也就是 Hi

A: 將 ca 轉型成 void * 後再印出

cout << (void *) ca << endl;             // 使用 C 風格的轉型 (不建議)
cout << static_cast<void *>(ca) << endl; // 使用 C++ 風格的轉型

詳解

首先要先知道 cout 是一個 std::ostream 類別的物件,所以執行 cout << ia 時,實質上是利用運算子多載的特性呼叫了 cout.operator<<(ia) 或者 operator<<(cout, ia)。

接著我們要去找看看有那些函式是可以這樣被呼叫的,如果沒找到的話,那這裡就會是編譯錯誤。

但是也有可能會找到一個以上的函式都符合這樣的呼叫法,那就會自動選裡面最符合的那一個 (這稱為函式多載)。如果無法自動判斷哪一個最符合的話,則一樣是編譯錯誤。

所以因為 cout << ia 是可以被成功呼叫而沒有編譯錯誤的。這意味著必定有一個函式是可以被這樣呼叫而且是最符合的。

問題是, 是哪一個呢?

首先我們先看 cout 所屬的類別,也就是 std::ostream 裡面是否有符合的函式: 請點我看連結 C++ Reference ostream::operator<<

在這些不同版本的 operator<< 函式裡面,有任何一個版本的參數是個陣列嗎?答案是:一個都沒有!

但是剛剛不是提到了因為沒有編譯錯誤所以必定有一個版本是最符合的嗎?但是現在卻一個都沒有?

原因是我們可以對引數做隱性轉型,而這裡使用到一個陣列跟指標之間的關係:『陣列可以隱性轉型成指向該陣列第一個元素的指標』。

換句話說,這裡呼叫的是 ostream& operator<<(void *) 這個版本的函式,因為可以把 int[3] 隱性轉型成 int *,而 int * 又可以隱性轉型成 void *。

而這個版本的功能就是會印出該指標的值,也就是該指標所儲存的記憶體位址。因此 cout << ia 會印出該整數陣列的第一個元素的位址。

那為什麼 cout << ca 不會將 char[3] 轉型成 char * 後變成 void * 用 ostream& operator<<(void *) 印出位址呢?

原因是在於還有一個更適合的在這裡:請點我看連結 C++ Reference: operator<<(ostream)

問題就發生在當我們將 char[3] 轉型成 char * 的時候,編譯器發現 ostream& operator<< (ostream& os, const char* s); 這個版本就符合呼叫的需要。

因為將 char[3] 轉成 char * 只需要經過一次隱性轉型,但是 char[3] 轉成 void * 需要經過兩次隱性轉型。編譯器發現這個版本比之前的版本更符合呼叫的引數型態,所以就使用了這個版本。但是這個版本的功能是將字元指標指向的陣列當作字串印出來,所以就造成一開始問題裡面提到的字元陣列與整數陣列不同結果的原因。

我們也可以想想下面的程式碼會做甚麼?

char d = 'A';
char *p = &d;
cout << p << endl; // 會印出甚麼呢?

雖然這裡的 p 並不是指向一個陣列的開頭元素,但是編譯器依然呼叫 ostream& operator<< (ostream& os, const char* s); 這個用來印字串的版本,造成了因為會讀取到 d 後面的字元而產生印出雜亂的字串內容或記憶體存取違規等『未定義行為』。

至於解決的方法同樣是將 p 轉型成 void * 來強制其呼叫 ostream& operator<<(void *) 這個版本的函式來印出記憶體位址

cout << (void *)p << endl;             // 會印出甚麼呢?
cout << static_cast<void*>(p) << endl; // 會印出甚麼呢?

而這一系列問題的根本在 C 裡面陣列與指標之間可隱性轉型的關係,還有 C 用字元指標來傳遞字串。這些原罪讓 C++ 在撰寫時多了一些陷阱。

2013年11月15日 星期五

const 的使用 (5) - 參考篇

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

參考 (reference) 是 C++ 相對於 C 的新功能。在介紹參考使用 const 時的問題前,先稍微複習一下參考的定義語法:

int a = 3;
int &b = a;  // 定義 b 為 a 的參考,也就是說『 b 就是 a 』
b = 5;       // 將 b 賦值為 5,也就是將 a 賦值為 5

定義參考跟定義指標一個很大的不同就在於定義參考時並不會進行任何的變數建構,但是定義指標時會建構一個變數來儲存指標。

我們可以在參考初始化的時候不指定參考的對象嗎?我們可以初始化後修改參考所參考的對象嗎?這兩個問題的答案都是否定的。所以參考與本身為 const 的指標在性質上有些類似:初始化時要指定參考對象且初始化之後不能改變要參考的對象。

同樣地,在處理指向變數的 const 性質轉換時,具有相似的規則:『一個非 const 變數可以被一個 const 變數的參考所參考』

const int c = a;

再來想想下面這段程式碼是不是合法的?

int **d;
int **&e = d;        // [編譯成功]
int ** const &f = d; // [編譯成功]
int * const *&g = d; // [編譯失敗] 為什麼?
int * const * h = d; // [編譯成功]
int * const * const &i = d; // [編譯成功] 為什麼?
const int ** &j = d; // [編譯失敗]
const int **  k = d; // [編譯失敗]

那參考跟指標在使用 const 時有什麼不同呢?不同點分為兩個方向:

  • 無法把參考本身設為 const 或非 const: 這可以想成參考預設就是無法改變參考對象,所以沒有 const 與否的問題。
  • 參考所參考的變數不能也是個參考型態: 換句話說不會有上一章類似巢狀指標轉型的巢狀參考問題。

int & const l = a;  // [編譯失敗]
int &&m = b;        // [編譯失敗]
int &n = b;         // [編譯成功] 等同於 int &n = a;

const 變數的參考

const 變數的參考 (reference-to-const) 具備一個特殊的用途:可用來參考右值。

我們先來看看下面的程式碼:

int &o = 3;     // [編譯失敗] 3 是個字面常數 (右值)
int &p = a + 2; // [編譯失敗] a+2 的結果是個暫時變數 (右值)

上述的兩行程式碼都是不合法的。

不合法的原因可以從比較簡單的角度來看:3 與 a+2 可以放在等號左邊嗎?

能放在等號左邊的表示式我們稱之為左值,反之則稱之為右值。而依照規定我們無法用非 const 變數的參考來參考右值。

為什麼呢?如果可以用非 const 變數的參考來參考右值的話,那就會發生下面的詭異情況:

o = 5;  // o 只是個參考,那是誰變成 5 了?
p = 10; // p 只是個參考,那是誰變成 10 了?

為了避免上面的情況,所以我們無法讓非 const 變數的參考來參考右值。

但是如果今天是一個 const 變數的參考時,情況就不同了:

const int &q = 3;     // [編譯成功]
const int &r = a + 2; // [編譯成功]

因為這樣就不會發生我們上面提到的詭異情況:

q = 5;  // [編譯失敗]
r = 10; // [編譯失敗]

聽起來事情好像很完美的解決:如果我們要參考一個右值的話,就用 const 變數的參考就好了。

不過事情沒有想的那麼好,因為你會發現雖然 3 跟 a+2 的生命週期都只活在該表示式內,但是 q 跟 r 是在 3 跟 a+2 生命週期結束之後作為一個 const 變數使用。照參考的意義來說:『q 就是 3』與『r 就是 a+2』這樣的說法就行不通。例如當我們要去讀取 r 的時候,r 所參考的 a+2 可能已經結束生命週期而釋放了。去讀取一個已經釋放的變數內容是個『未定義行為』。所以為了不造成未定義行為,C++ 額外規定讓 const 變數的參考可視需要將該 const 變數的生命週期延長直到該參考離開可視範圍!

舉例來說,當我們對 q 與 r 取址的時候,你會發現這個動作是合法的。但是我們卻不能對 3 與 (a+2) 取址:

&q;     // [編譯成功]
&r;     // [編譯成功]
&3;     // [編譯失敗]
&(a+2); // [編譯失敗]

這表示 q 並不就是 3, 而 r 並不就是 a+2。這些微的語意差異使得 const 變數的參考可視需要經過特殊處理去延長所參考變數的生命週期並因此具有特殊性。


讓 const 變數的參考可以參考右值的好處?

上一個章節我們提到 const 變數的參考可以參考右值。但是有一個很根本的問題是:我們為什麼要讓他可以這麼做?就讓所有的右值都不能被參考不就好了?

考慮下面這兩個函式:

int sum1(int lhs, int rhs) {
  return lhs+rhs;
}
int sum2(const int &lhs, const int &rhs) {
  return lhs+rhs;
}

這兩個函式的差別一個是傳值呼叫,而另一個是傳參考呼叫。

傳參考呼叫的好處是不用複製一份引數到函式內,有機會可以省下複製所需要的空間與時間成本。(雖然這個例子因為參考的是 int 型態所以不見得比較省,但是當我們的參數型態是大型類別或結構時則有機會比較省。)

如果我們不允許 const 變數的參考可以參考右值,那下面這些呼叫都是不合法的:

int s = 3;
sum2(s, 3);          
sum2(3, 4);
sum2(s+1, 5);
sum2(sum2(s, s), s); // 裡面的 sum2 是可以的,但是外面的不行

因此允許 const 變數的參考可以參考右值有其使用上的好處。


下一篇: const 的使用 (6) - 類別與結構篇 (撰寫中)

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) - 參考篇

const 的使用 (3) - 基礎測驗篇

上一篇: const 的使用 (2) - 指標篇
試著想看看下面的程式碼是否合法?
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; // 編譯成功或失敗?

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

2013年11月9日 星期六

const 的使用 (2) - 指標篇

上一篇: const 的使用 (1) - 基礎篇

指標在概念上牽涉到兩個東西,一個是儲存位址的指標本身,另一個是指標所指向的變數。為了分辨他們,這邊稍微複習一下指標的語法:

int a, b;     // a 和 b 分別都是一個 int
int *p1;      // p1 是一個指標,指向一個 int
int *p2 = &a; // p2 是一個指標,指向一個 int,而該 int 就是 a
p2 = &b;      // [賦值運算] p2 指向的 int 從原本的 a 變成了 b
*p2 = 3;      // [賦值運算] *p2 就是 b 被賦值為 3

上面的程式碼有兩個賦值運算,第 4 行是將指標本身賦值為 b 的位址而第 5 行是將指標指向的 int 變數 b 賦值為 3。換句話說,在談論指標的 const 性質時,可分為兩個部分:

  • 指標本身是否為 const?
  • 指標指向的變數是否為 const?

當指標本身是 const 時,指標本身不能再被賦值,指標指向哪個對象是不能改變的,而當指向的變數是 const 時,則所指向的變數不能再被賦值。


指標本身是否為 const ?

如果要指定指標本身的 const 性質,在宣告指標變數時,要在 * 『後面』加上 const 修飾字:

int * const p3 = &a;   
// [編譯成功] p3 是一個 const 的指標,指向一個 int,而該 int 就是 a
之前基礎篇我們對 const 變數的限制也套用在這個指標上,也就是說『初始化時要給予值且初始化後不能重新賦值』:
int * const p4;
// [編譯失敗] 因為 p4 是個 const 的變數,初始化時一定要給予值
p3 = &b; 
// [編譯失敗] 因為 p3 本身是 const 的,不能做賦值運算

指標指向的變數是否為 const?

當指標指向的變數是 const 時,指標本身不一定是 const 的:

const int c = 0, d = 1;
const int *p5;
// [編譯成功] p5 本身不是 const 的,不需要在初始化時給予值
// 請與第 8 行做比較
p5 = &c;  
// [編譯成功] p5 本身不是 const 的,所以可以被賦值
// 請與第 10 行做比較

當指標指向的變數是 const 時,從指標取出所指的變數具有唯讀性:

*p5 = 5;  
// [編譯失敗] p5 指向一個 const 的 int,所以 *p5 是 const 的

當指標指向的變數是 const 時,指標本身也可以是 const 的,但是一樣要在 * 『後面』加上 const 修飾:

const int * const p6 = &c;
// [編譯成功] p6 是個 const 的指標,指向一個 const 的 int,
// 而該 int 就是 c
const int * const p7;
// [編譯失敗] 因為 p7 是 const 的指標,初始化時要給予值
// 請與第 13 行做比較
p6 = &d; 
// [編譯失敗] p6 本身是 const 的,不能做賦值
// 請與第 16 行做比較

對於 const 在指標型態上的使用是要先了解每個 const 修飾的是指標本身還是指標指向的變數。


轉型的難題

指標使用 const 裡面最困惑的時候通常是關於轉型,考慮下面這段程式碼:

int e = 10;
const int *p8 = &e; // (const int *) = (int *)
// [編譯成功] p8 是一個指標,指向一個 const 的 int ,而該 int 就是 e
const int f = 10;
int *p9 = &f;       // (int *) = (const int *)
// [編譯失敗] p9 是一個指標,指向一個 int,但是該 int 不能是 f

這裡的問題在於為什麼 p8 的定義可以而 p9 的定義不行?

我們先想想為什麼 p8 的定義可以? p8 指向的變數需要是一個 const 的 int,但這裡讓他去指向一個非 const 的 int (也就是 e)。理論上因為型態不同,這個操作應該要失敗,但是因為這個 const 表示的是 p8 所指向的變數是唯讀的,就算指向的是一個可以重新賦值的 e ,也並不會讓 p8 無法提供需要的操作,所以 C/C++ 允許這樣的隱性指標轉型。

相對地,為什麼 p9 的定義不可以? p9 指向的變數需要是一個一般的 int,但這裡試著讓他去指向一個 const 的 int (也就是 f)。我們可以像 p8 一樣做隱性指標轉型嗎? 答案是不行。因為 p9 的型態表示這個指標所指向的變數是可以被重新賦值,但是 f 卻是一個唯讀的 const int,是不能重新賦值的,這會讓 p9 無法提供需要的操作。例如可以用 *p9 = 0 來間接修改唯讀的 f,並不合理,所以 C/C++ 不允許這樣的轉型。


[小結]一個指向非 const 變數的指標 (例如 int *) 可以隱性轉型成一個指向 const 變數的指標 (例如 const int *),但是反過來不行!


下一篇: const 的使用 (3) - 基礎測驗篇

const 的使用 (1) - 基礎篇

以下我們先不考慮類別或結構,而是用在一般函式內的情境


const 所修飾的變數會具有『唯讀性』,但這個唯讀性是在『初始化』之後才保證,也就是在『初始化』時是可以指定值的:

const int a = 3;  // [編譯成功] 初始化
a = 5;            // [編譯失敗] 賦值運算
const 型態的變數無法被放於賦值運算子 (=) 左方。

因為唯一可以設定 const 變數值的時間點是在『初始化』時,所以 const 變數初始化時一定要指定值:

const int b = 3; // [編譯成功]
const int c;     // [編譯失敗] 沒有在初始化時給與 b 變數值

綜合上面所說,以下程式碼變數定義的部份都是合法的,因為放在等號右方的變數無論是否是 const 都只會被讀取 (唯讀性),而放在左方的變數無論是否是 const 都可以在初始化時被指定值:

int d = 3;                
int e = d;            // [編譯成功] (int) = (int)
const int f = d;      // [編譯成功] (const int) = (int)
int g = f;            // [編譯成功] (int) = (const int)
const int h = f;      // [編譯成功] (const int) = (const int)

但是在使用賦值運算子 (=) 時,左方不能放 const 變數:

e = a; // [編譯成功] 
f = a; // [編譯失敗] 因為 f 是 const 
g = a; // [編譯成功] 
h = a; // [編譯失敗] 因為 h 是 const

[小結] const 變數在初始化後就不能再改變內容,但是在初始化時可以且必需指定內容!


下一篇:const 的使用 (2) - 指標篇