--- title: '指標 & Array & typedef' disqus: kyleAlien --- 指標 & Array & typedef === ## OverView of Content 指標對於底層系統開發來說相當重要,而驅動又是透過控制 Register 來控制硬體,在這操控中就常常使用到指標 :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**指標 & Array & typedef | 指標應用的關鍵 9 點 | 指標應用、細節**](https://devtechascendancy.com/pointers-arrays-const-typedef-sizeof-null/) ::: [TOC] ## 指標概念 * **普通變數**:首先我們要知道一般的讀寫都不會涉及 **強制轉換**,哪種類型的變數,就會以相對應的格式存在 RAM 中 ```c= int a = 10; long b = (long) a; ``` > ![](https://i.imgur.com/Od8sces.png) * **指標變數**: 1. 可以把指標當成是另一種類型的宣告 2. 指標的內容:跟普通變數一樣,是儲存某一個數值,只是 **指標是儲存一個地址** (使用關鍵字 `&` 來取得某個變數的地址) ```c= int a = 10; // 儲存 10 int* p = &a; // 儲存 a 的地址, p 本身也有地址 ``` :::info 指標本身也有地址 ::: > ![](https://i.imgur.com/jpg9Pmi.png) ### 指標特性 - 首地址 * 內存的大小:這取決於 **尋址總線數量**,如果尋址總現有 32 條,那地址最大可到 2^32^;相對來說,如果有 64 條,那地址最大可到 2^64^ * 由於 **在電腦中,硬體是以 1 Byte 作為單元切割**,所以當一個指標拿到一個類型的首地址,就會自動順延到符合該類型的長度內容 ```c= int a = 10; // 取得 a 的第一個 Byte 的首地址,自動往後推 3 Byte // 最終以 4 Byte 作為該變量的空間 int* p = &a; // -------------------------------------------------------- struct hello_t { int a; int b; int c; }; struct hello_t hello_world = {0}; // 取得 hello_world 的第一個 Byte 的首地址,自動往後推 11 Byte (符合類型) // 最終以 12 Byte 作為該變量空間的地址 struct hello_t* hw = &hello_world; ``` :::info * 指標可以透過類型推導出接下來需要幾個 Byte 的數據 * 一個地址能儲存的大小就為 1 個 Byte (跟硬體有關) > ![](https://i.imgur.com/18R4g8Z.png) ::: ### 指標級量 * 指標可以指向另外一個指標,層層疊加,這就是指標的級量 ```c= int a = 10; // 一級指標 int* b = &a; // 二級指標 int** c = &b; // 三級指標 int*** d = &c; ``` > (n + 1 級) = &(n 級) :::danger 建議指標不要超過 3 級,不僅降低了可讀性,效率也會不好 ::: ### 指標其他作用 - 作用域 * 一般我們在規範函數不讓其它文件訪問時會使用 static,但是只要透過指標就可以取得該函數並執行 `static` 關鍵字在 C 語言中相當於「存取限制符號」,使用 `static` 描述的函數、屬性,只能在該檔案中被訪問,其他檔案不可訪問! ```c= // 只有該檔案內部可訪問 static void hello() { } // 只要有 include 的檔案都可以使用 void world() { } ``` ## 指標使用 ### 指標符號 * **`*` 符號**: 1. **宣告指標變量使用 ,前後都可** ```c= int *p; // 同上 int* p; // ------------------------------------------------ int *p1, *p2; // 兩個指標變量 int *p1, p2; // 一個指標、一個整數變量 ``` 2. **解引用** ```c= int a = 10; int *p = &a; // 解引用,並賦予值 *a = 20; ``` * **`&` 符號**: 取變量的第一個 Byte 的地址 ```c= int a = 10; int *p = &a; ``` ### 野指標 * 只要指標可能出現未知性錯誤,它就是一個野指標;可能產生野指標的操作如下 :::warning * `Segmentation Fault`:程式為了防止雪崩性錯誤,會使用 `Segmentation Fault` 停止該應用程式;而錯誤又分為兩種 * 大段錯誤:地址不存在 * 小段錯誤:地址存在,但訪問受限 ::: 1. 尚未初始化就直接使用 ```c= void wild_ptr() { char* p; *p += 1; // Segment Error } ``` 2. 不清楚空間權限,試圖訪問、修改資料 ```c= void wild_ptr_2() { char *p = "hello"; // "hello" 放置在常量區,常量區不許須改 *(p + 1) = 'w'; // Segment Error } ``` 3. 越界訪問 ```c= void wild_ptr_3() { int buf[4] = {0}; *(buf + 4) = 10; // + 4 已越界 } ``` :::info * `*(buf + 4)` 與 `*buf + 4` 是不同的,這有關於符號的優先度 1. `*(buf + 4)` 是 buf 這個地址加上 4 個 Byte 2. `*buf + 4` 是數組第一個數 buf[0] 再加 4 ::: ### What is Null * Null 在 C/C++ 中是不同定義的存在,C++ 中被定義為 0,但是在 C 中定義為 `(void*) 0`,會被嚴格檢查 ```c= #ifdef _cplusplus #define NULL 0 #else #define NULL (void*) 0 #endif ``` ## const 修飾符 const 也就是 constant 代表不變,用來修飾變量,**希望變量轉為常量** ### const 修飾普通變數 * 修飾變數,不管 const 是在前還是在後都可以,只要保證在變數之前即可 1. 類型之前 ```c= #include <stdio.h> int main() { const int a = 10; a = 30; // const 不可修改,所以編譯會錯 printf("Hello: %d\n", a); return 0; } ``` > ![](https://i.imgur.com/2uttY4B.png) 2. 類型之後 ```c= #include <stdio.h> int main() { // 不同之處 int const a = 10; a = 30; // const 不可修改,所以編譯會錯 printf("Hello: %d\n", a); return 0; } ``` ### const 修飾指標 * **const 修飾指標有三種表現方式**,個代表了不同的限制(注意 `const` 放置的位置) 1. **const 修飾指標指向的空間**:及說明 **該空間的內容物為常量**;修飾指標指向的空間常量有 2 種表達方式,代表的意思是相同 ```c= void const_ptr_1() { int tmp = 20; int const *a = &tmp; // const int *a = &tmp; // 同上 *a = 100; // Read-only 編譯器檢查錯誤,不可修改 ! printf("Hello: %d\n", *a); } ``` > ![](https://i.imgur.com/KdR7AHX.png) 2. **const 修飾指標**:使用 const 修飾指標說明 **該指標指向不可在修改**,也就是 **指標無法再指去其他地方**,但其 **值能是可被修改** ```c= void const_ptr_2() { int tmp = 20; int * const a = &tmp; *a = 100; // OK int tmp2 = 30; a = &tmp2; // Read-only 編譯器檢查錯誤,不可修改 ! printf("Hello: %d\n", *a); } ``` > ![](https://i.imgur.com/BYRFhmT.png) 3. **const 修飾指標 & 修飾指向空間**:代表指標 & 其指向的內容物都不可以修改 ```c= void const_ptr_3() { int tmp = 20; int const * const a = &tmp; // const int * const a = &tmp; // 同上 *a = 100; // Read-only 編譯器檢查錯誤,不可修改 ! int tmp2 = 30; a = &tmp2; // Read-only 編譯器檢查錯誤,不可修改 ! printf("Hello: %d\n", *a); } ``` > ![](https://i.imgur.com/773OIQw.png) ### 指標修改 const * **const 機制是通過編譯器檢查實現**,實際上真正運行的過程中並不關心變數是否被 const 修飾,只要保證編譯通過,程式仍可跳過 const 檢查 ```c= void const_ptr_4() { const int a = 100; int *p = NULL; p = &a; // 會有警告而已 *p = 300; printf("Hello: %d\n", a); // 可修改 a 的值 } ``` :::success * 既然可以被修改,那為何要使用 const 修飾? 讓程式更加健壯,提醒使用者某修地方不能被修改,或是保證不會被修改 ::: ### const & 變量 & 常量 * 在程式中我們常會使用 `1`、`2`、`3`、`"HELLO"`... 等等數值,而這些數值在編譯器的處理下會以兩種方式存在 RAM 中 1. **變量** 經過編譯後,會將 **變量放置在 `.data`、`.bss` 中,常出現在 `堆`、`棧` 中**,這些變量都是 **可讀可寫**;經過編譯檢查 const 關鍵字,可將這些數值看做 **偽常量** > 真正的常量不可修改,而偽常量 其實仍可修改 :::info Linux 可使用 `readelf` 來查看編譯出來的執行檔中的 `.data`、`.bss` 區塊 ::: 2. **常量** 經過編譯後,會將 **變量放置在 `.ro.data` 中,訪問權限為 可讀 (不可改)** ```c= // p 儲存首字 `H` 的地址,而 "Hello const" 則是放置在常量區 char *p = "Hello const"; // 如果透過指標修改這個變量,則會失敗 (Segmention Fault) ``` ## Array 深刻了解一維數組,是了解二維(甚至多維)數組的關鍵 ```c= // 格式如下 <類型> <變量名>[<數量>] int a[100]; // 0 ~ 100 long b[1] // 0 ~ 1 ``` :::info 以內存(記憶體)的角度來看,Array 的物理記憶體是連續的,並不會斷開,所以訪問速度也快 ::: ### Array 訪問 1. **變量名訪問**:最基礎的訪問方式就是透過變量名稱來訪問 ```c= void accessByName() { int a[10] = {0}; a[0] = 100; printf("a[0]: %d\n", a[0]); a[10] = 9; // 越界,但仍可正常設定值 printf("a[10]: %d\n", a[10]); } ``` > ![](https://i.imgur.com/Y5O4WQ0.png) 2. **指標訪問**:**不受到編譯器的 作用域檢查 規範** ```c= void accessByPtr() { int a[10] = {0}; int *p = a; // a 本身就是一個地址,加上了 `[]` 才能解析其內容 *p = 100; printf("a[0]: %d\n", a[0]); *(p + 10) = 9; printf("a[10]: %d\n", a[10]); } ``` > ![](https://i.imgur.com/CjakUPF.png) ### 一維數組 & 符號 * 數組的符號有 4 種不同的意思(而部分其中還有細分,說明如下):^1^ `a`、^2^ `a[0]`、^3^ `&a[0]`、^4^ `&a` ```c= int a[10] = {0}; ``` 1. **Array 符號 `a`**:有兩種含意 * Array 名稱:`sizeof(a)` 時,可以計算出該數組占用幾個 byte 大小 * Array 的第一個地址:等同於 `&a[0]`,是 **數組的首元素的首個字節**,是一個常量值 :::info * 如果是地址,那代表了是常量不可修改,所以永遠不會是左值 ```c= int a[10] = {0}; a = 1000; // a 是常量,不可為左值 (被賦予) ``` ::: 2. **Array 符號 `a[0]`**:取第一個元素的空間,並可以對其讀寫操作 ```c= int a[10] = {0}; printf("a[0]: %d\n", a[0]); // read a[0] = 1000; // write ``` 3. **Array 符號 `&a[0]`**:取締一個元素的首位元空間地址,就等同於符號 `a` 4. **Array 符號 `&a`**:數組首地址,代表一個地址常量,同樣不可為左值 :::info * **符號 `&a`、`a` 的差異 ?** 兩者的數值皆是 Array 的首地址,但是 **==意義完全不同==**;`&a` 代表該空間的全體,而 `a` 只代表了該空間的 1 個元素 ```c= void symbleTest2() { int a[5] = {0}; printf("a: %p, &a: %p\n\n", a, &a); printf("a+1: %p, &a+1: %p\n", a+1, &a+1); } ``` * `&a+1` 代表地址前進 `int a[5]` * `a+1` 代表地址前進 1 個 `int` > ![](https://i.imgur.com/yzw6Uhm.png) ::: ### 指標 & Array * 一般訪問 Array 是透過 index 來指定要訪問 Array 的第幾個元素 ```c= void iterate_array() { int array[10] = {0}; for(int i = 0; i < sizeof(array)/sizeof(array[0]); i++) { // array[i] 使用 index 取元素 printf("array[%d]: %d\n", i, array[i]); } } ``` > ![](https://i.imgur.com/ur5JGek.png) * **Array 的 ==變數名本身就有指標的意義==**,所以可以透過指標來訪問,這指標也分為兩中 ^1.^ 常量指標 (不可修改)、^2.^ 變量指標 (可修改) 1. **常量指標**:常量指標其實就是代表 Array 宣告的變數名 (Symble),該變數不可再修改,它是一個常量值 ! ```c= void ptr_with_array_1() { int array[10] = {0}; for(int i = 0; i < sizeof(array)/sizeof(array[0]); i++) { // 使用 `array + i` 改變常量指標 array printf("array[%d]: %d\n", i, *(array + i)); } } ``` :::danger * **可否修改成 `*(array++)` ?** **不行!因為 Array 宣告出的變數名是一個常量,既然是常量就不可以修改 !** > ![](https://i.imgur.com/2wIcJzW.png) ::: 2. **變量指標**: ```c= void ptr_with_array_2() { int array[10] = {0}; int *p = array; for(int i = 0; i < sizeof(array)/sizeof(array[0]); i++) { // 如果是變數,就可以修改為 *(p++),以下還有幾種方案,都可以達到相同的效果 // *(p++) 可以寫作 // 1. p[i] // 2. *(p + 1) // 3. *(p + 1 * sizeof(int)) printf("array[%d]: %d\n", i, *(p++)); } } ``` :::danger * 以下寫法是錯誤的 ```c= int array[10] = {0}; // 錯誤! array 原本就是首地址的第一個 Byte,語意變成了 // array 首地址的首地址 p = &array; ``` ::: * 使用指標位置 + 1,編譯器會依照指標的大小,新增一個單元 ```c= int array[10] = {0} int *p = array; int a = *(p + 1); // (p + 1) 相等於 (p + 1 *sizeof(int)) ``` ## 指標類型 & 強制轉換 對於編譯器來說,數據類型就是告訴編譯器該變數,要已什麼樣的格式儲存 (數據結構),儲存的空間又是多大 ? 1. **儲存空間**:依照類型、硬體裝置,編譯器會給予不同變量,不同空間大小 ```c= sizeof(char); // 1 Byte sizeof(short); // 2 Byte sizeof(int); // 4 Byte sizeof(float); // 4 Byte sizeof(double); // 8 Byte ``` 2. **儲存結構**:即便是相同大小的 **`int`、`float` 儲存格式也不同** > float 是用科學計數法形式儲存 (其中就包括:小數、指數、符號... 等等) ```c= int a = 10; printf("%d", a); // 正確 printf("%f", a); // 亂碼 // -------------------------------------------- float b = 10; printf("%d", b); // 亂碼 printf("%f", b); // 正確 ``` ### 一般類型強制轉換 - 顯示 * 強制轉換一般類型,需要注意幾個點:**`空間`、`結構`** * **空間大小改變** 1. **小轉大**:沒啥問題,資料可以正常轉換 ```c= void small_to_big() { short a = 10; int b = (int)a; printf("b value: %d, size: %d", b, sizeof b); } ``` > ![](https://i.imgur.com/hLd5r4j.png) 2. **大轉小**:**小心數據丟失、改變**!(並非一定會丟失,但是很大機率會將數據解釋錯) ```c= void big_to_small() { int a = 0x0000ffff; printf("a value: %d, size: %d\n", a, sizeof a); short b = (short)a; printf("b value: %d, size: %d", b, sizeof b); } ``` > ![](https://i.imgur.com/ZTJGt9g.png) * **結構改變**:將原儲存資料方式就不同的結構強制轉型,像是浮點數(浮點數的儲存結構很不同)與整數的轉換 1. 整數轉浮點數,**數據不丟失** ```c= void change_struct_1() { int a = 100; // 轉換儲存結構 float b = (float) a; printf("b value: %f, size: %d", b, sizeof b); } ``` > ![](https://i.imgur.com/Z5oiMu8.png) 2. 浮點數轉整數,**數據丟失** ```c= void change_struct_2() { float a = 3.1415926; // 丟失小數部分數據 int b = (int)a; printf("b value: %d, size: %d", b, sizeof b); } ``` > ![](https://i.imgur.com/kFmEkhJ.png) ### 一般類型強制轉換 - 隱示 * 上面使用 `(<類型>)` 來做強制轉換,而隱式轉換則如下 1. **使用 `=` 符號的隱示轉換**: ```c= void implict_change_1() { char c = 0x11223344; printf("c value: %d, size: %d", c, sizeof c); } ``` > ![](https://i.imgur.com/KVaKN3j.png) :::info * 這種隱示轉換不安全,通常會有編譯器發出警告(會警告,但並不代表有問題) > ![](https://i.imgur.com/e1j6IPu.png) ::: 2. **當使用 `return` 關鍵字,在返回之前隱示轉換**: ```c= int implict_change_2() { char c = 0x11223344; return c; } ``` :::info * 這種 `return` 隱示轉換不安全,通常會有編譯器發出警告(會警告,但並不代表有問題) > ![](https://i.imgur.com/R8OBcx9.png) ::: ### 指標強制轉換 * 指標轉換「最好使用」顯示轉換,盡量不要使用隱式轉換;指標轉換也涉及兩個層面:^1.^ 指標類型轉換、^2.^ 指標指向類型 1. **指標類型轉換**:改變數據的解析方式 ```c= void ptr_value_change_1() { int a = 100; int *pa = &a; printf("a ptr value: %d, size: %d\n", *pa, sizeof(*pa)); float *pb = NULL; // 指標類型轉換 pb = (float*) pa; printf("b ptr value: %f, size: %d", *pb, sizeof(*pb)); } ``` > ![](https://i.imgur.com/TWaa9aY.png) :::warning * **結構的解析方式不同,導致數據解析異常** 從這邊可以看出,改變指標的類型,使用 `*` 也會改變對於該結構的解析方式;上面改變指標為 `float*` 導致解析時不以 `int` 的結構來解析數據內容 ::: 2. **指標指向類型**(不改變指標,改變解指標後的數據) ```c= void ptr_value_change_2() { int a = 0; float b = 3.1415926; int *pa = &a; float *pb = &b; *pa = (int)*pb; printf("a ptr value: %d, size: %d", *pa, sizeof(*pa)); } ``` > ![](https://i.imgur.com/ryhoQRy.png) ## sizeof `sizeof` 看似 Function,但其實它是 C 語言中的 **運算符號 !** ### sizeof vs. array ```c= char array[] = "Hello"; ``` | sizeof 計算 | 結果 | 說明 | 注意 | | -------- | -------- | -------- | - | | sizeof(array) | 6 | 前面有說過 array 符號用在 **`sizeof` 會計算該 Array 所有的空間** | 包括 `\0` (字符算的結尾) | | sizeof(array[0]) | 1 | 一個 array 元素大小 | | | strlen(array) | 5 | 使用 C 標準函式庫 | 會解析到 `\0` 為止 (不包含),所以結果會少 1 | * 測試 1:測試 Array 首指標、Array 元素、C 標準庫的 `strlen` 測試 ```c= #include <stdio.h> #include <stdlib.h> void sizeof_vs_array() { char array[] = "Hello"; printf("sizeof(array): %d\n", sizeof(array)); printf("sizeof(array[0]): %d\n", sizeof(array[0])); printf("strlen: %d\n", strlen(array)); } ``` > ![](https://i.imgur.com/VRviSfv.png) * 測試 2:`sizeof` 配合 Array 首元素、首地址、C 標準庫的 `strlen` 測試 ```c= #include <stdio.h> #include <stdlib.h> void test_ptr_strlen() { char str[] = "Hello"; char* p = str; printf("sizeof(*p): %d, sizeof(p): %d, strlen(p): %d\n", sizeof(*p), sizeof(p), strlen(p)); // 會計算 *p 到 '\0' 之間的位元數 } ``` > ![](https://i.imgur.com/IeHCn4Z.png) ### sizeof vs. 指標 ```c= char array[] = "Hello"; ``` | sizeof 計算 | 結果 | 說明 | 注意 | | -------- | -------- | -------- | - | | sizeof\(p) | 4 | **指標 p 的大小** | sizeof 會根據類型判別大小,array 才會計算整體 | | sizeof(\*p) | 1 | 一個 char 大小 | | | strlen(p\) | 5 | 使用 C 標準函式庫 | 會解析到 `\0` 為止 (不包含) | * 測試:指標、指標取得的元素、C 標準庫的 `strlen` 測試 ```c= void sizeof_vs_ptr() { char array[] = "Hello"; char *p = array; printf("sizeof(p): %d\n", sizeof(p)); printf("sizeof(*p): %d\n", sizeof(*p)); printf("strlen(p): %d\n", strlen(p)); } ``` > ![](https://i.imgur.com/Insfmgb.png) ### array 作為參數傳遞 * C 語言由於 **考慮到 `Stack` 大小的關係** (入參會導致棧溢出),設計在 **傳遞 Array 時,傳入的入參是 `Array address` 而不是整體** ```c= void transfer_array(int array[20]) { printf("inner sizeof(array): %d\n", sizeof(array)); } void template_array() { int array[20] = {0}; printf("outsize sizeof(array): %d\n", sizeof(array)); transfer_array(array); } ``` > ![](https://i.imgur.com/y5iZXJ5.png) * 知道 Function 傳入的是 `Array address` 後,其實可以修改如下(將接收的類型改為 `pointers`),也會有一樣的功能 ```c= void transfer_array_2(int *array) { for(int i = 0; i < 20; i++) { printf("Value: %d\n", *(array + i)); } } void template_array() { int array[20] = {0}; printf("outsize sizeof(array): %d\n", sizeof(array)); transfer_array_2(array); } ``` > ![](https://i.imgur.com/mbJ2KJc.png) :::success * 那要決定用哪一種,要選宣告接收 Pointer 還是 Array? 對於程式的「**可讀性**」來說,我們還是會 **選擇使用 `a[10]` 這種寫法**,因為 **這能明確標示請使用者傳入的是一個數組,而不是一個普通數值的 Ptr** ::: * 使用 Array 類型的另類寫法:使用 Array 前,宣告大小變數,這個變數就可以讓 Array 變數使用 ```c= void transfer_array_3(int size, int array[size]) { for(int i = 0; i < size; i++) { printf("Value: %d\n", *(array + i)); } } void template_array() { int array[20] = {0}; printf("outsize sizeof(array): %d\n", sizeof(array)); transfer_array_3(5, array); } ``` :::warning * **`size` 參數必須定義在 array 之前**! 否則編譯檢查不能通過 > ![](https://i.imgur.com/URN3eX7.png) ::: > ![](https://i.imgur.com/b2OSUwY.png) ## 高級指標 其實沒啥高級指標,應該說是指標比較高級的用法 ### 指標數組 & 數組指標 * C 語言語法的重點是:**++前面是修飾詞++,++後面才是主語++** * 變數 - **指標數組**:代表該變數,主語是 Array,並且修飾(類型)是 Ptr * 變數 - **數組指標**:代表該變數,主語是 Ptr,並且修飾(類型)是 Array | 類型 | 主語 (本質) | 修飾 | 舉例 | | -------- | -------- | -------- | - | | 指標 數組 | 數組 (Array) | 指標 (Ptr) | `int *p[5];` (本質是 Array,每個元素都為 `(int*)`,代表 5 個指標) | | 數組 指標 | 指標 (Ptr) | 數組 (Array) | `int (*p)[5]` (本質是 1 指標,指向 `int[5]` 的空間) | * **`*`、`[]` 在符號的前後,指明了該變量是指標還是數組 !(這還有關於到優先級)** 定義一個符號時思考的步驟如下 1. **找到定義的符號,誰是「核心」** ```c= // `p` 是核心 // `int`、`*`、`[]` 都是為了定義 p int *p[6]; ``` 2. **看誰跟核心最近,誰的結合優先度 (優先級) 高,就先與之結合**;以下嘗試讓核心與不同的符號結合 ```c= // 以下分號 `;` 不結合 // 核心 a,跟 [] 結合,所以是數組 int a[5]; // 核心 p,跟 * 結合,所以是數組 int *p; // 核心 function,跟 `()` 結合,所以是函數 int function(); ``` * 以下列出幾個常見的優先度,從上到下代表優先度的高到低 | 運算符號 | 描述 | 結合性 | | -------- | -------- | -------- | | () | 函數呼叫 | | | [] | Array 引用 | 先於 `*` 符號結合 | | -> | 指標指向成員 | 左到右 | | . | Struct 成員引用 | | | -/+ | 負號、正號 | | | ++/-- | 遞增、遞減 | | | ! | 邏輯否 | | | ~ | 1 的補數 | 右到左 | | * | 指標引用 | | | & | 記憶體位置 | | | sizeof | 計算物件 Byte 大小 | | | (type) | 強制轉型 | | | * | 乘法 | | | / | 除法 | 左到右 | | % | 取模 | | | +/- | 加、減 | 左到右 | :::success 符號的建立、分析,都先從基礎去分析,沒有無緣無故的規則 ! ::: * 以下範例是「指標函數」、「函數指標」正確的使用方式 ```c= // 指標數組 void major_array() { int *p[5]; // 主體是 Array(5 個指標) // int array[5] = {0}; // p = &array; // Error *(p + 0) = 1; // 可用指標的方式訪問 Array *(p + 1) = 2; *(p + 2) = 3; *(p + 3) = 4; *(p + 4) = 5; printf("p: %p, p+1: %p\n", p, (p+1)); } // 數組指標 (主要用在二維數組) void major_ptr() { int (*p)[5]; // 主體是指標(1 個指標) int array[5] = {0}; p = &array; (*p)[0] = 1; // 可用 Array 的方式訪問訪問指標指向的元素 (*p)[1] = 2; (*p)[2] = 3; (*p)[3] = 4; (*p)[4] = 5; printf("p: %p, p+1: %p\n", p, (p+1)); } ``` > ![](https://i.imgur.com/2QlR1tP.png) ### 函數指標 - function pointer * 首先要知道,**函數指標也是一個 ==指標==**,與其它指標並無不同; :::info * **函數(`Function`)到底是什麼**? **函數本值一段程式**,在經過編譯器編譯成匯編碼後,**載入到記憶體 (RAM) 中,是一段連續記憶體**,**而 ==函數指標就是該記憶體的第一個地址==**! ```c= // 偽程式,以下記憶體地址都是假的 // 下面這段程式載入 RAM 中佔用了 0x11221100 ~ 0x11221140 的連續空間 // 函數指標則是 0x11221100 int function_hello() { // 0x11221100 char str[] = "Hello"; // 0x11221110 char* p = str; // 0x11221120 printf("sizeof(*p): %d, sizeof(p): %d, strlen(p): %d\n", // 0x11221130 sizeof(*p), sizeof(p), strlen(p)); } // 0x11221140 ``` ::: * 我們可以看到 `數組指標` 的類型是 `<數組類型> (*)[]`;而 `函數指標` 也是,主要是指標所以它的類型是 `<回傳類>(*)(接收參數)` ```c= #include <stdio.h> void test_hello(void) { printf("Hello Function\n"); } int main(void) { // pFunc 是一個指標 void (*pFunc)(void); // pFunc = &test_hello; // 同上 pFunc = test_hello; pFunc(); // 加上 `()` 代表調用函數 return 0; } ``` > ![](https://i.imgur.com/j5xRmnw.png) ### typedef & 函數指標 * **typedef 這個關鍵字是用來定義新類型**,其實我們上面看到的都是自定義類型 * `數組指標`:像是 `int (*p)[5]` ```c= #include <stdio.h> typedef int (*IntArrayPointer)[5]; int main() { int arr[5] = {1, 2, 3, 4, 5}; IntArrayPointer p = &arr; for (int i = 0; i < 5; ++i) { printf("%d ", (*p)[i]); } return 0; } ``` * `指標數組`:像是 `int *p[5]` ```c= #include <stdio.h> typedef int* IntPointerArray[5]; int main() { int arr[5] = {1, 2, 3, 4, 5}; IntPointerArray p; for (int i = 0; i < 5; ++i) { p[i] = &arr[i]; printf("%d ", *p[i]); } return 0; } ``` * `函數指標`:像是 `void (*p)(int)` ```c= #include <stdio.h> typedef void (*FunctionPointer)(int); void printNumber(int num) { printf("Number: %d\n", num); } int main() { FunctionPointer p = printNumber; p(42); // 調用函數指標 return 0; } ``` :::info * typedef 定義出的新類型並不占用 RAM ::: ### 二重指標 * `二重指標` 可以存放 `一重指標` 的地址 ```c= #include <stdio.h> int main() { int num = 42; int *ptr = &num; // 一重指標指向變數 num int **doublePtr = &ptr; // 二重指標指向一重指標 ptr printf("Value of num: %d\n", num); printf("Value through ptr: %d\n", *ptr); printf("Value through doublePtr: %d\n", **doublePtr); return 0; } ``` * `二重指標` 可以指向 指標數組 (`*p[]`):二重指標也就是 **用來儲存 `指標數組` 的第一個元素的指標變量** | 指標 | 儲存 | | -------- | -------- | | 一重 int* | `int` 的 addr | | 二重 int** | `int*` 的 addr | ```c= #include <stdio.h> int main() { int num1 = 1, num2 = 2, num3 = 3; int *arr[] = {&num1, &num2, &num3}; // 指標數組 int **doublePtr = arr; // 二重指標指向指標數組的第一個元素 for (int i = 0; i < 3; ++i) { printf("Value through doublePtr[%d]: %d\n", i, *doublePtr[i]); } return 0; } ``` > ![image](https://hackmd.io/_uploads/SyWC5xWjp.png) ## typedef 1. **typedef 用來定義新類型,形式越複雜 typedef 的優勢則越明顯** 2. **typedef 的另一個優點是 ==方便移植==** :::danger * **typedef 是一個儲存類的關鍵字**,而 **變量只能被一種儲存類的關鍵字修飾** > 其他儲存類的關鍵字:`auto`、`extern`、`static`、`register` ```c= typedef static int ClzNum[10]; // 編譯錯誤 ``` 錯誤如下 > ![](https://i.imgur.com/rVv18Y5.png) ::: ### typedef 看法解析 1. typedef 是給類型取別名,所以 typedef 定義的東西都是類型;所以 **typedef 定義中一定會有一個 `類型`** 2. typedef 定義出的類型:**移除定義中 typedef 關鍵字**,再將 **類型看作變量**,就能知道它的類型 :::info * 從這裡可以發現將 typedef 關鍵字 移除後,它就只是一個普通的變量語句 ::: * 數組類型 ```c= // MyClass 就是類型 typedef int MyClass[5]; // 去除 typedef 關鍵字 int MyClass[5]; // 再將 MyClass 看成變量 int <變量>[]; // 數組類型 ``` * 函數指標類型 ```c= // MyFunc 就是類型 typedef int* (MyFunc*)(int); // 去除 typedef 關鍵字 int* (MyFunc*)(int); // 再將 MyFunc 看成變量 // MyFunc 變量是一個指標 // MyFunc 是函數指標(因為後面是 `()`) // MyFunc 該函數指標回傳一個 int* int* (<變量>*)(int); ``` ### define & typedef | 使用 | 功能 | 編譯時機 | | -------- | -------- | -------- | | define | 簡單 **宏替換** | **==預處理==** | | typedef | **重新定義類型** | **==編譯期==** | * **`define` & `typedef` 的區別**: 1. **typedef 不是簡單替換,而是 typedef 對類型重新定義** ```c= #define dpInt int* // 不可加 `;` 號 typedef int* tpInt; void typedef_define_diff_1() { // int* dp1, dp2; // 同下 dpInt dp1, dp2; // int* tp1, *tp2; // 同下 tpInt tp1, tp2; int a = 20; tp1 = tp2 = dp1 = &a; dp2 = a; printf("tp1: %d\n", *tp1); printf("tp2: %d\n", *tp2); printf("dp1: %d\n", *dp1); printf("dp2: %d\n", dp2); // 這是一個 int 類型 } ``` 2. **#define 可以實現類型組合,而 typedef 不行**:define 過的仍可再使用其他關鍵字修飾,而 typedef 不行 ```c= #define dInt int typedef int tInt; void typedef_define_diff_2() { unsigned dInt c1; // unsigned tInt c2; // 不可再組合 } ``` > ![](https://i.imgur.com/OqYQ8vk.png) 3. **define 無法創建新類型** ```c= // 定義新類型 (新類型為 Class) typedef char Class[10]; void typdef_create_new_definition() { Class clz; for(int i = 0; i < sizeof(clz)/sizeof(clz[0]); i++) { *(clz + i) = i * 3; printf("index: %d, value: %d\n", i, *(clz + i)); } } ``` ### typedef & struct * struct 結構最簡單的定義如下 ```c= struct Node {}; ``` * `struct` 配合使用上 `typedef` 有以下幾種情況 1. 串上 `typedef` 可省去 `struct` 關鍵字 ```c= typedef struct MyNode {} Node_T; void my_node() { Node_T node; } ``` 2. **定義兩個類型**:一個結構類型,另一個結構指標類型 ```c= // 定義等同於 // typedef <類型> Node_T2; // typedef <類型> *pNode_T2; typedef struct MyNode_2 {} Node_T2, *pNode_T2; void my_node_2() { Node_T2 node; pNode_T2 pNode; } ``` ### typedef & const * 我們知道 `const` 是如何修飾指標的,有分為 3 個種類 1. `const int* p`、`const int* p`:修飾指標指向內容不可改 2. `int* const p`:修飾指標指向不可改 3. `const int* const p`:內容、指向都不可改 * 如果以上功能要配合 `typedef` 使用 1. **`const` 修飾新類型變量**:指向不可修改 ```c= typedef int* pInt; void const_typedef() { int apply = 10; int bannana = 5; const pInt p = &apply; // 等同於 `int* const p` // pInt const p = &apply; // 同上 // p = &bannana; // read-only, Error 編譯錯誤 printf("p value: %d\n", *p); } ``` > ![](https://i.imgur.com/vTpMgpA.png) 2. **`const` 修飾新類型宣告**:修飾內容不可修改 ```c= void const_typedef_2() { short apply = 10; pShort p = &apply; printf("initialize p value: %d\n", *p); short bannana = 5; p = &bannana; // *p = bannana; // read-only, Error 編譯錯誤 printf("After change p value: %d\n", *p); } ``` > ![](https://i.imgur.com/gETpQqR.png) 3. **`const` 修飾內容、指向皆不可改** ```c= typedef const long* pLong; void const_typedef_3() { long apply = 200; const pLong p = &apply; printf("initialize p value: %d\n", *p); short bannana = 103; p = &bannana; p = bannana; printf("After change p value: %d\n", *p); } ``` > ![](https://i.imgur.com/VRCoRqH.png) ### typedef & 函數指標 * 我們就分析一個比較複雜的函數指標,以下兩個是相等意思 1. **函數指標原型** ```c= void printTest(int count) { for(int i = 0; i < count; i++) { printf("Hello: %d\n", i); } } // --------------------------------------------------------- // 1. 首先知道 a[10] 是一個指標數組 (主體是數組 // 2. 之後接上 `()` 代表是一個函數,得知外層是一個函數指標 // 3. 該函數指標返回 void、接收 `void(*)(int)` 函數指標 void (*a[10]) (void(*)(int)); void func_ptr_1() { a[0] = printTest; // 指定函數指標 a[0](5); // 呼叫函數 } ``` 2. **`typedef` 改寫上面的範例** ```c= // 功能完全同上 void printTest(int count) { for(int i = 0; i < count; i++) { printf("Hello: %d\n", i); } } // 宣告一個新類型 pFunc (函數指標) typedef void (*pFunc)(void(*)(int)); // 定義一個 Array 的 pFunc pFunc pFuncArray[10]; void func_ptr_2() { pFuncArray[0] = printTest; pFuncArray[0](10); } ``` > ![](https://i.imgur.com/lwbAIOr.png) ### typedef & sizeof * `typedef` 在使用 `sizeof` 要注意**一定要括號**,否則會報錯誤 1. 正常可以測量出 size 是 8 byte 2. 兩者配合使用沒有括號,會拋出錯誤 `error: expected expression` ```c= typedef struct { char a; short b; int c; } Test_T; int main() { //"1. " printf("%d\n", sizeof (Test_T) ); //"2. " printf("%d\n", sizeof Test_T); return 0; } ``` ## 二維 Array ```c= // 二維 Array int a[2][5]; // [2] 代表一維,[5] 代表二維,可解釋成 2 個 [5] 的空間 ``` > ![](https://i.imgur.com/iez7j5q.png) ### 二維 Array 的首地址 * 在一維 Array 中我們可以知道,一維 Array 的符號,等價於 `&a[0]` 的地址 ```c= void One_dimen_array_head() { int a[6]; if(a == &a[0]) { printf("Same"); } else { printf("Different"); } } ``` > ![](https://i.imgur.com/XDB06ec.png) * 推斷可得知二維的符號,等價於 `&(&a[0])[0]` 的地址 ```c= void Two_dimen_array_head() { int a[6][6]; if(a == &(&a[0])[0]) { printf("Same on a == &&a[0][0]"); } else { printf("Different"); } } ``` > ![](https://i.imgur.com/gh1HCIT.png) ### 訪問 二維 Array * 使用普通 Pointer 訪問 ```c= void visit_by_ptr() { int array[6][6] = {0}; array[0][0] = -1; array[0][1] = 10; array[0][2] = 7; array[1][0] = -3; array[1][1] = 100; array[1][2] = 97; int *p1 = array[0]; // 指向第一行的第一個元素 int *p2 = array[1]; // 指向第二行的第一個元素 printf("array[0][0]: %d\n", *p1); printf("array[0][1]: %d\n", *(p1 + 1)); printf("array[0][2]: %d\n", *(p1 + 2)); // 證明 Array 是連續空間,其實可以用一個 ptr 訪問全部二維 Array printf("array[1][0]: %d\n", *(p1 + 6)); printf("array[1][1]: %d\n", *(p2 + 1)); printf("array[1][2]: %d\n", *(p2 + 2)); } ``` > ![](https://i.imgur.com/An97YyK.png) * 使用 **陣列指標** 訪問 ```c= void visit_by_ptr_array() { int array[6][6] = {0}; array[0][0] = -1; array[0][1] = 10; array[0][2] = 7; array[1][1] = 100; array[1][2] = 97; int (*p)[6] = array; // 指向第一個元素 printf("array[0][0]: %d\n", *(*p)); printf("array[0][1]: %d\n", *(*p + 1)); // *p 是第一個地址 printf("array[0][2]: %d\n", *(*p + 2)); printf("array[1][1]: %d\n", *(*(p + 1) + 1)); printf("array[1][2]: %d\n", *(*(p + 1) + 2)); } ``` > ![](https://i.imgur.com/SWVm3hP.png) :::info 1. `a[i][j]` 對於陣列指標來說,等同於 `*( *(p + i) + j)` 2. 上面宣告的陣列指標 `int (*p)[6]`,其中的 **`[6]` 並不能亂定義,必須要與二維數組的數量相同才可以** ! ::: ## 更多的 C 語言相關文章 關於 C 語言的應用、研究其實涉及的層面也很廣闊,但主要是有關於到系統層面的應用(所以 C 語言又稱之為系統語言),為了避免文章過長導致混淆重點,所以將文章係分成如下章節來幫助讀者更好地從不同的層面去學習 C 語言 ### C 語言基礎 * **C 語言基礎**:有關於到 C 語言的「語言基礎、細節」 :::info * [**理解C語言中的位元操作:位元運算基礎與宏定義**](https://devtechascendancy.com/bitwise-operations-and-macros-in-c/) * [**C 語言解析:void 意義、NULL 意義 | main 函數調用、函數返回值意義 | 臨時變量的產生**](https://devtechascendancy.com/meaning_void_null_return-value_temp-vars/) * [**C 語言中的 Struct 定義、初始化 | 對齊、大小端 | Union、Enum**](https://devtechascendancy.com/c-struct_alignment_endianness_union_enum/) * [**C 語言儲存類別、作用域 | 修飾語、生命週期 | 連結屬性**](https://devtechascendancy.com/c-storage-scope-modifiers-lifecycle-linkage/) * [**指標 & Array & typedef | 指標應用的關鍵 9 點 | 指標應用、細節**](https://devtechascendancy.com/pointers-arrays-const-typedef-sizeof-null/) ::: ### 編譯器、系統開念 * **編譯器、系統開念**:是學習完 C 語言的基礎(或是有一定的程度)之後,從編譯器以及系統的角度重新檢視 C 語言的一些細節 :::warning * [**理解電腦記憶體管理 | 深入瞭解記憶體 | C 語言程式與記憶體**](https://devtechascendancy.com/computer-memory_manager-c-explained/) * [**C 語言記憶體區塊規劃 | Segment 段 | 字符串特性**](https://devtechascendancy.com/c-memory-segmentation-string-properties/) * [**編譯器的角度看程式 | 低階與高階、作業系統、編譯器、直譯器、預處理 | C語言函數探討**](https://devtechascendancy.com/compiler-programming-os-c-functions/) ::: ### C 語言與系統開發 * **C 語言與系統開發**:在這裡會說明 C 語言的實際應用,以及系統為 C 語言所提供的一些函數、庫... 等等工具,看它們是如何實現、應用 :::danger * [**了解 C 語言函式庫 | 靜態、動態函式庫 | 使用與編譯 | Library 庫知識**](https://devtechascendancy.com/understanding-c-library-static-dynamic/) * [**Linux 宏拓展 | offsetof、container_of 宏、鏈表 | 使用與分析**](https://devtechascendancy.com/linux-macro_offsetof_containerof_list/) ::: ## Appendix & FAQ :::info ::: ###### tags: `C`