深度閱讀:C語言指針,從底層原理到花式技巧,圖文+代碼透析

如下文章來源於公衆號IOT物聯網小鎮 ,做者道哥程序員

 

前言

 

若是問 C 語言中最重要、威力最大的概念是什麼,答案必將是指針!編程

威力大,意味着使用方便、高效,同時也意味着語法複雜、容易出錯。指針用的好,能夠極大的提升代碼執行效率、節約系統資源;若是用的很差,程序中將會充滿陷阱、漏洞。數組

這篇文章,咱們就來聊聊指針。從最底層的內存存儲空間開始,一直到應用層的各類指針使用技巧,按部就班、抽絲剝繭,以最直白的語言進行講解,讓你一次看過癮。網絡

說明:爲了方便講解和理解,文中配圖的內存空間的地址是隨便寫的,在實際計算機中是要遵循地址對齊方式的。數據結構

 

變量與指針的本質

 

2.1 內存地址

咱們編寫一個程序源文件以後,編譯獲得的二進制可執行文件存放在電腦的硬盤上,此時它是一個靜態的文件,通常稱之爲程序。編程語言

當這個程序被啓動的時候,操做系統將會作下面幾件事情:ide

  • 把程序的內容(代碼段、數據段)從硬盤複製到內存中;
  • 建立一個數據結構 PCB(進程控制塊),來描述這個程序的各類信息(例如:使用的資源,打開的文件描述符...);
  • 在代碼段中定位到入口函數的地址,讓 CPU 從這個地址開始執行。

當程序開始被執行時,就變成一個動態的狀態,通常稱之爲進程。函數

內存分爲:物理內存和虛擬內存。操做系統對物理內存進行管理、包裝,咱們開發者面對的是操做系統提供的虛擬內存。學習

這 2 個概念不妨礙文章的理解,所以就統一稱之爲內存。測試

在咱們的程序中,經過一個變量名來定義變量、使用變量。

變量自己是一個確確實實存在的東西,變量名是一個抽象的概念,用來表明這個變量。就好比:我是一個實實在在的人,是客觀存在與這個地球上的,道哥是我給本身起的一個名字,這個名字是任意取的,只要本身以爲好聽就行,若是我願意還能夠起名叫鳥哥、龍哥等等。

那麼,咱們定義一個變量以後,這個變量放在哪裏呢?那就是內存的數據區。

內存是一個很大的存儲區域,被操做系統劃分爲一個一個的小空間,操做系統經過地址來管理內存。

內存中的最小存儲單位是字節(8 個 bit),一個內存的完整空間就是由這一個一個的字節連續組成的。

在上圖中,每個小格子表明一個字節,可是好像你們在書籍中沒有這麼來畫內存模型的,更常見的是下面這樣的畫法:

也就是把連續的 4 個字節的空間畫在一塊兒,這樣就便於表述和理解,特別是深刻到代碼對齊相關知識時更容易理解。(我認爲根本緣由應該是:你們都這麼畫,已經看順眼了~~)

2.2 32 位與 64 位系統

咱們平時所說的計算機是 32 位、64 位,指的是計算機的 CPU 中寄存器的最大存儲長度,若是寄存器中最大存儲 32bit 的數據,就稱之爲 32 位系統。

在計算機中,數據通常都是在硬盤、內存和寄存器之間進行來回存取。CPU 經過 3 種總線把各組成部分聯繫在一塊兒:地址總線、數據總線和控制總線。地址總線的寬度決定了 CPU 的尋址能力,也就是 CPU 能達到的最大地址範圍。

剛纔說了,內存是經過地址來管理的,那麼 CPU 想從內存中的某個地址空間上存取一個數據,那麼 CPU 就須要在地址總線上輸出這個存儲單元的地址。

假如地址總線的寬度是 8 位,能表示的最大地址空間就是 256 個字節,能找到內存中最大的存儲單元是 255 這個格子(從 0 開始)。即便內存條的實際空間是 2G 字節,CPU 也無法使用後面的內存地址空間。若是地址總線的寬度是 32 位,那麼能表示的最大地址就是 2 的 32 次方,也就是 4G 字節的空間。

【注意】這裏只是描述地址總線的概念,實際的計算機中地址計算方式要複雜的多,好比:虛擬內存中採用分段、分頁、偏移量來定位實際的物理內存,在分頁中還有大頁、小頁之分,感興趣的同窗能夠本身查一下相關資料。

