前言:

一直覺得 C語言跟 FORTH 語言在對記憶體的使用用法上,觀念非常的類似。兩個語言的運作核心都是直接對電腦記憶體位址做存取,跟控制,來達到各種程式的目的。所以操作的觀念上非常的接近。所以一直想來篇簡短的心得比較,來比較兩種語言如何的操作記憶體,這樣的比較,可以大大地加深對兩種語言的了解,所以來留個紀錄吧,同時也可以清楚看到兩種語言的優劣性。基本上,我是覺得 FORTH 是略勝一籌的啦!

從歷史上來看, FORTH 是1960年代末期,由查理斯·摩爾 (Charles H. Moore) 所開發的語言,最早是為了控制巨大的電波天文望遠鏡所使用。 而C語言則是在1969年到1973年之間,由丹尼斯·里奇(Dennis Ritchie)為了在PDP-11電腦上運行的Unix系統所設計出來的程式語言。

也就是說其實兩個語言出現的時間非常的接近,而 FORTH 可能要比C語言早個幾年。也許年代接近吧,兩個語言的發明人不約而同的都有類似的思維,真的是非常的有趣,也許是英雄所見略同吧。只是兩個語言在操作逼近目標的方式迴異,而這方面我覺得 FORTH 高明許多,間單又漂亮的解決了問題。而C語言則將問題複雜化,反而產生了更多問題,使得C語言變得非常的反直覺跟難以學習。雖然 C語言已經是主流每個資訊科系必學的語言了,但我們來用個程式比較看看,看看C語言會有什麼問題吧!

 

記憶體的操作:

這裡筆者要說的,其實就是 C 語言裡面最複雜,艱澀難懂的,所謂指標的東西。 指標,其實就是記憶體位址的抽象化的產物。 C 語言要學好,要很徹底的了解跟熟悉指標如何運作才可以。因為這塊是 C語言裡面運作的主要核心。

FORTH 語言也是高度利用指標來運作的語言。可是,指標這個艱澀的名詞,只存在 C語言之中。 FORTH 是不需要這種東西的,FORTH 因為有參數堆疊的關係,所以稱它為記憶體的位址。記憶體的位置簡單且直覺多了。

而 C語言為了抽象化,因為且沒有參數堆疊可以使用的關係,只能利用變數來操作記憶體的位址。所以 C語言引進了跟一般變數不一樣的變數,叫做指標變數,用來儲存並操作這些記憶體的位址。指標變數所指向的記憶體位址的資料是有型態的,為了操作,抽象化的需要,所以指標也有型態。型態就是所指位置資料的型態,這讓指標的加一減一可以依據資料的型態,變成是對指向資料的往前一個或往後一個資料的移動。為了偵錯,當指標指向另一個指標時,叫做 double pointers 雙指標型態。雙指標只能指向另一個指標,C語言透過這樣層層的語法規則跟設計來協助編譯器的編譯跟檢查跟偵錯。最後這些複雜的,抽象的語法,反而讓語言複雜化,高度增加了大家學習跟熟悉 C 語言的困難跟時間。

 

題目:

請寫出一個函式或指令來幫我們動態配置一個一維陣列的空間,函式或指令的轉入值為: (1) 記憶體位址 addr ,裡面用來儲存被配置後陣列的位址,(2) 希望被配置的陣列大小 size,(3) 陣列內容的起始值 value。

 

I. FORTH 語言的版本

底下是 FORTH 語言的版本。

 

: fill-n ( addr n value --) \ start from addr and fill n cells with value
  -rot 0
  do  ( data addr)
      2dup !
      cell+
  loop  2drop
;

: allocateArray ( addr size value --)  \ addr, variable that store the addr of array
   over cells allocate throw    ( allocate memory for array)
     dup >r   -rot fill-n       ( fill memory with value)
     r> swap !                  ( store array address to pointer variable)
;

參數堆疊上傳入三個資料,分別是用來儲存被配置後陣列位址的變數位址 addr, 被配置陣列大小 size, 陣列內容起始值 value

over cells 把參數堆疊上 size 陣列數目大小轉成 bytes 數目, allocate 則跟系統要求此 bytes 數目的記憶體,並將要到的位址及有沒有成功的 ior 錯誤碼回傳至堆疊, throw 則將 ior 非零的話,則配置記憶體失敗,發生錯誤,將錯誤丟給系統的錯誤處理 catcher 來處理,如沒錯誤則繼續執行。

dup >r  將要到的位址暫存一下到返回堆疊,  -rot  整理一下堆疊上的資料,讓 fill-n 可以順利的從剛剛要到位址開始,填入 size 數目的 value 資料來起始陣列內的數值。

最後 r> swap ! 則是把剛剛要到位址從返回堆疊推回來,並存入使用者所給的位址 addr 中,就收工。

 

如何使用?

variable 'array                  定義一個變數來儲存要到的陣列的位址
'array 5 45 allocateArray   跟系統要 5個 cells 大小的陣列,每個 cell 用資料 45來當起始值。

怎樣,很直覺吧! FORTH 從來沒有說什麼指標不指標的,就只是記憶體位址的資料存來存去而已啊,為什麼會那麼麻煩跟複雜呢?

 

 

II. C語言的版本,版本一

再來,我們來看看 C 語言的版本,

版本一

假如你是 C 語言的初學者,很可能會寫出底下這樣的版本

void allocateArray(int *arr, int size, int value) {
   arr = (int*) malloc(size * sizeof(int));
     if (arr != NULL) {
        for (int i=0; i<size; i++)
               arr[i] = value;
     }
}

