# C語言自學筆記及練習 [C語言入門](https://www.youtube.com/@KenYiLee) [你所不知道的 C 語言系列講座](https://hackmd.io/@sysprog/c-prog/%2F%40sysprog%2Fc-programming) ## 資料型別 資料處理的核心問題:記憶及計算,這個問題會由資料型別用以描述這個資料如何儲存及計算。 電腦內部具有記憶體空間而該空間由`Byte(8 bits)`所組成大多數情況下(實作定義) ![image](https://hackmd.io/_uploads/Hk_RfZXpA.png) 任何資料型別的資料都是由0或1所組成,所以為何硬碟可以儲存照片、影音、word、PPT檔案等。 | 資料型別 | 名稱 | 大小(byte) | 例子 | | ---------------------------------------------- | ---- | ---------- | ---- | | 短整數(short integer) | `short int ` | 2* | 32 | | 整數(integer) | `int` | 4* | 32 | | 長整數(long integer) | `long int` | 4* | 32 | | 字元(character | `char` | 1 | '3' | | 單精度浮點數(single-precision floating points) | `float` | 4* | 3.2 | | 倍精度浮點數(double-precision floating points)| `double` | 8* | 3.2 | | 無 | `void` | ? | | `*` 代表為實作定義(表示不同編譯器會有不一樣的大小),如果想要知道其大小會使用 `sizeof` 這個運算子來去求出某個資料型別佔記憶體空間之大小。 ```c #include<stdio.h> int main() { long int x=50; printf("long int: %d.\n", sizeof(long int)); return 0; } output: 8. ``` 表示我所使用的`gcc`編譯器`long int`的資料型別為`8 byte` * 變數在使用前必須先宣告或定義,變數定義時必須指定型別及名稱 * 讀取未指定值的變數在大部分情形都是未定義行為,未定義行為是指程式執行過程會發生不可預測的行為,可能會導致`error`。 * 在定義時給定值叫做初始化`(initalization)` `Ex: int num=0;` | 資料型別 | 變數名稱 | | -------- | -------- | | int | num | * 以下keyword保留其他功能使用不能當作變數名稱![image](https://hackmd.io/_uploads/rJA7CZ7TR.png) * 不同資料型別的差異為 * 表示意涵不同`(int vs char)` * 表示原理不同 `(int vs float)` * 表示值域不同`(short int vs long int)` * 表示的精確度不同`(float vs double)` * 符號不同 `(int vs unsigned int)` ### 整數型別 ![image](https://hackmd.io/_uploads/rk2LO-IfJg.png) 數字的表示式:有號數及無號數 ![image](https://hackmd.io/_uploads/S19Tt-Ifyx.png) 在計算機科學中會使用許多種數字型態的表示方法:有無號數、有號數、一補數、二補數等,其中無號數的缺點為不能用以計算負值,再來有號數的缺點為有可能會有溢位導致未定義行為情形發生,一補數是為了改進有號數數字表示法的缺點而去延伸出來的表示方式,編碼仍屬於阿貝爾群,一補數系統的缺點會是編碼時會產生 `+0`和` -0`,後有了二補數這個編碼方式,並且成為計算機編碼最常見的方式,一補數系統仍常用通訊領域相關的用途。 ### 浮點數型別 * 實數表示法:因為現實世界的數學有無數種實數,理論上要有無限的記憶體空間用以儲存才能表示所有實數,但這是不切實際的方法,故在某些無窮的循環小數或是因為記憶體空間的限制,會將實數採用約略值例如:圓周率。 * 浮點數:是用以表示實數的一種方法,目前常用的為`IEEE 754`的浮點數表示法有單精度及雙精度表示法。 > * 浮點數都表示約略之值 > * 在使用一樣大小的記憶體空間下,浮點數可表示的範圍都大於整數 > * 浮點數可以想成一種科學記號表示法:以`IEEE 754`為例:![image](https://hackmd.io/_uploads/r1ZThIwMJe.png) ![image](https://hackmd.io/_uploads/S1b8CUwM1x.png) > * 有效數字越多表示值的精度越準確,但其佔用之記憶體空間會越大。 | data type | Name | byte | Mantissa | exponent | | ------------------------------- | ---- | ---- | ---- | --- | | single-precision floating point | float | 4* | 23* | 8* | | double-precision floating point | double | 8* | 52*| 11* | | | long double|8 |?* | ?* | * *為實作定義 ![image](https://hackmd.io/_uploads/Hk56lPvGkx.png) ### printf與scanf的定義(I/O) #### printf * `printf`為內建的標準輸出的函式 * `printf`使用必須給予想要印出之字串。 Ex: `printf("Hello World !");` * `printf`輸出的字串可以用類似字元跳脫的方法,放置一些特定的格式符號來輸出特定的資料 Ex:`int sum =10; printf("Sum is %d /n",sum);` ![image](https://hackmd.io/_uploads/By4zmDwzye.png) ![image](https://hackmd.io/_uploads/r17NmDDMJg.png) ![image](https://hackmd.io/_uploads/H1M-Vwwf1l.png) * 使用不同型別時要使用對應的格式符號 #### scanf * `scanf` 是標準輸入的內建函式。//鍵盤輸入文字 * `scanf` 使用時要給予讀入資料格式及變數的記憶體位址 Ex: `scanf("%d", &a);` &為取址運算子 ### 不同資料型別的轉換 * 算術運算的結果也是具有資料型別 * 字面常數(literal constant):在程式碼內可以使用字面常數來表示已知之值。 ![image](https://hackmd.io/_uploads/rJjd_PDMyg.png) * 不同型別的計算會優先隱性轉型至可以表示較大範圍的型別: ![image](https://hackmd.io/_uploads/BJgPtPDz1e.png) * 每次資料型態轉型時都要確實了解其轉型後是否精確度或表示方法會有問題? ### 字元型別 * 儲存字元將字長什麼樣及是哪種字兩種資訊分開 * 給每個字元獨一無二的編號:編碼(encoding) * 在`C`語言裡字元型別算是整數型別的特例:ASCII編碼![image](https://hackmd.io/_uploads/H1N4WuPfyg.png) * 字元的可能通常會是由英文鍵盤能輸入的字元 * `C`語言編碼仍然是實作定義 * `C`語言中字元型別有:`char`(1 byte)和`wchar_t`兩種型別,`wchar_t`為寬字元 * 單引號'A'表示字元,雙引號"Ae"表示字串 ![image](https://hackmd.io/_uploads/SJ-UBuDfke.png) ### 運算 * `表示式(expression): $3+4\times 2$`:其中3及4和2代表`運算元(operand)`具有資料及型別 $+,\times$ 為`運算子(operator)`對資料做運算,在進行運算時,因為運算有其優先順序,會產生暫時物件用以儲存運算結果,暫時物件也具有資料型別和值 | operator | int | float | ex | priority | Column 3 | | -------- | --- | ----- | --- | -------- | ------------- | | + | ok | ok | a+b | low | left to right | | - | ok | ok | a-b | low | left to right | | * | ok | ok | a*b | high | left to right | | / | ok | ok | a/b | high | left to right | | % | ok | no | a%b | high | left to right | 註解:在除法時須注意資料型別不同所造成的結果不同如整數除以浮點數,若沒將其轉型會是以整數除法之值顯現 > * `=`是賦值運算子`(assign operator)`其用以會將右方的值複製給左方變數,不能寫成` 3=a`之類的,左方要放被改變的變數,在做賦值運算時會改變程式執行的狀態,所以改變變數之值為其的 `side effect` > * 賦值運算的優先順序比大多運算子還低,順序為由右至左 #### 表示是非對錯 * 流程控制(有條件的執行程式碼) * 條件:在 `C`語言內為表示式,真為條件成立,假為條件不成立 * `C99`新增了_bool代表儲存`true or false`的資料型別 #### 等號與關係運算 * `true`代表為`1`,`fasle`代表為`0` * 在`C`語言中比較只能兩個兩個比較。 * ![image](https://hackmd.io/_uploads/By5cvftvkg.png) #### 邏輯運算 * 如果要使用`and`替代`&&`,則需要包含標頭檔`<iso646.h>` * ![image](https://hackmd.io/_uploads/ByqQ2GFwkg.png) * ![image](https://hackmd.io/_uploads/B1_n0MKwkl.png) ### 程式流程控制 程式是由每一個結構來去控制程式碼執行流程 * 1.循序式結構:正常沒有`if、for、while、do while`等程式碼。 * 2.選擇性結構:`if、if-else、if-else if`等敘述。 * 3.重複性結構:`for、while、do-while`等敘述迴圈。 * ![image](https://hackmd.io/_uploads/BJ5wx4sPyx.png) #### if statement `def`: `if{expression}{body}` 若`expression`成立則執行`body`內的程式碼。 #### if-else statement `def`: `if{expression}{body1}else{body2}` 若`expression`成立則執行`body1`內的程式碼,不成立則執行`body2`內的程式碼。 #### switch statement `def`:`switch(int){case (constant):body1;break; default:body2}`,用`break`隔開不同`case`的程式區塊。 #### while loop `def`:`while(expression){body}`:若`expression`成立則執行`body`內的程式碼。 #### do-while loop `def`:`do{body}while(expression)`:若`expression`成立則執行`body`內的程式碼(至少會做一次)。 #### 複合賦值及遞增遞減運算 * ![image](https://hackmd.io/_uploads/BJGVo6svye.png) * ![image](https://hackmd.io/_uploads/r1iw3powkx.png) #### for loop `def`:`for(initial;expression;loop in/decrement){code body}` ![image](https://hackmd.io/_uploads/ByCKNFnvke.png) ### 函式 * ![image](https://hackmd.io/_uploads/ByZDSa3v1g.png) * `def`:`(return_type) functio_name(var_type var_name,...){code body ... return return_value;}` * ![image](https://hackmd.io/_uploads/BktyIThDJx.png) * 函式內無法再定義其他函式。 * ![image](https://hackmd.io/_uploads/BymjIp2wJe.png) * ![image](https://hackmd.io/_uploads/BkXixkpvJx.png) * 呼叫在定義之前時編譯器會警告或報錯。 * 在函式呼叫之前可以選擇先宣告或定義,可以讓函式不至於報錯。 * ![image](https://hackmd.io/_uploads/H1cTfyavJx.png) #### C 標準函式庫 * ![image](https://hackmd.io/_uploads/Hyx4X1pDJl.png) * ![image](https://hackmd.io/_uploads/Hy0H4k6D1l.png) * ![image](https://hackmd.io/_uploads/BkiFNkaD1x.png) * ![image](https://hackmd.io/_uploads/Sy9qV1pDJx.png) * ![image](https://hackmd.io/_uploads/HkU4rkpPkg.png) * ![image](https://hackmd.io/_uploads/r1UqSkaDJe.png) * ![image](https://hackmd.io/_uploads/ByLfUyaP1l.png) * ![image](https://hackmd.io/_uploads/HklXP1Twkl.png) #### 變數名稱可視範圍(Scope) 1.變數名稱的宣告: * 1.全域變數: * 宣告於函式定義外。 * 容易造成名稱汙染,盡量避免使用全域變數。 ```c int i; int main(){return 0} ``` * 2. 區域變數: * 宣告定義於函式內 ```c int main() {int i; return 0;} ``` * 3.函式參數: * 變數屬於函數`block`裡面。 ```c int f(int j) {return 0;} ``` 2.同一組區塊(`block`)內同名稱變數只能有一個 * ![image](https://hackmd.io/_uploads/H1HDPlTDye.png) * ![image](https://hackmd.io/_uploads/BktYvxTP1x.png) 3.變數名稱可視範圍 * ![image](https://hackmd.io/_uploads/rkSBugTvJx.png) * ![image](https://hackmd.io/_uploads/r15xKxpvkl.png) 4.` void`當作回傳值型態時,表示此函式無須回傳值且當函式型態為`void`時,`return`可以省略。 5.亂數函式:`int rand(void)`包含在`stdlib`在使用`rand`其給予亂數的序列都會相同,所以必須使用`srand`來去給予亂數種:`void srand(unsigned int seed)` #### 函式遞迴 `def`:在函式定義裡,呼叫到所定義的函式,會達到類似無窮迴圈的概念,若回傳值定義不夠明確會一直占用記憶體空間程式會有非預期的結束(未定義行為)。 * ![image](https://hackmd.io/_uploads/B1NlWeAPyx.png) * ![image](https://hackmd.io/_uploads/Hk2p-eCwJg.png) ### 陣列(array) 陣列是一群具有相同資料型態的元素集合的資料型態。 * 在記憶體中,一個陣列會使用一段連續的記憶體空間來存放。 `Array def`:`element_type array_varname[element_num];` * ![image](https://hackmd.io/_uploads/ry3PxWCDye.png) * 陣列第一個用途:取代多個變數的定義:![image](https://hackmd.io/_uploads/H1-3gZ0Dye.png) * ![image](https://hackmd.io/_uploads/BJHOM-CPyx.png) * 陣列第二個用途:因為陣列編號可以使用變數去替代,故可以循序去存取陣列。![image](https://hackmd.io/_uploads/S10pNZCvkg.png) * 存取陣列外的元素會被定義為未定義行為:![image](https://hackmd.io/_uploads/SJ_TuW0w1e.png) #### 陣列排序問題 * ![image](https://hackmd.io/_uploads/HyFdCPgOJl.png) #### Bubble Sort ```c #include<stdio.h> int main() { int n[5]={0}; int i,j; for(i=0;i<5;i++) { scanf("%d", &n[i]); } for(i=0;i<4;i++) { for(j=0;j<4-i;j++) { if(n[j]>n[j+1]){ int t = n[j]; n[j]=n[j+1]; n[j+1]=t;} } } for(i=0;i<5;i++) { printf("%d", n[i]); } return 0; } ``` #### 陣列記憶體配置 ![image](https://hackmd.io/_uploads/HJ_ApYx_ye.png) #### 二維陣列 ![image](https://hackmd.io/_uploads/HkcUCYe_Jg.png) ![image](https://hackmd.io/_uploads/r1MF0KluJl.png) ![image](https://hackmd.io/_uploads/Byxly9g_1l.png) * 對於存取陣列元素可使用作`counter`、`swap`等技巧,很重要。 #### 陣列的複製 ![image](https://hackmd.io/_uploads/B1ZmeRlukx.png) ![image](https://hackmd.io/_uploads/SJF2I0eOJe.png) ![image](https://hackmd.io/_uploads/rJVaURxOJe.png) * 在函式接受到是一個陣列時,函式運作是其陣列本身,但如果是整數的話不會是該整數進行函式運作。 * 1. 陣列複製代價太高。 * 2. 陣列無法用等於直接複製。 #### 函式中傳遞陣列 ```c #include<stdio.h> #include<stdlib.h> #include<time.h> void arryrand(int[10]); int arrymax(int[10]); void arryprtf(int[10]); int main(){ srand(time()); int v[10]; arryrand(v); arryprtf(v); printf("Max: %d\n",arrymax(v)); return 0; } void arryrand(){ int i; for(i=0;i<10;i++){ v[i]=rand%100; } } void arryprtf(int v[10]){ int i; for(i=0;i<10;i++){ printf("%d",v[i]); } printf("\n"); } int arrymax(int v[10]){ int max = v[0],i; for(i=1;i<10;i++){ if(v[i]>max){ max = v[i]; } } return max; } ``` * ` arryprtf` 的陣列與原本的陣列相同是直接拿來運算的 * 函式回傳值不能是陣列型態。 * 不能在賦值或初始化時在等號右邊放陣列。 * ![image](https://hackmd.io/_uploads/S12qoRe_Jl.png) * 函式參數是陣列型態時,其大小並不重要。 * 在`C`語言中,函式傳遞時,陣列大小跟其是哪個陣列的資訊是分開的。 * ![image](https://hackmd.io/_uploads/SJJ-11Z_ye.png) * ` v[]`是指標的一種。 #### 存取陣列外元素的問題 * 陣列的存取之引值必須為整數型別。 * 存取陣列外元素是未定義行為必須避免。 * ![image](https://hackmd.io/_uploads/rk2wLPzO1e.png) * ![image](https://hackmd.io/_uploads/HyIodDz_1l.png) * `C`語言的`sizeof`運算子是實作定義 ```c #include<stdio.h> void f(int[3]); int main(){ int v[3]={1,2,3}; f(v); return 0; } void f(int v[3]){ printf("Size of int: %zu\n", sizeof(int)); printf("Size of v[0]: %zu\n", sizeof(v[0])); printf("Size of v: %zu\n", sizeof(v); printf("length of v: %zu\n", sizeof(v)/sizeof(v[0])); } ``` 上述程式中,在函式傳遞中`v`並不是陣列型態。 #### 使用保留值標記陣列長度 ```c #include<stdio.h> int length(int[]); int main(){ int v[]={1,2,3,-1};// -1為保留值,一般要存取的資料不能有此資料。 printf("%d\n", length(v)); return 0; } int length(int v[]){ int i = 0; while(v[i]!=-1){ i++ } return i; } ``` * 函式裡若使用陣列傳遞,不一定要傳`N`至函式裡,可以使用保留值標記陣列長度。 #### 存取陣列元素的原理 ```c #include<stdio.h> int main(){ int v[3]={1,2,3};// 陣列存取的記憶體空間會相鄰 return 0; } ``` * ![image](https://hackmd.io/_uploads/S14n1_zdke.png) #### 函式傳遞陣列的原理 * ![image](https://hackmd.io/_uploads/SJqU4dMu1l.png) * ![image](https://hackmd.io/_uploads/rkDjNOzu1x.png) ### 全域變數及區域變數 #### 全域變數之特殊性 ```c int main() { int i; printf("%d \n",i); return 0; } ``` 上述其中 `i` 是未初始化的區域變數會導致未定義行為。 ```c int i; int main() { printf("%d \n",i); return 0; } ``` 上述其中 `i` 是全域變數會直接初始化為零。 ```c int i = 0; void f() {i++;} int main() { f(); printf("%d \n",i); return 0; } ``` * 全域變數會讓函式間的關係變得較不明確。 * ![image](https://hackmd.io/_uploads/SkPOETg_kl.png) ```c int f() {return 0;} int i = f(); int main() { f(); printf("%d \n",i); return 0; } ``` * 我們不能透過函式呼叫來去初始化全域變數。 * 也無法透過另一個全域變數來去初始化另一個全域變數。 #### 全域變數與靜態區域變數(static) ```c #include<stdio.h> int count(void) { int k = 0; //void表示函式無參數 k++; return k; } int main(){ for(int i=1;i<=5;i++) {printf("%d\n", count())} return 0; } ``` `Output`:`1 1 1 1 1` 若想讓印出的數字為:`1 2 3 4 5`可以選擇將`k`設置為全域變數。 ```c #include<stdio.h> int k = 0; int count(void) { //void表示函式無參數 k++; return k; } int main(){ for(int i=1;i<=5;i++) {printf("%d\n", count())} return 0; } ``` * 每次函式呼叫都是存取同一個全域變數,但這樣違背我們設計函式的精神,通常會盡量避免使用全域變數。 ```c #include<stdio.h> int count(void) { static int k = 0;//void表示函式無參數 k++; return k; } int main(){ for(int i=1;i<=5;i++) {printf("%d\n", count())} return 0; } ``` * 上述 `static` 表示這區域變數只會有一份並且只會初始化一次,得到類似全域變數的效果並且也不會在其餘函式中使用到。 #### 整數溢位(overflow) ```c #include<stdio.h> int main(){ int a = 1000; int b = a * a * a; int c = a * a * a * a; printf("%d\n", b); printf("%d\n", c); return 0; } ``` * 有號整數的溢位是未定義行為。 * ![image](https://hackmd.io/_uploads/rymTv6gdyg.png) * 無號整數的溢位不是未定義行為。 * ![image](https://hackmd.io/_uploads/S1ebOpeu1l.png) #### 自行實作偽亂數 怎麼產生偽亂數? * 線性同餘法 * linear congruential generator * $X_{n+1}=(X_n \times a+c)\mod m$ * ![image](https://hackmd.io/_uploads/BJqqtTgd1x.png) ```c #include<stdio.h> int main(){ unsigned int next= 1;//故意使用unsigned int在溢位時會直接求取餘數。 for(i=1;i<=5;i++){next = next*1103515245+12345; int rand = (unsigned int)(next / 65636) % 32768;//只取高位使其更亂。 printf("%u\n",next);} return 0; } ``` * ![image](https://hackmd.io/_uploads/HkHp9alu1g.png) ```c #include<stdio.h> #include<time.h> unsigned int _next = 1; int rand(void) { _next = _next *1103515245+12345; return (_next / 65636) % 32768;//只取高位使其更亂。 } void srand(unsigned int seed){ _next = seed; } int main(){ srand(time(0)); for(i=1;i<=5;i++){ printf("%d\n",rand()); } return 0; } ``` * ![image](https://hackmd.io/_uploads/rkFY2pguke.png) ### 字串(String) * 字串是字元的序列。 * 字元型別(`char`) 可以儲存一個字元,但需要處理的文字通常是一串字元 * `C`語言中並沒有替字串定義一個新的資料型別。 * 字串是使用字元陣列(`char[]`) 的形式來儲存。 * 透過在`<string.h>`內提供各種處理字元陣列的函式實現對字串的操作行為。 #### 字元陣列 ```c #include<stdio.h> void str_print(char str[]){ int i; for(i=0;str[i]!='/0';i++){ printf("%c", str[i]);} printf("\n");} int main(){ char str[]={'H','e','l','l,'o','\0'}; str_print(str); return 0; } ``` * ![image](https://hackmd.io/_uploads/Bk9_dOMuyl.png) * ![image](https://hackmd.io/_uploads/ryg2ddzuyl.png) * `%zu`是`C99`標準才有支援,若編譯器採用`C89`、`C90`實作需要改為`%u`或`%lu`。 * 字元陣列若是沒有初始化所有元素也會自動補零。 * ![image](https://hackmd.io/_uploads/BJ_8oOzuJg.png) * ![image](https://hackmd.io/_uploads/BkqG3dGO1x.png) #### 讀入字串時緩衝區溢位的問題 ```c #include<stdio.h> void str_read(char[],int); int main(){ char str[15]; str_read(str,n); printf("%s\n",str); return 0; } void str_read(char str[],int n){ int i; for(i=0;i<n;i++){ scanf("%c",&str[i]) if(str[i]=='\n') break; i++ } str[i]='\0'; } ``` * ![image](https://hackmd.io/_uploads/BJXNzFf_yl.png) #### scanf函式讀入資料時的問題 * 會忽略空白字元 * ![image](https://hackmd.io/_uploads/H14CzFfd1x.png) * 可以透過`scanf`的回傳值判斷其成功讀入幾份資料。 * ![image](https://hackmd.io/_uploads/BJCcQFfuJg.png) ### 指標(Pointer) * 指標是一個資料型別,用來儲存記憶體位址。 * 可以解決下列問題: * 在被呼叫的函數中修改引數值。 * 直接複製陣列。 * 直接複製字串。 * 動態改變陣列長度。 * 指標(`Pointer`)是`C`語言的主要特性,是種儲存記憶體位址的資料型別。 * 指標語法宣告: `data_type *var_name` * 表示變數內存放的是一個存放這種資料型別值的記憶體位址。 * ![image](https://hackmd.io/_uploads/Hkd-WXXOyl.png) * ![image](https://hackmd.io/_uploads/Bkp8WmQ_yx.png) ```c int count = 9; int a; int *b; a = count;// (int) = (int) b = count;// (int *) != (int) a = &count;//(int) = (int *) b = &count;//(int *)=(int *) ``` #### 指標的間接運算 * ![image](https://hackmd.io/_uploads/B1VrsQXu1e.png) * ![image](https://hackmd.io/_uploads/SkGni7Xu1l.png) ```c int countA = 9; int countB = 10; int *countAddr; countAddr = &countA; *countAddr = 0; countAddr = &countB; *countAddr = 0; ``` ![image](https://hackmd.io/_uploads/S1bpOwmuyl.png) ![image](https://hackmd.io/_uploads/SJkdKwQ_kx.png) #### 指標與函式呼叫 * 函式呼叫的特性:呼叫函式時,做為引數的變數會被複製一份至函式裡成為參數,在被呼叫的函式內對參數做任何變動都不會改變原本的參數。 * ![image](https://hackmd.io/_uploads/BJYJHu7_yg.png) #### 該傳值還是位址 ```c #include<stdio.h> void swap(int *,int *) int main(){ int a = 3, b= 5; swap(&a,&b); printf("a:%d \n",a); printf("b: %d\n",b); return 0; } void swap(int *a,int *b){ int t = *a; *a = *b; *b =t; } ``` * 基本原則: * 可以傳值就傳值(因為會複製一份,不用怕被偷改,確保函式之間乾淨的關係)。 * 用起來比較方便(可以傳一般的常數)。 * 例外: * 作為引數的變數在呼叫函式值會變動的時候(`ex:swap`)。 * 無法直接複製值的時候(`ex:array、string`)。 * 複製成本較高時(較複雜的結構)。 #### 指標對整數的加減運算 ```c int v[5]; &v[0]+1 == &v[1] //可以透過將陣列元素的位址加減一個整數來求得求餘元素的位址。 &v[0]+&v[1]// 編譯失敗,C語言沒有定義指標相加指標代表的意義。 &v[2]-&v[1] == 1//從v[2]的位址到v[1]的位址距離一個元素 ``` * ![image](https://hackmd.io/_uploads/S1FctOmuye.png) #### 指標與陣列 * ![image](https://hackmd.io/_uploads/SJZ0-FQdke.png) * 陣列可以退化成指標,陣列只是一連串指標所構成的。 * ![image](https://hackmd.io/_uploads/B1sfzFm_kg.png) ```c #include<stdio.h> int main(){ int v[5]={1,2,3,4,5}; int *n = v;// int *n =&v[0] for(n=v;n!=&v[5];n++){ printf("%d\n", *n); } return 0; } ``` * ![image](https://hackmd.io/_uploads/S1TeEKX_yg.png) #### 指標與下標運算子 ```c int v[5]; int *n = v; n[0]=0;//a[b] = *(a+b) n[0]就是*(n+0); ``` * 下標運算子類似語法糖,無論是對指標或是陣列做都可以用同樣方式去解釋。 * ![image](https://hackmd.io/_uploads/BJC8HFQ_yl.png) #### 在函式間傳遞陣列(使用指標) * 在傳遞陣列時,最重要的是陣列中起始值的記憶體位址,後續可以透過加減運算得到陣列其餘的記憶體位址。 * ![image](https://hackmd.io/_uploads/H1LQItm_yx.png) #### 指標與陣列的關係 * 指標儲存某陣列元素的記憶體位址的特殊性 * 可以透過加減整數算出同陣列其他元素的記憶體位址。 *` a[b]`運算等同於`*(a+b)`,反之亦同。 >* 在該陣列中從a開始往後移動b所在的陣列元素 >* 當指標儲存某陣列第一個元素的記憶體位址,用起來與陣列相同。 * 陣列可以隱性轉型成該陣列第一個元素的記憶體位址。 #### 指標與遞增及遞減運算子的關係 `Ex`:陣列歸零 ```c int main(){ int v[5]; int *p; /*for(p=v;p!=&v[5];p++){//v[5]雖然不能讀取,但其本身記憶體位址是存在的 *p=0; }*/ while(p!=v+5){ *p++=0; //*(p++)=0 } return 0; } ``` #### 指標與字串 * 字串字面常數可以直接隱性轉型成字元指標。 * ![image](https://hackmd.io/_uploads/r1STFtX_Jx.png) #### 字串字面常數的特殊性。 * 將字串字面常數宣告成字元指標時,雖有記憶體空間儲存,但是唯讀,若要修改會變成未定義行為。 * 宣告字串陣列,可以更改單獨字元,但無法全部替換。 * 宣告字元指標,可以替換全部字串,但無法更改單獨字元。 * 上述兩種差異為宣告定義變數之記憶體空間的差異。 #### const修飾字 * 資料型別被`const`修飾的變數在初始化之後不能再被賦值。 * 在`C`語言可以看待成唯讀的屬性。 * ![image](https://hackmd.io/_uploads/SJVnoFQOJl.png) * ![image](https://hackmd.io/_uploads/BkEniFmdkg.png) #### 字串字面常數與 `const char*` * 一般來說有放字串字面常數的位置通常都是`const char*`的形式。 * ![image](https://hackmd.io/_uploads/ryVEnFX_kx.png) * ![image](https://hackmd.io/_uploads/ry_82Y7dye.png) #### 指標與const * 一個`type*` 可以轉型成`const type*` * ![image](https://hackmd.io/_uploads/BkDkRFQuJe.png) #### 使用函式複製字串 * ![image](https://hackmd.io/_uploads/SkAOAKQ_1l.png) * ![image](https://hackmd.io/_uploads/r1-3AFmOJl.png) * ![image](https://hackmd.io/_uploads/BJ3ZkqQukx.png) * ![image](https://hackmd.io/_uploads/rJTQJ9m_Jx.png) #### 指標陣列 ```c int v[3]={1,2,3}; int *p[3]={&v[0],&v[1],&v[2]}; //循序存取 int i; for(i=0;i<3;i++){ *p[i] = 0; } // 隨機存取 *p[2] = 5; ``` * 指標陣列與陣列隱性轉型成指標不同點為:前者可以儲存整個陣列的記憶體位址並且可以做更改,而後者為指向陣列第一個元素的記憶體位址。 * ![image](https://hackmd.io/_uploads/S1BZqwNdyl.png) * ![image](https://hackmd.io/_uploads/ryu1jPNdJl.png) #### 陣列的指標 ```c int v[3]={1,2,3} //address of array時,其取址的型別為 (type(*)[n])&array_name int(*q)[3]= &v;// int i; for(i=0;i<3;i++){ (*q)[i]=0; } ``` * `q`是一個指標指向一個記憶體空間,空間有三個元素,每個元素都是`int`。 * `(int *[3])`像是陣列有三個元素每個都是`(int *)`而 `(int (*)[3])`才代表本身是個指標指向三個元素的整數陣列 ```c #include<stdio.h> void print(int(*q)[3]){ int i; for(i=0;i<3;i++){ printf("%d",(*q)[i]); } printf("\n"); } int main(){ int v[3]={1,2,3}; print(&v); return 0; } //在函式中傳遞固定大小的陣列 ``` #### 在函式間該使用何種指標傳遞陣列 ```c void print(int(*q)[3]){ int i; for(i=0;i<sizeof(*q)/sizeof((*q)[0]);i++){ printf("%d",(*q)[i]); } printf("\n"); } int main(){ int v[3]={1,2,3}; print(&v); return 0; } //在函式中傳遞固定大小的陣列 ``` ```c void print(int *n,int size){ int i; for(i=0;i<size;i++){ printf("%d", n[i]); } printf("\n"); } int main(){ int v[3]={1,2,3}; print(v,size); return 0; } ``` * 當把整數陣列隱性轉型成指標時 就失去長度的概念 此時需額外再傳一個長度資訊進來,所以用前者的寫法我們可以獲得正確的陣列長度 * 後者寫法雖需額外傳長度參數,但可以共用函式,前者寫法不能共用函式。 #### 在函式中傳遞二維陣列 ```c void print(int (**v),int height,int width)//v是一個指標指向這個陣列 { int i,j; for(i=0;i<height;i++){ for(j=0;j<width;j++){ printf("%d"); } printf("\n"); } } int main(){ int v[3][3]={{1,2,3},{4,5,6},{7,8,9}}; print(v,2,3);//(int(*)[3])=(int(*)[3]) //(x)int(*)(*)!=int(*)[3] return 0; } ``` #### 使用指標陣列在函式間傳遞二維陣列 ```c void print(int (**v),int height,int width)//v是一個指標指向這個陣列 { int i,j; for(i=0;i<height;i++){ for(j=0;j<width;j++){ printf("%d"); } printf("\n"); } } int main(){ int v[3][3]={{1,2,3},{4,5,6},{7,8,9}}; int *p[2]={v[0];v[1]};//p是兩個元素的陣列 每個元素都int的指標,(int *)=(int[3]) print(&p[0],2,3);//(int ** )=(int **) return 0; } ``` #### 在函式間傳遞任意長寬的二維陣列 * ![image](https://hackmd.io/_uploads/Hkk9vuNd1g.png) * ![image](https://hackmd.io/_uploads/H1_Cv_VOkg.png) ```c void print(int (**v),int height,int width)//v是一個指標指向這個陣列 { int i,j; for(i=0;i<height;i++){ for(j=0;j<width;j++){ printf("%d",v[i][j]); } printf("\n"); } } int main(){ int v[3][3]={{1,2,3},{4,5,6},{7,8,9}}; int *p[2]={v[0];v[1]};//p是兩個元素的陣列 每個元素都int的指標,(int *)=(int[3]) print(&p[0],2,3);//(int ** )=(int **) return 0; } ``` * ![image](https://hackmd.io/_uploads/rk8JztEuJg.png) #### 儲存多個字串 ``` char strA[3][4]={"How","Are","you"};//使用二維陣列儲存字串 const char *strB[3]={"How","Are","you"};//使用指標陣列儲存字串 ``` * ![image](https://hackmd.io/_uploads/rJk67YEdJx.png) * ![image](https://hackmd.io/_uploads/H10b4KN_ke.png) ##### 可修改內容的多個字串 * ![image](https://hackmd.io/_uploads/Byz48FVOye.png) #### 輸入不定個數字串的練習 ```c #include<stdio.h> #include<string.h> int main(){ char input[5]; char str[100][5]; int len = 0; while(1){ scanf("%s",input); if(strcmp(input, "END") == 0){break;} strcpy(ste[len], input); ++len ; } printf("------\n"); int i; for(i=0;i<len;i++){ printf("%s", str[i]);} printf("\n"); return 0; } ``` * optimization ```c #include<stdio.h> #include<string.h> int main(){ char raw[5000]; char input[50]; char *str[100]; int size = 0; int len = 0; while(1){ scanf("%s",input); if(strcmp(input, 'END')==0) break; str[len] = &raw[size]; strcpy(&raw[size], input); size += strlen(input)+1; ++len; } printf("------\n"); int i; for(i=0;i<len;i++){ printf("%s", str[i]);} printf("\n(%d, %d\n)",len,size); return 0; } ``` #### 指標轉型的限制 * 絕大部分的情況下,指向不同型別的指標是不能隱性轉型的。 ```c int intVar; double doubleVar; int *intPointer = &doubleVar;// (int *)=(double *)(?) double *doublePointer = &intVar;//(double *)=(int *)(?) int **intPointerPointer1 = &intVar //(int **)=(int*)(?) int **intPointerPointer2 = &intArray //(int **)=(int(*)[3])(?) int **intPointerPointer3 = intArray //(int **)=(int[3])(?) const int *c = &a; ``` * `int (*q)[ ] =>int *(*q) => q`本身是個指標 指向一個整數的指標 * 整數的指標,不能隱性轉型成整數的指標的指標。 * 指向型別不一樣,就是不行隱性轉型。 #### 合法的隱性轉型 * ![image](https://hackmd.io/_uploads/HJkYL9E_Jg.png) ```c int v[3]; int *n; const int *p; //陣列可以隱性轉型成指向第一個元素的指標。 n = v;//(int *) =(int [3])(o) v = n;//(int [3]) = (int *)(x) //Type*可以隱性轉型成 const Type* p =n;//(const int*) =(int*)(o) n = p;//(int*) = (const int*)(?) ``` * ![image](https://hackmd.io/_uploads/By7Xuc4OJx.png) * 可以將`void`指標想像成泛用型別 * 但`void`沒辦法被取值。 #### 指標與整數間的轉型 電腦如何儲存指標? * 指標是儲存記憶體位址的資料型別 * 記憶體位址長怎樣? * 把記憶體想成一段連續的空間,我們以位元組為單位,替每個位元組給一個獨一無二的編號或表示法。 * 編號要怎麼編? * ![image](https://hackmd.io/_uploads/HkWrqc4uke.png) * ![image](https://hackmd.io/_uploads/HJoFcqEdyl.png) * ![image](https://hackmd.io/_uploads/HyGhc9E_Jg.png) #### 指標與指標間的強制轉型 * 記憶體對齊:每個型別對齊大小是實作定義。 * ![image](https://hackmd.io/_uploads/ry46iqVOye.png) * ![image](https://hackmd.io/_uploads/BkRpsc4O1e.png) #### 使用同一個指標指向不同陣列 * ![image](https://hackmd.io/_uploads/r12XKABdJg.png) * ![image](https://hackmd.io/_uploads/HkmAtABdyg.png) * ![image](https://hackmd.io/_uploads/r1oi5RB_ke.png) * ![image](https://hackmd.io/_uploads/BJBnq0r_Jx.png) #### 變數的生命週期 * ![image](https://hackmd.io/_uploads/S1pR_1IOkl.png) * ![image](https://hackmd.io/_uploads/S1leYyUdJx.png) ### 動態記憶體配置 #### 使用 `malloc`函式動態配置記憶體 * `<stdlib.h>`提供`malloc`函式讓我們動態配置記憶體。 * `void malloc(size_t size);` 。 * `size`為非負整數型態(`size_t`),表示要配置的記憶體空間大小(`byte`)。 * 可以用`sizeof`運算子來得知需要配置的記憶體空間。 * 回傳值型態為`void`表示可以隱性轉型成其他資料型態的指標。 * ![image](https://hackmd.io/_uploads/SJBZi18_yx.png) * ![image](https://hackmd.io/_uploads/B1PuskLukg.png) * `larger`一樣是被視為自動變數會釋放記憶體空間,但被`larger`使用`malloc`配置所指向的記憶體空間不會被釋放。 #### 使用`free`函式釋放動態配置的記憶體 * 有借但沒還的記憶體空間會導致計算機產生記憶體洩漏`(Memory leak)` * `<stdlib.h>`提供`free`函式來釋放動態配置的記憶體。 * `void free(void* ptr);` * 當需要使用`free`時的指標沒有明確指向記憶體空間,會產生未定義行為 * 所以通常初始化會先用`NULL pointer(空指標)`來去確定指標是否有指向記憶體位址,`free`若是碰到`NULL pointer(空指標)`會知道說喔喔沒有指向記憶體空間。 #### 使用`realloc`函式重新配置記憶體大小 * `<stdlib.h>`提供`realloc`函式來複製記憶體內容。 * `void* realloc(void* ptr,size_t size);`。 * `ptr`是要重新配置的記憶體空間開頭位址。 * 原本由`malloc`或其他動態記憶體配置函式所配置。 * `size`是重新配置後的記憶體空間大小(`byte`)。 * 回傳值為重新配置後的記憶體空間開頭位址。 #### 函式指標(function pointer) ```c #include<stdio.h> int main(){ int a = 3; int* b = &a; *b = 5; // *b=*(&a)=a; printf("a: %d\n",a); return 0; } ``` ```c #include<stdio.h> //void hello(); void hello(){ printf("Hello world!\n"); } int main(){ void hello(); //void func()=hello();//函式型態不能複製(x) void (*func)()=&hello;//宣告定義func指標,指向void型態的函式 (*func)();//呼叫func函式 return 0; } ``` * 函式宣告可以放在另一個函式定義裡。 * 函式型態不能複製,但指標型態可以複製 #### 函式指示符及其特殊性 函式與物件的異同 在`C`語言中函式與物件是不同的概念但有些相似的地方。 * 共用名稱的概念。 * 變數:可以來存取對應的特定物件。 * 函式名稱/指示符:可以來代表某個特定函式。 * 都可以使用指標相關的運算。 * 可對名稱取址(`&`)、獲得指標值(`T*`)。 * 可用指標變數(`T*`)儲存位址。 * 對指標(`T*`)進行間接運算(`*`)可以取得該位置代表的特定物件或特定函式 ```c #include<stdio.h> int main(){ printf("Hello world!\n");//printf是函式名(name)也是函式指示符(designator),printf可以隱性轉型成&printf (&printf)("Hello world\n");//&printf是個函式指標,指向printf所代表的函式。 (*printf)("Hello world\n");//可以自動轉型成&printf // *printf => *&printf => printf => &printf } ``` * 在`C`語言中函式都是使用函式指標進行呼叫的。 #### 函式指標相關運算化簡 ```c #include<stdio.h> void hello(); void hello(){ printf("Hello world!\n"); } int main(){ //void hello(); //void func()=hello();//函式型態不能複製(x) void (*func)()=hello;//宣告定義func指標,指向void型態的函式,且hello會自動隱性轉型成&hello func();//呼叫func指標所指向的函式 //(*******func) ()=(*func)()=func() return 0; } ``` #### 在函式間傳遞函式 * 函式指標最常見的用途為在函式間傳遞函式。 ```c #include<stdio.h> int add(int,int);//add是函式指示符,該函式型態為int(int,int) int main(){ int (*op)(int,int)=add;//op是函式指標,指向型態為int(int,int)的函式。 int result = op(3,5); printf("%d\n",result); return 0; } int add(int a,int b) { return a+b; } ``` * ![image](https://hackmd.io/_uploads/r1sjOxIuyg.png) * `op`必定指向必須要為函式。 #### main 函式的回傳值 * ![image](https://hackmd.io/_uploads/BkGmYlU_yg.png) * 在程式內執行其他程式 * 使用`system()`函式可以執行其它程式(宣告於`stdlib.h`)。 * `int system(const char* command)`; * `system`會執行檔案路徑為`command`的程式。 * `system(hello.exe)`會執行同一個目錄為`hello.exe`的執行檔(可以想成會呼叫`hello.exe`的`main`函式) * `system()`的回傳值就是所呼叫程式`main`函式的回傳值。 * ![image](https://hackmd.io/_uploads/HkSKqeIO1x.png) * ![image](https://hackmd.io/_uploads/Byancl8Okx.png)