2.3 變量

咱們在 C 程序中使用變量來「表明」一個數據,使用函數名來「表明」一個函數,變量名和函數名是程序員使用的助記符。變量和函數最終是要放到內存中才能被 CPU 使用的,而內存中全部的信息(代碼和數據)都是以二進制的形式來存儲的,計算機根據就不會從格式上來區分哪些是代碼、哪些是數據。CPU 在訪問內存的時候須要的是地址,而不是變量名、函數名。

問題來了:在程序代碼中使用變量名來指代變量,而變量在內存中是根據地址來存放的,這兩者之間如何映射(關聯)起來的?

答案是:編譯器!編譯器在編譯文本格式的 C 程序文件時,會根據目標運行平臺(就是編譯出的二進制程序運行在哪裏?是 x86 平臺的電腦?仍是 ARM 平臺的開發板?)來安排程序中的各類地址,例如:加載到內存中的地址、代碼段的入口地址等等,同時編譯器也會把程序中的全部變量名,轉成該變量在內存中的存儲地址。

變量有 2 個重要屬性:變量的類型和變量的值

示例:代碼中定義了一個變量

int a = 20;

類型是 int 型,值是 20。這個變量在內存中的存儲模型爲:

咱們在代碼中使用變量名 a,在程序執行的時候就表示使用 0x11223344 地址所對應的那個存儲單元中的數據。

所以,能夠理解爲變量名 a 就等價於這個地址 0x11223344。換句話說,若是咱們能夠提早知道編譯器把變量 a 安排在地址 0x11223344 這個單元格中,咱們就能夠在程序中直接用這個地址值來操做這個變量。

在上圖中,變量 a 的值爲 20,在內存中佔據了 4 個格子的空間,也就是 4 個字節。爲何是 4 個字節呢?在 C 標準中並無規定每種數據類型的變量必定要佔用幾個字節,這是與具體的機器、編譯器有關。

好比:32 位的編譯器中:

char: 1 個字節;
short int: 2 個字節;
int: 4 個字節;
long: 4 個字節。

好比:64 位的編譯器中:

char: 1 個字節;
short int: 2 個字節;
int: 4 個字節;
long: 8 個字節。

爲了方便描述,下面都以 32 位爲例,也就是 int 型變量在內存中佔據 4 個字節。

另外,0x11223344,0x11223345,0x11223346,0x11223347 這連續的、從低地址到高地址的 4 個字節用來存儲變量 a 的數值 20。

在圖示中,使用十六進制來表示,十進制數值 20 轉成 16 進制就是:0x00000014,因此從開始地址依次存放 0x00、0x00、0x00、0x14 這 4 個字節(存儲順序涉及到大小端的問題,不影響文本理解)。

根據這個圖示,若是在程序中想知道變量 a 存儲在內存中的什麼位置,可使用取地址操做符&,以下:

printf("&a = 0x%x \n", &a);

這句話將會打印出:&a = 0x11223344。

考慮一下,在 32 位系統中:指針變量佔用幾個字節?

2.4 指針變量

指針變量能夠分 2 個層次來理解:

  • 指針變量首先是一個變量,因此它擁有變量的全部屬性:類型和值。它的類型就是指針,它的值是其餘變量的地址。 既然是一個變量,那麼在內存中就須要爲這個變量分配一個存儲空間。在這個存儲空間中,存放着其餘變量的地址。
  • 指針變量所指向的數據類型,這是在定義指針變量的時候就肯定的。例如:int *p; 意味着指針指向的是一個 int 型的數據。

首先回答一下剛纔那個問題,在 32 位系統中,一個指針變量在內存中佔據 4 個字節的空間。由於 CPU 對內存空間尋址時,使用的是 32 位地址空間( 4 個字節),也就是用 4 個字節就能存儲一個內存單元的地址。而指針變量中的值存儲的就是地址,因此須要 4 個字節的空間來存儲一個指針變量的值。

示例:

int a = 20;

int *pa;

pa = &a;

printf("value = %d \n", *pa);

在內存中的存儲模型以下:

對於指針變量 pa 來講,首先它是一個變量,所以在內存中須要有一個空間來存儲這個變量,這個空間的地址就是 0x11223348;

其次,這個內存空間中存儲的內容是變量 a 的地址,而 a 的地址爲 0x11223344,因此指針變量 pa 的地址空間中,就存儲了 0x11223344 這個值。

