2016年4月3日 星期日

C語言的指標及實作如何讀取應用程式執行時所附帶的參數


    今天要來介紹C語言的指標,以及如何利用指標來讀取執行應用程式時所附帶的參數.
原始程式碼下載:
https://github.com/Ashing00/pointer01/tree/master

        完整原始碼已放在在上述連結,只不過我是用visual studio 2013的win32控制台程式
去編譯,須注意你的編譯環境是否與我相同,如果使用不同的編譯環境有可能會發生
編譯不過發生錯誤情形,請再自行參考修正編譯錯誤問題,不過基本的原理是一樣的.

      一般來說學習C語言比較有可能遇到兩個較難理解的主題,一個是指標(pointer)另一個遞迴,有關遞迴會在另一篇文章介紹,然而了解遞迴之前最好先了解指標.

     指標(pointer) 簡單的來說就是指向我們電腦記憶體裡的位址(address),而一般我們所知道的
數值或是字元字串等等資料就是存放在這些記憶體位址裡的值(value).這樣的一個觀念在以前X86系統的組合語言裡反而是部會有太大的困擾的,因為學習組合語言必然要知道定址的方法
所以也必然會知道現在存放在暫存器如EBX,EDX的到底是位址(address)還是值(value).
    那C語言其實就是用指標(pointer) 來達到可以存取位址(address)的方法,只不過可能
是因為資料型態定義的方式在使用上容易跟原本的數值字串等等取值的資料型態搞混.
要搞懂它最好的辦法還是實機操作,底下的範例程式利用printf 的功能來一步步確認區分
究竟現在是位址(address)還是值(value).

    首先必須先知道C語言裡定義的兩個符號一個是*,另一個是&.
&指的是取出這個資料型態所存放值(value)的位址(address) 所在.
 所以他應該得到一個位址(address).

例如 pointer.c 宣告 一個int DataA  數值變數並給他初始值55.
當使用adr1 = &DataA; 這樣的敘述時我們可以指定存放數值55的這個記憶體位址給adr1
  於是adr1可以得到這個數值(value=55)存放的位址(address),請看圖一的說明.

*指的是從指標(pointer) 所存放的位址(address)去取出值(value),也就是它應該得到的是
 一個值(value),然而有趣的是在多重指標裡這個取出的值(value),本身也可能是代表指向另一地方的位址(address).請看圖一的說明再配合程式的執行.

<圖一> 底下應是偏移4位元組 <筆誤> 



在程式一開始宣告了兩個指標變數,如下.
int *adr1;
int *adr2;
在讀code常常會搞混,究竟什麼是代表值(value)什麼是代表位址(address)?
這裡利用printf 去列印它的內涵值,即可明白哪個是值(value)哪個位址(address).

從code 28-35行可以分辨出,在讀code時,"adr1","adr2",他們的內含都是代表著位址(address)
而"*adr1","*adr2"所代表著的則是取出的值(value).

   我們也可以任意試著指定任一位址(address)給指標變數如adr1adr2,然後再去取出值
但是這樣有可能會遇到執行時程式中斷或當機,如圖三,原因是系統有些記憶體是被保護住的無法任意存取.
    可以試著將0xFFFFFFC0 直接指定給adr2去做存取,利用VS debug即會出現圖三現象.及程式被中斷執行.

圖二pointer.c原始碼





圖三 
    



如果一個指標的變數可以理解後,接著我們可以實作出利用兩層指標如**P1 來讀取程式執行所附帶的參數.
例如 在windows的dos 命令列視窗窗執行編譯好的pointer.exe 並帶進3個任意參數
C:>pointer.exe abc b123 shing

   在這裡pointer.exe 是程式本身的名稱,而"abc","b123","shing",則是附帶進的三個參數
 通常我們可以利用帶進不同的參數去處理不同的功能,在這裡則是針對如何讀取帶進的參數
至於讀到帶進的參數後,利用printf 列印出該參數名. 這裡只是舉了這個三個參數,實際上不只可輸入3個參數,而每個參數也並未限制字串長短,也就是說如果執行以下例子

C:>pointer.exe ab bcd defg q12345 shing1234 ttttttt
應當也是可以執行的.
  實際執行的狀況請參考下圖四

在這程式可以親自去執行試看看會比較容易理解其中運作

int _tmain(int argc, _TCHAR* argv[])
argc :所代表的是有幾個參數,在vs 裡的程式,程式名稱pointer.exe 也會被算一個
 所以在這個圖四範例 argc會=4.

_TCHAR* argv[] ,在別的編譯器上也寫成 **argv.
基本上* argv[]  可以看成等同於 **argv.原因是因為陣列本身就是帶有address的含意
也就陣列本身也是儲存在某一塊記憶體,而程式就利用&(argv[i]);來讀取它的起始位址.
p1 = &(argv[i]);
而字串本身也是類似一維的陣列.

故在本程式我宣告了**P1,及**P2[10],來讀取* argv[].
至於**P1,及**P2[10] 的差別在於,指標變數也可以宣告成陣列.
**P1 只能完整讀到一個參數字串,有多個參數必須利用**P2[10]來完成
預設指標陣列大小為10表示最多可以讀到11個參數,從0開始.

這裡宣告的二重指標其含義可以看成 位址1(address)指到另一個2(address) 
2(address) 才是真正存著值(value) ,這裡的值(value)指的是字元也就是字串的各個字元.

假如有三重的指標呢?如 ***P3
那其意義其實一樣
可以看成 位址1(address)指到另一個位址2(address) ,再指到另一個位址3(address)
位址3(address) 才是真正存著所要的值(value)
 ***P3 最後取到的是所要的值(value)
**P3,*P3,P3 可以看成分別三個代表不同的位址(address).



圖四:




   最後指標的運用也可以用在函數上,但是只要分清楚現在變數代表的是位址(address)或是
值(value)就不會搞混,而利用printf這樣的函式可以幫助我們debug或是分清他們,所以重點還是
要實際上機試驗才會更了解這之間的關係.