精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

C語言指針-從底層原理到花式技巧,用圖文和代碼幫你講解透徹

開發 后端
如果問C語言中最重要、威力最大的概念是什么,答案必將是指針!威力大,意味著使用方便、高效,同時也意味著語法復雜、容易出錯。指針用的好,可以極大的提高代碼執行效率、節約系統資源;如果用的不好,程序中將會充滿陷阱、漏洞。
  •  一、前言
  • 二、變量與指針的本質
  • 三、指針的幾個相關概念
  • 四、指向不同數據類型的指針
  • 五、總結

一、前言

如果問C語言中最重要、威力最大的概念是什么,答案必將是指針!威力大,意味著使用方便、高效,同時也意味著語法復雜、容易出錯。指針用的好,可以極大的提高代碼執行效率、節約系統資源;如果用的不好,程序中將會充滿陷阱、漏洞。

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

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

二、變量與指針的本質

1. 內存地址

我們編寫一個程序源文件之后,編譯得到的二進制可執行文件存放在電腦的硬盤上,此時它是一個靜態的文件,一般稱之為程序。

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

把程序的內容(代碼段、數據段)從硬盤復制到內存中;

創建一個數據結構PCB(進程控制塊),來描述這個程序的各種信息(例如:使用的資源,打開的文件描述符...);

在代碼段中定位到入口函數的地址,讓CPU從這個地址開始執行。

 

當程序開始被執行時,就變成一個動態的狀態,一般稱之為進程。

內存分為:物理內存和虛擬內存。操作系統對物理內存進行管理、包裝,我們開發者面對的是操作系統提供的虛擬內存。

這2個概念不妨礙文章的理解,因此就統一稱之為內存。

在我們的程序中,通過一個變量名來定義變量、使用變量。變量本身是一個確確實實存在的東西,變量名是一個抽象的概念,用來代表這個變量。就比如:我是一個實實在在的人,是客觀存在與這個地球上的,道哥是我給自己起的一個名字,這個名字是任意取得,只要自己覺得好聽就行,如果我愿意還可以起名叫:鳥哥、龍哥等等。

那么,我們定義一個變量之后,這個變量放在哪里呢?那就是內存的數據區。內存是一個很大的存儲區域,被操作系統劃分為一個一個的小空間,操作系統通過地址來管理內存。

 

內存中的最小存儲單位是字節(8個bit),一個內存的完整空間就是由這一個一個的字節連續組成的。在上圖中,每一個小格子代表一個字節,但是好像大家在書籍中沒有這么來畫內存模型的,更常見的是下面這樣的畫法:

 

也就是把連續的4個字節的空間畫在一起,這樣就便于表述和理解,特別是深入到代碼對齊相關知識時更容易理解。(我認為根本原因應該是:大家都這么畫,已經看順眼了~~)

2. 32位與64位系統

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

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

 

剛才說了,內存是通過地址來管理的,那么CPU想從內存中的某個地址空間上存取一個數據,那么CPU就需要在地址總線上輸出這個存儲單元的地址。假如地址總線的寬度是8位,能表示的最大地址空間就是256個字節,能找到內存中最大的存儲單元是255這個格子(從0開始)。即使內存條的實際空間是2G字節,CPU也沒法使用后面的內存地址空間。如果地址總線的寬度是32位,那么能表示的最大地址就是2的32次方,也就是4G字節的空間。

【注意】:這里只是描述地址總線的概念,實際的計算機中地址計算方式要復雜的多,比如:虛擬內存中采用分段、分頁、偏移量來定位實際的物理內存,在分頁中還有大頁、小頁之分,感興趣的同學可以自己查一下相關資料。

3. 變量

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

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

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

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

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

  1. int a = 20; 

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

 

我們在代碼中使用變量名a,在程序執行的時候就表示使用0x11223344地址所對應的那個存儲單元中的數據。因此,可以理解為變量名a就等價于這個地址0x11223344。換句話說,如果我們可以提前知道編譯器把變量a安排在地址0x11223344這個單元格中,我們就可以在程序中直接用這個地址值來操作這個變量。

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