這裏對兩個操做符&和*進行說明:

&:取地址操做符,用來獲取一個變量的地址。上面代碼中&a就是用來獲取變量 a 在內存中的存儲地址,也就是 0x11223344。

*:這個操做符用在 2 個場景中:定義一個指針的時候,獲取一個指針所指向的變量值的時候。

  • int pa; 這個語句中的表示定義的變量 pa 是一個指針,前面的 int 表示 pa 這個指針指向的是一個 int 類型的變量。不過此時咱們沒有給 pa 進行賦值,也就是說此刻 pa 對應的存儲單元中的 4 個字節裏的值是沒有初始化的,多是 0x00000000,也多是其餘任意的數字,不肯定;
  • printf 語句中的 * 表示獲取 pa 指向的那個 int 類型變量的值,學名叫解引用,咱們只要記住是獲取指向的變量的值就能夠了。

2.5 操做指針變量

對指針變量的操做包括 3 個方面:

  • 操做指針變量自身的值;
  • 獲取指針變量所指向的數據;
  • 以什麼樣數據類型來使用/解釋指針變量所指向的內容。

指針變量自身的值

int a = 20;這個語句是定義變量 a,在隨後的代碼中,只要寫下 a 就表示要操做變量 a 中存儲的值,操做有兩種:讀和寫。

printf("a = %d \n", a); 這個語句就是要讀取變量 a 中的值,固然是 20;
a = 100;這個語句就是要把一個數值 100 寫入到變量 a 中。

一樣的道理,int *pa;語句是用來定義指針變量 pa,在隨後的代碼中,只要寫下 pa 就表示要操做變量 pa 中的值:

printf("pa = %d \n", pa); 這個語句就是要讀取指針變量 pa 中的值,固然是 0x11223344;
pa = &a;這個語句就是要把新的值寫入到指針變量 pa 中。

再次強調一下,指針變量中存儲的是地址,若是咱們能夠提早知道變量 a 的地址是 0x11223344,那麼咱們也能夠這樣來賦值:pa = 0x11223344;

思考一下,若是執行這個語句 printf("&pa =0x%x \n", &pa);,打印結果會是什麼?

上面已經說過,操做符&是用來取地址的,那麼&pa 就表示獲取指針變量 pa 的地址,上面的內存模型中顯示指針變量 pa 是存儲在 0x11223348 這個地址中的,所以打印結果就是:&pa = 0x11223348。

獲取指針變量所指向的數據

指針變量所指向的數據類型是在定義的時候就明確的,也就是說指針 pa 指向的數據類型就是 int 型,所以在執行 printf("value = %d \n", *pa);語句時,首先知道 pa 是一個指針,其中存儲了一個地址(0x11223344),而後經過操做符*來獲取這個地址(0x11223344)對應的那個存儲空間中的值;又由於在定義 pa時,已經指定了它指向的值是一個 int 型,因此咱們就知道了地址 0x11223344 中存儲的就是一個 int 類型的數據。

以什麼樣的數據類型來使用/解釋指針變量所指向的內容

以下代碼:

int a = 30000;

int *pa = &a;

printf("value = %d \n", *pa);

根據以上的描述,咱們知道 printf 的打印結果會是 value = 30000,十進制的 30000 轉成十六進制是 0x00007530,內存模型以下:

如今咱們作這樣一個測試:

char *pc = 0x11223344;

printf("value = %d \n", *pc);

指針變量 pc 在定義的時候指明:它指向的數據類型是 char 型,pc 變量中存儲的地址是 0x11223344。當使用*pc 獲取指向的數據時,將會按照 char 型格式來讀取 0x11223344 地址處的數據,所以將會打印 value = 0(在計算機中,ASCII 碼是用等價的數字來存儲的)。

這個例子中說明了一個重要的概念:在內存中一切都是數字,如何來操做(解釋)一個內存地址中的數據,徹底是由咱們的代碼來告訴編譯器的。

剛纔這個例子中,雖然 0x11223344 這個地址開始的 4 個字節的空間中,存儲的是整型變量 a 的值,可是咱們讓 pc 指針按照 char 型數據來使用/解釋這個地址處的內容,這是徹底合法的。

以上內容,就是指針最根本的心法了。把這個心法整明白了,剩下的就是多見識、多練習的問題了。

指針的幾個相關概念

 

3.1 const 屬性

