作者LPH66 (f0VMRgEBA)
看板C_and_CPP
標題[心得] 轉型、指標、enum 跟 handle
時間Mon Aug 26 23:14:54 2013
看到上面我那篇回文下面討論的盛況(?)
再加上這篇上面的 enum 那篇文章
我想我要來講一些寫這麼多年程式以來我所認知到的東西了
不過因為東西還不少標題有點難下 只好這樣把要講的東西的關鍵字拿上來排 XD
---
說到底 一支程式編成二進位碼之後
不管程式裡到底寫了些什麼高階功能 最終終究是變成二進位的零與壹
到了這個層級一個值究竟會被拿去做什麼運算其實沒人會管
差別只在於這個運算跟它的結果對於要來讀取這個值的人/機器來說代表什麼意義而已
那麼一支程式裡 變數的"型別"其實就只是代表著
「這些零與壹要怎麼讀 以及它們要怎麼運算」的記號而已
因此有些操作對有些型別的變數來說沒有意義
例如拿一個指標去乘上別的東西 這種操作除非人為定義 不然它怎麼看都不會有意義
(這裡先不談一些程式語言理論裡的那種「型別」
那個是比較抽象一點的東西 不過不是這裡想聊的)
但是有的時候 我們為了要能讓一個某型別的值使用另一種型別的運算
必須要在各種型別之間進行轉換
(也許是只有另一個型別才能做某些事 也許是另一個型別提供了不一樣的限制等等)
這個想法就是產生高階語言裡稱做「轉型」這個操作的原始動機
但是說起使用另一種型別的運算
以機器語言/組合語言裡的角度來看
直接把這些位元的排列拿去做另一種運算也是一種「使用另一種運算」
但以高階語言的角度來看 這其實就成了位元意義上的轉變
由於值也可能一併改變了 所以通常為了跟上面那種意義的轉型分別
會有「強制轉型」、「硬轉」等等說法存在
也就是說 這兩種「轉型」在語意上是很不一樣的概念
C++ 裡提供了明確指示這兩種轉型的語法
前者稱做 static_cast 後者稱做 reinterpret_cast
(以下為行文方便我就用這兩個字指稱這兩種轉型)
不過由於 C 語言只有規定抽象運算的關係
因此 C 語言裡的轉型當標準裡有指明時 它就是屬於 static_cast
C99 的標準裡 6.3 節的標題是 "Conversions"
http://port70.net/~nsz/c/c99/n1256.html#6.3
裡面規定的就是這些轉換的詳細規則
像是當一個 short 要轉型成 long 時要造成什麼樣的效果
一個 int 要轉型成 unsigned int 時要造成什麼樣的效果
一個 int * 要轉型成 char * 時要造成什麼樣的效果 等等
有些狀況這一條裡沒有提到 例如前面推文提到的指標轉型成 double
那種就是屬於不能轉型的狀況
之所以這些狀況不規定它能轉型是因為常理上不可能會有這種轉型的需求的關係
不過其中有幾項雖然有提到能夠轉型
但是轉型出來的結果卻明言「隨實作而定」(implementation-defined)
這種狀況之下就給了 reinterpret_cast 鑽空子的機會
因為隨實作而定的關係 如果在二進位層級上什麼也不做當然是最簡單的方式之一
因此在許多常見的實作上就會將這些狀況給定為 reinterpret_cast 了
這其中包含了我上一篇回文所回的那種狀況: 一個整數跟一個指標互轉 [6.3.2.3p5-p6]
那篇文章的狀況其實是這樣的
嚴格照標準來說的話 int 雖然能跟 xxx* 互轉 但互轉後不能保證會恢復原狀
能保證的只有一個: void * 轉成 intptr_t 再轉回 void * 會還原
所以我的回文裡提到的 #153 那行指定最嚴格符合標準的寫法其實是
*GMGID = (int)(intptr_t)(void*)GMG_ptr;
那要轉回來就也必須要
MF2KGMG_operator* GMG_ptr=(MF2KGMG_operator*)(void*)(intptr_t)*GMGID;
不過因為在 pc 上面 這些轉型大多都是被實作成 reinterpret_cast 的關係
所以就出現了偷懶寫法: 直接轉成最終型態
就只是這樣而已
gcc 的警告就是在告訴你「這可能不會產生你所想要的結果」
---
我在我那篇回文的推文裡拿了
http://tinyurl.com/3d487sk 這篇問答出來救援(?)
只不過看起來碰上的這個打者還是很堅持他自己的打法...
我引這篇的目的是這一段話 (第一個問答的答案)
※ 引述《http://tinyurl.com/3d487sk》
: 並不是所有機器都使用「平面記憶體模型」(flat memory model),可以把記憶體當
: 成一個巨大、連續的表格看待。除了標準(參考 N1256 6.2.5p27)規定的幾種狀況外
: ,不同型態的指標可以長得完全不一樣。最後,函式指標(function pointer)是另
: 一世界的東西,可以跟物件指標完全沒關係。並不是所有機器都把物件和程式碼都擺
: 在同一大塊記憶體裡面。
: 如果你測試發現一樣,那只是你測試的實作用同一種方法表示所有指標,請參考你作
: 業系統或機器的文件確認你的發現。世界上的確有實作用不同格式表示不同型態的指
: 標,不可攜的程式在那些機器上可能會出現嚴重的錯誤。(參考 clc FAQ 5.17 看實
: 際例子)
我個人相信這一段話已經足夠解決那篇的推文裡的爭端了
這段話裡講了許多事情:
1. 記憶體位址並不一定是由連續的數字所表示
2. 不同型態的指標可以有不同的表示方式
3. 存在一種"指標"可以跟其他的指標完全無關
4. 除非你的機器對這些有所說明, 不然擅自假設以上這些是如何就可能會出事
在我看來這幾條觀念正是許多可能寫過很多 C 的人仍然不足的
下面幾條問答順便回答了「指標跟指標之間可以任意轉型」的錯誤觀點 這就表過不提
---
至於我上面那篇 enum 的轉型
C++ 裡是這麼規定的: 一個 unscoped enum 可以轉成整數 但不能反之
(unscoped enum 如 enum {a,b,c}; 或 enum vowels {a,e,i,o,u};)
一個 scoped enum 則不能跟整數互轉
(scoped enum 如 enum class numbers {one,two,three};)
而我不太懂的是為什麼你想要用一個 void * 傳來傳去
直接傳一個 int 進去不行嗎?
或者既然已經 typedef 出來了 那傳入型態設為 eGOPLAYER_STATE 就好啦
這樣是最不會有問題的方式 if 裡面連轉型都不用轉
---
最後我想提一下上篇我提到的 handle 到底是什麼
handle 這個詞在程式設計中的意思是一個代表某資源的值
它可以有多種描述法 常見的如 unix 系統上的 file descriptor 它是一個整數值
實際上它是 os 內部一個已開啟檔案的結構陣列裡的某個位置的索引
這陣列的前三個會先行依序填入標準輸入、標準輸出、標準錯誤輸出
所以才會有標準輸入是 0, 標準輸出是 1, 標準錯誤輸出是 2 的結果
又例如我們熟悉的 FILE * 這是 C runtime library 所提供的對於檔案的 handle
那由於 handle 最常表示一個內部結構
所以它最常見的型式是指標 其次則是整數
有時為了抽象化 不讓使用者端知道那其實是個指標
因此常常會另定一個型態做為 handle 型態 它可以放入任何指標
C 語言的標準明定所有物件指標可以跟 void * 互轉 [C99 6.3.2.2]
所以這種 handle 型態就最常 typedef 成 void *
例如 Win32 API 就是這麼定義的:
typedef void *PVOID;
typedef PVOID HANDLE;
(ref:
http://tinyurl.com/lmhgj27 )
當然也有直接定義成該種內部結構的指標的型式
例如 C++ 常見的 pimpl idiom 那個指標就是這種型式 兼具細節隱藏跟實作簡潔
這種型態的指標有一個特殊名詞叫 opaque pointer (不透明指標)
因此廣義來說 這些東西都可以視為 handle 的概念
我的上篇回文所回的程式即是使用這種概念來隱藏實作細節
只不過它選擇了用 int 表示一個其實是指標的 handle
這個選擇引入了指標跟整數互轉的不必要的麻煩而已
而 enum 那一篇之所以會想用 void * 我的猜測也只是想要試試看而已
只不過那裡的狀況跟 handle 是差了十萬八千里 硬用 void * 只是讓自己苦惱而已
---
想到要講的大概就是這些了
希望這篇可以對大家有一些觀念上的釐清
如果這篇有所誤謬也請不吝指正 :)
--
LPH [acronym]
= Let Program Heal us
-- New Uncyclopedian Dictionary, Minmei Publishing Co.
--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 114.41.10.216
→ Feis:這一篇文章值 1000 Ptt幣.. 好想要~ 08/26 23:18
→ purincess:了解C語言比flat memory model還早出現 對於釐清 記憶體 08/26 23:24
→ purincess:位址不一定是連續數字 這件事 很有幫助 XD 08/26 23:25
→ Feis:上上一篇用 void * 應該是 C 為了實作不定型態的方法 08/26 23:43
→ Feis:只是這裡實務上應該轉指標再傳會合理些 08/26 23:44
推 bibo9901: 08/26 23:44
→ LPH66:不定型態的 void * 並不適合拿來直接傳 primitive type 08/27 00:02
→ LPH66:而是如五樓所說取其指標傳遞 08/27 00:03
→ LPH66:但我也看不太出來那種狀況為何需要做這種事... 08/27 00:03
→ LPH66:所以我才會猜他可能只是看到這方法想試試看而已 08/27 00:04
→ Feis:因為 player_open 在實務上通常不是他自己寫的. 08/27 00:05
→ Feis:初學者常無法理解 call_back 機制在C 語言中丟 void *的涵義 08/27 00:05
→ Feis:算是函式指標的原罪? 08/27 00:07
→ LPH66:咦對耶, 我沒注意到那是 callback...那就是如你所說沒錯了 08/27 00:13
推 damody:啊哈哈~ 你講的太抽象了 要學OO比較看的懂~ 開玩笑的~ 08/27 00:21
→ a27417332:找到MSDN裡面有寫說,reinterpret cast保證了任何指標轉 08/27 10:27
→ a27417332:指標,還有整數轉指標跟指標轉整數。 08/27 10:28
→ a27417332:然後不保證移植性(XD) 08/27 10:28
→ a27417332:不過想再多問一下,在ASM或是機器語言的層級中,整數和 08/27 10:29
→ a27417332:浮點數的表達方法不一樣嗎? 08/27 10:30
→ a27417332:不好意思一直問問題 08/27 10:30
→ Feis:機器的可能性太大(沒有標準) 08/27 10:40
→ a27417332:不過剛剛拿隔壁樓的去試,成員函數指針沒辦法跟void*轉 08/27 10:48
→ a27417332:MSDN上似乎沒有提到這一點囧 08/27 10:49
推 Feis:X指向成員的指標是大魔王 08/27 11:00
推 shadow0326:說到成員含式指標,c++98有mem_fun這種東西 08/27 11:13
→ shadow0326:不知道c++11有沒有更直觀好用的tool? 08/27 11:13
→ Feis:a27417332: 一般資料成員應該也不行 08/27 11:25
→ Feis:應該把一般指標跟成員指標分開討論會好一些. 08/27 11:26
→ Feis:shadow0326: 大大想拿來做什麼? 08/27 11:29
→ LPH66:shadow0326: C++11 的好像叫 mem_fn 08/27 11:33
→ LPH66:en.cppreference.com/w/cpp/utility/functional/mem_fn 08/27 11:33
推 shadow0326:比如要餵給<algorithm>之類的時候會用到 08/27 11:58
→ azureblaze:std::function + std::bind? 08/27 12:02
推 remember11:確實是callback沒錯,player_open()確實不是我寫的 08/27 13:48
→ remember11:這樣c++沒辦法用這種寫法囉? 08/27 13:48
推 b98901056:推 受教了 08/27 14:13
→ a27417332:這一整串完全會體會到指標的恐怖XD現在好像有點了解但又 08/27 14:36
→ a27417332:不太能理解的樣子。 08/27 14:37
→ suhorng:有用過std::function + std::bind來弄mem_fn, 08/27 15:48
→ suhorng:可是從來沒弄懂為什麼.. 08/27 15:48
→ a27417332:那個牽涉到模板吧?中間沒有硬轉過,因為都是編譯器可解 08/27 17:08
→ AntaresStar:"指標之間不可任意轉型" "所有指標可跟void*互轉" 08/27 21:31
→ AntaresStar:這個就矛盾了 因為透過void*就可達成前者 08/27 21:32
→ a27417332:這裡指的應該是直接轉型? 08/27 21:58
→ Feis:沒有. 標準沒保證能轉. 只是說如果能轉. 要轉得回來 08/27 22:41