2017年9月21日 星期四

C 語言測試 : 指標

暑假期間我家二哥把 C 語言最重要的陣列與指標看過一遍, 對於指標的意義與用法有了基本了解, 但指標究竟用來幹嘛卻沒有具體概念. 老實說我也沒有, 因為除了寫 Arduino 有用到基本的 C 語言技巧外, 我從來沒用 C 寫過像樣的東西. 所以就趁這個機會把 C 語言測試一下, 就從最難處理的指標開始吧!

C 語言雖然是高階語言, 但指標功能卻讓 C 語言也擁有低階的記憶體操作能力. 許多高階語言也有支援指標, 例如 C++, FORTRAN, PASCAL, BASIC, Perl, C# 等等, 其中 C++ 除了完整支援 C 的指標功能外, 也新增了 Smart Pointers 以提供較原始指標更安全的功能. 部分支援指標的高階語言因為安全性等原因而對指標做了限制, 例如 PASCAL 與 C#, 而 Java 則完全不支援指標, 參考 :

# Support_in_various_programming_languages

指標的主要用途如下 :
  1. 函式之間若要傳遞字串或陣列使用指標做傳址呼叫可避免資料的複製與搬移
  2. 需要從函式中傳回一個以上的值時必須使用指標, 因為 return 只能傳回一個一般變數值.
  3. 較複雜之資料結構需要利用指標以鏈結串列方式實作.
  4. 使用指標處理字串較方便. 
事實上, 許多 C 語言內建函式內部都是使用指標實作的.

以下的測試程式我參考了下列幾本書的範例加以改編 :
  1. C 語言初學指引第四版 (博碩, 陳錦輝)
  2. C 語言程式設計剖析 (全華, 簡聰海)
  3. C 語言從零開始 (博碩, 資訊教育研究室)
  4. C 語言入門 (易習, 丁國棟)
  5. C 語言程式設計實例入門第四版 (博碩, 高橋麻奈)
  6. C 語言程式設計與應用 (全華, 陳會安)
使用工具包括 Windows 上的 Dev C++, 樹莓派的 gcc, 以及線上 C 語言編譯器 TutorialsPoint (此為 Linux 主機, 使用 gcc 編譯, 不須存檔現打現譯) :

https://www.tutorialspoint.com/compile_c_online.php


一. 指標變數的宣告與賦值

1. 指標是甚麼?

指標是 C 語言中一種特別的資料型態, 專門用來儲存某個資料的記憶體位址, 其內容用來指向該資料. 編譯器使用 CPU 的間接定址法來存取指標變數所指之記憶體位址. 指標最早出現在 PL/I 語言中, 參考 :

https://zh.wikipedia.org/wiki/指標_(電腦科學)

指標變數與一般變數不同, 其儲存的內容是記憶體位址, 因此占用的記憶體大小是固定的, 即等於 CPU 的位址寬度, 例如 32 位元系統之位址匯流排寬度是 32 位元 (4 個 bytes), 可定址 2^32=4G Bytes 的 DRAM, 其指標變數長度為 4 個 Bytes; 而 64 位元系統位址匯流排 64 位元 (8 個 bytes), 定址能力為 2^64=16GB 之 DRAM,  其指標變數長度為 8 個 Bytes.


2. 指標變數的宣告

指標變數宣告方式是在變數名稱前面加 * 號 (ANSI C) 或是在資料型態後面加 * (C++ 新增方式), 例如 :

int *ptr;     //ANSI C 的宣告方式
int* ptr;     //C++ 新增的宣告方式

這都是宣告指向一個整數資料的指標變數 ptr. 指標變數宣告之後, 編譯器會在記憶體中指配記憶位址來儲存所指之位址. 指標變數在賦值之前其內容為記憶體之前的殘留值或隨機初始值, 此未初始化的指標變數是懸空的狀態 (dangling), 不可拿來使用, 因為它可能指向不允許存取的系統保留區或其他程序正在使用的記憶體位址, 可能導致運算結果錯誤或系統當機.

指標變數除了指向一般變數與陣列外, 也可以指向函數, 稱為函數指標, 其宣告方式例如 :

int (*ftpr) (int x,int y);

這是宣告一個指向函數的指標 fptr, 詳如後述.


3. 指標變數的賦值 (初始化) :

指標變數在使用前必須 "初始化 (賦值)" 以指向某一個記憶體位址; 亦即透過指定運算將一個記憶體位址存入指標內. 指標賦值有兩種方式 :
  1.  使用取址運算子 & (reference operator) 
  2.  將本身就是位址的陣列名稱函數名稱指派給指標 
取址運算子 & 可以取得任意變數 (包含指標變數) 之記憶體位址, 例如 :

int a=100;                      //宣告一般變數 a 並賦值 100
int *ptr;                          //宣告指向整數資料的指標
ptr=&a;                 //取得 a 的記憶體位址並賦值給指標 ptr

也可以在宣告指標的同時予以賦值, 上面的程式可改為 :

int a=100;
int *ptr=&a;          //取得 a 的記憶體位址並賦值給指標 ptr


4. 陣列指標 :

指標也可以指向陣列元素, 這種指標稱為陣列指標. 陣列的每一個元素相當於一般的變數, 因此可以用取址運算子 & 取得任一元素之記憶體位址, 然後指派給指標, 例如 :

int a[]={0,1,2,3,4,5};
int *ptr=&a[0];              //宣告整數指標 ptr 指向整數陣列 a 的開頭位址
ptr=&a[3];                      //指標改為指向陣列 a 的第四個元素

將指標指向陣列開頭除了用 &a[0] 取得第一個元素的開頭位址外賦值外, 也可以直接用陣列名稱賦值, 因為陣列名稱本身就是指向其第一個元素的開始位址, 例如 :

int *ptr=a;           //宣告整數指標 ptr 指向整數陣列 a 的開頭位址 (等於 &a[0])

從下面的範例可知, &a[0] 與 a 的內容是一樣的, 都是陣列 a 的開始位址 :

#include <stdio.h>

int main() {
    int a[]={0,1,2,3,4,5};
    printf("a=%p\n",a);
    printf("&a[0]=%p\n",&a[0]);
    int *ptr=a;
    printf("ptr=%p\n",ptr);
    return 0;
    }

結果如下 :

a=0x7ffff331aa90       (陣列開始位址)
&a[0]=0x7ffff331aa90   (陣列開始位址)
ptr=0x7ffff331aa90     (指向陣列開始位址)

陣列其實是一種特殊的指標, 在編譯器的符號表中, a 儲存了第一個元素的開頭位址 &a[0], 其內容是不可更改的 (否則會找不到陣列的開頭), 因此陣列又稱為常數指標. 一般的指標是變數, 其所儲存的內容 (記憶體位址) 是可以改變的, 可以做遞增遞減等算術運算, 例如 ++ptr 或 ptr-- 等等, 但是陣列 a 是常數指標, 不允許做 a++ 或 --a 等運算.

下列程式碼將編譯失敗, 因為陣列的起始位址是不能更改的 :

    int x=5;
    int a[]={0,1,2,3,4,5};
    int a=&x;      //不可更改陣列位址

在 Dev C++ 會出現如下錯誤訊息, 表示 a 已經被宣告為陣列, 不能更改 a 的位址 :

[Error] conflicting declaration 'int a'
[Note] previous declaration as 'int a [6]'

陣列指標除了可使用 *(ptr+i) 來存取陣列元素外, 也可以把指標當成陣列名稱, 使用索引 ptr[i] 來存取陣列元素, 例如 :

