指標 & Array & typedef | 指標應用的關鍵 9 點 | 指標應用、細節

指標 & Array & typedef | 指標應用的關鍵 9 點 | 指標應用、細節

OverView of Content

指標對於底層系統開發來說相當重要,而驅動又是透過控制 Register 來控制硬體,在這操控中就常常使用到指標

在學習C語言時,理解指標(pointers)、數組(arrays)、以及常見的語法規則如何運作是至關重要的。本文將探討指標和數組的基本概念,以及如何使用它們來更有效地編寫C代碼。

指標的概念和特性
指標是C語言中一個重要且強大的概念。它們是用來存儲變量地址的變量,通常用來間接訪問數據。指標具有首地址(即變量的地址)的特性,這使得它們可以在程序中輕鬆地操作和訪問內存中的數據。

Null 指標和 const 修飾符
在C語言中,Null指標是一個特殊的指標值,表示該指標不指向任何有效的內存地址。這在程序中很常見,用於標識指標的無效狀態。此外,const修飾符用於聲明常量,可以應用於變量和指標,以確保它們的值不會被修改。

陣列 雙重指標數組和二維數組
數組是一系列相同類型的元素的集合。它們在C語言中被廣泛使用,可以通過索引來訪問元素。二維數組則是數組的一種特殊形式,其中的元素是一維數組。理解如何訪問和操作數組對於有效地處理複雜數據結構至關重要。

比較使用 typedefdefine
在C語言中,typedef和define都是用來創建類型別名的工具。它們可以使代碼更易讀且更易於維護。通過將複雜的類型別名簡化為更可讀的形式,typedef和define有助於提高代碼的可讀性和可理解性。

強制轉換和 sizeof 運算符
強制轉換(type casting)在C語言中用於將一種數據類型轉換為另一種數據類型。這在處理不同類型的數據時很有用。另一方面,sizeof運算符用於返回數據類型或變量的大小(以字節為單位),這對於動態分配內存和確定數據結構的大小非常有用。

通過深入理解這些基本概念,您將能夠更好地理解和應用C語言中的指標和數組。在接下來的文章中,我們將逐一探討每個主題,並提供清晰的示例代碼和詳細的解釋,以幫助您更好地理解和應用這些概念。


指標概念

普通變數:首先我們要知道一般的讀寫都不會涉及 強制轉換,哪種類型的變數,就會以相對應的格式存在 RAM 中


int a = 10;

long b = (long) a;

指標變數

A. 可以把指標當成是另一種類型的宣告

B. 指標的內容:跟普通變數一樣,是儲存某一個數值,只是 指標是儲存一個地址 (使用關鍵字 & 來取得某個變數的地址)


int a = 10;     // 儲存 10

int* p = &a;    // 儲存 a 的地址, p 本身也有地址

指標本身也有地址

指標特性 - 首地址

● 內存的大小:這取決於 尋址總線數量,如果尋址總現有 32 條,那地址最大可到 232;相對來說,如果有 64 條,那地址最大可到 264

● 由於 在電腦中,硬體是以 1 Byte 作為單元切割,所以當一個指標拿到一個類型的首地址,就會自動順延到符合該類型的長度內容


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;

● 指標可以透過類型推導出接下來需要幾個 Byte 的數據

● 一個地址能儲存的大小就為 1 個 Byte (跟硬體有關)

指標級量

● 指標可以指向另外一個指標,層層疊加,這就是指標的級量


int a = 10;

// 一級指標
int* b = &a;

// 二級指標
int** c = &b;

// 三級指標
int*** d = &c;

(n + 1 級) = &(n 級)

建議指標不要超過 3 級,不僅降低了可讀性,效率也會不好

指標其他作用 - 作用域

● 一般我們在規範函數不讓其它文件訪問時會使用 static,但是只要透過指標就可以取得該函數並執行

static 關鍵字在 C 語言中相當於「存取限制符號」,使用 static 描述的函數、屬性,只能在該檔案中被訪問,其他檔案不可訪問!


// 只有該檔案內部可訪問
static void hello() {
    
}

// 只要有 include 的檔案都可以使用
void world() {
    
}

指標使用

指標符號

* 符號

A. 宣告指標變量使用 ,前後都可


int *p;

// 同上
int* p;

// ------------------------------------------------
int *p1, *p2;    // 兩個指標變量

int *p1, p2;    // 一個指標、一個整數變量

B. 解引用


int a = 10;

int *p = &a;

// 解引用,並賦予值
*a = 20;

& 符號

取變量的第一個 Byte 的地址


