顯示廣告
隱藏 ✕
看板 TL
作者 TL (踢欸樓)
標題 [筆記] [分享] 混沌四神:陣列、指標、宣告、定義
時間 2013年01月27日 Sun. PM 06:14:27


看板 C_and_CPP
作者 purpose (purpose)
標題 [分享] 混沌四神:陣列、指標、宣告、定義
時間 Wed Sep 14 05:32:06 2011



陣列與指標,宣告與定義,就像桂綸鎂與柯佳嬿一樣,總是這麼容易讓人搞混。

本文列出一些與九九乘法表一樣,值得記下的觀念,總共 12 條而已。

                                 背起來
(1)                                    
宣告:介紹名稱給 Compiler,使編譯器知  
      道這個 name 及其 type。          

                                 背起來
(2)                                    
定義:決定 name 的內容,如函數定義。  

                                 背起來
(3)                                    
宣告可以有多次,定義只能有一次。      

                                 背起來
(4)                                    
在 C/C++ 中,宣告就是定義。            
--                                    
例外狀況:                            
  函數原型宣告、用 extern 宣告的外部  
  變數、類別宣告內的 static 資料成員、
  class forward declaration...        

  ※ 詳見:http://msdn.microsoft.com/en-us/library/0kw7hsf3.aspx

                                 背起來
(5)                                    
初始化:變數定義時,給予預設值。      

  ※ int i = 4; 表示宣告/定義型態為 int 的變數 i,並初始化為 4。

     int j; 宣告/定義變數 j,但沒有進行初始化。

            在 VC 環境下,變數 j 的值將被編譯器定義為 0xCC。
            若在後面使用 if (j == 2) 之類的運算,在 Debug 組態下,執行後跳出
            錯誤:Run-Time Check Failure - The variable 'j' is being used
                  without being initialized.

     j = 5; 不代表初始化,這是 assignment (賦值)。

            比如 class CMyWnd wnd1;
            ↑已呼叫預設建構子,已完成物件初始化。

            比如 CMyWnd::CMyWnd() : xCoord(100) { xCoord = 300; }
            ↑其中 xCoord 被初始化為 100,然後才被賦值為 300。
              而 yCoord 未被初始化。

     TIP:宣告變數的時候,總是進行初始化。

                                  背起來
(6)                                    
在 C99 及 C++ 標準下,允許變數宣告不必  
放在 statements 之前。但此時禁止 goto  
跳過任何變數宣告。                      

  ※ 一旦 goto 跳過變數宣告,就會連帶使得對該變數的引用,都未經過初始化。

     在 VC 2008 SP1 下,這樣寫可以通過編譯,
     但會警告:「warning C4700: 使用了未初始化的區域變數」。

     TIP:非要用 goto 的情況下,必須自己避開變數宣告,對編譯器別指望太多。

     詳見:http://www.jeffhung.net/blog/articles/jeffhung/1245/

                                  背起來
(7)                                    
若 P 為陣列,則 P 與 &P 有相同數值。    
但兩者 Data Type 不同。                

                                  背起來
(8)                                    
若 xType 為指標,則 xType + 1 之值,    
是由 xType 指向目標的 Data Type 決定。  

  ※ 設 xType = 0x1000;   (在 x86 32-Bit 平台下,其他平台請洽處理器製造商)

     若 xType 為 char *           則 xType + 1 = 0x1001;
     若 xType 為 double *         則 xType + 1 = 0x1008;
     若 xType 為 char (*)[10]     則 xType + 1 = 10 * sizeof(char) = 0x100A;
     若 xType 為 char (*)[2][10]  則 xType + 1 = 0x1014;

                                  背起來
(9)                                    
xType[2] 的運算,等於 *(xType + 2)。    
  第一階段:計算 xType + 2 的位址      
  第二階段:對上述位址做 dereference(*)

                                  背起來
(10)                                    
argument = 實際參數 (Actual Parameter)  
parameter = 形式參數 (Formal Parameter)

                                  背起來
(11)                                    
傳陣列當 argument 時,函數的「形式參數」
之宣告,有以下兩種形式:                
  形式1:最左維度改成 [],其餘不變      
  形式2:pointer to arr[0]              

                                  背起來
(12)                                    
陣列識別項在 expression 中,會自動轉型成
指標:pointer to arr[0]                
--                                      
例外狀況:                              
  sizeof、address-of(&)、參考的初始化  

  ※ 詳見:http://msdn.microsoft.com/en-us/library/266kd92t.aspx

_______________________________________

關於 (11) 中的兩種形式,補充說明於本段。

假設要傳進函數 foo 的 argument 為 int arr[3][4][5],
則函數 foo 的可用宣告如下。

  foo(int arr2[][4][5]);   就是所謂的形式1。                        
  foo(int (*arr2)[4][5]);  就是所謂的形式2,亦即 pointer to arr[0]。