#include <stdio.h>

int main() {
    int a[]={0,1,2,3,4,5};
    int *ptr=a;
    printf("a[2]=*(ptr+2)=%d\n",*(ptr+2));    //使用取值運算子
    printf("a[2]=ptr[2]=%d\n",ptr[2]);             //陣列指標也可以使用索引存取陣列元素
    return 0;
    }

執行結果如下 :

a[2]=*(ptr+2)=2
a[2]=ptr[2]=2

指標既然可以指向陣列, 當然也可以在函數中當虛引數 (參數) 接收呼叫者傳遞之陣列. 在函數之間傳遞陣列是以傳址呼叫方式將陣列名稱 (即起始位址) 傳入函數, 例如下列求陣列元素和的程式, 被呼叫的函式必須宣告一個同型態陣列來接收引數 :

int sum(int a[], int len) {    //宣告一個整數陣列接收引數
    int s=0;
    for (int i=0; i<len; i++) {
        s=s + a[i];
        }
    return s;
    }

int main() {
    int len=6;
    int a[]={0,1,2,3,4,5};
    int s=sum(a,len);          //將陣列起始位址傳給函數 sum()
    printf("sum=%d",s);
    }

但是也可以改用指標來接收傳入之引數 :

int sum(int *ptr, int len) {   //宣告一個整數指標接收引數
    int s=0;
    for (int i=0; i<len; i++) {
        s=s + *(ptr + i);     //或者 ptr[i] 亦可
        }
    return s;
    }

int main() {
    int len=6;
    int a[]={0,1,2,3,4,5};
    int s=sum(a,len);         //將陣列起始位址傳給函數 sum()
    printf("sum=%d",s);
    }


5. 字串指標 :

由於 C 語言沒有字串資料類型, 因此字串是利用陣列來儲存, 但與字元陣列不同之處是結尾必須加上一個 ASCII 的 NULL 字元 '\0', 例如 :

char cha[5]={'H','E','L','L','O'};       //一般的字元陣列
char str[6]={'H','E','L','L','O','\0'};   //字串

也可以用雙引號給字元陣列賦值, 這時編譯器會自動在結尾處加上 NULL 字元 :

char str[]="HELLO";   //用雙引號賦值

在 printf() 中輸出字串必須使用 %s 格式, 對應的變數就是陣列名稱; 如果是輸出其中的某個字元則要用 %c 格式, 例如 :

int main() {
    char str[]="HELLO";
    printf("str=%s\n",str);             //輸出整個字串
    printf("str[1]=%c\n",str[1]);    //輸出單一字元
    return 0;
    }

既然字串是一種字元陣列, 指標可以指向陣列, 指向字串的指標稱為字串指標, 此指標必須宣告為 char 類型, 例如

char *ptr="HELLO";       //指向字串的指標
printf("*ptr=%s\n",ptr);   //輸出字串

也可以在迴圈中用 putchar() 輸出字串, 例如 :

int main() {
    char *ptr="HELLO";
    for (; *ptr != '\0'; *ptr++) {    //或 while(*ptr != '\0')  亦可
        putchar(*ptr);
        }
    return 0;
    }

如果是用陣列的話必須用一個索引計數器, 上面的指標就不用 :

int main() {
    char str[]="HELLO";
    int i=0;    //索引計數器
    while(str[i] != '\0') {
        putchar(str[i]);
        ++i;
        }
    return 0;
    }

雖然陣列與指標都能用來處理字串, 但它們最大的不同是, 陣列字串一經賦值即不可再指派新值, 因為陣列是常數指標, 其內容是不可變的, 例如下面的程式碼無法通過編譯 :

    char str[]="HELLO";   //陣列名稱 str 是常數不是變數 (常數指標)
    str="WORLD";     //無法通過編譯

因為陣列 str 已經指向 "HELLO" 的開頭, 不能再指向 "WORLD".

而指標是變數, 可隨時指向任何字串, 上面的字串改用指標就可以通過編譯了, 例如 :

    char *ptr="HELLO";   //原指向 "HELLO"
    ptr="WORLD";           //改為指向 "WORLD"

如果要交換兩個字串必須使用字串指標, 不能用字元陣列, 參考簡聰海寫的 "C 語言程式設計剖析" 8-6 節 :

int main() {
    char *a="Hello";      //不可以用 a[] 取代 *a
    char *b="World";     //不可以用 b[] 取代 *b
    char *tmp;
    printf("交換前:a=%s b=%s\n",a,b);
    tmp=a;
    a=b;
    b=tmp;
    printf("交換後:a=%s b=%s\n",a,b);
    return 0;
    }

執行結果 :

交換前:a=Hello b=World
交換後:a=World b=Hello

雖然指標與陣列都能用來存取字串, 但上面 main 的前兩行若改用陣列 :

    char a[]="Hello";    
    char b[]="World";  

這樣就無法通過編譯了, 因為它在後面的 a=b 與 b=bmp 就會發生編譯錯誤 :

main.c:9:6: error: assignment to expression with array type
     a=b;
      ^
main.c:10:6: error: assignment to expression with array type
     b=tmp;
      ^
因為陣列 a 與 b 一經宣告後, 其內容 (指向陣列開頭之位址) 就固定不能再改變了.

指標除了可以指向陣列外, 也可以指向函數, 與陣列名稱代表陣列的開始位址一樣, 函數名稱代表函數的開始位址, 因此可以將函數名稱指派給指標變數, 使其指向該函數, 留待後面再行測試.


6. 不可指派常數值給指標 : 

直接對指標變數本身或其所指位址指派一個整數常數或字面值 (Literal) 是非常危險與錯誤的用法, 例如 :

int *ptr=1000;  



int *ptr;
ptr=1000;  

此指令在宣告指標變數 ptr 的同時也對其賦值 1000, 由於 ptr 在宣告後, 這位址 1000 可能指向系統或其他程序所使用的記憶體位址, 輕則破壞其他程式之運算結果, 重則導致保護較不周全之作業系統 (如 DOS) 當機.

事實上這些錯誤用法都無法通過編譯, 如下測試 1 所示 :


測試 1 : 錯誤的指標賦值方式 

#include <stdio.h>

int main() {
    int a=2;
    int *ptr=1000;  
    return 0;
  }

此程式在 TutorialsPoint 編譯會得到下列錯誤訊息 :

$gcc -o main *.c
main.c: In function ‘main’:
main.c:3:14: warning: initialization makes pointer from integer 
                      without a cast [-Wint-conversion]
     int *ptr=1000;
              ^~~~
$main

若在 Dev C++ 編譯則是 :

[Error] invalid conversion from 'int' to 'int*' [-fpermissive]


測試 2 : 顯示未初始化之指標內容

#include <stdio.h>

int main() {
    int *ptr;
    printf("未初始化指標 ptr 位址=%p\n", &ptr);
    printf("未初始化指標 ptr 內容=%p", ptr);
    return 0;
   }

此程式第一個 printf() 顯示用 & 取址運算子取得之指標變數 ptr 本身的位址, 在 64 位元系統為 8 個 bytes; 第二個 printf() 則顯示指標變數之內容. 注意, 雖然指標的內容是整數 (記憶體位址), 但是在 printf() 中顯示指標內容必須使用 %p, 不是 %d.

上面程式在 TutorialsPoint 執行結果未初始化指標內容是 nil (無值) :

$gcc -o main *.c
$main
未初始化指標 ptr 位址=0x7ffedc8ffa08
未初始化指標 ptr 內容=(nil)