比如:32位的編譯器中:

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

比如:64位的編譯器中:

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

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

另外,0x11223344,0x11223345,0x11223346,0x11223347這連續的、從低地址到高地址的4個字節用來存儲變量a的數值20。在圖示中,使用十六進制來表示,十進制數值20轉成16進制就是:0x00000014,所以從開始地址依次存放0x00、0x00、0x00、0x14這4個字節(存儲順序涉及到大小端的問題,不影響文本理解)。

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

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

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

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

4. 指針變量

指針變量可以分2個層次來理解:

指針變量首先是一個變量,所以它擁有變量的所有屬性:類型和值。它的類型就是指針,它的值是其他變量的地址。 既然是一個變量,那么在內存中就需要為這個變量分配一個存儲空間。在這個存儲空間中,存放著其他變量的地址。

指針變量所指向的數據類型,這是在定義指針變量的時候就確定的。例如:int *p; 意味著指針指向的是一個int型的數據。

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

示例:

  1. int a = 20; 
  2. int *pa; 
  3. pa = &a; 
  4. 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類型變量的值,學名叫解引用,我們只要記住是獲取指向的變量的值就可以了。

5. 操作指針變量

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

  1. 操作指針變量自身的值;
  2. 獲取指針變量所指向的數據;
  3. 以什么樣數據類型來使用/解釋指針變量所指向的內容。

5.1 指針變量自身的值

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。

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

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

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

如下代碼:

  1. int a = 30000; 
  2. int *pa = &a; 
  3. printf("value = %d \n", *pa); 

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

 

現在我們做這樣一個測試:

  1. char *pc = 0x11223344; 
  2. printf("value = %d \n", *pc); 

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

這個例子中說明了一個重要的概念:在內存中一切都是數字,如何來操作(解釋)一個內存地址中的數據,完全是由我們的代碼來告訴編譯器的。剛才這個例子中,雖然0x11223344這個地址開始的4個字節的空間中,存儲的是整型變量a的值,但是我們讓pc指針按照char型數據來使用/解釋這個地址處的內容,這是完全合法的。

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

三、指針的幾個相關概念

1. const屬性

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

  1. const int b = 20; 

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

  1. int a = 20; 
  2. int b = 20; 
  3. int * const p = &a; 

內存模型如下:

 

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

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

與下面的代碼區分一下:

  1. int a = 20; 
  2. int b = 20; 
  3. const int *p = &a; 
  4. p = &b; 

這里的const沒有放在p的旁邊,而是放在了類型int的旁邊,這就說明const符號不是用來修飾p的,而是用來修飾p所指向的那個變量的。所以,如果我們寫p = &b;把變量b的地址賦值給指針p,就是合法的,因為p的值可以被改變。

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

  1. short int getDataCRC(const char *pData, int len) 
  2.     short int crc = 0x0000; 
  3.     // 計算CRC 
  4.     return crc; 

2. void型指針

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

  1. 函數的返回值和形參;
  2. 定義指針時不明確規定所指數據的類型,也就意味著可以指向任意類型。

指針變量也是一種變量,變量之間可以相互賦值,那么指針變量之間也可以相互賦值,例如:

  1. int a = 20; 
  2. int b = a; 
  3. int *p1 = &a; 
  4. int *p2 = p1; 

變量a賦值給變量b,指針p1賦值給指針p2,注意到它們的類型必須是相同的:a和b都是int型,p1和p2都是指向int型,所以可以相互賦值。那么如果數據類型不同呢?必須進行強制類型轉換。例如:

  1. int a = 20; 
  2. int *p1 = &a; 
  3. char *p2 = (char *)p1; 

內存模型如下:

 

p1指針指向的是int型數據,現在想把它的值(0x11223344)賦值給p2,但是由于在定義p2指針時規定它指向的數據類型是char型,因此需要把指針p1進行強制類型轉換,也就是把地址0x11223344處的數據按照char型數據來看待,然后才可以賦值給p2指針。