因為 arr 是三維陣列,所以 arr[0] 也就是 *(arr+0) 是一個二維陣列,且其
型態為 char [4][5]。故指向 arr[0] 這個二維陣列的指標,是為 char (*)[4][5]。

根據 (12) 所述,arr 這個陣列識別項,在運算式中會自動轉型成 pointer to arr[0]。
這英文不太好記,我的記法是:

  看到陣列名稱 arr,就無條件當成指標。
  因為 * 運算子的優先權不是最高,所以立刻加上括號,變成 (*arr)
  最後再決定「指向的型態」一定會是少掉最左維度後的樣子,即 int [4][5]
  合成後就是 int (*arr)[4][5] 型態了。

以 main 函數為例,其形式參數的宣告,通常是 char **argv 或者是 char *argv[]。
根據以上觀念,可以知道 argv 的實際參數會是一維陣列,每個元素為 char * 型態。

如果實際參數是二維陣列,比如 char [10][20],則 argv 會宣告成
char argv[][20] 或 char (*argv)[20] 才對。

_______________________________________

真的可以把「陣列名稱」當成「指標」嗎?                                        
                                        ___________________


陣列名稱在運算式內,本來就會自動轉型成,上述的那種指標。
在 (12) 中也提過,有三種例外:sizeof、address-of、參考初始化。

要注意的只有 sizeof(arr) 跟 &arr。

若陣列 arr 真的完全等同於指標,那 sizeof(arr) 應該等於 4,
且 &arr 的值不會等於 arr 的值。

那我們寫的時候,以及編譯器在處理的時候,都一定知道 arr 在目前函數內的宣告型態
為 int arr[3][4][5] 陣列,所以不至於傻到在這兩種運算下,還把他當成指標看。

當我們把 arr 傳進函數 foo,使用 int (*arr2)[4][5] 去當形式參數接收時,
本來就知道此時的 arr2,其型態是一個指標,只是收藏了當初那個 arr 的位址而已。
所以在 foo 內做 sizeof(arr2) 與 &arr2 自然有不同的結果。

所謂無條件將陣列名稱當成指標來用,應該是說 arr[1][2] 或 arr2[1][2]、
arr + 3、arr2 + 3 這些運算的結果一定會相同,不要無限上綱到 sizeof、
address-of 去就不會出事了。

_______________________________________

[學越多想越多] 組合語言觀點下,陣列名稱跟指標明明就不同,為什麼 C 可以亂用?  
                                        ___________________

  編譯器默默承受...


C/C++ 的撰寫者,可以無腦的把 arr 跟 arr2 當成一樣的東西,
即便一個是陣列,一個是指標也沒差。聰明的 Compiler 會依據資料型態而做調整。

以 arr + 1 與 arr2 + 1 為例:

  因為 arr 是陣列,是本地的區域變數,所以它的起始位址,會固定是 ebp - 0xF8 這
  類地址,也就是與 ebp 暫存器相隔固定 N 位元組的地方,此例 N = 248。

  而 arr2 是 foo 內唯一的形式參數,他的存放位址會固定在 ebp + 0x8。

  在計算 arr2 + 1 時,要先到 ebp + 0x8 取出裡面存放的位址,其實就是 &arr。
    004113E0    8B45 08         mov eax, dword ptr ss:[ebp+8]
  其指令如上所述,其中 004113E0 是 EIP 位址,而 8B45 08 是機械碼,
  最後當然就是反組譯後的指令。上面這行指令會到 arr2 裡面取出 &arr 的值,然後
  因為指標型態是 int (*arr2)[4][5],所以接著還要加上 20 * 4 (add eax, 80),
  最終在 eax 暫存器內,才能得到 arr2 + 1 的值。

  在計算 arr + 1 時,直接用 lea 指令,把 ebp 暫存器目前的值減掉 0xF8,就可以
  得到 &arr 的值。但因為 arr + 1 不過隔了 80,所以直接改成用 lea 指令,計算
  ebp - 0xF8 + 0x50,也就是用 lea 指令算 ebp -0xA8 的結果即得到 arr + 1。

  ※ 不管是 arr + 1 或 arr2 + 1 得到的值都一樣,但底層的計算方式卻不同,
     一切都由 Compiler 去操心就好。

計算 arr[1] 與 arr2[1] 的背後:

  就 C/C++ 觀點來看,兩者得到的東西都一樣,沒有什麼好多想的,可以關電視了。

  但私底下,Compiler 到底瞞著我們偷偷做了什麼事呢?

  根據 (9) 可以知道:arr[1] 等於 *(arr + 1),及 arr2[1] 等於 *(arr2 + 1)。
  實際反組譯可以發現,其實 arr[1] 跟 arr + 1 的 CPU 指令根本一樣。
  而 arr2[1] 與 arr2 + 1 也是用一樣的 CPU 指令在應付。

  那是因為當 Compiler 發現:
    Dereference (*) 後的目標,其資料型態為陣列時,一律省略取值動作。

  換句話說,因為 arr[1] 是一個陣列,所以 * 運算不用做了。

  假設有 short blahA[10];
    則 blahA[3] 的計算,會先求出 blahA + 3 這個位址,然後真的到
    該位址去取 2 Bytes 來: mov ecx, word ptr[ blahA + 3 的位址 ]

  假設有 char *blahB[20];
   則 blahB[3] 也是同理,在最後會執行取值運算:
    mov eax, dword ptr[ blahB + 3 的位址 ],此時 eax 的為 pointer to char。

  ※ 在以上組合語言中,word 指 2 Bytes,dword 指 4 Bytes,所以到某個記憶體
     位置去讀取兩位元組資料,範例指令為:mov ecx, word ptr[記憶體位址]