而在 Dev C++ 結果是未初始化指標內容是 1, 這是記憶體殘留值 :

未初始化指標 ptr 位址=000000000022FE48   
未初始化指標 ptr 內容=0000000000000001  


可見指標只要一經宣告, 編譯器就會指派一個位址給它, 例如上面的 0x7ffedc8ffa08 與0x22FE48, 此位址每次執行都可能會不同.

存取指標所指之資料須使用取值運算子 * (dereference operator), 例如 :

int a=100;             //宣告一般變數 a 並賦值 100
int *ptr;                 //宣告指向整數資料的指標
ptr=&a;                 //取得 a 的記憶體位址並賦值給指標 ptr (指向 a)
*ptr=50;                //將 a 的值被改為 50 了

這裡 ptr 指標指向一般變數 a, 因此 *ptr=50 就會把 a 的內容改為 50, 例如 :


測試 3 : 利用指標更改所指位址內容

#include <stdio.h>

int main() {
    int a=100;
    printf("a 的位址=%p\n",&a);
    printf("a 的初始值=%d\n",a);
    int *ptr;                        //宣告指標 ptr
    ptr=&a;               //指標 ptr 指向 a
    printf("ptr 的內容=%p\n", ptr);      //ptr 內容=a 的位址
    printf("*ptr 的初始值=%d\n", *ptr);
    *ptr=50;
    printf("*ptr 的新值=%d\n",*ptr);
    printf("a 的新值=%d",a);
    return 0;
   }

執行結果如下 :

a 的位址=0x7ffd5899e0d4
a 的初始值=100
ptr 的內容=0x7ffd5899e0d4 (與 a 的位址一樣)
*ptr 的初始值=100
*ptr 的新值=50
a 的新值=50  (a 的值被改了)


7. 用指標當函數參數進行傳址呼叫

C 語言函數的 return 只能傳回一個值, 如果要傳回多個值必須利用指標或陣列的傳址呼叫才能達成, 在 "C 語言從零開始" 這本書的 11-7 節以及 "最新 C 語言程式設計實例入門" 的 9-3 節提到的兩個變數數值交換函數 swap() 唯一極佳範例, 改寫如下 :

測試 4 : 利用指標傳址呼叫交換兩個變數之數值

#include <stdio.h>

void swap(int *x,int *y) {  //函數的參數為指標 (不需傳回值)
   int tmp=*x;
   *x=*y;
   *y=tmp;
   }

int main() {
    int a=5,b=15;
    printf("交換前 : a=%d b=%d\n",a,b);
    swap(&a,&b);    //以傳址呼叫將 a, b 之位址傳給函數
    printf("交換後 : a=%d b=%d\n",a,b);
    return 0;
    }

由於要傳回的值有兩個 (即交換後的 a, b), 因此將 a, b 的位址傳給 swap() 去運算, 因為指標指向了變數的位址, 直接在變數上進行交換動作, 因此也用不到 return 將值傳回來, 故傳回值宣告為 void. 執行結果如下 :

交換前 : a=5 b=15
交換後 : a=15 b=5

如果將 swap() 改成如下用傳值呼叫的話, 由於傳進去的是資料的副本, 如果不將資料傳回來的話就是做白工 :

int swap(int x,int y) {  //傳值呼叫 : 無效的數值交換
   int tmp=x;
   x=y;
   y=tmp;
   }

交換後無法將兩個值同時傳回來等於做白工, 一定要用指標才行.


二. 指標的長度 :

指標變數與一般變數不同, 其儲存的內容是記憶體位址 (整數), 因此不論指標指向哪一種型態的資料, 其所占記憶體大小是固定的, , 即等於 CPU 的位址寬度, 例如 32 位元系統之位址匯流排寬度是 32 位元 (4 個 bytes), 可定址 2^32=4G Bytes 的 DRAM, 其指標變數長度為 4 個 Bytes; 而 64 位元系統位址匯流排為 64 位元 (8 個 bytes), 定址能力為 2^64=16GB 之 DRAM,  其指標變數長度為 8 個 Bytes. 例如 :


測試 5 : 指向各種型態的指標都占用相同大小的記憶體  


#include <stdio.h>

int main() {
    char c;
    int i;
    float f;
    double d;
    char *cptr=&c;
    int *iptr=&i;
    float *fptr=&f;
    double *dptr=&d;
    printf("變數名稱     記憶體位址          占用記憶體 (bytes)\n");
    printf("========    ================   ==================\n");
    printf("   c  \t    %p \t  %d\n", &c, sizeof(c));
    printf("   i  \t    %p \t  %d\n", &i, sizeof(i));
    printf("   f  \t    %p \t  %d\n", &f, sizeof(f));
    printf("   d  \t    %p \t  %d\n", &d, sizeof(d));
    printf(" cptr  \t    %p  \t  %d\n", &cptr, sizeof(cptr));
    printf(" iptr  \t    %p  \t  %d\n", &iptr, sizeof(iptr));
    printf(" fptr  \t    %p  \t  %d\n", &fptr, sizeof(fptr));
    printf(" dptr  \t    %p  \t  %d\n", &dptr, sizeof(dptr));
    return 0;
   }

在 Win10 64 位元系統的 Dev C++ 執行結果如下 :



可見指標內容不管所指資料類型為何都是 8 bytes (64 位元). 在 TutorialsPoint 上也是 8 bytes :




而在樹莓派 B 上則是 4 個 bytes, 因為其 CPU 是 32 位元的.





三. 指標的運算

指標儲存的是記憶體位址, 事實上也就是整數, 若呼叫 printf() 輸出時用 %d 會顯示整數值, 而用 %p 才會顯示位址值, 例如 :

int main() {
    int a=5;
    int *ptr=&a;
    printf("ptr=%d\n",ptr);   //以整數格式輸出指標內容
    printf("ptr=%p\n",ptr);   //以位址格式輸出指標內容
    return 0;
    }

執行結果如下 :

ptr=1065793828
ptr=0x7fff3f86b924

因此除了上述使用 "=" 運算子進行指定 (賦值) 運算外, 指標還可以進行算術運算, 但只限於與整數之加減運算以及遞增遞減運算, 不允許乘除等運算, 因為指標運算之用途只是為了用來計算記憶體位址之位移以存取資料而已, 指標乘除運算並無意義.


1. 指標加減運算 : 

指標的值可以與整數進行加減來改變指標所指向之位址, 但因為指標儲存的是記憶體位址, 不是一般的整數, 因此加減的整數其單位並非 bytes, 而是指標所指之資料型態, 例如當指標指向陣列時, ptr + i 中的 i 是指陣列元素移位個數 :

int a[]={0,1,2,3,4,5};    //宣告一個整數陣列 a
int *ptr=&a[0];      //宣告指標 ptr 指向陣列開頭元素 a[0]
ptr=ptr + 2;                   //指標向下移動 2 個 int 單位, 指向 a[2]

指標 ptr 原先指向 a[0], 指標加 2 並非 ptr 所儲存的位址加 2 個 bytes, 而是加 2 個整數單位, 例如 Dev C++ 中整數是 4 個 bytes, 則 ptr+2 就是指標移位 2*4=8 bytes; 而在樹莓派的 gcc 編譯器, 整數占 2 個 bytes, 則 ptr + 2 就是移位 2*2=4 個 bytes.


測試 6 : 指標加減指向陣列元素

#include <stdio.h>

