文章程式碼顯示

2018年2月13日 星期二

《筆記》C語言 - 07_6:指標陣列(存放一堆指標的陣列)、指標函數陣列(選單驅動系統)

Hint : 此章是一個特別複雜艱澀的部分,建議可多花一些心思去思考這之間的關係。若真的無法理解(搞到頭很痛),可以稍微休息或跳過此章(前提是你努力的去嘗試理解過了 .. )

指標陣列

一個陣列所存放的物件也可以是指標 

指標陣列常用來建構字串陣列( string array ) 

陣列中的每一個項目都是一個字串

字串的 "名稱" 本質上是一個指向字串第一個字元的指標(位址)

#include "stdio.h"

int main(){

    // 宣告一個具有 4 個元素的指標陣列 parr
    // 你以為他存放的都是一個個字串,實際上不是。他存放的都是"第一個字元"的位址
    char *parr[4] = { "Nooooooooooo.", "name", "Age", "Sex" };

    // &parr[0] 指標變數 parr[0] 本身的位址;parr[0] 以 %p 得到字串第一個字元的位址;以字串第一個字元的位址得到字串的內容
    printf("parr[0]_address = %p, string[0]_address = %p, string[0]_head = %c \n", &parr[0], parr[0], *parr[0]);
    printf("parr[1]_address = %p, string[1]_address = %p, string[1]_head = %c \n", &parr[1], parr[1], *parr[1]);
    printf("parr[2]_address = %p, string[2]_address = %p, string[2]_head = %c \n", &parr[2], parr[2], *parr[2]);
    printf("parr[3]_address = %p, string[3]_address = %p, string[3]_head = %c \n", &parr[3], parr[3], *parr[3]);

    const char *TestADDRESS = 0x00405072;
    printf("%s\n",TestADDRESS);

    printf("%s\n", parr[0]); // string address
    printf("%s\n", *(parr + 3) );
}


看起來好像每個字串都放進了 parr 陣列裡,實際上只有指標存在陣列中,而每一個指標都指向對應字串的第一個字元 ( N, n, A, S )

雖然 parr 陣列的大小是固定的,但它卻能夠用來存取任何長度的字串,這也是 C 的強大資料結構功能的一個例子。

:這部分可能有點複雜,不過我們可以回想一下以前是怎樣去取用一個字串的。
我們會宣告一個字串(字元陣列) 例如

char parr[] = "hello";

然後使用 printf 函數直接輸入陣列名稱 printf("%s", parr); 將之列印出來

回想一下為能這麼做呢?

因為陣列的名稱(又或者說字串)本來就代表第一個字元的位址(指標)

而這個 parr 陣列是把各個字串的指標(第一個字元的位址)都存放在陣列中

所以我們自然的就可以用 printf("%s"), parr[0] ) 來列印出第一個字串了

parr[0] 同樣是一個指標變數,它指向的是字串 "Nooooooooooo." 的第一個字元的位址

同理

parr[1] 同樣是一個指標變數,它指向的是字串 "name" 的第一個字元的位址
parr[2] 同樣是一個指標變數,它指向的是字串 "Age" 的第一個字元的位址
parr[3] 同樣是一個指標變數,它指向的是字串 "Sex" 的第一個字元的位址

函式指標

指向函式的指標變數(pointer to a function) 存放著函式在記憶體中的位址

我們多次說過陣列名稱實際上是此陣列第一個元素在記憶體內的位址,同樣的,函式的名稱實際上也是執行此函式之程式碼的起始位址

指向函式的指標可傳遞給函式、由函式傳回來、存放在陣列中、指定給其它的函式指標。

#include "stdio.h"

int add(int a, int b) {
  return a + b;
}

int mult(int a, int b) {
  return a * b;
}

int main() {

  // 宣告指標函數
  int (*FuncPtr)(int a, int b);

  FuncPtr = add;
  printf("FuncPtr(3,5) = %d, function address = %p \n", FuncPtr(3,5) ,FuncPtr);

  FuncPtr = mult;
  printf("FuncPtr(3,5) = %d, function address = %p \n", FuncPtr(3,5) ,FuncPtr);
}



首先我們像以往一樣宣告了兩個函數分別為 add 以及 mult 。兩個函數同樣都具有兩個 int型別 的輸入引數,一個 int型別 的回傳値。