const 標識符用來表示一個對象的不可變的性質,例如定義:

const int b = 20;

在後面的代碼中就不能改變變量 b 的值了,b 中的值永遠是 20。一樣的,若是用 const 來修飾一個指針變量:

int a = 20;

int b = 20;

int * const p = &a;

內存模型以下:

這裏的 const 用來修飾指針變量 p,根據 const 的性質能夠得出結論:p 在定義爲變量 a 的地址以後,就固定了,不能再被改變了。也就是說指針變量 pa 中就只能存儲變量 a 的地址 0x11223344。若是在後面的代碼中寫 p = &b;,編譯時就會報錯,由於 p 是不可改變的,不能再被設置爲變量 b 的地址。

可是,指針變量 p 所指向的那個變量 a 的值是能夠改變的,即:*p = 21;這個語句是合法的,由於指針 p 的值沒有改變(仍然是變量 c 的地址 0x11223344),改變的是變量 c 中存儲的值。

與下面的代碼區分一下:

int a = 20;

int b = 20;

const int *p = &a;

p = &b;

這裏的 const 沒有放在 p 的旁邊,而是放在了類型 int 的旁邊,這就說明 const 符號不是用來修飾 p 的,而是用來修飾 p 所指向的那個變量的。因此,若是咱們寫 p = &b;把變量 b 的地址賦值給指針 p,就是合法的,由於 p 的值能夠被改變。

可是這個語句*p = 21 就是非法了,由於定義語句中的 const 就限制了經過指針 p 獲取的數據,不能被改變,只能被用來讀取。這個性質經常被用在函數參數上,例以下面的代碼,用來計算一塊數據的 CRC 校驗,這個函數只須要讀取原始數據,不須要(也不能夠)改變原始數據,所以就須要在形參指針上使用 const 修飾符:

short int getDataCRC(const char *pData, int len)

{

    short int crc = 0x0000;

    // 計算CRC

    return crc;

}

3.2 void 型指針

關鍵字 void 並非一個真正的數據類型,它體現的是一種抽象,指明不是任何一種類型,通常有 2 種使用場景:

  • 函數的返回值和形參;
  • 定義指針時不明確規定所指數據的類型,也就意味着能夠指向任意類型。

指針變量也是一種變量,變量之間能夠相互賦值,那麼指針變量之間也能夠相互賦值,例如:

int a = 20;

int b = a;

int *p1 = &a;

int *p2 = p1;

變量 a 賦值給變量 b,指針 p1 賦值給指針 p2,注意到它們的類型必須是相同的:a 和 b 都是 int 型,p1 和 p2 都是指向 int 型,因此能夠相互賦值。那麼若是數據類型不一樣呢?必須進行強制類型轉換。例如:

int a = 20;

int *p1 = &a;

char *p2 = (char *)p1;

內存模型以下:

p1 指針指向的是 int 型數據,如今想把它的值(0x11223344)賦值給 p2,可是因爲在定義 p2 指針時規定它指向的數據類型是 char 型,所以須要把指針 p1 進行強制類型轉換,也就是把地址 0x11223344 處的數據按照 char 型數據來看待,而後才能夠賦值給 p2 指針。

若是咱們使用 void *p2 來定義 p2 指針,那麼在賦值時就不須要進行強制類型轉換了,例如:

int a = 20;

int *p1 = &a;

void *p2 = p1;

指針 p2是void* 型,意味着能夠把任意類型的指針賦值給 p2,可是不能反過來操做,也就是不能把 void* 型指針直接賦值給其餘肯定類型的指針,而必需要強制轉換成被賦值指針所指向的數據類型,以下代碼,必須把 p2 指針強制轉換成 int* 型以後,再賦值給 p3 指針:

int a = 20;

int *p1 = &a;

void *p2 = p1;

int *p3 = (int *)p2;

咱們來看一個系統函數:

void* memcpy(void* dest, const void* src, size_t len);

第一個參數類型是 void*,這正體現了系統對內存操做的真正意義:它並不關心用戶傳來的指針具體指向什麼數據類型,只是把數據挨個存儲到這個地址對應的空間中。

第二個參數一樣如此,此外還添加了 const 修飾符,這樣就說明了 memcpy 函數只會從src指針處讀取數據,而不會修改數據。

3. 3 空指針和野指針