int main() {
    int a[]={0,1,2,3,4,5};
    printf("元素\t位址\n");
    for (int i=0; i<5; i++) {
        printf("a[%d]\t%p\n",i,&a[i]);
        }
    int *ptr=&a[0];    //指標指向 a[0]
    printf("ptr=%p *ptr=%d\n",ptr,*ptr);
    ptr=ptr + 2;          //指標指向 a[2]
    printf("ptr=%p *ptr=%d\n",ptr,*ptr);
    ptr=ptr - 1;           //指標指向 a[1]
    printf("ptr=%p *ptr=%d",ptr,*ptr);
    return 0;
    }

在 TutorialsPoint 測試結果如下 :

元素 位址
a[0] 0x7fff4003fa10 
a[1] 0x7fff4003fa14
a[2] 0x7fff4003fa18 
a[3] 0x7fff4003fa1c
a[4] 0x7fff4003fa20
ptr=0x7fff4003fa10 *ptr=0
ptr=0x7fff4003fa18 *ptr=2
ptr=0x7fff4003fa14 *ptr=1

TutorialsPoint 的 int 占 4 個 bytes, 陣列的每個元素在記憶體內會在儲存在連續位址中, 因此指標 ptr 向前移動 2 個單位, 實際上位址是由 0x7fff4003fa10 向前移動 8 個 bytes 到 0x7fff4003fa18.

下面範例是參考 "C 語言初學指引第四版" 加以改編 :

測試 7  :  指標加減在各類型資料的位址的移位距離 

#include <stdio.h>

int main() {
    int a;
    short int *p;
    int *q;
    float *r;
    double *s;
    char *t;
    printf("指標移位前位址\n");
    p=(short int*) &a;    //強制轉型為 short int
    q=&a;
    r=(float*) &a;          //強制轉型為 float
    s=(double*) &a;      //強制轉型為 double
    t=(char*) &a;           //強制轉型為 char
    printf("=============\n");
    printf("p=%p\n",p);
    printf("q=%p\n",q);
    printf("r=%p\n",r);
    printf("s=%p\n",s);
    printf("t=%p\n",t);
    printf("指標移位後位址\n");
    p=p+1;
    q=q+1;
    r=r+1;
    s=s+1;
    t=t+1;
    printf("=============\n");
    printf("p=%p\n",p);
    printf("q=%p\n",q);
    printf("r=%p\n",r);
    printf("s=%p\n",s);
    printf("t=%p\n",t);  
    return 0;
    }

這裡用 p, q, r, s, t 五個指標分別指向五個不同的資料類型, 先印出移位前所指向之位址, 再讓指標向前移動一個單位, 然後印出所指向的新位址, 這會因不同資料型態而有所不同. 注意, 因為 a 是 int 類型, 因此除了 q 以外, 其他指標都必須取址後予以強制轉型.

在 TutorialsPoint 測試結果如下 :

指標移位前位址
=============
p=0x7ffd2bd96354
q=0x7ffd2bd96354
r=0x7ffd2bd96354
s=0x7ffd2bd96354
t=0x7ffd2bd96354
指標移位後位址
=============
p=0x7ffd2bd96356  (short int 占 2 個 bytes)
q=0x7ffd2bd96358  (int 占 4 個 bytes)
r=0x7ffd2bd96358  (float 占 4 個 bytes)
s=0x7ffd2bd9635c  (double 占 8 個 bytes)
t=0x7ffd2bd96355  (char 占 1 個 bytes)

可見在 TutorialsPoint 的主機上短整數 short int 類型移位一個單位為 2 個 bytes; 整數 int 為 4 個 bytes; 浮點數 float 為 4 個 bytes; 倍準數 double 為 8 個 bytes; 字元 char 為 1 個 byte.


3. 指標差值運算 :

兩個指向相同資料類型的指標變數可以相減, 得到的差是一個整數, 表示兩個指標間之距離, 其單位是該資料型別的長度. 如果這兩個指標指向同一陣列, 則差值表示兩個指標之間隔了多少元素.

測試 8 : 兩個指標間之距離

#include <stdio.h>

int main() {
    int a[]={0,1,2,3,4,5};
    printf("元素\t位址\n");
    for (int i=0; i<5; i++) {
        printf("a[%d]\t%p\n",i,&a[i]);
        }
    int *ptr1=&a[1];
    int *ptr2=&a[2];
    printf("ptr1=%p *ptr1=%d\n",ptr1,*ptr1);
    printf("ptr2=%p *ptr2=%d\n",ptr2,*ptr2);
    printf("ptr2-ptr1=%d\n",ptr2-ptr1);
    printf("ptr1-ptr2=%d",ptr1-ptr2);
    return 0;
    }

TutorialsPoint 執行結果 :

元素 位址
a[0] 0x7ffdcf7a8540
a[1] 0x7ffdcf7a8544
a[2] 0x7ffdcf7a8548
a[3] 0x7ffdcf7a854c
a[4] 0x7ffdcf7a8550
ptr1=0x7ffdcf7a8544 *ptr1=1
ptr2=0x7ffdcf7a8548 *ptr2=2
ptr2-ptr1=1  
ptr1-ptr2=-1  

可見 ptr2 與 ptr1 位址差距為 4 bytes, 由於 int 長度為 4 bytesm, 所以 ptr2-ptr1 為 1 個整數單位.

注意, 兩個相減的指標必須是指向同類型, 否則無法通過編譯. 其次, 兩個指標相減是允許的, 但兩個指標的相加運算卻是不允許的, 因為兩個位址相加沒有意義. 例如將上面的 ptr2-ptr1 改為 ptr2+ptr1 將無法通過編譯.


4. 遞增 (++) 與遞減 --) 運算 :

遞增與遞減運算就是指標加 1 或減 1 運算, 各有前置 (先做運算) 與後置 (後做運算) 之分 :

  1. 前遞增 ++ptr : 先加 1 再處理
  2. 後遞增 ptr++ : 先處理再加 1
  3. 前遞減 --ptr : 先減 1 再處理
  4. 後遞減 ptr-- : 先處理再減 1

++ 與 -- 是單元運算子, 如果只是做變數的單元運算, 前置或後置對運算結果相同. 例如 :

#include <stdio.h>

int main() {
    int a=1;
    ++a;   //前遞增單元運算 a=2
    printf("a=%d\n",a);
    a=1;
    a++;   //後遞增單元運算 a=2
    printf("a=%d\n",a);
    return 0;
    }

不論前遞增或後遞增, 單元運算的結果都是 a=2 :

$gcc -o main *.c
$main
a=2
a=2

結果無差別的原因是上述單元運算沒有再進行指定處理.

有差異的情況是單元運算後還進一步再做處理 (即指定運算), 例如 :

#include <stdio.h>

int main() {
    int a=1;
    int b=++a;    //前遞增後指定給 b, 故 b=2
    printf("b=%d\n",b);
    a=1;
    b=a++;         //指定給 b 後再做後遞增, 故 b=1
    printf("b=%d\n",b);
    return 0;
    }

結果 :

b=2
b=1

參考 :

30天C語言巔峰之路(Day13:運算子-遞增與遞減運算子)

指標變數做遞增遞減運算時, 由於指標的取址 & 與取值 * 運算子與遞增 ++ 遞減 -- 運算子優先等級一樣, 都是第二級 (第一級是括號), 因此當 &, *, ++, --, () 混合運算時需仔細判斷, 例如 *ptr++ 是先取 ptr 所指之值後, 再將 ptr 指向下一個資料下列; 而 *++ptr 則是先將指標增量指向下一個資料後, 再取出其值.