如果我們使用void *p2來定義p2指針,那么在賦值時就不需要進行強制類型轉換了,例如:

  1. int a = 20; 
  2. int *p1 = &a; 
  3. void *p2 = p1; 

指針p2是void*型,意味著可以把任意類型的指針賦值給p2,但是不能反過來操作,也就是不能把void*型指針直接賦值給其他確定類型的指針,而必須要強制轉換成被賦值指針所指向的數據類型,如下代碼,必須把p2指針強制轉換成int*型之后,再賦值給p3指針:

  1. int a = 20; 
  2. int *p1 = &a; 
  3. void *p2 = p1; 
  4. int *p3 = (int *)p2; 

我們來看一個系統函數:

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

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

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

3. 空指針和野指針

一個指針必須指向一個有意義的地址之后,才可以對指針進行操作。如果指針中存儲的地址值是一個隨機值,或者是一個已經失效的值,此時操作指針就非常危險了,一般把這樣的指針稱作野指針,C代碼中很多指針相關的bug就來源于此。

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

在定義一個指針變量之后,如果沒有賦值,那么這個指針變量中存儲的就是一個隨機值,有可能指向內存中的任何一個地址空間,此時萬萬不可以對這個指針進行寫操作,因為它有可能指向內存中的代碼段區域、也可能指向內存中操作系統所在的區域。

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

  1. #ifdef __cplusplus 
  2.      #define NULL    0 
  3. #else     
  4.      #define NULL    ((void *)0) 
  5. #endif 

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

我們都知道,函數中的局部變量存儲在棧區,通過malloc申請的內存空間位于堆區,如下代碼:

  1. int *p = (int *)malloc(4); 
  2. *p = 20; 

內存模型為:

 

在堆區申請了4個字節的空間,然后強制類型轉換為int*型之后,賦值給指針變量p,然后通過*p設置這個地址中的值為14,這是合法的。如果在釋放了p指針指向的空間之后,再使用*p來操作這段地址,那就是非常危險了,因為這個地址空間可能已經被操作系統分配給其他代碼使用,如果對這個地址里的數據強行操作,程序立刻崩潰的話,將會是我們最大的幸運!

  1. int *p = (int *)malloc(4); 
  2. *p = 20; 
  3. free(p); 
  4. // 在free之后就不可以再操作p指針中的數據了。 
  5. p = NULL;  // 最好加上這一句。 

四、指向不同數據類型的指針

1. 數值型指針

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

2. 字符串指針

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

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

用一個char *指針來表示,例如:char *name2 = "zhangsan";

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