傳入記憶體的位址,所以我們用 int *arr 來定義一個指向記憶體的指標變數 arr,且這個指標變數所指向的記憶體位址的資料是 int 整數型態。

所以傳入 int *arr 指向記憶體的指標變數, 及 int size 來指示要求系統配置多少記憶體給陣列元素,然後以 int value 的資料來設定陣列元素的初值。

malloc(size * sizeof(int)) 以 int 型態的資料 bytes 數目,乘上size 得到總 bytes 數來配置記憶體空間,取得所配置的記憶體位址。

malloc() 所配置出來的指標是 void 型態的,所以 (int*) 轉型成指向整數的指標型態,然後放入 arr 指標變數之中。

假如 arr 為 NULL ,代表 malloc() 配置記憶體失敗,所以直接結束。

假如 arr 不是 NULL 代表配置成功,所以用個 for-loop 將 value 逐一依序的放入 arr所指示的前後位置中,來起始陣列的元素。

使用方式

int *pArray = NULL;
allocateArray(&pArray, 5, 45);

 

這一切的一切,看起來非常的合情合理。

但, 這時候 C語言要發威了,這個函式是錯的示範。真的執行的話,執行完畢 pArray 所指向的位址還是 NULL。然後你用 malloc() 所要到的記憶體空間會因為沒有正確被傳出來,你搞丟了記憶體位址,之後也無法被 free() 釋放而造成 memory leak。

What???  怎麼回事,怎麼看都很正確啊,怎麼會這樣???

是啊,我們是送入的 &pArray 指標變數的位址進去了啊,並期待最後將要到的陣列位址塞進去裡面,回傳給我們。

所以一開始, arr = &pArray 是沒錯, arr 指標變數裡面有了 &pArray 的指標變數的位址。

但是後來的 arr = (int*) malloc(size * sizeof(int)) 立刻又將這個 arr 指標變數的內容給改成 malloc() 所要到的位址了,所以立刻把 &pArray 的這個指標位址給蓋過去,立馬遺失了 &pArray 指標變數的位址。

最後當函數執行完畢, arr 區域變數資料被清掉,結果所有資訊都被丟掉囉,最後造成 memory leak,然後 pArray 也始終指向 NULL的錯誤結果。

 

 

II. C語言的版本,版本二

版本二

正確的寫法要使用指標的指標,雙指標 double pointer 

 

void allocareArray(int **arr, int size, int value) {
   *arr = (int*) malloc(size * sizeof(int));
     if (*arr != NULL) {
        for (int i=0; i<size; i++)
               *(*arr + i) = value;
     }
}

使用方式

int *pArray = NULL;
allocateArray(&pArray, 5, 45);

 

不能用單指標,否則會像剛剛那樣, malloc() 一傳回系統配置完畢的記憶體位址時,原來的位址資料會立刻被蓋掉而丟失要儲存的位址在哪裡。必須再間接參考一次才可以。

所以 int **arr 定義一個指向記憶體的雙指標變數 arr,其內容指向另一個指標變數,這個指標變數裡面儲存並指向被配置所得到的真實陣列的位址。

再來是重要的解參考語法,假如對 arr 做一次解參考, *arr , 就得到 arr 所指的記憶體位址的那個變數。

arr = XXX 代表將 arr 裡面的位址資料做改變

*arr = XXX 代表將 arr 裡面所指的記憶體位址當指標變數,將指標變數裡的資料做改變。 怎樣,粉複雜然後非常容易搞混吼! (這就是 C語言,攤手)

所以 *arr = (int*) malloc(size * sizeof(int)) ,這次就可以將 malloc() 跟系統所要到的記憶體位址,正確的放入我們所傳入的 &pArray 的指標變數之中。

接續的 for-loop 將以 value 起始陣列的起始值。

C語言的規則,等號左邊的是解參考語法,用來告訴編譯器如何根據給你的指標變數取得真實所要存入資料的位址,等號右邊則是欲存入的資料,可能是一般的純資料,也可能是位址資料。視解參考後最後指向的位置而定。

這裡,左邊 *(*arr + i) 表示

arr 解參考 *arr 得到 arr 所指向位置的內容,在這是陣列的位址。給它加上陣列索引 i 後, (*arr + i) 指向陣列的第 i 元素的位址。

將這個位址再解參考一次, *(*arr + i) = value ,意即要將 value 資料放入陣列的第 i 元素的位址中。

 

結語:

有沒有覺得昏頭轉向了呢??? 這就是 C語言啊!

用了一堆語法所抽象化堆砌出來的記憶體操作模型,這樣的模型跟語法企圖將記憶體的操作抽離硬體,做到跟硬體完全無關,然後透過編譯器的「解釋」來達到跨平台的目的。

當然啦,語言這種東西是熟悉度的問題,透過練習,習慣後即使再多難搞的特例也一樣,終究會熟悉的。特別是 C語言這種資訊系所,電腦科學,乃至於所有的處理器設計跟產業界所採用的主流語言,再難搞大家還是會努力鑽研跟熟悉它的。

只是從 FORTH 語言使用者的角度來看,C語言那些複雜的語法只是庸人自擾而已。 FORTH 也是高度使用指標來操作的語言,但學習的過程中,你不會發現原來你正在使用指標來操作,一切居然那麼簡單跟自然和水到渠成。

 

 

 

 

 

 

arrow
arrow

    ohiyooo2 發表在 痞客邦 留言(0) 人氣()