下面的範例使用一個陣列 a 與指標 ptr 來測試 *, ++, () 的 7 種混合運算 :

int a[5]={0,1,2,3,4};


測試 9 : 指標 *, ++,  () 的混合運算


#include <stdio.h>

int main() {
    int a[5]={0,1,2,3,4};
    printf("元素\t值\t位址\n");
    for (int i=0; i<5; i++) {
        printf("a[%d]\t%d\t%p\n",i,a[i],&a[i]);
        }  
    int *ptr=a;
    printf("ptr=a=%p\n",ptr);
    int x=*(ptr++);
    printf("x=*(ptr++) x=%d ptr=%p *ptr=%d\n",x,ptr,*ptr);
    x=*(++ptr);
    printf("x=*(++ptr) x=%d ptr=%p *ptr=%d\n",x,ptr,*ptr);
    x=(*ptr)++;
    printf("x=(*ptr)++ x=%d ptr=%p *ptr=%d\n",x,ptr,*ptr);
    x=++(*ptr);
    printf("x=++(*ptr) x=%d ptr=%p *ptr=%d\n",x,ptr,*ptr);
    x=*ptr++;
    printf("x=*ptr++ x=%d ptr=%p *ptr=%d\n",x,ptr,*ptr);
    x=*++ptr;
    printf("x=*++ptr x=%d ptr=%p *ptr=%d\n",x,ptr,*ptr);
    x=++*ptr;
    printf("x=++*ptr x=%d ptr=%p *ptr=%d\n",x,ptr,*ptr);
    return 0;
    }

在 TutorialsPoint 執行結果如下 :

元素 值 位址
a[0] 0 0x7ffe4aa8a9e0
a[1] 1 0x7ffe4aa8a9e4
a[2] 2 0x7ffe4aa8a9e8
a[3] 3 0x7ffe4aa8a9ec
a[4] 4 0x7ffe4aa8a9f0
ptr=a=0x7ffe4aa8a9e0
x=*(ptr++) x=0 ptr=0x7ffe4aa8a9e4 *ptr=1
x=*(++ptr) x=2 ptr=0x7ffe4aa8a9e8 *ptr=2
x=(*ptr)++ x=2 ptr=0x7ffe4aa8a9e8 *ptr=3
x=++(*ptr) x=4 ptr=0x7ffe4aa8a9e8 *ptr=4
x=*ptr++ x=4 ptr=0x7ffe4aa8a9ec *ptr=3
x=*++ptr x=4 ptr=0x7ffe4aa8a9f0 *ptr=4
x=++*ptr x=5 ptr=0x7ffe4aa8a9f0 *ptr=5

結果摘要如下表 :

 運算式 相當於 執行結果 說明
 x=*(ptr++); x=*ptr;
 ptr=ptr+1;
 x=0
 *ptr=1
 先取值 *ptr 給 x, 再遞增 ptr
 x=*(++ptr); ptr=ptr+1;
 x=*ptr;
 x=2
 *ptr=2
 先遞增 ptr 再取值給 x
 x=(*ptr)++; x=*ptr;
 *ptr=*ptr+1;
 x=2
 *ptr=3
 先取值 *ptr 給 x, 再把 *ptr 加 1
 x=++(*ptr); *ptr=*ptr+1;
 x=*ptr; 
 x=4
 *ptr=4
 把 *ptr 加 1 同時設給 x
 x=*ptr++; x=*ptr;
 ptr=ptr+1;
 x=4
 *ptr=3
 先取值 *ptr 給 x, 再把 *ptr 加 1
 x=*++ptr; ptr=ptr+1;
 x=*ptr;
 x=4
 *ptr=4
 先遞增 ptr 再取值給 x,  與 x=*(++ptr) 同
 x=++*ptr x=*ptr+1; x=5
 *ptr=5
 把 *ptr 加 1 同時設給 x, 與 x=++(*ptr) 同

可知 *(++ptr) 與 *++ptr 作用相同, 而 ++(*ptr) 與 ++*ptr 作用相同, 因為 ++, --, * 這些第二優先等級的運算子, 運算順序為由右向左之故.


5. 指標比較運算 :

兩個指標可以在 if 敘述中使用 ==, >, <, >=, <=, 或 != 等關係運算子進行比較運算, 用來判斷所儲存的記憶體位址是否相等, 或何者之記憶體位址較高或較低.


#include <stdio.h>

int main() {
    int a,b;
    int *ptr1=&a;
    int *ptr2=&b;
    printf("ptr1=%p\n",ptr1);
    printf("ptr2=%p\n",ptr2);
    if (ptr2 > ptr1) {
        printf("變數 b 位址高於變數 a 位址\n");
        }
    else {
        printf("變數 a 位址高於變數 b 位址\n");
        }
    return 0;
    }

在 Win10 上的 Dev C++ 執行結果如下 :

ptr1=000000000022FE3C
ptr2=000000000022FE38
變數 a 位址高於變數 b 位址

可見 Dev C++ 在編譯時, 變數位址是依據宣告先後由高位址往低位址指派. 在 TutorialsPoint 上執行的結果也是如此 :

$gcc -o main *.c
$main
ptr1=0x7ffe5964392c
ptr2=0x7ffe59643928
變數 a 位址高於變數 b 位址

在 Arduino Nano 編譯也是高位址先指派 :

void setup() {
    Serial.begin(9600);
    int a,b;
    int *ptr1=&a;
    int *ptr2=&b;
    Serial.println((long)ptr1);
    Serial.println((long)ptr2);
    if (ptr2 > ptr1) {
        Serial.println("變數 b 位址高於變數 a 位址\n");
        }
    else {
        Serial.println("變數 a 位址高於變數 b 位址\n");
        }
    }
void loop() {}

結果如下 :

2298
2296
變數 a 位址高於變數 b 位址

注意, 必須指向相同資料類型的指標才可以互相比較, 若將上面程式中的 b 與 ptr2 改為 float 類型, 則編譯時將會失敗, 出現如下錯誤訊息 (Dev C++) :

comparison between distinct pointer types 'float*' and 'int*' lacks a cast [-fpermissive]


四. 指標的指標 :  

指標變數用來儲存資料的記憶體位址, 指標除了用來指向一般變數, 也可以用來指向另一個指標變數, 稱為雙重指標. C 語言允許多重指標, 但因較複雜而用得不多. 宣告多重指標的方式是使用連續的 "*", 例如 :

char ***ppp;  //宣告三重指標 ppp

從下面範例可以觀察三重指標儲存的位址之關係 :

測試 10 : 三重指標

#include <stdio.h>

int main() {
    int a=100;
    int *ptr1=&a;             //指向整數的指標
    int **ptr2=&ptr1;      //指向整數指標的指標
    int ***ptr3=&ptr2;    //指向整數指標的指標的指標
    printf("&a=%p a=%d\n",&a,*ptr1);
    printf("&ptr1=%p ptr1=%p *ptr1=%d\n",&ptr1,ptr1,*ptr1);
    printf("&ptr2=%p ptr2=%p *ptr2=%p\n",&ptr2,ptr2,*ptr2);
    printf("&ptr3=%p ptr3=%p *ptr3=%p\n",&ptr3,ptr3,*ptr3);
    return 0;
    }

TutorialsPoint 執行結果如下 :