一個指針必須指向一個有意義的地址以後,才能夠對指針進行操做。若是指針中存儲的地址值是一個隨機值,或者是一個已經失效的值,此時操做指針就很是危險了,通常把這樣的指針稱做野指針,C 代碼中不少指針相關的 bug 就來源於此。

空指針:不指向任何東西的指針

在定義一個指針變量以後,若是沒有賦值,那麼這個指針變量中存儲的就是一個隨機值,有可能指向內存中的任何一個地址空間,此時萬萬不能夠對這個指針進行寫操做,由於它有可能指向內存中的代碼段區域、也可能指向內存中操做系統所在的區域。

通常會將一個指針變量賦值爲 NULL 來表示一個空指針,而 C 語言中,NULL 實質是 ((void*)0) , 在 C++中,NULL 實質是 0。在標準庫頭文件 stdlib.h中,有以下定義:

#ifdef __cplusplus

     #define NULL    0

#else    

     #define NULL    ((void *)0)

#endif

野指針:地址已經失效的指針

咱們都知道,函數中的局部變量存儲在棧區,經過 malloc 申請的內存空間位於堆區,以下代碼:

int *p = (int *)malloc(4);

*p = 20;

內存模型爲:

在堆區申請了 4 個字節的空間,而後強制類型轉換爲 int* 型以後,賦值給指針變量 p,而後經過 *p 設置這個地址中的值爲 14,這是合法的。若是在釋放了 p 指針指向的空間以後,再使用 *p 來操做這段地址,那就是很是危險了,由於這個地址空間可能已經被操做系統分配給其餘代碼使用,若是對這個地址裏的數據強行操做,程序馬上崩潰的話,將會是咱們最大的幸運!

int *p = (int *)malloc(4);

*p = 20;

free(p);

// 在free以後就不能夠再操做p指針中的數據了。

p = NULL;  // 最好加上這一句。

指向不一樣數據類型的指針

 

4.1 數值型指針

經過上面的介紹,指向數值型變量的指針已經很明白了,須要注意的就是指針所指向的數據類型。

4.2 字符串指針

字符串在內存中的表示有 2 種:

  • 用一個數組來表示,例如:char name1[8] = "zhangsan";
  • 用一個 char *指針來表示,例如:char *name2 = "zhangsan";

name1 在內存中佔據 8 個字節,其中存儲了 8 個字符的 ASCII 碼值;name2 在內存中佔據 9 個字節,由於除了存儲 8 個字符的 ASCII 碼值,在最後一個字符'n'的後面還額外存儲了一個'\0',用來標識字符串結束。

對於字符串來講,使用指針來操做是很是方便的,例如:變量字符串 name2:

char *name2 = "zhangsan";

char *p = name2;

while (*p != '\0')

{

    printf("%c ", *p);

    p = p + 1;

}

在 while 的判斷條件中,檢查 p 指針指向的字符是否爲結束符'\0'。

在循環體重,打印出當前指向的字符以後,對指針比那裏進行自增操做,由於指針 p 所指向的數據類型是 char,每一個 char 在內存中佔據一個字節,所以指針 p 在自增 1 以後,就指向下一個存儲空間。

也能夠把循環體中的 2 條語句寫成 1 條語句:

printf("%c ", *p++);

假如一個指針指向的數據類型爲 int 型,那麼執行 p = p + 1;以後,指針 p 中存儲的地址值將會增長 4,由於一個 int 型數據在內存中佔據 4 個字節的空間,以下所示:

思考一個問題:void* 型指針可以遞增嗎?以下測試代碼:

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

void *p = a;

printf("1: p = 0x%x \n", p);

p = p + 1;

printf("2: p = 0x%x \n", p);

打印結果以下:

1: p = 0x733748c0 

2: p = 0x733748c1

說明 void* 型指針在自增時,是按照一個字節的跨度來計算的。

4.3 指針數組與數組指針

這 2 個說法常常會混淆,至少我是如此,先看下這 2 條語句:

int *p1[3];   // 指針數組

int (*p2)[3]; // 數組指針

指針數組

第 1 條語句中:中括號[]的優先級高,所以與 p1 先結合,表示一個數組,這個數組中有 3 個元素,這 3 個元素都是指針,它們指向的是 int 型數據。

能夠這樣來理解:若是有這個定義 char p[3],很容易理解這是一個有 3 個 char 型元素的數組,那麼把 char 換成 int*,意味着數組裏的元素類型是 int*型(指向 int 型數據的指針)。內存模型以下(注意:三個指針指向的地址並不必定是連續的):