對于字符串來說,使用指針來操作是非常方便的,例如:變量字符串name2:

  1. char *name2 = "zhangsan"
  2. char *p = name2; 
  3. while (*p != '\0'
  4.     printf("%c ", *p); 
  5.     p = p + 1; 

在while的判斷條件中,檢查p指針指向的字符是否為結束符'\0'。在循環體重,打印出當前指向的字符之后,對指針比那里進行自增操作,因為指針p所指向的數據類型是char,每個char在內存中占據一個字節,因此指針p在自增1之后,就指向下一個存儲空間。

 

也可以把循環體中的2條語句寫成1條語句:

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

假如一個指針指向的數據類型為int型,那么執行p = p + 1;之后,指針p中存儲的地址值將會增加4,因為一個int型數據在內存中占據4個字節的空間,如下所示:

 

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

  1. int a[3] = {1, 2, 3}; 
  2. void *p = a; 
  3. printf("1: p = 0x%x \n", p); 
  4. p = p + 1; 
  5. printf("2: p = 0x%x \n", p); 

打印結果如下:

  1. 1: p = 0x733748c0  
  2. 2: p = 0x733748c1 

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

3. 指針數組與數組指針

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

  1. int *p1[3];   // 指針數組 
  2. int (*p2)[3]; // 數組指針 

3.1 指針數組

第1條語句中:中括號[]的優先級高,因此與p1先結合,表示一個數組,這個數組中有3個元素,這3個元素都是指針,它們指向的是int型數據。可以這樣來理解:如果有這個定義char p[3],很容易理解這是一個有3個char型元素的數組,那么把char換成int*,意味著數組里的元素類型是int*型(指向int型數據的指針)。內存模型如下(注意:三個指針指向的地址并不一定是連續的):

 

如果向指針數組中的元素賦值,需要逐個把變量的地址賦值給指針元素:

  1. int a = 1, b = 2, c = 3; 
  2. char *p1[3]; 
  3. p1[0] = &a; 
  4. p1[1] = &b; 
  5. p1[2] = &c; 

3.2 數組指針

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

 

在前面我們說到取地址操作符&,用來獲得一個變量的地址。凡事都有特殊情況,對于獲取地址來說,下面幾種情況不需要使用&操作符:

字符串字面量作為右值時,就代表這個字符串在內存中的首地址;

數組名就代表這個數組的地址,也等于這個數組的第一個元素的地址;

函數名就代表這個函數的地址。

因此,對于一下代碼,三個printf語句的打印結果是相同的:

  1. int a[3] = {1, 2, 3}; 
  2. int (*p2)[3] = a; 
  3. printf("0x%x \n", a); 
  4. printf("0x%x \n", &a); 
  5. printf("0x%x \n", p2); 

思考一下,如果對這里的p2指針執行p2 = p2 + 1;操作,p2中的值將會增加多少?

答案是12個字節。因為p2指向的是一個數組,這個數組中包含3個元素,每個元素占據4個字節,那么這個數組在內存中一共占據12個字節,因此p2在加1之后,就跳過12個字節。

 

4. 二維數組和指針

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

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

  1. int a[3][3] = {{1,2,3}, {4,5,6}, {7,8,9}}; // 二維數組 
  2. int (*p0)[3] = NULL;   // p0是一個指針,指向一個數組 
  3. int (*p1)[3] = NULL;   // p1是一個指針,指向一個數組 
  4. int (*p2)[3] = NULL;   // p2是一個指針,指向一個數組 
  5. p0 = a[0]; 
  6. p1 = a[1]; 
  7. p2 = a[2]; 
  8. printf("0: %d %d %d \n", *(*p0 + 0), *(*p0 + 1), *(*p0 + 2)); 
  9. printf("1: %d %d %d \n", *(*p1 + 0), *(*p1 + 1), *(*p1 + 2)); 
  10. printf("2: %d %d %d \n", *(*p2 + 0), *(*p2 + 1), *(*p2 + 2)); 

打印結果是:

  1. 0: 1 2 3  
  2. 1: 4 5 6  
  3. 2: 7 8 9 

我們拿第一個printf語句來分析:p0是一個指針,指向一個數組,數組中包含3個元素,每個元素在內存中占據4個字節。現在我們想獲取這個數組中的數據,如果直接對p0執行加1操作,那么p0將會跨過12個字節(就等于p1中的值了),因此需要使用解引用操作符*,把p0轉為指向int型的指針,然后再執行加1操作,就可以得到數組中的int型數據了。

5. 結構體指針

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

  1. typedef struct  
  2.     int age; 
  3.     char name[8]; 
  4. } Student; 
  5.  
  6. Student s; 
  7. s.age = 20; 
  8. strcpy(s.name"lisi"); 
  9. Student *p = &s; 
  10. printf("age = %d, name = %s \n", p->age, p->name); 

看起來似乎沒有什么技術含量,如果是結構體數組呢?例如:

  1. Student s[3]; 
  2. Student *p = &s; 
  3. printf("size of Student = %d \n", sizeof(Student)); 
  4. printf("1: 0x%x, 0x%x \n", s, p); 
  5. p++; 
  6. printf("2: 0x%x \n", p); 

打印結果是:

  1. size of Student = 12  
  2. 1: 0x4c02ac00, 0x4c02ac00  
  3. 2: 0x4c02ac0c 

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

 

6. 函數指針

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

 

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