$gcc -o main *.c
$main
&a=0x7fff5e45bb2c a=100
&ptr1=0x7fff5e45bb20 ptr1=0x7fff5e45bb2c *ptr1=100
&ptr2=0x7fff5e45bb18 ptr2=0x7fff5e45bb20 *ptr2=0x7fff5e45bb2c
&ptr3=0x7fff5e45bb10 ptr3=0x7fff5e45bb18 *ptr3=0x7fff5e45bb20
在上面的範例中, 指標 ptr1 指向一個整數變數 a, 儲存的是 a 的位址 7fff5e45bb2c, 而指標 ptr2 又指向 ptr1; 儲存的是ptr1 的位址 7fff5e45bb20; 指標 ptr3 又指向 ptr2, 儲存的是 ptr2 的位址 7fff5e45bb18, 形成三重指標. 由於最終是指向一個整數, 因此這三個指標都必須宣告為 int.



五. 函數指標 : 

上面測試指標賦值時提到, 指標也可以指向函數,  稱為函數指標. 因為函數名稱在符號表中儲存的是函數在記憶體中的開始位址, 如同陣列名稱是陣列的開始位址一樣, 可以直接將函數名稱指派給指標, 使其指向該函數. 函數指標可以讓我們在程式中動態地呼叫不同的函數.

1. 函數指標的宣告與呼叫方式  :

函數指標的宣告方式如下 :

傳回值類型 (*指標名稱)([參數類型]);   

例如 :

int (*fptr)(int,int);     //宣告具有兩個 int 參數, 傳回值為 int 的函數指標 fptr

函數指標的賦值只要將函數名稱指派給指標即可 (不必用 &), 賦值後指標即指向該函數 :

指標名稱=函數名稱;  

例如 :

fptr=swap;     //函數指標指向函數 swap()

這樣就可以利用函數指標來呼叫函數, 呼叫方式有下列兩種 :
  1. (*指標名稱)([參數]);
  2. 指標名稱([參數]);        
例如 :

(*fptr)(5,25);     //呼叫函數指標 fptr
fptr(5,25);          //呼叫函數指標 fptr

注意, 第一種呼叫方式 *fptr 外面的括弧是必須的, *fptr 要先結合, 表示 fptr 是一個指標變數; 然後再與後面的括號結合, 表示它指向一個函數 (括號優先順序高於 * 運算子).

下面範例使用指標 fptr 先後指向 add() 與 sub() 這兩個函數, 統一使用 ftpr() 或 (*fptr)() 來呼叫不同函數 :


測試 11 : 函數指標與函數位址

#include <stdio.h>

int add(int x,int y) {
   return x+y;
   }
int sub(int x,int y) {
   return x-y;
   }

int main() {
    printf("add=%p\n",add);
    printf("sub=%p\n",sub);
    int (*fptr)(int x,int y);
    fptr=add;      //指標 fptr 指向 add() 函數
    printf("fptr=add=%p\n",fptr);
    printf("ftpr(8,3)=add(8,3)=8+3=%d\n",fptr(8,3));   //用函數指標名稱呼叫函數
    fptr=sub;      //指標 fptr 指向 sub() 函數
    printf("fptr=add=%p\n",fptr);
    printf("ftpr(8,3)=sub(8,3)=8-3=%d\n",fptr(8,3));
    return 0;
    }

在 TutorialsPoint 執行結果如下 :

add=0x4004d7
sub=0x4004eb
fptr=add=0x4004d7
ftpr(8,3)=add(8,3)=8+3=11
fptr=add=0x4004eb
ftpr(8,3)=sub(8,3)=8-3=5
可見透過指派函數名稱可以用同一名稱 fptr() 呼叫不同的函數, 雖然這跟直接呼叫 add() 與 sub() 沒兩樣, 但若配合條件判斷可達到動態呼叫不同函數之效.

如同陣列名稱代表陣列開始位址, 函數名稱代表函數開頭位址, 因此可以像陣列與指標一樣當引數傳給另一個函數, 這時被呼叫函數中接收此引數的參數必須宣告為函數指標.

下列範例是我參考 "易習 C 語言入門" 第 8-5 節範例改寫而來, 其中兩個函數 add 與 sub 被當成呼叫 compute() 函數時的引數, 因此在 compute() 中必須使用函數指標來接收才行 :


測試 12 : 用函數指標接收函數參數

#include <stdio.h>

int add(int x,int y) {
   return x+y;
   }
int sub(int x,int y) {
   return x-y;
   }
int compute(int x,int y,int (*fptr)(int x,int y)) {   //用 int fptr(int x, int, y) 亦可
    return (*fptr)(x,y);
    }

int main() {
    printf("compute(8,3,add)=%d\n",compute(8,3,add));   //以函數 add 當引數
    printf("compute(8,3,sub)=%d\n",compute(8,3,sub));    //以函數 sub 當引數
    return 0;
    }

在 TutorialsPoint 執行結果如下 :

compute(8,3,add)=11
compute(8,3,sub)=5

注意, 接收引數的函數指標不一定要用 * 運算子, 可以直用函數指標的名稱, 所以上面的 compute() 也可以改為 :

int compute(int x,int y,int fptr(int x,int y)) {   //以函數指標接收函數引數
    return (*fptr)(x,y);
    }

下面的範例則是參考 "C 語言程式設計應用" 10-7-2 改寫, 此程式中利用一個額外函數 compare() 的第三參數 (函數指標) 來呼叫 max() 與 min() 這兩個函數 :


測試 13 : 用函數指標接收函數參數

#include <stdio.h>

int max(int,int);   //函數原型宣告
int min(int,int);   //函數原型宣告
int compare(int,int,int (*)(int,int));   //函數原型宣告 (第三參數為函數指標)

int main() {  
    int x=5,y=15;
    printf("x=%d y=%d 最大值=%d\n",x,y,compare(x,y,max));   //以函數名當引數
    printf("x=%d y=%d 最小值=%d\n",x,y,compare(x,y,min));    /以函數名當引數
    return 0;
    }
 
int max(int x,int y) {
    if (x > y) {return x;}
    else {return y;}
    }
 
int min(int x,int y) {
    if (x < y) {return x;}
    else {return y;}
    }
 
int compare(int x,int y,int (*ptr)(int,int)) {   //第三參數為函數指標
    return (*ptr)(x,y);    //利用函數指標呼叫函數
    }

在 TutorialsPoint 執行結果如下 :

x=5 y=15 最大值=15
x=5 y=15 最小值=5

其實只要直接呼叫 max() 與 min() 就可以完成的事, 卻要多寫一個 compare() 來做, 似乎有點捨近求遠, 但上面的範例主要在說明函數指標可以用在函數的引數以及使用同一個介面 (compare) 呼叫不同的函數而已.


2. 函數指標陣列 : 

函數指標也可以組成一個陣列, 稱為函數指標陣列, 這樣可以透過索引在迴圈中呼叫不同函數, 其宣告方式如下, 其實只是將單一函數指標改成陣列而已 :

傳回值類型 (*指標名稱[長度])([參數]);     //參數可有可無

在下列範例中宣告了一個函數指標陣列 (*ptr[3])(int,int) 用來指向三個函數, 每一個函數也可以用 ptr[i] 來代表, 呼叫函數可改用 ptr[i](x,y) :

測試 14 : 函數指標陣列

#include <stdio.h>

int add(int x,int y) {
   return x+y;
   }
int sub(int x,int y) {
   return x-y;
   }
int mul(int x,int y) {
   return x*y;
   }

