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++ 在撰寫時多了一些陷阱。

沒有留言: