C 語言常見誤解/指標/表示法與轉型

指標(一)表示法和轉型

編輯

問:指標不就是記憶體的位置嗎?不同型態的指標只有在位移(指標運算)時不一樣,實際上連函式指標都只是記憶體中的位置不是嗎?

答:並不是所有機器都使用「平面記憶體模型」(flat memory model),可以把記憶體當成一個巨大、連續的表格看待。除了標準(參考 N1256 6.2.5p27)規定的幾種狀況外,不同型態的指標可以長得完全不一樣。最後,函式指標(function pointer)是另一世界的東西,可以跟物件指標完全沒關係。並不是所有機器都把物件和程式碼都擺在同一大塊記憶體裡面。

如果你測試發現一樣,那只是你測試的實作用同一種方法表示所有指標,請參考你作業系統或機器的文件確認你的發現。世界上的確有實作用不同格式表示不同型態的指標,不可攜的程式在那些機器上可能會出現嚴重的錯誤。(參考 clc FAQ 5.17 看實際例子)


問:所有指標都能轉型成 void* 或是 char* 不是嗎?

答:標準不保證函式指標也可以轉型成 void* 或 char*.(參考 clc FAQ 4.13N1256 6.3.2.3p1)


問:指標可以轉型成 int 或是 long 不是嗎?

答:就算不考慮函式指標,還是不保證成功,而且轉過去的數字可能也無法直接讀。實際的例子像是 AMD64 指令集下 Linux/Mac OS/Solaris/FreeBSD 等等(採用 LP64 資料模型)裡面的 int 或是 Microsoft Windows 的 X64 版本(採用 LLP64 資料模型)裡面的 intlong 都存不下 64 位元的指標。可攜程式碼不需要因為指標大小變了改寫。 (參考標準 N1256 6.3.2.3p6)


問:把指標(直接)轉型成 intptr_tuintptr_t 總可以了吧?

答:同上。就算不考慮函式指標,還是不保證成功。


問:那到底要怎麼寫才能轉型成整數存起來?

答:先轉型成 void*(注意函式指標可能不能轉)然後再轉型成 intptr_tuintptr_t.

intptr_t i = (intptr_t)(void*)p; // 假設 p 的型態是 int*

要用時先轉回 void* 再轉回原來指標。

int* pi = (int*)(void*)i;

這是萬無一失,絕對可攜的作法。唯一的問題是實作可能沒有提供 intptr_t. 在某些機器上指標也不用數字表示。(參考 N1256 7.18.1.4p1 和 clc FAQ 5.17 看實際例子)


問:如果兩個指標比較(==運算子)相等,(成功)轉型成整數型態後比較也會相等嗎?

答:沒有保證,因此用此種方法來做 hash 之類的不可攜。可轉型回原本的指標型態再比較是否相等。(參考 comp.std.c 的討論


問:假如指標成功轉型成另一個型態的指標,應該就可以用轉型後的指標去存取物件吧?

答:否,轉型成功不代表可以用來存取。標準對於什麼型態的指標能存取什麼型態的物件有嚴格規定(type-based aliasing rules)。例如 int 物件不能被指向 short 的指標存取,即使指標轉型成功也不行。除了標準規定的例外外,編譯器可以假設不同型態的存取指向不同物件,做超乎想像的最佳化,把運算順序隨意調動;如下列程式碼:

int i = 0;
short *s = (short*) &i;
i = 0;
*s = 1;

在實際的機器上,最後 i 的值可能還是 0. 如果想違反標準,請了解如何關掉相關的最佳化。(參考 N1256 6.5p7, Linus Torvalds 的抱怨gcc 應該可用 -fno-strict-aliasing 關掉N1520: Fixing the rules for type-based aliasing


問:假設 p 是一個指標,那麼 printf("%p", p) 就該印出 0x7fff12345678 之類的東西不是嗎? 答:printf 系列函式的 %p 只能印指向 void 的指標,所以必須先轉型成 void*(注意函式指標可能不能轉)。只要 p 不是函式指標就可以這樣寫:

printf("%p", (void*)p);

注意即使轉型成功,指標要怎麼印是實作決定,不一定要印成十六進位,也不一定要加上 0x。(參考 N1256 7.19.6.1p8)


問:我作業系統的文件說 %p 其實可以當作 %#x 或是 %#lx. 這是不是代表其實可以用 %#x%#lx 印出?

答:查作業系統文件值得鼓勵,但如同問題所描述,這頂多只有在該作業系統上才對會。印指標還是要用 %p 或是用上述方法轉成 intptr_t 再印出來。