一直覺得C語言較其他語言最偉大的地方就是C語言中的指針,有些人認為指針很簡單,而有些人認為指針很難,當然這里的對簡單和難并不是等價于對指針的理解程度。
為此在這里對C語言中的指針進行全面的總結,從底層的內存分析,徹底讓讀者明白指針的本質。
首先讀者要明白指針是一個變量,為此作者寫了如下代碼來驗證之:
#include "stdio.h"int main(int argc, char **argv){ unsigned int a = 10; unsigned int *p = NULL; p = &a; printf("&a=%d/n",a); printf("&a=%d/n",&a); *p = 20; printf("a=%d/n",a); return 0;}
這說明編譯器確實是在解引時無法確定*p的大小,因此這里必須告訴編譯器p的類型或者*p的大小,如何告訴呢?很簡單,用強制類型轉換即可,如下:
*(int*)p
這樣上面的程序就可以寫為如下:
#include <stdio.h>int main(int argc, char **argv){ int a=10; void *p; p=&a; printf("p=%d/n",*(int*)p); return 0;}
編譯運行后:
可以看到結果確實是正確的,也和預期的想法一致。由于void指針沒有空間大小屬性,因此void指針也沒有++操作。
函數指針在Linux內核中用的非常多,而且在設計操作系統的時候也會用到,因此這里將詳細講解函數指針。既然函數指針也是指針,那函數指針也占用4個字節(32位編譯器)。
下面以一個簡單的例子說明:
#include <stdio.h>int add(int a,int b){ return a+b;}int main(int argc, char **argv){ int (*p)(int,int); p=add; printf("add(10,20)=%d/n",(*p)(10,20)); return 0;}
程序運行結果如下:
可以看到,函數指針的申明為:
函數指針的解引操作與普通的指針有點不一樣。
對于普通的指針而言,解引只需要根據類型來取出數據即可,但函數指針是要調用一個函數,其解引不可能是將數據取出,實際上函數指針的解引本質上是執行函數的過程,只是這個執行函數是使用的call指令并不是之前的函數,而是函數指針的值,即函數的地址。
其實執行函數的過程本質上也是利用call指令來調用函數的地址,因此函數指針本質上就是保存函數執行過程的首地址。函數指針的調用如下:
為了確認函數指針本質上是傳遞給call指令一個函數的地址,下面用一個簡單例子說明:
上面是編譯后的匯編指令,可以看到,使用函數指針來調用函數時,其匯編指令多了如下:
0x4015e3 mov DWORD PTR [esp+0xc],0x4015c00x4015eb mov eax,DWORD PTR [esp+0xc]0x4015ef call eax
分析:第一行mov指令將立即數0x4015c0賦值給寄存器esp+0xc的地址內存中,然后將寄存器esp+0xc地址的值賦值給寄存器eax(累加器),然后調用call指令,此時pc指針將會指向add函數,而0x4015c0正好是函數add的首地址,這樣就完成了函數的調用。
細心的讀者是否發現一個有趣的現象,上述過程中函數指針的值和參數一樣是被放在棧幀中,這樣看起來就是一個參數傳遞的過程。
因此可以看到,函數指針最終還是以參數傳遞的形式傳遞給被調用的函數,而這個傳遞的值正好是函數的首地址。
從上面可以看到函數指針并不是和一般的指針一樣可以操作內存,因此作者覺得函數指針可以看作是函數的引用申明。
在linux驅動面向對象編程思想中用的最多,利用函數指針來實現封裝,下面以一個簡單的例子說明:
#include <stdio.h>typedef struct TFT_DISPLAY{ int pix_width; int pix_height; int color_width; void (*init)(void); void (*fill_screen)(int color); void (*tft_test)(void);}tft_display;static void init(void){ printf("the display is initialed/n");}static void fill_screen(int color){ printf("the display screen set 0x%x/n",color);}tft_display mydisplay={ .pix_width=320, .pix_height=240, .color_width=24, .init=init, .fill_screen=fill_screen,};int main(int argc, char **argv){ mydisplay.init(); mydisplay.fill_screen(0xfff); return 0;}
上面的例子將一個tft_display封裝成一個對象,上面的結構體成員中最后一個沒有初始化,這在Linux中用的非常多。
最常見的是file_operations結構體,該結構體一般來說只需要初始化常見的函數,不需要全部初始化。
上面代碼中采用的結構體初始化方式也是在Linux中最常用的一種方式,這種方式的好處在于無需按照結構體的順序一對一。
有時候會遇到這樣一種情況,當上層人員將一個功能交給下層程序員完成時,上層程序員和下層程序員同步工作,這個時候該功能函數并未完成,這個時候上層程序員可以定義一個API來交給下層程序員。
而上層程序員只要關心該API就可以了而無需關心具體實現,具體實現交給下層程序員完成即可(這里的上層和下層程序員不指等級關系,而是項目的分工關系)。
這種情況下就會用到回調函數(Callback Function),現在假設程序員A需要一個FFT算法,這個時候程序員A將FFT算法交給程序員B來完成,現在來讓實現這個過程:
#include <stdio.h>int InputData[100]={0};int OutputData[100]={0};void FFT_Function(int *inputData,int *outputData,int num){ while(num--) { }}void TaskA_CallBack(void (*fft)(int*,int*,int)){ (*fft)(InputData,OutputData,100);}int main(int argc, char **argv){ TaskA_CallBack(FFT_Function); return 0;}
上面的代碼中TaskA_CallBack是回調函數,該函數的形參為一個函數指針,而FFT_Function是一個被調用函數。
可以看到回調函數中申明的函數指針必須和被調用函數的類型完全相同。
本文鏈接:http://www.tebozhan.com/showinfo-26-88564-0.html還是結構體指針寫的代碼看著舒服
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com