int main() {
    int (*ptr[3])(int,int);   //宣告一個函數指標陣列, 用來指向三個函數
    ptr[0]=add;   //函數指標賦值為函數名稱以指向函數
    ptr[1]=sub;
    ptr[2]=mul;
    char op[]={'+','-','*'};
    int opr1=15;
    int opr2=6;
    for (int i=0; i<3; i++) {
        printf("%d%c%d=%d\n",opr1,op[i],opr2,ptr[i](opr1,opr2));    //透過函數指標呼叫函數
        }
     return 0;
    }

在 TutorialsPoint 執行結果如下 :

15+6=21
15-6=9
15*6=90


六. 泛型指標  void :

在上面測試 4 的傳址呼叫中, swap() 函數交換的是兩個整數變數之值; 如果要交換的是兩個字元變數的話, 這個 swap() 函數就不能用了, 必須另外為 char 參數撰寫新的函數. 在下列範例中, 交換整數的函數改為 iswap(), 而交換字元的函數則為 cswap(), 對於不同類型參數須分別呼叫這兩個函數 :

測試 15 : 函數指標

#include <stdio.h>

void cswap(char *x,char *y) {  //函數的參數為指標 (不需傳回值)
   char tmp=*x;
   *x=*y;
   *y=tmp;
   }

void iswap(int *x,int *y) {  //函數的參數為指標 (不需傳回值)
   int tmp=*x;
   *x=*y;
   *y=tmp;
   }

int main() {
    int i1=5,i2=15;
    printf("交換前 : i1=%d i2=%d\n",i1,i2);
    iswap(&i1,&i2);    //以傳址呼叫將 i1, i2 之位址傳給函數
    printf("交換後 : i1=%d i2=%d\n",i1,i2);  
    char c1='A',c2='B';
    printf("交換前 : c1=%c c2=%c\n",c1,c2);
    cswap(&c1,&c2);    //以傳址呼叫將 c1, c2 之位址傳給函數
    printf("交換後 : c1=%c c2=%c\n",c1,c2);
    return 0;
    }

在 TutorialsPoint 執行結果如下 :

交換前 : i1=5 i2=15
交換後 : i1=15 i2=5
交換前 : c1=A c2=B
交換後 : c1=B c2=A

上面範例只是導入泛型 void 指標之前的楔子. 接下來先回顧測試 12 與 13, 這兩個範例除了展示以函數指標當作函數參數的方法外, 同時也展示了如何利用額外定義的一個 compute() 與 compare() 函數來當作統一的呼叫介面, 藉由傳入不同的函數指標當引數來呼叫不同的函數之方法. 這是因為這兩個範例中的兩個函數介面都一樣, 即傳回值與參數型態, 參數個數都相同, 這樣才統一得起來. 然而, 上面這個整數與字元交換的範例由於介面不同, 無法像測試 12, 13 那樣使用統一的函數來呼叫, 必須為 int 與 char 分別寫一個函數 iptr() 與 cptr() :

測試 16 : 函數指標

#include <stdio.h>

void iswap(int *,int *);  //函數原型宣告
void cswap(char *,char *);  //函數原型宣告
void iptr(int *,int *,void (*)(int *,int *));  //函數原型宣告
void cptr(char *,char *,void (*)(char *,char *));  //函數原型宣告

int main() {
    int i1=5,i2=15;
    printf("交換前 : i1=%d i2=%d\n",i1,i2);
    iptr(&i1,&i2,iswap);    //以傳址呼叫將 i1, i2 之位址傳給函數
    printf("交換後 : i1=%d i2=%d\n",i1,i2);
    char c1='A',c2='B';
    printf("交換前 : c1=%c c2=%c\n",c1,c2);
    cptr(&c1,&c2,cswap);    //以傳址呼叫將 c1, c2 之位址傳給函數
    printf("交換後 : c1=%c c2=%c\n",c1,c2);
    return 0;
    }
 
void iswap(int *x,int *y) {  //函數的參數為指標 (不需傳回值)
   int tmp=*x;
   *x=*y;
   *y=tmp;
   }

void cswap(char *x,char *y) {  //函數的參數為指標 (不需傳回值)
   char tmp=*x;
   *x=*y;
   *y=tmp;
   }

void iptr(int *x,int *y,void (*ptr)(int *x,int *y)) {
   return (*ptr)(x,y);   //以函數指標 ptr 呼叫函數
   }

void cptr(char *x,char *y,void (*ptr)(char *x,char *y)) {
   return (*ptr)(x,y);    //以函數指標 ptr 呼叫函數
   }

上面的程式中, 函數被當成引數傳遞給 iswap() 與 cswap() 的第三個參數, 所以在這兩個函數中必須以指標函數來接收. 把函數當引數傳遞原先的目的是希望能將呼叫函數統一起來, 但由於 int 與 char 是不同類型而無法達成, 使得上面程式的寫法看起來挺彆扭的. 既然無法統一, 還不如像測試 15 那樣直接呼叫 iswap() 與 cswap() 算了, 不需要再多寫 iptr() 與 cptr() 這兩個轉手的函數.

但救星來了, 在 "C 語言程式設計與應用" 的 17-7-3 節介紹了 C 語言的泛型指標 void , 當兩個函數的參數是不同資料類型時, 則可以將其每一個參數都宣告為 void 類型, 任何型態的引數傳入時會被轉換成 void 類型, 函數執行完再轉回原來型態, 這樣就可以傳遞任何型態之參數, 使得上面測試 16 函數無法統一的困境得到解決, 如下列範例所示 :


測試 17 : 使用泛型指標呼叫不同函數 (交換兩個整數或字元)

#include <stdio.h>

void iswap(int *,int *);  //函數原型宣告
void cswap(char *,char *);  //函數原型宣告
void swap(void *,void *,void(*)(void *,void *));  //函數原型宣告

int main() {
 
    int i1=5,i2=15;
    printf("交換前 : i1=%d i2=%d\n",i1,i2);
    swap((void *)&i1,(void *)&i2,(void(*)(void *,void *))iswap);
    printf("交換後 : i1=%d i2=%d\n",i1,i2);  
    char c1='A',c2='B';
    printf("交換前 : c1=%c c2=%c\n",c1,c2);
    swap((void *)&c1,(void *)&c2,(void(*)(void *,void *))cswap);
    printf("交換後 : c1=%c c2=%c\n",c1,c2);
    return 0;
    }

void cswap(char *x,char *y) {  //函數的參數為指標 (不需傳回值)
   char tmp=*x;
   *x=*y;
   *y=tmp;
   }

void iswap(int *x,int *y) {  //函數的參數為指標 (不需傳回值)
   int tmp=*x;
   *x=*y;
   *y=tmp;
   }

void swap(void *a,void *b,void(*vptr)(void *,void *)) {  //泛型函數 : 參數類型全部是 void
  return (*vptr)(a,b);
  }

可見透過 void 泛型指標, 測試 14 中的 iptr() 與 cptr() 兩個函數就被統一為 swap() 一個函數了. 注意, 這裡泛型函數 swap() 中的參數 a, b 都被宣告為泛型指標 void *, 使得不管傳入的指標是 int 或 char 類型都被強迫改成 void 類型. 第三個參數是函數指標, 用來接收呼叫者傳入的函數名稱, 其參數正是對應第一與第二參數, 因此也都要宣告為 void *. 而 swap() 的內容就是呼叫函數指標所指之函數並回傳結果, 這時被迫轉型為 void 的參數又被轉回原來之類型. 由於 iswap() 與 cswap() 都無傳回值, 故 swap() 雖有 return, 其傳回值型態亦為 void.

