---
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;
```
> 
* **指標變數**:
1. 可以把指標當成是另一種類型的宣告
2. 指標的內容:跟普通變數一樣,是儲存某一個數值,只是 **指標是儲存一個地址** (使用關鍵字 `&` 來取得某個變數的地址)
```c=
int a = 10; // 儲存 10
int* p = &a; // 儲存 a 的地址, p 本身也有地址
```
:::info
指標本身也有地址
:::
> 
### 指標特性 - 首地址
* 內存的大小:這取決於 **尋址總線數量**,如果尋址總現有 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 (跟硬體有關)
> 
:::
### 指標級量
* 指標可以指向另外一個指標,層層疊加,這就是指標的級量
```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;
}
```
> 
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);
}
```
> 
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);
}
```
> 
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);
}
```
> 
### 指標修改 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]);
}
```
> 
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]);
}
```
> 
### 一維數組 & 符號
* 數組的符號有 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`
> 
:::
### 指標 & 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]);
}
}
```
> 
* **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 宣告出的變數名是一個常量,既然是常量就不可以修改 !**
> 
:::
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);
}
```
> 
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);
}
```
> 
* **結構改變**:將原儲存資料方式就不同的結構強制轉型,像是浮點數(浮點數的儲存結構很不同)與整數的轉換
1. 整數轉浮點數,**數據不丟失**
```c=
void change_struct_1() {
int a = 100;
// 轉換儲存結構
float b = (float) a;
printf("b value: %f, size: %d", b, sizeof b);
}
```
> 
2. 浮點數轉整數,**數據丟失**
```c=
void change_struct_2() {
float a = 3.1415926;
// 丟失小數部分數據
int b = (int)a;
printf("b value: %d, size: %d", b, sizeof b);
}
```
> 
### 一般類型強制轉換 - 隱示
* 上面使用 `(<類型>)` 來做強制轉換,而隱式轉換則如下
1. **使用 `=` 符號的隱示轉換**:
```c=
void implict_change_1() {
char c = 0x11223344;
printf("c value: %d, size: %d", c, sizeof c);
}
```
> 
:::info
* 這種隱示轉換不安全,通常會有編譯器發出警告(會警告,但並不代表有問題)
> 
:::
2. **當使用 `return` 關鍵字,在返回之前隱示轉換**:
```c=
int implict_change_2() {
char c = 0x11223344;
return c;
}
```
:::info
* 這種 `return` 隱示轉換不安全,通常會有編譯器發出警告(會警告,但並不代表有問題)
> 
:::
### 指標強制轉換
* 指標轉換「最好使用」顯示轉換,盡量不要使用隱式轉換;指標轉換也涉及兩個層面:^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));
}
```
> 
:::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));
}
```
> 
## 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));
}
```
> 
* 測試 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' 之間的位元數
}
```
> 
### 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));
}
```
> 
### 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);
}
```
> 
* 知道 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);
}
```
> 
:::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 之前**! 否則編譯檢查不能通過
> 
:::
> 
## 高級指標
其實沒啥高級指標,應該說是指標比較高級的用法
### 指標數組 & 數組指標
* 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));
}
```
> 
### 函數指標 - 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;
}
```
> 
### 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
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;
}
```
> 
## typedef
1. **typedef 用來定義新類型,形式越複雜 typedef 的優勢則越明顯**
2. **typedef 的另一個優點是 ==方便移植==**
:::danger
* **typedef 是一個儲存類的關鍵字**,而 **變量只能被一種儲存類的關鍵字修飾**
> 其他儲存類的關鍵字:`auto`、`extern`、`static`、`register`
```c=
typedef static int ClzNum[10]; // 編譯錯誤
```
錯誤如下
> 
:::
### 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; // 不可再組合
}
```
> 
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);
}
```
> 
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);
}
```
> 
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);
}
```
> 
### 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);
}
```
> 
### 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] 的空間
```
> 
### 二維 Array 的首地址
* 在一維 Array 中我們可以知道,一維 Array 的符號,等價於 `&a[0]` 的地址
```c=
void One_dimen_array_head() {
int a[6];
if(a == &a[0]) {
printf("Same");
} else {
printf("Different");
}
}
```
> 
* 推斷可得知二維的符號,等價於 `&(&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");
}
}
```
> 
### 訪問 二維 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));
}
```
> 
* 使用 **陣列指標** 訪問
```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));
}
```
> 
:::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`