若是向指針數組中的元素賦值,須要逐個把變量的地址賦值給指針元素:

int a = 1, b = 2, c = 3;

char *p1[3];

p1[0] = &a;

p1[1] = &b;

p1[2] = &c;

數組指針

第 2 條語句中:小括號讓 p2 與 * 結合,表示 p2 是一個指針,這個指針指向了一個數組,數組中有 3 個元素,每個元素的類型是 int 型。能夠這樣來理解:若是有這個定義 int p[3],很容易理解這是一個有 3 個 char 型元素的數組,那麼把數組名 p 換成是 *p2,也就是 p2 是一個指針,指向了這個數組。內存模型以下(注意:指針指向的地址是一個數組,其中的 3 個元素是連續放在內存中的):

在前面咱們說到取地址操做符&,用來得到一個變量的地址。凡事都有特殊狀況,對於獲取地址來講,下面幾種狀況不須要使用&操做符:

  • 字符串字面量做爲右值時,就表明這個字符串在內存中的首地址;
  • 數組名就表明這個數組的地址,也等於這個數組的第一個元素的地址;
  • 函數名就表明這個函數的地址。

所以,對於一下代碼,三個 printf 語句的打印結果是相同的:

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

int (*p2)[3] = a;

printf("0x%x \n", a);

printf("0x%x \n", &a);

printf("0x%x \n", p2);

思考一下,若是對這裏的 p2 指針執行 p2 = p2 + 1;操做,p2 中的值將會增長多少?

答案是 12 個字節。由於 p2 指向的是一個數組,這個數組中包含 3 個元素,每一個元素佔據 4 個字節,那麼這個數組在內存中一共佔據 12 個字節,所以 p2 在加 1 以後,就跳過 12 個字節。

4.4 二維數組和指針

一維數組在內存中是連續分佈的多個內存單元組成的,而二維數組在內存中也是連續分佈的多個內存單元組成的,從內存角度來看,一維數組和二維數組沒有本質差異。

和一維數組相似,二維數組的數組名錶示二維數組的第一維數組中首元素的首地址,用代碼來講明:

int a[3][3] = {{1,2,3}, {4,5,6}, {7,8,9}}; // 二維數組

int (*p0)[3] = NULL;   // p0是一個指針,指向一個數組

int (*p1)[3] = NULL;   // p1是一個指針,指向一個數組

int (*p2)[3] = NULL;   // p2是一個指針,指向一個數組

p0 = a[0];

p1 = a[1];

p2 = a[2];

printf("0: %d %d %d \n", *(*p0 + 0), *(*p0 + 1), *(*p0 + 2));

printf("1: %d %d %d \n", *(*p1 + 0), *(*p1 + 1), *(*p1 + 2));

printf("2: %d %d %d \n", *(*p2 + 0), *(*p2 + 1), *(*p2 + 2));

打印結果是:

0: 1 2 3 

1: 4 5 6 

2: 7 8 9

咱們拿第一個 printf 語句來分析:p0 是一個指針,指向一個數組,數組中包含 3 個元素,每一個元素在內存中佔據 4 個字節。如今咱們想獲取這個數組中的數據,若是直接對 p0 執行加 1 操做,那麼 p0 將會跨過 12 個字節(就等於 p1 中的值了),所以須要使用解引用操做符 *,把 p0 轉爲指向 int 型的指針,而後再執行加 1 操做,就能夠獲得數組中的 int 型數據了。

4.5 結構體指針

C 語言中的基本數據類型是預約義的,結構體是用戶定義的,在指針的使用上能夠進行類比,惟一有區別的就是在結構體指針中,須要使用->箭頭操做符來獲取結構體中的成員變量,例如:

typedef struct 

{

    int age;

    char name[8];

} Student;




Student s;

s.age = 20;

strcpy(s.name, "lisi");

Student *p = &s;

printf("age = %d, name = %s \n", p->age, p->name);

看起來彷佛沒有什麼技術含量,若是是結構體數組呢?例如:

Student s[3];

Student *p = &s;

printf("size of Student = %d \n", sizeof(Student));

printf("1: 0x%x, 0x%x \n", s, p);

p++;

printf("2: 0x%x \n", p);

打印結果是:

size of Student = 12 

1: 0x4c02ac00, 0x4c02ac00 