在 "C 語言程式設計與應用" 書中 10-7-3 節的範例是比較兩個整數與兩個字元, 如果相等傳回 0, 大於傳回 1, 小於傳回 -1. 我將其改寫如下以與上例做比較 :


測試 17 : 使用泛型指標呼叫不同函數 (比較整數或字元大小)

#include <stdio.h>

int icmp(int *,int *);  //函數原型宣告
int ccmp(char *, char *);  //函數原型宣告
int cmp(void *,void *,int (*) (void *,void *));   //函數原型宣告

int main() {
    int i1=10,i2=5;
    int r=cmp((void *)&i1,(void *)&i2,(int (*)(void *,void *))icmp);
    printf("i1=%d i2=%d 傳回值=%d\n",i1,i2,r);
    i1=5,i2=5;
    r=cmp((void *)&i1,(void *)&i2,(int (*)(void *,void *))icmp);
    printf("i1=%d i2=%d 傳回值=%d\n",i1,i2,r);
    i1=5,i2=10;
    r=cmp((void *)&i1,(void *)&i2,(int (*)(void *,void *))icmp);
    printf("i1=%d i2=%d 傳回值=%d\n",i1,i2,r);      
    char c1='A',c2='B';
    r=cmp((void *)&c1,(void *)&c2,(int (*)(void *,void *))ccmp);
    printf("c1=%c c2=%c 傳回值=%d\n",c1,c2,r);
    c1='A',c2='A';
    r=cmp((void *)&c1,(void *)&c2,(int (*)(void *,void *))ccmp);
    printf("c1=%c c2=%c 傳回值=%d\n",c1,c2,r);
    c1='B',c2='A';
    r=cmp((void *)&c1,(void *)&c2,(int (*)(void *,void *))ccmp);
    printf("c1=%c c2=%c 傳回值=%d\n",c1,c2,r);    
    return 0;
    }
 
int icmp(int *x,int *y) {
    if (*x == *y) {return 0;}
    else if (*x > *y) {return 1;}
    else {return -1;}
    }
 
int ccmp(char *x,char *y) {
    if (*x == *y) {return 0;}
    else if (*x > *y) {return 1;}
    else {return -1;}
    }
 
int cmp(void *x,void *y,int (*vptr)(void *,void *)) {
    return (*vptr)(x,y);
    }

此程式中由於兩個比較函數 icmp() 與 ccmp() 都傳回 int 類型資料, 因此泛型函數 cmp() 也是傳回 int. 在呼叫 cmp() 時, 傳入參數前兩個用 (void *) 強迫轉型為 void 類型, 第三個參數則用 (int (*)(void *,void *)) 將 icmp 與 ccmp 強迫轉型為泛型函數.

執行結果如下 :

i1=10 i2=5 傳回值=1
i1=5 i2=5 傳回值=0
i1=5 i2=10 傳回值=-1
c1=A c2=B 傳回值=-1
c1=A c2=A 傳回值=0
c1=B c2=A 傳回值=1



#include <stdio.h>

int imax(int *,int *);
char cmax(char *, char *);
void vptr(void *,void *,void (*) (void *,void *));

int main() {
    int i1=5,i2=10;
    printf("i1=%d i2=%d max=%d\n",i1,i2,vptr((void *)&i1,(void *)&i2,(void(*)(void *,void *))imax));
    char c1='A',c2='B';
    printf("i1=%d i2=%d max=%c\n",i1,i2,vptr((void *)&c1,(void *)&c2,(void(*)(void *,void *))cmax));
    return 0;
    }
 
int imax(int *x,int *y) {
    if (*x > *y) {return *x;}
    else {return *y;}
    }
 
char cmax(char *x,char *y) {
    if (*x > *y) {return *x;}
    else {return *y;}
    }
 
void vptr(void *x,void *y,void (*max)(void *,void *)) {
    return (*max)(x,y);
    }


參考 :

指標與動態記憶體配置介紹
为什么说指针是 C 语言的精髓?
C語言泛型編程技巧
避免編譯時產生錯誤 - error: void value not ignored as it ought to be
C语言中的泛型编程(一)
printf() 與 scanf()

3 則留言 :

321 提到...

請問最後那個範例式故意式犯錯誤的嗎?
因為程式無法正確執行會出現invalid use of void expression
但也無法向他上面那個範例直接 給予void r = ...

321 提到...

你好想請問,在測試17中
如果將c2改成int型態後,會發現傳入cswap的型態還是char(1bytes),但是回傳回swap後印出來的型態是宣告的int(4bytes)。這可以說明文中所說的傳回值型態亦為void.
但是!
交換過後資料類型為何還是跟交換前一樣?
交換後宣告型態不變?這樣存放不屬於該型態類型資料不會有問題嗎?所占記憶體空間位置不同於該值存放類別不會有問題嗎?
現實中有沒有類似的範例?還是根本不會這樣使用?


程式碼如下:

#include

void iswap(int *,int *); //函數原型宣告
void cswap(char *,char *); //函數原型宣告

void swap(void *,void *,void(*)(void*,void*));
/* 改成 使用泛型指標呼叫不同函數
void iptr(int *,int *,void (*)(int *,int *)); //函數原型宣告
void cptr(char *,char *,void (*)(char *,char *)); //函數原型宣告
*/

int main() {
int i1=5,i2=15;
printf("交換前 : i1=%d i2=%d\n",i1,i2);
swap((void*)&i1,(void*)&i2,(void(*)(void*,void*))iswap);
//iptr(&i1,&i2,iswap); //以傳址呼叫將 i1, i2 之位址傳給函數
printf("交換後 : i1=%d i2=%d\n",i1,i2);


char c1='A';
int c2 = 20;
printf("交換前 : c1=%c c2=%d\t sizeof:c1=%d, c2=%d \n",c1,c2,sizeof(c1),sizeof(c2));
printf("交換前 address: c1=%p c2=%p\n",&c1,&c2);
swap((void*)&c1,(void*)&c2,(void(*)(void*,void*))cswap);
//cptr(&c1,&c2,cswap); //以傳址呼叫將 c1, c2 之位址傳給函數
printf("交換後 : c1=%d c2=%c\t sizeof:c1=%d, c2=%d \n",c1,c2,sizeof(c1),sizeof(c2));
printf("交換後 address: c1=%p c2=%p\n",&c1,&c2);
return 0;
}

void iswap(int *x,int *y) { //函數的參數為指標 (不需傳回值)

int tmp=*x;
*x=*y;
*y=tmp;

}

void cswap(char *x,char *y) { //函數的參數為指標 (不需傳回值)

char tmp=*x;
printf("tmp=%c sizeof:%d\t",tmp,sizeof(tmp));
*x=*y;
printf("*x=%d sizeof:%d\t",*x,sizeof(*x));
*y=tmp;
printf("*y=%c sizeof:%d\n",*y,sizeof(*y));

}

void swap(void *x,void *y,void(*vptr)(void *,void *)){
return(*vptr)(x,y);

}
/*改成 使用泛型指標呼叫不同函數
void iptr(int *x,int *y,void (*ptr)(int *x,int *y)) {
return (*ptr)(x,y); //以函數指標 ptr 呼叫函數
}

void cptr(char *x,char *y,void (*ptr)(char *x,char *y)) {
return (*ptr)(x,y); //以函數指標 ptr 呼叫函數
}
*/


小狐狸事務所 提到...

嗨 321, 真是不好意思, 太久沒用 C 語言, 細節都忘了, 容我找時間看一下, 看能否恢復功力.