int a = 10;

int *p = &a;

野指標

● 只要指標可能出現未知性錯誤,它就是一個野指標;可能產生野指標的操作如下

Segmentation Fault:程式為了防止雪崩性錯誤,會使用 Segmentation Fault 停止該應用程式;而錯誤又分為兩種

● 大段錯誤:地址不存在

● 小段錯誤:地址存在,但訪問受限

A. 尚未初始化就直接使用


void wild_ptr() {
    char* p;

    *p += 1;    // Segment Error
}

B. 不清楚空間權限,試圖訪問、修改資料


void wild_ptr_2() {
    char *p = "hello";    // "hello" 放置在常量區,常量區不許須改
    
    *(p + 1) = 'w';    // Segment Error
}

C. 越界訪問


void wild_ptr_3() {
    int buf[4] = {0};
    
    *(buf + 4) = 10;    // + 4 已越界
}

*(buf + 4)*buf + 4 是不同的,這有關於符號的優先度

A. *(buf + 4) 是 buf 這個地址加上 4 個 Byte

B. *buf + 4 是數組第一個數 buf[0] 再加 4

What is Null

● Null 在 C/C 中是不同定義的存在,C 中被定義為 0,但是在 C 中定義為 (void*) 0,會被嚴格檢查


#ifdef _cplusplus
#define NULL            0
#else 
#define NULL            (void*) 0
#endif

const 修飾符

const 也就是 constant 代表不變,用來修飾變量,希望變量轉為常量

const 修飾普通變數

● 修飾變數,不管 const 是在前還是在後都可以,只要保證在變數之前即可

A. 類型之前


#include <stdio.h>

int main()
{    
    const int a = 10;

    a = 30;    // const 不可修改,所以編譯會錯

    printf("Hello: %d\n", a);
    return 0;
}

B. 類型之後


#include <stdio.h>

int main()
{    
    // 不同之處
    int const a = 10;

    a = 30;    // const 不可修改,所以編譯會錯

    printf("Hello: %d\n", a);
    return 0;
}

const 修飾指標

const 修飾指標有三種表現方式,個代表了不同的限制(注意 const 放置的位置)

A. const 修飾指標指向的空間:及說明 該空間的內容物為常量;修飾指標指向的空間常量有 2 種表達方式,代表的意思是相同


void const_ptr_1() {
 
    int tmp = 20;
    
    int const *a = &tmp;
    // const int *a = &tmp;    // 同上

    *a = 100;    // Read-only 編譯器檢查錯誤,不可修改 !

    printf("Hello: %d\n", *a);
}

B. const 修飾指標:使用 const 修飾指標說明 該指標指向不可在修改,也就是 指標無法再指去其他地方,但其 值能是可被修改


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);
}

C. const 修飾指標 & 修飾指向空間:代表指標 & 其指向的內容物都不可以修改


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 檢查


void const_ptr_4() {
    const int a = 100;

    int *p = NULL;    
    p = &a;        // 會有警告而已

    *p = 300;
    printf("Hello: %d\n", a);    // 可修改 a 的值
}

● 既然可以被修改,那為何要使用 const 修飾?

讓程式更加健壯,提醒使用者某修地方不能被修改,或是保證不會被修改

const & 變量 & 常量

● 在程式中我們常會使用 123"HELLO"... 等等數值,而這些數值在編譯器的處理下會以兩種方式存在 RAM 中

A. 變量

經過編譯後,會將 變量放置在 .data.bss 中,常出現在 ,這些變量都是 可讀可寫;經過編譯檢查 const 關鍵字,可將這些數值看做 偽常量

真正的常量不可修改,而偽常量 其實仍可修改

:::info

Linux 可使用 readelf 來查看編譯出來的執行檔中的 .data.bss 區塊

B. 常量

經過編譯後,會將 變量放置在 .ro.data 中,訪問權限為 可讀 (不可改)


// p 儲存首字 `H` 的地址,而 "Hello const" 則是放置在常量區

char *p = "Hello const";    // 如果透過指標修改這個變量,則會失敗 (Segmention Fault)

Array

深刻了解一維數組,是了解二維(甚至多維)數組的關鍵


// 格式如下
<類型>    <變量名>[<數量>]

int     a[100];        // 0 ~ 100
long    b[1]           // 0 ~ 1

以內存(記憶體)的角度來看,Array 的物理記憶體是連續的,並不會斷開,所以訪問速度也快


Array 訪問

A. 變量名訪問:最基礎的訪問方式就是透過變量名稱來訪問


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]);
}