--
--
※ 發信站: 批踢踢實業坊(ptt.cc)
◆ From: 124.8.138.96
tropical72: 有神快拜啊 !!1F 09/14 05:57
diabloevagto:太陽都還沒出現神就先出現了!2F 09/14 08:17
ericinttu:   好文M3F 09/14 09:26
VictorTom:推:)4F 09/14 09:59
erotic:好聞,我覺得桂綸美、張鈞甯這兩位女藝人的氣質也很相似5F 09/14 11:42
NIKE74731:桂跟柯真的超像的6F 09/14 12:14
momokokuo:推推推7F 09/14 12:45
angleevil:有太陽神,快拜8F 09/14 12:52
tropical72:(小聲,其實太陽神都是女的 : 赫利烏斯,羲和)9F 09/14 13:57
shadow0326:Helius是男的吧 XD10F 09/14 14:04
tropical72:!! 是我記錯了,抱歉 XD11F 09/14 14:09
angleevil:月神通常是女的,日神通常是男的.~"~沒記錯埃及的太陽神12F 09/14 14:27
angleevil:應該是男的
angleevil:tropical72對神話也守備範圍那麼大喔
cutecpu:tropical72 本身就是神話?15F 09/14 14:50
iWRZ:日照御大神一般認為是女的16F 09/14 16:17
iWRZ:天照 打錯
tropical72:其實.. 只看過人月神話而已  XD18F 09/14 16:24
xatier:有神快拜!19F 09/14 20:09
GZ79:來拜一下大神好了...20F 09/14 21:53
realtemper:猛~~~21F 09/14 22:57
deangogi:好文 m一下啦22F 09/14 23:12
firejox:(worship)23F 09/14 23:14
firejox:太陽神可以是女的呀 精靈寶鑽裡就是呀~
diabloevagto:f大有在用plurk喔~25F 09/14 23:17
firejox:(驚!!) 被發現了...26F 09/14 23:20
diabloevagto:(evilsmirk)27F 09/15 08:43
angleevil:越說越遠.而且我針對是神話.我是記埃及的主神-太陽神28F 09/15 08:53
leonjye:有神快拜啊  (全部背起來XD)29F 09/15 09:20
fon909:推了解底層,但對本文主題實用意義似乎不大30F 09/15 10:42
fon909:對初學者而言會頭昏眼花吧,舉C/C++程式碼實戰例子似乎更好
fon909:(以上提出一點不同的意見)

   感謝您無私提出意見。

   之所以寫成這樣,是為了讓文章好寫。
   一開始設定的讀者對象,只考慮學程式有段時間的版友。
   大家都是寫過點程式的人,這種筆記、條列型格式,你們看得快,我也省時間。


   稍微點一下,其餘的讓大家發文互相討論交流,應該就能解決。
   如果是我會回答的問題,我會幫忙回答。
※ 編輯: purpose         來自: 124.8.137.91         (09/15 15:33)
tropical72:< 神也是人,只是做到一般人做不到的事,所以才叫神 >33F 09/15 16:18
tropical72:purpose 大 人好到升級成大神了
sawang:感謝無私分享 :)35F 09/15 16:33
ericinttu:有些東西不知道就不會了解它裡面隱含的風險.36F 09/15 20:22
ericinttu:這樣列出來, 也好讓後人有一些線索來抓病根.
ericinttu:有線索有病根,要怎麼治,再去找相關文章即可.
※ 編輯: purpose         來自: 124.8.148.106        (09/22 00:20)
coal511464:感謝分享 先存起來慢慢吸收!39F 10/07 23:32
jenny2921:我強烈的不懂第三條!!"宣告可以有多次,定義只能有一次"40F 01/23 18:15
jenny2921:是否打相反了?
purpose:沒有打相反,比如你可以對函式 foo 作多次原形宣告,但其42F 03/01 23:19
purpose:內容只能定義一次,當然是指同 signature 才算同個函式

--
Shaken, Not Stirred.
--
※ 作者: TL 時間: 2013-01-27 18:14:27
※ 看板: TL 文章推薦值: 0 目前人氣: 0 累積人氣: 53 
分享網址: 複製 已複製
guest
x)推文 r)回覆 e)編輯 d)刪除 M)收藏 ^x)轉錄 同主題: =)首篇 [)上篇 ])下篇