如下示例代碼:

  1. int add(int x,int y) 
  2.     return x+y; 
  3.  
  4. int main() 
  5.     int a = 1, b = 2; 
  6.     int (*p)(intint); 
  7.     p = add
  8.     printf("%d + %d = %d\n", a, b, p(a, b)); 

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

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

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

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

  1. int (*p)(intint); 
  2. int (*p2)(intint); 

這里的參數比較簡單,如果函數很復雜,這樣的定義方式豈不是要煩死?可以用typedef關鍵字來定義一個函數指針類型:

  1. typedef int (*pFunc)(intint); 

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

注意:這里有幾個小細節稍微了解一下:

在賦值函數指針時,使用p = &a;也是可以的;

使用函數指針調用時,使用(*p)(a, b);也是可以的。

這里沒有什么特殊的原理需要講解,最終都是編譯器幫我們處理了這里的細節,直接記住即可。

函數指針整明白之后,再和數組結合在一起:函數指針數組。示例代碼如下:

  1. int add(int a, int b) { return a + b; } 
  2. int sub(int a, int b) { return a - b; } 
  3. int mul(int a, int b) { return a * b; } 
  4. int divide(int a, int b) { return a / b; } 
  5.  
  6. int main() 
  7.     int a = 4, b = 2; 
  8.     int (*p[4])(intint); 
  9.     p[0] = add
  10.     p[1] = sub; 
  11.     p[2] = mul; 
  12.     p[3] = divide; 
  13.     printf("%d + %d = %d \n", a, b, p[0](a, b)); 
  14.     printf("%d - %d = %d \n", a, b, p[1](a, b)); 
  15.     printf("%d * %d = %d \n", a, b, p[2](a, b)); 
  16.     printf("%d / %d = %d \n", a, b, p[3](a, b)); 

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

 

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

五、總結

我已經把自己知道的所有指針相關的概念、語法、使用場景都作了講解,就像一個小酒館的掌柜,把自己的美酒佳肴都呈現給你,但愿你已經酒足飯飽!

如果以上的內容太多,一時無法消化,那么下面的這兩句話就作為飯后甜點為您奉上,在以后的編程中,如果遇到指針相關的困惑,就想一想這兩句話,也許能讓你茅塞頓開。

指針就是地址,地址就是指針。

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

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

本文轉載自微信公眾號「IOT物聯網小鎮」,可以通過以下二維碼關注。轉載本文請聯系IOT物聯網小鎮公眾號。

 

責任編輯:武曉燕 來源: IOT物聯網小鎮
相關推薦

2020-12-02 10:27:40

C語言

2023-02-07 08:55:04

進程棧內存底層

2025-09-29 05:00:00

Linux線程棧內存

2021-05-11 07:51:30

React ref 前端

2011-07-15 01:20:58

C指針函數函數指針

2024-07-07 21:49:22

2010-07-30 12:19:04

無線路由連接局域網

2025-04-02 07:29:14

2018-05-17 15:18:48

Logistic回歸算法機器學習

2025-11-13 08:08:15

2016-12-05 13:35:02

C語言數組指針

2010-02-01 15:01:34

C++拋出異常

2010-06-29 14:20:52

2024-03-27 10:14:48

2025-10-27 01:22:00

HTTP接口API

2025-11-07 04:00:00

2021-01-13 06:58:35

C語言函數指針

2023-09-14 12:35:28

寄存器

2025-03-17 01:55:00

TCP服務迭代

2009-12-14 15:56:40

網頁編輯器Bluefi
點贊
收藏

51CTO技術棧公眾號