B. 指標訪問不受到編譯器的 作用域檢查 規範


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 a2 a[0]3 &a[0]4 &a


int a[10] = {0};

A. Array 符號 a:有兩種含意

● Array 名稱:sizeof(a) 時,可以計算出該數組占用幾個 byte 大小

● Array 的第一個地址:等同於 &a[0],是 數組的首元素的首個字節,是一個常量值

● 如果是地址,那代表了是常量不可修改,所以永遠不會是左值


int a[10] = {0};

a = 1000;    //  a 是常量,不可為左值 (被賦予)

B. Array 符號 a[0]:取第一個元素的空間,並可以對其讀寫操作


int a[10] = {0};

ntf("a[0]: %d\n", a[0]);    // read

a[0] = 1000;    // write

C. Array 符號 &a[0]:取締一個元素的首位元空間地址,就等同於符號 a

D. Array 符號 &a:數組首地址,代表一個地址常量,同樣不可為左值

符號 &aa 的差異 ?

兩者的數值皆是 Array 的首地址,但是 意義完全不同&a 代表該空間的全體,而 a 只代表了該空間的 1 個元素


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 的第幾個元素


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. 變量指標 (可修改)

A. 常量指標:常量指標其實就是代表 Array 宣告的變數名 (Symble),該變數不可再修改,它是一個常量值 !


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));
    }
}

可否修改成 *(array) ?

不行!因為 Array 宣告出的變數名是一個常量,既然是常量就不可以修改 !

B. 變量指標


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++));
    }
}

● 以下寫法是錯誤的


int array[10] = {0};

// 錯誤! array 原本就是首地址的第一個 Byte,語意變成了
// array 首地址的首地址
p = &array;     

● 使用指標位置 + 1,編譯器會依照指標的大小,新增一個單元


int array[10] = {0}

int *p = array;

int a = *(p + 1);    // (p + 1) 相等於 (p + 1 *sizeof(int))

指標類型 & 強制轉換

對於編譯器來說,數據類型就是告訴編譯器該變數,要已什麼樣的格式儲存 (數據結構),儲存的空間又是多大 ?

A. 儲存空間:依照類型、硬體裝置,編譯器會給予不同變量,不同空間大小


sizeof(char);        // 1 Byte    
sizeof(short);       // 2 Byte
sizeof(int);         // 4 Byte
sizeof(float);       // 4 Byte
sizeof(double);      // 8 Byte

B. 儲存結構:即便是相同大小的 intfloat 儲存格式也不同


int a = 10;

printf("%d", a);    // 正確
printf("%f", a);    // 亂碼

// --------------------------------------------
float b = 10;

printf("%d", b);    // 亂碼
printf("%f", b);    // 正確

一般類型強制轉換 - 顯示

float 是用科學計數法形式儲存 (其中就包括:小數、指數、符號... 等等)

● 強制轉換一般類型,需要注意幾個點:空間結構

空間大小改變

A. 小轉大:沒啥問題,資料可以正常轉換


void small_to_big() {
    short a = 10;

    int b = (int)a;

 printf("b value: %d, size: %d", b, sizeof b);
}

B. 大轉小小心數據丟失、改變!(並非一定會丟失,但是很大機率會將數據解釋錯)


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);
}

結構改變:將原儲存資料方式就不同的結構強制轉型,像是浮點數(浮點數的儲存結構很不同)與整數的轉換

A. 整數轉浮點數,數據不丟失


void change_struct_1() {
    int a = 100;

    // 轉換儲存結構
    float b = (float) a;

    printf("b value: %f, size: %d", b, sizeof b);
}

B. 浮點數轉整數,數據丟失


void change_struct_2() {
    float a = 3.1415926;

    // 丟失小數部分數據
    int b = (int)a;

    printf("b value: %d, size: %d", b, sizeof b);
}

一般類型強制轉換 - 隱示

● 上面使用 (<類型>) 來做強制轉換,而隱式轉換則如下

A. 使用 = 符號的隱示轉換


void implict_change_1() {
    char c = 0x11223344;

    printf("c value: %d, size: %d", c, sizeof c);
}

:::info

● 這種隱示轉換不安全,通常會有編譯器發出警告(會警告,但並不代表有問題)

B. 當使用 return 關鍵字,在返回之前隱示轉換


int implict_change_2() {
    char c = 0x11223344;

    return c;
}

● 這種 return 隱示轉換不安全,通常會有編譯器發出警告(會警告,但並不代表有問題)

指標強制轉換

