關注+星標公衆號,不錯過最新文章算法
半個月前寫的那篇關於指針最底層原理的文章,獲得了不少朋友的承認(連接: C語言指針-從底層原理到花式技巧,用圖文和代碼幫你講解透徹),特別是對剛學習C語言的小夥伴來講,很容易就從根本上理解指針到底是什麼、怎麼用,這也讓我堅信一句話;用心寫出的文章,必定會被讀者感覺到!在寫這篇文章的時候,我列了一個提綱,寫到後面的時候,發現已經超過一萬字了,可是提綱上還有最後一個主題沒有寫。若是繼續寫下去,文章體積就太大了,因而就留下了一個尾巴。編程
今天,我就把這個尾巴給補上去:主要是介紹指針在應用程序的編程中,常用的技巧。若是以前的那篇文章勉強算是「道」層面的話,那這篇文章就屬於「術」的層面。主要經過 8 個示例程序來展現在 C 語言應用程序中,關於指針使用的常見套路,但願能給你帶來收穫。數組
記得我在校園裏學習C語言的時候,南師大的黃鳳良老師花了大半節課的時間給咱們解釋指針,如今最清楚地記得老師說過的一句話就是:指針就是地址,地址就是指針!數據結構
// 交換 2 個 int 型數據 void demo1_swap_data(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; } void demo1() { int i = 1; int j = 2; printf("before: i = %d, j = %d \n", i, j); demo1_swap_data(&i, &j); printf("after: i = %d, j = %d \n", i, j); }
這個代碼不用解釋了,你們一看就明白。若是再過多解釋的話,好像在侮辱智商。ide
代碼的目的是:在被調用函數中,從堆區分配 size
個字節的空間,返回給主調函數中的 pData
指針。函數
void demo2_malloc_heap_error(char *buf, int size) { buf = (char *)malloc(size); printf("buf = 0x%x \n", buf); } void demo2_malloc_heap_ok(char **buf, int size) { *buf = (char *)malloc(size); printf("*buf = 0x%x \n", *buf); } void demo2() { int size = 1024; char *pData = NULL; // 錯誤用法 demo2_malloc_heap_error(pData, size); printf("&pData = 0x%x, pData = 0x%x \n", &pData, pData); // 正確用法 demo2_malloc_heap_ok(&pData, size); printf("&pData = 0x%x, pData = 0x%x \n", &pData, pData); free(pData); }
剛進入被調用函數 demo2_malloc_heap_error
的時候,形參 buff
是一個 char* 型指針,它的值等於 pData
變量的值,也就是說 buff
與 pData
的值相同(都爲 NULL),內存模型如圖:學習
在被調用函數中執行 malloc
語句以後,從堆區申請獲得的地址空間賦值給 buf
,就是說它就指向了這個新的地址空間,而 pData
裏仍然是NULL
,內存模型以下:flex
從圖中能夠看到,pData
的內存中一直是 NULL
,沒有指向任何堆空間。另外,因爲形參 buf
是放在函數的棧區的,從被調函數中返回的時候,堆區這塊申請的空間就被泄漏了。加密
剛進入被調用函數 demo2_malloc_heap_error
的時候,形參 buf
是一個 char* 型的二級指針,就是說 buf
裏的值是另外一個指針變量的地址,在這個示例中 buf
裏的值就是 pData
這個指針變量的地址,內存模型以下:3d
在被調用函數中執行 malloc
語句以後,從堆區申請獲得的地址空間賦值給 *buf,由於 buf = &pData
,因此 *buf 就至關因而 pData
,那麼從堆區申請獲得的地址空間就賦值 pData
變量,內存模型以下:
從被調函數中返回以後,pData
就正確的獲得了一塊堆空間,別忘了使用以後要主動釋放。
從上篇文章中咱們知道,函數名自己就表明一個地址,在這個地址中存儲着函數體中定義的一連串指令碼,只要給這個地址後面加上一個調用符(小括號),就進入這個函數中執行。在實際程序中,函數名經常做爲函數參數來進行傳遞。
熟悉C++的小夥伴都知道,在標準庫中對容器類型的數據進行各類算法操做時,能夠傳入用戶本身的提供的算法函數(若是不傳入函數,標準庫就使用默認的)。
下面是一個示例代碼,對一個 int 行的數組進行排序,排序函數 demo3_handle_data
的最後一個參數是一個函數指針,所以須要傳入一個具體的排序算法函數。示例中有 2 個候選函數可使用:
- 降序排列: demo3_algorithm_decend;
- 升序排列: demo3_algorithm_ascend;
typedef int BOOL; #define FALSE 0 #define TRUE 1 BOOL demo3_algorithm_decend(int a, int b) { return a > b; } BOOL demo3_algorithm_ascend(int a, int b) { return a < b; } typedef BOOL (*Func)(int, int); void demo3_handle_data(int *data, int size, Func pf) { for (int i = 0; i < size - 1; ++i) { for (int j = 0; j < size - 1 - i; ++j) { // 調用傳入的排序函數 if (pf(data[j], data[j+1])) { int tmp = data[j]; data[j] = data[j + 1]; data[j + 1] = tmp; } } } } void demo3() { int a[5] = {5, 1, 9, 2, 6}; int size = sizeof(a)/sizeof(int); // 調用排序函數,須要傳遞排序算法函數 //demo3_handle_data(a, size, demo3_algorithm_decend); // 降序排列 demo3_handle_data(a, size, demo3_algorithm_ascend); // 升序排列 for (int i = 0; i < size; ++i) printf("%d ", a[i]); printf("\n"); }
這個就不用畫圖了,函數指針 pf
就指向了傳入的那個函數地址,在排序的時候直接調用就能夠了。
在嵌入式開發中,指向結構體的指針使用特別普遍,這裏以智能家居中的一條控制指令來舉例。在一個智能家居系統中,存在各類各樣的設備(插座、電燈、電動窗簾等),每一個設備的控制指令都是不同的,所以能夠在每一個設備的控制指令結構體中的最前面,放置全部指令都須要的、通用的成員變量,這些變量能夠稱爲指令頭(指令頭中包含一個表明命令類型的枚舉變量)。
當處理一條控制指令時,先用一個通用命令(指令頭)的指針來接收指令,而後根據命令類型枚舉變量來區分,把控制指令強制轉換成具體的那個設備的數據結構,這樣就能夠獲取到控制指令中特定的控制數據了。
本質上,與 Java/C++ 中的接口、基類的概念相似。
// 指令類型枚舉 typedef enum _CMD_TYPE_ { CMD_TYPE_CONTROL_SWITCH = 1, CMD_TYPE_CONTROL_LAMP, } CMD_TYPE; // 通用的指令數據結構(指令頭) typedef struct _CmdBase_ { CMD_TYPE cmdType; // 指令類型 int deviceId; // 設備 Id } CmdBase; typedef struct _CmdControlSwitch_ { // 前 2 個參數是指令頭 CMD_TYPE cmdType; int deviceId; // 下面都有這個指令私有的數據 int slot; // 排插上的哪一個插口 int state; // 0:斷開, 1:接通 } CmdControlSwitch; typedef struct _CmdControlLamp_ { // 前 2 個參數是指令頭 CMD_TYPE cmdType; int deviceId; // 下面都有這個指令私有的數據 int color; // 顏色 int brightness; // 亮度 } CmdControlLamp; // 參數是指令頭指針 void demo4_control_device(CmdBase *pcmd) { // 根據指令頭中的命令類型,把指令強制轉換成具體設備的指令 if (CMD_TYPE_CONTROL_SWITCH == pcmd->cmdType) { // 類型強制轉換 CmdControlSwitch *cmd = pcmd; printf("control switch. slot = %d, state = %d \n", cmd->slot, cmd->state); } else if (CMD_TYPE_CONTROL_LAMP == pcmd->cmdType) { // 類型強制轉換 CmdControlLamp * cmd = pcmd; printf("control lamp. color = 0x%x, brightness = %d \n", cmd->color, cmd->brightness); } } void demo4() { // 指令1:控制一個開關 CmdControlSwitch cmd1 = {CMD_TYPE_CONTROL_SWITCH, 1, 3, 0}; demo4_control_device(&cmd1); // 指令2:控制一個燈泡 CmdControlLamp cmd2 = {CMD_TYPE_CONTROL_LAMP, 2, 0x112233, 90}; demo4_control_device(&cmd2); }
這個示例在上篇文章中演示過,爲了完整性,這裏再貼一下。
int add(int a, int b) { return a + b; } int sub(int a, int b) { return a - b; } int mul(int a, int b) { return a * b; } int divide(int a, int b) { return a / b; } void demo5() { int a = 4, b = 2; int (*p[4])(int, int); p[0] = add; p[1] = sub; p[2] = mul; p[3] = divide; printf("%d + %d = %d \n", a, b, p[0](a, b)); printf("%d - %d = %d \n", a, b, p[1](a, b)); printf("%d * %d = %d \n", a, b, p[2](a, b)); printf("%d / %d = %d \n", a, b, p[3](a, b)); }
先不解釋概念,咱們先來看一個代碼示例:
// 一個結構體,成員變量 data 是指針 typedef struct _ArraryMemberStruct_NotGood_ { int num; char *data; } ArraryMemberStruct_NotGood; void demo6_not_good() { // 打印結構體的內存大小 int size = sizeof(ArraryMemberStruct_NotGood); printf("size = %d \n", size); // 分配一個結構體指針 ArraryMemberStruct_NotGood *ams = (ArraryMemberStruct_NotGood *)malloc(size); ams->num = 1; // 爲結構體中的 data 指針分配空間 ams->data = (char *)malloc(1024); strcpy(ams->data, "hello"); printf("ams->data = %s \n", ams->data); // 打印結構體指針、成員變量的地址 printf("ams = 0x%x \n", ams); printf("ams->num = 0x%x \n", &ams->num); printf("ams->data = 0x%x \n", ams->data); // 釋放空間 free(ams->data); free(ams); }
在個人電腦上,打印結果以下:
能夠看到:該結構體一共有 8 個字節(int 型佔 4 個字節,指針型佔 4 個字節)。
結構體中的 data
成員是一個指針變量,須要單獨爲它申請一塊空間纔可使用。並且在結構體使用以後,須要先釋放 data
,而後釋放結構體指針 ams
,順序不能錯。
這樣使用起來,是否是有點麻煩?
因而,C99 標準就定義了一個語法:flexible array member(柔性數組),直接上代碼(下面的代碼若是編譯時遇到警告,請檢查下編譯器對這個語法的支持):
// 一個結構體,成員變量是未指明大小的數組 typedef struct _ArraryMemberStruct_Good_ { int num; char data[]; } ArraryMemberStruct_Good; void demo6_good() { // 打印結構體的大小 int size = sizeof(ArraryMemberStruct_Good); printf("size = %d \n", size); // 爲結構體指針分配空間 ArraryMemberStruct_Good *ams = (ArraryMemberStruct_Good *)malloc(size + 1024); strcpy(ams->data, "hello"); printf("ams->data = %s \n", ams->data); // 打印結構體指針、成員變量的地址 printf("ams = 0x%x \n", ams); printf("ams->num = 0x%x \n", &ams->num); printf("ams->data = 0x%x \n", ams->data); // 釋放空間 free(ams); }
打印結果以下:
與第一個例子中有下面幾個不一樣點:
- 結構體的大小變成了 4;
- 爲結構體指針分配空間時,除告終構體自己的大小外,還申請了 data 須要的空間大小;
- 不須要爲 data 單獨分配空間了;
- 釋放空間時,直接釋放結構體指針便可;
是否是用起來簡單多了?!這就是柔性數組的好處。
從語法上來講,柔性數組就是指結構體中最後一個元素個數未知的數組,也能夠理解爲長度爲 0,那麼就可讓這個結構體稱爲可變長的。
前面說過,數組名就表明一個地址,是一個不變的地址常量。在結構體中,數組名僅僅是一個符號而已,只表明一個偏移量,不會佔用具體的空間。
另外,柔性數組能夠是任意類型。這裏示例你們多多體會,在不少通信類的處理場景中,經常見到這種用法。
這個標題讀起來彷佛有點拗口,拆分一下:在一個結構體變量中,能夠利用指針操做的技巧,獲取某個成員變量的地址、距離結構體變量的開始地址、之間的偏移量。
在 Linux 內核代碼中你能夠看到不少地方都利用了這個技巧,代碼以下:
#define offsetof(TYPE, MEMBER) ((size_t) &(((TYPE*)0)->MEMBER)) typedef struct _OffsetStruct_ { int a; int b; int c; } OffsetStruct; void demo7() { OffsetStruct os; // 打印結構體變量、成員變量的地址 printf("&os = 0x%x \n", &os); printf("&os->a = 0x%x \n", &os.a); printf("&os->b = 0x%x \n", &os.b); printf("&os->c = 0x%x \n", &os.c); printf("===== \n"); // 打印成員變量地址,與結構體變量開始地址,之間的偏移量 printf("offset: a = %d \n", (char *)&os.a - (char *)&os); printf("offset: b = %d \n", (char *)&os.b - (char *)&os); printf("offset: c = %d \n", (char *)&os.c - (char *)&os); printf("===== \n"); // 經過指針的強制類型轉換來獲取偏移量 printf("offset: a = %d \n", (size_t) &((OffsetStruct*)0)->a); printf("offset: b = %d \n", (size_t) &((OffsetStruct*)0)->b); printf("offset: c = %d \n", (size_t) &((OffsetStruct*)0)->c); printf("===== \n"); // 利用宏定義來獲得成員變量的偏移量 printf("offset: a = %d \n", offsetof(OffsetStruct, a)); printf("offset: b = %d \n", offsetof(OffsetStruct, b)); printf("offset: c = %d \n", offsetof(OffsetStruct, c)); }
先來看打印結果:
前面 4 行的打印信息不須要解釋了,直接看下面這個內存模型便可理解。
下面這個語句也不須要多解釋,就是把兩個地址的值進行相減,獲得距離結構體變量開始地址的偏移量,注意:須要把地址強轉成 char* 型以後,才能夠相減。
printf("offset: a = %d \n", (char *)&os.a - (char *)&os);
下面這條語句須要好好理解:
printf("offset: a = %d \n", (size_t) &((OffsetStruct*)0)->a);
數字 0 當作是一個地址,也就是一個指針。上篇文章解釋過,指針就表明內存中的一塊空間,至於你把這塊空間裏的數據看做是什麼,這個隨便你,你只要告訴編譯器,編譯器就按照你的意思去操做這些數據。
如今咱們把 0 這個地址裏的數據當作是一個 OffsetStruct 結構體變量(經過強制轉換來告訴編譯器),這樣就獲得了一個 OffsetStruct 結構體指針(下圖中綠色橫線),而後獲得該指針變量中的成員變量 a(藍色橫線),再而後經過取地址符 & 獲得 a 的地址(橙色橫線),最後把這個地址強轉成 size_t 類型(紅色橫線)。
由於這個結構體指針變量是從 0 地址開始的,所以,成員變量 a 的地址就是 a 距離結構體變量開始地址的偏移量。
上面的描述過程,若是感受拗口,請結合下面這張圖再讀幾遍:
上面這張圖若是能看懂的話,那麼最後一種經過宏定義獲取偏移量的打印語句也就明白了,無非就是把代碼抽象成宏定義了,方便調用:
#define offsetof(TYPE, MEMBER) ((size_t) &(((TYPE*)0)->MEMBER)) printf("offset: a = %d \n", offsetof(OffsetStruct, a));
可能有小夥伴提出:獲取這個偏移量有什麼用啊?那就請接着看下面的示例 8。
標題一樣比較拗口,直接結合代碼來看:
typedef struct _OffsetStruct_ { int a; int b; int c; } OffsetStruct;
假設有一個 OffsetStruct 結構體變量 os,咱們只知道 os 中成員變量 c 的地址(指針),那麼咱們想獲得變量 os 的地址(指針),應該怎麼作?這就是標題所描述的目的。
下面代碼中的宏定義 container_of
一樣是來自於 Linux 內核中的(你們日常沒事時多挖掘,能夠發現不少好東西)。
#define container_of(ptr, type, member) ({ \ const typeof( ((type *)0)->member ) *__mptr = (ptr); \ (type *)( (char *)__mptr - offsetof(type,member) );}) void demo8() { // 下面 3 行僅僅是演示 typeof 關鍵字的用法 int n = 1; typeof(n) m = 2; // 定義相同類型的變量m printf("n = %d, m = %d \n", n, m); // 定義結構體變量,並初始化 OffsetStruct os = {1, 2, 3}; // 打印結構體變量的地址、成員變量的值(方便後面驗證) printf("&os = 0x%x \n", &os); printf("os.a = %d, os.b = %d, os.c = %d \n", os.a, os.b, os.c); printf("===== \n"); // 假設只知道某個成員變量的地址 int *pc = &os.c; OffsetStruct *p = NULL; // 根據成員變量的地址,獲得結構體變量的地址 p = container_of(pc, OffsetStruct, c); // 打印指針的地址、成員變量的值 printf("p = 0x%x \n", p); printf("p->a = %d, p->b = %d, p->c = %d \n", p->a, p->b, p->c); }
先看打印結果:
首先要清楚宏定義中參數的類型:
- ptr: 成員變量的指針;
- type: 結構體類型;
- member:成員變量的名稱;
這裏的重點就是理解宏定義 container_of
,結合下面這張圖,把宏定義拆開來進行描述:
宏定義中的第 1 條語句分析:
- 綠色橫線:把數字 0 當作是一個指針,強轉成結構體 type 類型;
- 藍色橫線:獲取該結構體指針中的成員變量 member;
- 橙色橫線:利用 typeof 關鍵字,獲取該 member 的類型,而後定義這個類型的一個指針變量 __mptr;
- 紅色橫線:把宏參數 ptr 賦值給 __mptr 變量;
宏定義中的第 2 條語句分析:
- 綠色橫線:利用 demo7 中的 offset 宏定義,獲得成員變量 member 距離結構體變量開始地址的偏移量,而這個成員變量指針剛纔已經知道了,就是 __mptr;
- 藍色橫線:把 __mptr 這個地址,減去它本身距離結構體變量開始地址的偏移量,就獲得了該結構體變量的開始地址;
- 橙色橫線:最後把這個指針(此時是 char* 型),強轉成結構體 type 類型的指針;
上面這 8 個關於指針的用法掌握以後,再去處理子字符、數組、鏈表等數據,基本上就是熟練度和工做量的問題了。
但願你們都能用好指針這個神器,提升程序程序執行效率。
原創不易,若是這篇文章有幫助,請轉發、分享給您的朋友,道哥在此表示感謝!
做者:道哥(公衆號: IOT物聯網小鎮)
知乎:道哥
B站:道哥分享
掘金:道哥分享
CSDN:道哥分享
[1] C語言指針-從底層原理到花式技巧,用圖文和代碼幫你講解透徹
[2] 一步步分析-如何用C實現面向對象編程
[3] 原來gdb的底層調試原理這麼簡單
[4] 生產者和消費者模式中的雙緩衝技術
[5] 關於加密、證書的那些事
[6] 深刻LUA腳本語言,讓你完全明白調試原理
[7] 一個printf(結構體變量)引起的血案