欧美伦理片在线观看| 成人亚洲激情网| 好吊日免费视频| 日韩电影网站| 亚洲婷婷综合色高清在线| 99re视频| 中文字幕人妻色偷偷久久| 影视一区二区| 亚洲免费视频观看| 91大神免费观看| 蜜桃麻豆影像在线观看| 国产精品乱人伦中文| 国产伦理一区二区三区| 国产一级片一区二区| 黄色精品网站| 中文字幕欧美专区| 亚洲精品成人无码熟妇在线| 高清久久一区| 欧美性感美女h网站在线观看免费| 天堂资源在线亚洲视频| 男女视频在线看| jizz中文字幕| 一区二区在线视频观看| 日本乱人伦aⅴ精品| 欧美国产综合在线| 精品麻豆一区二区三区| 久久久蜜桃精品| 成人午夜电影免费在线观看| 亚洲天堂avav| 久久久久久久波多野高潮日日| 久99久在线视频| 在线观看免费小视频| 美国十次av导航亚洲入口| 欧美一区日本一区韩国一区| 日本熟妇人妻中出| 午夜伦理福利在线| 亚洲图片欧美色图| 免费看日b视频| 欧美r级在线| 久久精品一区二区| 久久精品五月婷婷| 亚洲老妇色熟女老太| 久久99精品国产麻豆不卡| 国产精品69久久| 亚洲天堂av片| 国产精品综合| 欧美性视频在线| 日本三级一区二区| 国产精品久久久久9999高清| 久久久久久久久网站| 欧美极品视频在线观看| 五月婷婷六月综合| 精品国产一区二区三区久久久狼| 亚洲av成人无码久久精品| 影视先锋久久| 亚洲美女av电影| 91成人破解版| 精品视频亚洲| 中文字幕视频一区二区在线有码 | 亚洲国产欧美一区二区丝袜黑人| 精品少妇一区二区30p| 日本黄色a视频| 午夜在线视频| 亚洲欧洲国产日韩| 黄色一级视频播放| 四虎av在线| 性做久久久久久免费观看| 日本福利视频在线| 暖暖成人免费视频| 欧美专区日韩专区| 亚洲一区二区福利视频| 精品国产亚洲一区二区三区大结局| 欧美精品丝袜久久久中文字幕| 伊人国产精品视频| 日韩一二三区| 亚洲丁香久久久| 欧美成人午夜精品免费| 欧美色女视频| 欧美国产在线视频| 国产成人免费看| 三级影片在线观看欧美日韩一区二区| 国产国语刺激对白av不卡| 一本一道精品欧美中文字幕| 福利一区二区在线| 蜜桃视频成人| 免费在线观看黄色| 亚洲国产视频一区二区| 男人日女人下面视频| 中韩乱幕日产无线码一区| 日韩一区二区免费电影| 亚洲成人av免费在线观看| 日韩大片在线| 欧美精品xxx| 国产乡下妇女三片| 福利电影一区二区三区| 日本一区免费在线观看| 午夜在线激情影院| 色综合久久天天| 污免费在线观看| 免费观看久久av| 久久久国产视频91| 中文字幕一区二区人妻电影| 国内一区二区视频| 欧美h视频在线| 日韩欧美三级视频| 成人免费观看视频大全| 亚洲一区二区三区爽爽爽爽爽| 日本一本二本在线观看| 久久丁香四色| 亚洲天天在线日亚洲洲精| 久久成人国产精品入口| 男女性色大片免费观看一区二区| 国产精品久久精品国产 | 99久久久精品视频| 国产经典一区| 日韩成人在线观看| 久久久久无码精品国产| 麻豆久久久久久久| 欧美福利精品| 爱情岛亚洲播放路线| 7777精品伊人久久久大香线蕉的| 久久久久国产精品无码免费看| 午夜久久免费观看| 国产精品91久久久| 神马亚洲视频| 亚洲成人你懂的| 被黑人猛躁10次高潮视频| 一本色道久久综合亚洲精品酒店 | 精品中文视频在线| 麻豆一区产品精品蜜桃的特点| 日本美女一区二区三区| 久久99精品久久久久久久久久| 国产原创在线观看| 欧美日韩中文精品| 538精品视频| 丝袜亚洲精品中文字幕一区| 好看的日韩精品视频在线| 午夜dj在线观看高清视频完整版 | 色啦啦av综合| 成人精品电影| 国产精品一区二区三区久久久| 国产女主播在线写真| 色视频欧美一区二区三区| 亚洲av无码一区二区二三区| 久久久久久久性潮| 岛国精品在线观看| 永久免费看av| 国产精品一区免费在线| 日韩一区二区精品视频| 中文字幕一区二区在线视频| 欧美激情一区二区三区四区| 日韩一级免费在线观看| 精品国产一区二区三区| 国产激情视频一区| 99riav在线| 欧美日韩在线观看一区二区| 91av手机在线| 国产伦精品一区二区三区免费| 亚洲激情免费视频| 亚洲日本一区二区三区在线| 久久久久久伊人| 无码国产精品96久久久久| 欧美网站在线观看| 谁有免费的黄色网址| 日本欧美一区二区| 在线不卡日本| 99久久免费精品国产72精品九九| 欧美大片在线看免费观看| 色呦呦中文字幕| 一本色道久久综合亚洲精品按摩| 国产传媒国产传媒| 国模娜娜一区二区三区| youjizz.com在线观看| 国产 日韩 欧美 综合 一区| 欧美一级淫片播放口| aaa日本高清在线播放免费观看| 欧美日韩一区二区在线观看视频 | 欧美日韩在线网站| 成人在线国产精品| 国产h片在线观看| 国产一区二区三区18| 国产色综合视频| 亚洲国产乱码最新视频 | 亚洲黄色在线视频| 中文字幕乱码一区| 免费在线观看不卡| 菠萝蜜视频在线观看入口| 天海翼亚洲一区二区三区| 国产精品你懂得| 久久久久久久人妻无码中文字幕爆| 在线中文字幕-区二区三区四区| 精品成人免费观看| а中文在线天堂| 亚洲精品免费播放| 李宗瑞91在线正在播放| 国精产品一区一区三区mba视频 | 免费影院在线观看一区| 91精品网站在线观看| 欧美亚洲成人网| 成人短视频在线| 日韩乱码在线视频| 国产视频一区二区三区四区五区| 黑人巨大精品欧美一区免费视频| 男人天堂资源网| 91视视频在线观看入口直接观看www | 国产肥白大熟妇bbbb视频| 国产成人免费在线观看不卡| 中文久久久久久| 一区二区国产在线观看| 性欧美18一19内谢| 国产精品亚洲片在线播放| 国产精品播放| 96视频在线观看欧美| 日韩免费黄色av| tube8在线hd| 久久婷婷国产麻豆91天堂| 嫩草在线播放| 亚洲成年人影院在线| jlzzjlzz亚洲女人18| 欧美性猛交一区二区三区精品| 国产午夜精品无码一区二区| 亚洲欧美日韩人成在线播放| 久久亚洲无码视频| 91在线视频播放地址| 红桃视频一区二区三区免费| 蜜臀av国产精品久久久久 | 成人性生活视频| 久久久久亚洲精品成人网小说| 日本中文字幕在线播放| 亚洲视频在线免费观看| 色视频在线观看免费| 亚洲区第一页| 999国产视频| 精品国产三级| 亚洲v日韩v综合v精品v| 日韩五码电影| 国产伦精品免费视频| 日本综合视频| 国产精品视频网址| 色猫猫成人app| 国产精品人人做人人爽| jizzjizz少妇亚洲水多| 国产精品久久久久久久天堂| 成人午夜精品| 国产精品高清免费在线观看| 3d欧美精品动漫xxxx无尽| 日本精品一区二区三区在线播放视频 | av免费播放网址| 亚洲国内自拍| 日韩av片在线看| 久久精品人人| 久久久久免费精品| 麻豆精品视频在线观看| 小明看看成人免费视频| 韩国欧美国产一区| 亚洲国产欧美日韩在线| 国产黄色精品网站| 麻豆短视频在线观看| 成人视屏免费看| 一区二区免费在线观看视频| 91在线精品一区二区| av无码一区二区三区| 久久综合狠狠综合久久综合88| 五月天综合视频| 国产精品乱码一区二区三区软件| 国产在线一卡二卡| 亚洲一卡二卡三卡四卡五卡| 五月天综合激情网| 欧美在线播放高清精品| 97国产精品久久久| 精品国产乱码久久久久久久久| 亚洲 欧美 激情 另类| 亚洲欧洲高清在线| lutube成人福利在线观看| www亚洲欧美| 爱情岛亚洲播放路线| 国产成+人+综合+亚洲欧美丁香花| 国产精品毛片久久久久久久久久99999999| 国产欧美一区二区三区四区| 欧州一区二区三区| 久久精品国产一区二区三区日韩| 欧美日韩精品一区二区视频| 警花观音坐莲激情销魂小说| 亚洲精品影院在线观看| 午夜电影久久久| 国产精品国产精品88| 亚洲第一久久影院| 中文字幕av网站| 精品欧美久久久| 国产精品免费观看| 欧美精品情趣视频| 波多野结衣亚洲| 亚洲精品免费网站| 亚洲另类春色校园小说| 中文字幕在线亚洲三区| 在线一区欧美| 拔插拔插华人永久免费| 99久久综合精品| 国产真实乱在线更新| 欧美性猛交xxxx乱大交蜜桃| 国产强被迫伦姧在线观看无码| 日韩极品精品视频免费观看| 好了av在线| 日本精品免费一区二区三区| 91在线一区| 在线视频不卡国产| 羞羞答答国产精品www一本| 在线a免费观看| 国产日韩欧美精品在线| 日韩人妻无码一区二区三区99| 欧美丰满一区二区免费视频| 肉丝一区二区| 国模精品视频一区二区| 婷婷久久综合九色综合99蜜桃| 欧美精品一区三区在线观看| 精品白丝av| 久久精品亚洲天堂| 国产精品萝li| 国产一级片免费在线观看| 日韩av在线影院| 欧洲成人综合网| 91免费在线视频| 日韩伦理视频| 午夜视频在线瓜伦| 99久久国产综合精品色伊| 免费一级片在线观看| 欧美一区二区美女| 亚洲s色大片| 国产精品你懂得| 欧美日韩国产传媒| 一级黄色香蕉视频| 久久综合久色欧美综合狠狠| 日韩欧美一区二区一幕| 亚洲第一区中文99精品| 羞羞视频在线免费国产| 91亚洲一区精品| 婷婷综合久久| 污网站在线免费| 亚洲桃色在线一区| 国产一区二区三区成人| 中文字幕欧美日韩| 日本a人精品| 四虎免费在线观看视频| 久久国产日韩欧美精品| 刘亦菲国产毛片bd| 欧美日韩国产中文| 午夜激情在线观看| 国产自产女人91一区在线观看| 日本视频www| 精品久久久国产精品999| 黄色av小说在线观看| 欧美国产日韩在线| 久久动漫网址| 国产aaa一级片| 国产婷婷精品av在线| 少妇又紧又色又爽又刺激视频 | b站大片免费直播| 色婷婷av久久久久久久| 阿v免费在线观看| 国产精品直播网红| 午夜激情久久| 国产麻豆剧传媒精品国产| 亚洲成在线观看| 天堂av在线7| 国产精品第三页| 天天超碰亚洲| 中文字幕一区二区三区人妻在线视频 | 色99中文字幕| 国产中文字幕一区| 久久丫精品久久丫| 国产午夜精品麻豆| 成人在线不卡| 欧洲精品在线播放| 91麻豆产精品久久久久久| 欧美一级黄视频| 插插插亚洲综合网| 欧美成人一区在线观看| 国产免费人做人爱午夜视频| 国产精品美女久久久久aⅴ国产馆 国产精品美女久久久久av爽李琼 国产精品美女久久久久高潮 | h1515四虎成人| japanese在线播放| 97se亚洲国产综合自在线观| 黄色网址中文字幕| 久久国产精品久久久久| 欧美调教在线| 一区二区三区欧美精品| 午夜精品爽啪视频| 精品电影在线| 3d精品h动漫啪啪一区二区| 99成人在线| 国产精品成人69xxx免费视频| 亚洲成人xxx| 日韩午夜视频在线| 日韩少妇内射免费播放| 中文字幕日本乱码精品影院| 天堂中文资源在线| 91av一区二区三区| 日韩黄色在线观看| 久久香蕉精品视频| xxx一区二区|