● 指標轉換「最好使用」顯示轉換,盡量不要使用隱式轉換;指標轉換也涉及兩個層面:1. 指標類型轉換、2. 指標指向類型

A. 指標類型轉換:改變數據的解析方式


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 的結構來解析數據內容

B. 指標指向類型(不改變指標,改變解指標後的數據)


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


char array[] = "Hello";

| sizeof 計算 | 結果 | 說明 | 注意 |

sizeof 計算結果說明注意
sizeof(array)6前面有說過 array 符號用在 sizeof 會計算該 Array 所有的空間包括 \0 (字符算的結尾)
sizeof(array[0])1一個 array 元素大小
strlen(array)5使用 C 標準函式庫會解析到 \0 為止 (不包含),所以結果會少 1

● 測試 1:測試 Array 首指標、Array 元素、C 標準庫的 strlen 測試


#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 測試


#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. 指標


char array[] = "Hello";
sizeof 計算結果說明注意
sizeof(p)4指標 p 的大小sizeof 會根據類型判別大小,array 才會計算整體
sizeof(*p)1一個 char 大小
strlen(p)5使用 C 標準函式庫會解析到 \0 為止 (不包含)

● 測試:指標、指標取得的元素、C 標準庫的 strlen 測試


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 而不是整體


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),也會有一樣的功能


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);
}

● 那要決定用哪一種,要選宣告接收 Pointer 還是 Array?

對於程式的「可讀性」來說,我們還是會 選擇使用 a[10] 這種寫法,因為 這能明確標示請使用者傳入的是一個數組,而不是一個普通數值的 Ptr

● 使用 Array 類型的另類寫法:使用 Array 前,宣告大小變數,這個變數就可以讓 Array 變數使用


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);
}

size 參數必須定義在 array 之前! 否則編譯檢查不能通過


高級指標

其實沒啥高級指標,應該說是指標比較高級的用法

指標數組 & 數組指標

● C 語言語法的重點是:前面是修飾詞,後面才是主語

● 變數 - 指標數組:代表該變數,主語是 Array,並且修飾(類型)是 Ptr

● 變數 - 數組指標:代表該變數,主語是 Ptr,並且修飾(類型)是 Array

類型主語 (本質)修飾舉例
指標 數組數組 (Array)指標 (Ptr)int *p[5]; (本質是 Array,每個元素都為 (int*),代表 5 個指標)
數組 指標指標 (Ptr)數組 (Array)int (*p)[5] (本質是 1 指標,指向 int[5] 的空間)

*[] 在符號的前後,指明了該變量是指標還是數組 !(這還有關於到優先級) 定義一個符號時思考的步驟如下

A. 找到定義的符號,誰是「核心」


// `p` 是核心
// `int`、`*`、`[]` 都是為了定義 p

int *p[6];

B. 看誰跟核心最近,誰的結合優先度 (優先級) 高,就先與之結合;以下嘗試讓核心與不同的符號結合


// 以下分號 `;` 不結合

// 核心 a,跟 [] 結合,所以是數組
int a[5];

// 核心 p,跟 * 結合,所以是數組
int *p;

// 核心 function,跟 `()` 結合,所以是函數
int function();

● 以下列出幾個常見的優先度,從上到下代表優先度的高到低

運算符號描述結合性
()函數呼叫
[]Array 引用先於 * 符號結合
->指標指向成員左到右
.Struct 成員引用
-/+負號、正號
++/–遞增、遞減
!邏輯否
~1 的補數右到左
*指標引用
&記憶體位置
sizeof計算物件 Byte 大小
(type)強制轉型
*乘法
/除法左到右
%取模
+/-加、減左到右

符號的建立、分析,都先從基礎去分析,沒有無緣無故的規則 !

● 以下範例是「指標函數」、「函數指標」正確的使用方式


// 指標數組 
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

● 首先要知道,函數指標也是一個 指標,與其它指標並無不同;

函數(Function)到底是什麼

函數本值一段程式,在經過編譯器編譯成匯編碼後,載入到記憶體 (RAM) 中,是一段連續記憶體函數指標就是該記憶體的第一個地址


// 偽程式,以下記憶體地址都是假的

// 下面這段程式載入 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

● 我們可以看到 數組指標 的類型是 <數組類型> (*)[];而 函數指標 也是,主要是指標所以它的類型是 <回傳類>(*)(接收參數)


#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]


#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]


#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)


#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;
}

● typedef 定義出的新類型並不占用 RAM

二重指標

二重指標 可以存放 一重指標 的地址


#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

#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

typedef

A. typedef 用來定義新類型,形式越複雜 typedef 的優勢則越明顯

