# **2018 Homework1 你所不知道的C語言** - [為什麼要深入學習 C 語言?](https://hackmd.io/s/HJpiYaZfl) - [x] [直播 - 開發工具和規格標準篇](https://www.youtube.com/watch?v=scLFY2CRtFo) - [你所不知道的 C 語言: 指標篇](https://hackmd.io/s/HyBPr9WGl#) - [x] [直播 - 指標篇 (上)](https://www.youtube.com/watch?v=G7vERppua9o&feature=youtu.be) - [x] [直播 - 指標篇 (下)](https://www.youtube.com/watch?v=Owxols1RTAg&feature=youtu.be) - [你所不知道的 C 語言: 函式呼叫篇](https://hackmd.io/s/SJ6hRj-zg ) - [你所不知道的 C 語言: 遞迴呼叫篇](https://hackmd.io/s/rJ8BOjGGl) - [你所不知道的 C 語言: 前置處理器應用篇](https://hackmd.io/s/S1maxCXMl) - [你所不知道的 C 語言: goto 和流程控制](https://hackmd.io/s/B1e2AUZeM) - [你所不知道的 C 語言: linked list 和非連續記憶體操作](https://hackmd.io/s/SkE33UTHf) - [你所不知道的 C 語言:技巧篇](https://hackmd.io/s/HyIdoLnjl) - [GNU/Linux 開發工具](https://hackmd.io/c/rJKbX1pFZ) ## *為甚麼要深入學習C語言 - ### Note : 1. #### 讀規格書可大幅省去臆測 ( 拋開過去的學習習慣 !!! ) - [C99規格書](http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf) 2. #### `&` 不要念成 and,涉及指標操作的時候,要讀為 "address of",是一種operator - 參閱 : **C99 標準 [6.5.3.2]** 3. #### 如果你沒辦法用英文來解說 C 程式的宣告,通常表示你不理解! - 安裝 `cdecl` 程式,可以幫你產生 C 程式的宣告 ( :+1: ) - 有時候用中文的翻譯在看解釋名詞時,會發覺跟程式功能本意有落差,所以盡量能用英文去理解就用英文去理解,否則有時候會產生一些不必要的誤會,甚至造成自己困擾。 4. #### 只用 printf 觀察資料,有問題嗎 ? - 只用 printf() 觀察的話,永遠只看到你設定的框架 (format string) 以內的資料,但很容易就忽略資料是否合法、範圍是否正確,以及是否看對地方。 - ### 心得 : 老實說我是第一次修到需要上課前先觀看線上課程影片的,然而大學時期毫無扎實的實作經驗,讓我聽課聽得很痛苦,因為幾乎完全沒有什麼共鳴,不過想說既然大學都這樣過了,而現在念到了研究所自然就要來還債,記得第一天老師上課就強調,要誠實的面對自己,承認自己與他人的差距,所以要更加努力地趕上進度,在大學時期在修有關程式語言方面的課程時,我完全不知道規格書是什麼東西,不知道語法如何使用,就只會try and error,編譯過了後就很開心,但也沒有實際的去了解,到底為什麼編譯不過,而為什麼這樣編譯會過,直到上了老師的課,提到了規格書C99這東西,才發覺原來C語言也有使用手冊這東西 ? 好奇去打開規格書後密密麻麻的全都是英文的文字,此刻才發現又要還大學英文很差的債了 ...,簡單來說,上完老師的課後,我感覺到自己好像沒學過程式語言似的 ...,雖然說有部分原因也是因為大學太混,不過也因如此讓我更加清楚明白自己的不足,而藉此提供了一個方向讓自己去努力邁進,我知道會很辛苦,但我會盡我所能的學好學滿。 --- ## *你所不知道的 C 語言: 指標篇 (上) - ### Note : 1. C 語言的用途 : 開發 UNIX 作業系統。 2. C 語言能充分掌握硬體架構。 3. 要說 C 語言是一種程式語言,不如說它是一種思維方式, 是一種文化,而且是UNIX的文化,甚至可說是UNIX的母語。 4. C 語言永遠只有 **call-by-value** (傳值) 。 5. 在 C 語言的物件就指在執行時期,==資料==儲存的區域,可以明確表示數值的內容。 6. 注意 "scalar type" 這個術語,日後我們看到 `++`, `--`, `+=`, `-=` 等操作,都是對 scalar (純量)。 **複習 :** ++ a 和 a ++ 的不同 - **++ a :** 會先將變數的值加 1 再指派 。 ```click= int x,a=1 ; x = ++a ; ``` **➨ 結果 a=2 , x=2** - **a ++ :** 會先將變數的值指派再加 1 。 ```click= int x,a=1 ; x = a++ ; ``` **➨ 結果 a=2 , x=1** 7. 純量只有大小,它們可用數目及單位來表示 ( 例如溫度=30度C )。純量遵守算數和普通的代數法則。 **注意**:純量有「單位」(可用 sizeof 操作子得知單位的「大小」),假設 ptr 是個 pointer type,對 ptr++ 來說,並不是單純 ptr = ptr + 1,而是遞增或遞移 1 個「**單位**」。 8. 沒有「雙指標」只有「指標的指標 ( a pointer of a pointer )」 9. C語言沒有真正的 陣列 / 矩陣,意思就是說,數學上不成立的 C 語言可以成立。 - ### Pionter ( 指標 ) - **指標可描述為 :** 只要我給你一個圖片網址,你可以藉由這個網址拿到這張圖片,而這個網址就是指向這個資料的「指標」。換句話說,原來資料的地址就是指標嘛 !!! - **說明 :** 參考以下程式碼 : ```clike= viod main() { int a = 2 ; int b = 4 ; int c = 6 ; } ``` 在宣告好變數、程式開始執行後,會去記憶體中要一塊儲存空間,然後把 2 這個資料放進去記憶體中。而記憶體就像一個大櫃子,每個格子都有相對應 的地址,而這個2 的地址就是在記憶體中的某一個地方。但我們說過,記憶 體中一個格子的大小是 1 個 byte,而一個 int(整數型)的大小就占了 4個 byte,所以這邊寫的地址,是 2 這個整數所占的這一塊記憶體空間的 起始地址。從起始地址開始起算,共佔了 4 個格子,也就是 4 byte。也就是說,當我們宣告一個變數時,總共會有三個要素: 1.變數位址 (記憶體位址) 、 2.變數值 (2) 、 3.變數名稱(a) 程式會向記憶體要一塊空間來儲存變數值,所以這個儲存空間有一個起始位址。再加上這個變數的名稱(a)與變數值(2)。通常我們會把變數的位址,稱為「指向該變數的指標」。在這邊需要特別注意的是,我講的是「指標」,而不是「指標變數」,這兩個是不同的東西。 拿一開始舉的例子來說,我們可以發現,變數位址就是圖片網址,變數值就是圖片,而圖片的名稱就是變數名稱。 - **運算符號介紹 :** 1. **`&` :** **address of** ( 取得某資料位址 ) 2. **`*` :** **valus of** ( 從某位址中取出某值 ) - **說明 :** 參考以下程式碼 : ```clike= #include <stdio.h> int main(){ int a = 2; printf("變數a的值為: %d\n",a); printf("變數a的值為: %d\n",&a); printf("變數a的值為: %d\n",*&a); //printf("變數a的值為: %d\n",*a); return 0; } ``` **輸出的結果為 :** 變數a的值為:2 變數a的值為:某記憶體位址 變數a的值為:2 **說明 :** 由程式碼可知第7行很明顯是編譯不過的,因為value of operator是用來從某地址中取出某值,而我們定義a是一個整數,而不是位址,故編譯失敗。 且由程式碼我們可以發現,第4行跟第6行的執行結果是相同的,因為第6行是先由address of operator先取得a的資料位址,然後value of operator再由剛剛取得的資料位址中取出a的值,故輸出結果會跟第4行相同,由此現象我們也可以說「*&a」和「a」的意義是相同的。 - ### Pionter varible ( 指標變數 ) - **指標(Pionter)和指標變數(Pionter varible)的差別** : - 指標(Pionter) : 某變數的位址 - 指標變數(Pionter varible) : 用來存放指標(某變數的位址)的變數 - 整理 : 一般來說,變數都是用來儲存一個「值」的,比如說整數型變數 int 就是存整數、字元型變數 char 就是存字元。所以這個指標變數就是用來存「地址」的變數。 - **要怎麼去宣告一個指標變數呢 ? ➜ 採用「*」這個運算符號。** ```clike= int* pinoter ; //「*」表示這變數是個指標,pionter表示這個變數的名稱 或 int *pionter ; //「*」表示這變數是個指標,pionter表示這個變數的名稱 ``` - **參考以下程式碼** ```clike= int a = 2 ; a = 2 ; int *pionter ; pinoter = &a ; ``` ==" Hint " : 這邊要注意,第四行的程式碼絕對不能寫成 **pionter = a ;** 因為pionter是專門放位址的變數,故要在a面前加一個&去抓他的位址。 如此一來,我們就稱「指標變數 pointer 指向了變數 a」== - **參考以下程式碼** ```clike= int a = 2 ; int *pionter = &a ; ``` 會發生這件事情 : ![](https://i.imgur.com/iTOqRWT.png) 也就是說變數 a 在記憶體中對應了一塊儲存空間,而這塊儲存空間總有一個起始的地址。 所以 pointer 對應到的就是這個起始地址。在這種狀況下,就可以用 * pointer 來拿到這個變數。這裡的「 * 」,和宣告指標變數的 int* pointer 的意義不太一樣。反而是和「&」相對應的「&」代表「取出地址」、「*」代表「取出內容」。 - **討論:** 那所謂的「*pointer 取出的內容」指的到底是變數 a、還是變數 a 的值 2? 這兩個是不同的東西,變數 a 是這塊區域,2 是個值。 **Ans**:*pointer 代表的就是變數 a。所以我們可以把 *pointer 當作變數 a 來使用。 - **參考以下程式碼** ```clike= #include <stdio.h> int main(void){ int a = 2; int* pointer = &a; printf("變數 a 的值:%d\n", a); printf("變數 a 的地址:%p\n", &a); printf("pointer 的值:%p\n", pointer); printf("指標變數 pointer 的位址:%p\n", &pointer); *pointer = 100; // 因pointer = &a,故*pointer = a 的意思 printf("*pointer 的值:%d\n", *pointer); printf("變數 a 的值:%d\n", a); printf("指標變數 pointer 的地址:%p\n", &pointer); return 0; } ``` **輸出結果為 :** 變數 a 的值:2 變數 a 的位址:0x7fffc32af124 pointer 的值:0x7fffc32af124 pointer 的位址:0x7fffc32af128 *pointer 的值:100 變數 a 的值:100 變數 pointer 的地址:0x7fffc32af128 **討論 :** 我們可以看到一開始的變數 a 的值被設定為 2,所以印出來也會是 2。然後用「&a」取出變數 a 的地址為「0x7ffffea4b894」。 由於 &a 的值被賦予給 pointer,所以把 pointer 印出來後同樣也是 「0x7ffffea4b894」,而pointer這個指標變數當然也需要一個記憶體位址去儲存&a這個地址,而儲存&a這個地址的記憶體位置為「0x7fffc32af128」。 因我們說過 *pointer 就是其指向的變數 a,所以在這邊我們試著把 *pointer 中的值改成 100,然後印看看原有的變數 a 會不會跟著改變,之後我們發現 2 被改成 100 了。 最後,由於存放 pointer 這個變數的地址,和變數 a 的地址不一樣,所以利用「&pointer」後,可發現地址「0x7ffffea4b898」和變數 a 的地址當然不同。 :::info *Quastion: 想請問老師,想不太通為甚麼&pointer後的地址會改變的原因,是因為a的值變了嗎 ? a的值變了後他還是維持原本的記憶體位址嗎 ? 還是他就固定用那塊記憶體區域儲存100這個值呢 ? 懇請老師解惑,謝謝老師 ... *更新 : 是否因為pionter這個指標變數也需要記憶體去存取位址,故&pointer的位置才會跟a的記憶體存放位置不同 ? ( 已經了解 ) ::: > 以上引用自 : [C語言 : 超好懂的指標,初學者請進~](https://kopu.chat/2017/05/15/c%E8%AA%9E%E8%A8%80-%E8%B6%85%E5%A5%BD%E6%87%82%E7%9A%84%E6%8C%87%E6%A8%99%EF%BC%8C%E5%88%9D%E5%AD%B8%E8%80%85%E8%AB%8B%E9%80%B2%EF%BD%9E/) > - ### Pionter to Pionter ( 指標的指標 ) - **延續以上討論 :** 如果在上例中,要儲存 pionter 這個指標變數的記憶體位址,也就是 「 0x7fffc32af128」 這個值,那麼如何作?由於 pionter 是個 int* 型態變 數,如同 int 變數必須宣告 int* 指標,所以 int* 型態變數就必須宣告 int** 型態的指標,例如: ```clike= int **ptr2 = &pionter; //ptr2為用來儲存pionter指標變數的位址 ``` 注意 : 指標的指標不能翻譯為 double pionter。 - **考慮以下程式碼 :** ```clike= #include <stdio.h> int main(void) { int p = 10; int *ptr1 = &p; int **ptr2 = &ptr1; printf("變數p 的值:%d\n", p); printf("變數p的記憶體位置:%p\n\n", &p); printf("指標變數 ptr1 的值 = %p\n", ptr1); printf("指標變數 ptr1 的記憶體位置:%p\n", &ptr1); printf("*ptr1 = %d\n", *ptr1); printf("指標的指標變數 ptr2 的值 = %p\n", ptr2); printf("指標的指標變數 ptr2 的位址 = %p\n", &ptr2); printf("*ptr2 = %p\n", *ptr2); printf("**ptr2 = %d\n", **ptr2); return 0; } ``` **輸出結果 :** 變數p 的值:10 變數p的記憶體位置:0x7fff9a0066fc 指標變數 ptr1 的值 = 0x7fff9a0066fc 指標變數 ptr1 的記憶體位置:0x7fff9a006700 *ptr1 = 10 指標的指標變數 ptr2 的值 = 0x7fff9a006700 指標的指標變數 ptr2 的位址 = 0x7fff9a006708 *ptr2 = 0x7fff9a0066fc **ptr2 = 10 **討論 :** 由程式碼我們可以知道,指標變數 ptr1 儲存了P的位址,指標的指標變數ptr2 儲存了 ptr1 的位址,而我們利用 a value of ( * ) 去取值可以發現,因 ptr1 儲存了 P 的位址,故 *ptr1 就為 P 的值,同樣的,因 ptr2 儲存了 ptr1 的位址,故 *ptr2 就為 ptr1 的值,而我們在對 *ptr2 做一次 a vaule of 的運算也就是 **ptr2 後可以發現,**ptr2 就為 P 的值。 > 以上引用自 : [雙重指標](https://openhome.cc/Gossip/CGossip/DoublePointer.html) > - ### Struct ( 結構 ) 在設計程式的過程中,經常遇到一組變數需要宣告在一起,比如說學號、姓名、性別、年齡、地址、成績等變數,全都是用來描述一個學生,有時候我們就想要把這組變數綁在一起、讓它看起來更像是一體的,使變數之間的關聯變得更直接,C 語言裡面有一個辦法能做到,叫 strutct (結構),在這組變數前面加上 struct、用大括號包起來。 - **舉個例子 :** ```clike= struct student{ //名稱為student的結構 int id; //學號為整數型 char name[8]; //姓名為字元陣列 char sex; //性別為字元型 int age; //年齡為整數型 char addr[30]; //地址為字元陣列 float score; //成績為浮點型 } ``` 現在開始,我們已經成功利用結構、宣告了一個新的資料類型「student」。在這邊必須強調的是,此處的「student」並不是一個變數,而是一種資料型態。也就是說這個 student 和 int, float, char 等資料類型一樣,是一種「用來宣告變數」的資料型態。所以說,結構 (struct) 就是一種由使用者自訂之資料型態。在這邊我們自己定義了一種資料型態叫「student」,裡面包括了 int、chat、float 等型態。 - **宣告結構變數** 宣告的方式,和我們之前宣告 int 變數和 float 變數、char 變數的方法都一樣,前面放「型別」、後面放「變數名稱」即可。 **例如 :** ```clike= struct student Amy, John; //宣告兩個新變數Amy跟John為結 構形式 ``` 或者我們也可以直接把變數宣告在結構的後面 ```clike= struct student{ int id; char name[8]; char sex; int age; char addr[30]; float score; }Amy, John; ``` - **參考以下範例 :** ```clike= #include <stdio.h> #include <ctype.h> //引入字元測試與轉換函數標頭檔 struct student{ int id; char name[10]; }; void new_student(struct student new_one){ new_one.id = 1000 + new_one.id; for(int i = 0; new_one.name[i] != '\0'; i++){ // '\0' 為空字元 new_one.name[i] = toupper(new_one.name[i]); //把所有字元改為大寫 } printf("新學生id:%d\n", new_one.id); printf("新學生姓名:%s\n", new_one.name); }; int main(void) { struct student john = {291, {'j', 'o', 'h', 'n', '\0'}}; new_student(john); printf("\n"); printf("學號:%d\n", john.id); printf("姓名:%s\n", john.name); } ``` **執行結果 :** 新學生id:1291 新學生姓名:JOHN 學號:291 姓名:john **討論 :** 我們同樣利用 struct 建好一個叫 student 的型別,接下來在 main 函數中宣告一個 student 變數 john,並給他一個初始值:291, john。 接下來把 john 這個變數當成一個參數,給 new_student 這個函數。 在 new_student 函數裡面,我又宣告了一個新的參數名稱叫 new_one, 然後用 new_one 來接受傳進來的 john 的值。 接下來就利用 new_one 來調整原有 id 和 name 變數的值。最後把 new_one 的值印出來。 因為 new_student 這個函數是 void 型,所以不用回傳 main function 任何值。 最後再把原本的 john id和 name印出來,會發現和 new_one 完全不同。 由於 main 函數把 john 複製了一份,給 new_student 裡面的 new_one,所以接下來對 one 做的任何修改都不會影響到主函數 main() 裡面的 john。 因為它們已經是兩個獨立的變數了。到最後 new_one 印 new_one 的、 john 印 john 的,不會互相干擾。這個程式驗證了一個結論,把一個結構 變數當作參數、傳遞給一個函數的時候,實際上是把這個變數複製一份傳遞給這個函數。 - ### Link list ( 鏈結串列 ) -