C 語言常見誤解/指標/表示法與轉型
指標(一)表示法和轉型
编辑問:指標不就是記憶體的位置嗎?不同型態的指標只有在位移(指標運算)時不一樣,實際上連函式指標都只是記憶體中的位置不是嗎?
答:並不是所有機器都使用「平面記憶體模型」(flat memory model),可以把記憶體當成一個巨大、連續的表格看待。除了標準(參考 N1256 6.2.5p27)規定的幾種狀況外,不同型態的指標可以長得完全不一樣。最後,函式指標(function pointer)是另一世界的東西,可以跟物件指標完全沒關係。並不是所有機器都把物件和程式碼都擺在同一大塊記憶體裡面。
如果你測試發現一樣,那只是你測試的實作用同一種方法表示所有指標,請參考你作業系統或機器的文件確認你的發現。世界上的確有實作用不同格式表示不同型態的指標,不可攜的程式在那些機器上可能會出現嚴重的錯誤。(參考 clc FAQ 5.17 看實際例子)
問:所有指標都能轉型成 void*
或是 char*
不是嗎?
答:標準不保證函式指標也可以轉型成 void* 或 char*.(參考 clc FAQ 4.13 和 N1256 6.3.2.3p1)
問:指標可以轉型成 int
或是 long
不是嗎?
答:就算不考慮函式指標,還是不保證成功,而且轉過去的數字可能也無法直接讀。實際的例子像是 AMD64 指令集下 Linux/Mac OS/Solaris/FreeBSD 等等(採用 LP64 資料模型)裡面的 int
或是 Microsoft Windows 的 X64 版本(採用 LLP64 資料模型)裡面的 int
和 long
都存不下 64 位元的指標。可攜程式碼不需要因為指標大小變了改寫。 (參考標準 N1256 6.3.2.3p6)
問:把指標(直接)轉型成 intptr_t
或 uintptr_t
總可以了吧?
答:同上。就算不考慮函式指標,還是不保證成功。
問:那到底要怎麼寫才能轉型成整數存起來?
答:先轉型成 void*
(注意函式指標可能不能轉)然後再轉型成 intptr_t
或 uintptr_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
再印出來。