B. typedef 的另一個優點是 方便移植

typedef 是一個儲存類的關鍵字,而 變量只能被一種儲存類的關鍵字修飾


typedef static int ClzNum[10];    // 編譯錯誤

其他儲存類的關鍵字:autoexternstaticregister

錯誤如下


typedef 看法解析

A. typedef 是給類型取別名,所以 typedef 定義的東西都是類型;所以 typedef 定義中一定會有一個 類型

B. typedef 定義出的類型:移除定義中 typedef 關鍵字,再將 類型看作變量,就能知道它的類型

● 從這裡可以發現將 typedef 關鍵字 移除後,它就只是一個普通的變量語句

● 數組類型


// MyClass 就是類型
typedef int MyClass[5];

// 去除 typedef 關鍵字
int MyClass[5];

// 再將 MyClass 看成變量
int <變量>[];    // 數組類型

● 函數指標類型


// MyFunc 就是類型
typedef int* (MyFunc*)(int);

// 去除 typedef 關鍵字
int* (MyFunc*)(int);

// 再將 MyFunc 看成變量
//    MyFunc 變量是一個指標
//    MyFunc 是函數指標(因為後面是 `()`)
//    MyFunc 該函數指標回傳一個 int*
int* (<變量>*)(int);

define & typedef

使用功能編譯時機
define簡單 宏替換預處理
typedef重新定義類型編譯期

define & typedef 的區別

A. typedef 不是簡單替換,而是 typedef 對類型重新定義


#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 類型

}

B. #define 可以實現類型組合,而 typedef 不行:define 過的仍可再使用其他關鍵字修飾,而 typedef 不行


#define		dInt		int

typedef 	int		tInt;

void typedef_define_diff_2() {
    unsigned dInt c1;
//	unsigned tInt c2;    // 不可再組合
}

C. define 無法創建新類型


// 定義新類型 (新類型為 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 結構最簡單的定義如下


struct Node {};

struct 配合使用上 typedef 有以下幾種情況

A. 串上 typedef 可省去 struct 關鍵字


typedef struct MyNode {} Node_T;

void my_node() {
    Node_T node;
}

B. 定義兩個類型:一個結構類型,另一個結構指標類型


// 定義等同於
//    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 個種類

A. const int* pconst int* p:修飾指標指向內容不可改

B. int* const p:修飾指標指向不可改

C. const int* const p:內容、指向都不可改

● 如果以上功能要配合 typedef 使用

A. const 修飾新類型變量:指向不可修改


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);
}

B. const 修飾新類型宣告:修飾內容不可修改


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);
}

C. const 修飾內容、指向皆不可改


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 & 函數指標

● 我們就分析一個比較複雜的函數指標,以下兩個是相等意思

A. 函數指標原型


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);    // 呼叫函數
}

B. typedef 改寫上面的範例


// 功能完全同上
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 要注意一定要括號,否則會報錯誤

A. 正常可以測量出 size 是 8 byte

B. 兩者配合使用沒有括號,會拋出錯誤 error: expected expression


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


// 二維 Array

int a[2][5];    // [2] 代表一維,[5] 代表二維,可解釋成 2 個 [5] 的空間

二維 Array 的首地址

● 在一維 Array 中我們可以知道,一維 Array 的符號,等價於 &a[0] 的地址


void One_dimen_array_head() {
    
    int a[6];

    if(a == &a[0]) {
        printf("Same");
    } else {
        printf("Different");
    }
}

● 推斷可得知二維的符號,等價於 &(&a[0])[0] 的地址


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 訪問


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));
}

● 使用 陣列指標 訪問


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));
}

A. a[i][j] 對於陣列指標來說,等同於 *( *(p + i) + j)

B. 上面宣告的陣列指標 int (*p)[6],其中的 [6] 並不能亂定義,必須要與二維數組的數量相同才可以 !


更多的 C 語言相關文章

關於 C 語言的應用、研究其實涉及的層面也很廣闊,但主要是有關於到系統層面的應用(所以 C 語言又稱之為系統語言),為了避免文章過長導致混淆重點,所以將文章係分成如下章節來幫助讀者更好地從不同的層面去學習 C 語言

編譯器、系統開念

編譯器、系統開念:是學習完 C 語言的基礎(或是有一定的程度)之後,從編譯器以及系統的角度重新檢視 C 語言的一些細節

C 語言與系統開發

C 語言與系統開發:在這裡會說明 C 語言的實際應用,以及系統為 C 語言所提供的一些函數、庫... 等等工具,看它們是如何實現、應用

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響