2: 0x4c02ac0c

在執行 p++操做後,p 須要跨過的空間是一個結構體變量在內存中佔據的大小(12 個字節),因此此時 p 就指向了數組中第 2 個元素的首地址,內存模型以下:

4.6 函數指針

每個函數在通過編譯以後,都變成一個包含多條指令的集合,在程序被加載到內存以後,這個指令集合被放在代碼區,咱們在程序中使用函數名就表明了這個指令集合的開始地址。

函數指針,本質上仍然是一個指針,只不過這個指針變量中存儲的是一個函數的地址。函數最重要特性是什麼?能夠被調用!所以,當定義了一個函數指針並把一個函數地址賦值給這個指針時,就能夠經過這個函數指針來調用函數。

以下示例代碼:

int add(int x,int y)

{

    return x+y;

}




int main()

{

    int a = 1, b = 2;

    int (*p)(int, int);

    p = add;

    printf("%d + %d = %d\n", a, b, p(a, b));

}

前文已經說過,函數的名字就表明函數的地址,因此函數名 add 就表明了這個加法函數在內存中的地址。int (*p)(int, int);這條語句就是用來定義一個函數指針,它指向一個函數,這個函數必須符合下面這 2 點(學名叫:函數簽名):

  • 有 2 個 int 型的參數;
  • 有一個 int 型的返回值。

代碼中的 add 函數正好知足這個要求,所以,能夠把 add 賦值給函數指針 p,此時 p 就指向了內存中這個函數存儲的地址,後面就能夠用函數指針 p 來調用這個函數了。

在示例代碼中,函數指針 p 是直接定義的,那若是想定義 2 個函數指針,難道須要像下面這樣定義嗎?

int (*p)(int, int);

int (*p2)(int, int);

這裏的參數比較簡單,若是函數很複雜,這樣的定義方式豈不是要煩死?能夠用 typedef 關鍵字來定義一個函數指針類型:

typedef int (*pFunc)(int, int);

而後用這樣的方式 pFunc p1, p2;來定義多個函數指針就方便多了。注意:只能把與函數指針類型具備相同簽名的函數賦值給 p1 和 p2,也就是參數的個數、類型要相同,返回值也要相同。

注意:這裏有幾個小細節稍微瞭解一下:

  • 在賦值函數指針時,使用 p = &a;也是能夠的;
  • 使用函數指針調用時,使用(*p)(a, b);也是能夠的。

這裏沒有什麼特殊的原理須要講解,最終都是編譯器幫咱們處理了這裏的細節,直接記住便可。

函數指針整明白以後,再和數組結合在一塊兒:函數指針數組。示例代碼以下:

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




int main()

{

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

}

這條語句不太好理解:int (*p[4])(int, int);,先分析中間部分,標識符 p 與中括號[]結合(優先級高),因此 p 是一個數組,數組中有 4 個元素;而後剩下的內容表示一個函數指針,那麼就說明數組中的元素類型是函數指針,也就是其餘函數的地址,內存模型以下:

若是仍是難以理解,那就回到指針的本質概念上:指針就是一個地址!這個地址中存儲的內容是什麼根本不重要,重要的是你告訴計算機這個內容是什麼。若是你告訴它:這個地址裏存放的內容是一個函數,那麼計算機就去調用這個函數。那麼你是如何告訴計算機的呢,就是在定義指針變量的時候,僅此而已!

總結

 

我已經把本身知道的全部指針相關的概念、語法、使用場景都做了講解,就像一個小酒館的掌櫃,把本身的美酒佳餚都呈現給你,希望你已經酒足飯飽!

若是以上的內容太多,一時沒法消化,那麼下面的這兩句話就做爲飯後甜點爲您奉上,在之後的編程中,若是遇到指針相關的困惑,就想想這兩句話,也許能讓你茅塞頓開。

  • 指針就是地址,地址就是指針。
  • 指針就是指向內存中的一塊空間,至於如何來解釋/操做這塊空間,由這個指針的類型來決定。

另外還有一點囑咐,那就是學習任何一門編程語言,必定要弄清楚內存模型,內存模型,內存模型!

 

若是你C/C++感興趣,想學編程,小編推薦一個C/C++技術交流羣【點擊進入】!

涉及到了:編程入門、遊戲編程、網絡編程、Windows編程、Linux編程、Qt界面開發、黑客等等......

相關文章
相關標籤/搜索