接著我們在第 13 行的地方宣告一個 函式指標變數 FuncPtr

第 13 行的宣告是這樣讀的

宣告一個函式指標變數 FuncPtr ,指到的函式為兩個整數輸入參數及回傳一個整數値

注意 : FuncPtr 在第 13 行時的括號是不能被省略的

第 13 行的程式碼看起來有點複雜,但我們先退回普通指標變數的宣告後再一步步加上去就可以理解。舉例來說我們先宣告一個普通的指標變數名為 FuncPtr ,指向 int 型別。

int* FuncPtr;

接著要怎麼變成指向函式的呢? 首先我們把指向函式的輸入引數給囊括進來變成

int* FuncPtr(int a, int b);

最後,由於運算符優先級的概念,若要將 FuncPtr 變成一個指向函式的指標變數,必須將指標名稱與  *(米字號) 括號起來,最後就完成了

int (*FuncPtr)(int a, int b);

接著我們讓函式指標變數 FuncPtr 指向 add 函式,具體的做法就是將函數的名稱丟給 FuncPtr 。 mult 同理

我們可以從程式碼的結果得到函式 add 以及函式 mult 實際上在記憶體中的位置為 0x00401460 以及 0x0040146D

各位有沒有發現,既然函式在記憶體中是具有位置的

這就代表著我們可以用指標變數指向記憶體任意一個位置的方式來執行函式內某一行的程式碼

實際上這樣的作法是頗危險的應用,常常被有心人士當做漏洞來對程式進行攻擊。

後記補充:

在 Arduino 中我們可以使用以下指標函式來達到藉由軟體重啟版子的功能

void( *resetFunc) (void) = 0; //製造重啟命令

之前我在撰寫一個 Arduino 專案時,遇到了需要在程式裡面對 MCU 進行重啟的需求,於是我找到了上面的方法。

當時我還看不懂這語句,只看過有人說這樣的做法會讓 MCU 跳到記憶體 0x0000 處,以達成 MCU 重啟的作用

學到這章節後我們就看的懂它在做什麼了 ! 首先宣告一個函式指標變數名為 restFunc ,回傳値跟輸入引數都是 void

然後在等號的後面它直接將這個指標函式指向記憶體中的 " 0 "。

所以當我們呼叫這個指標函式,它就會跳到記憶體 0x0000 的位址了

函式指標與指標陣列的結合

函式指標 搭配 指標陣列 (合併稱為指標函數陣列) 常會用在文字型態的選單驅動系統

在這種系統中使用者會被要求從選單裡選一個選項(例如數字 1~5 任一個數字),每一個選項會以不同的函式來加以服務,指向每個函式的指標會存放在一個函式指標陣列之中

使用者的選擇將當成此陣列的下標,而陣列中的指標則會用來呼叫適當的服務函式

具體實現的程式碼如下

#include "stdio.h"

void function0( int ); //plus
void function1( int ); //diff
void function2( int ); //multiple
void function3( int ); //divide

// 宣告一個指標函數陣列(裡面存放四個指標,函數的名稱實際上就是該函數在記憶體中的位址 )
void (*f[4])(int) = { function0, function1, function2, function3 };

int choice;
int number1, number2;

int main(){
setvbuf(stdout,NULL,_IONBF,0);

 printf( "Enter number1 : " );
 scanf( "%d", &number1 );
 printf( "Enter number2 : " );
 scanf( "%d", &number2 );

    printf("\nEnter a number between 0~3 \n");
    printf("0 : plus\n");
    printf("1 : diff\n");
    printf("2 : mutiple\n");
    printf("3 : divide\n");
    scanf( "%d", &choice );

    // 調用指標函式陣列,並用 choice 來選擇要調用哪一個 function
    // 第二個 choice 為 function 的輸入引數(在此沒加以處理,純粹傳進去而已)
    ( *f[choice] )( choice ); 

    return 0;
}

void function0 ( int a){
 printf("%d + %d = %d", number1, number2, number1 + number2);
}
void function1 ( int b){
 printf("%d - %d = %d", number1, number2, number1 - number2);
}
void function2 ( int c){
 printf("%d * %d = %d", number1, number2, number1 * number2);
}
void function3 ( int d){
 printf("%d / %d = %d", number1, number2, number1 / number2);
}


↓↓↓ 連結到部落格方針與索引 ↓↓↓

Blog 使用方針與索引