當(dāng)前位置:首頁 > 公眾號(hào)精選 > C語言與CPP編程
[導(dǎo)讀]如果問C語言中最重要、威力最大的概念是什么,答案必將是指針!威力大,意味著使用方便、高效,同時(shí)也意味著語法復(fù)雜、容易出錯(cuò)。


  • 一、前言

  • 二、變量與指針的本質(zhì)

  • 三、指針的幾個(gè)相關(guān)概念

  • 四、指向不同數(shù)據(jù)類型的指針

  • 五、總結(jié)

一、前言

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

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

說明:為了方便講解和理解,文中配圖的內(nèi)存空間的地址是隨便寫的,在實(shí)際計(jì)算機(jī)中是要遵循地址對(duì)齊方式的。

二、變量與指針的本質(zhì)

1. 內(nèi)存地址

我們編寫一個(gè)程序源文件之后,編譯得到的二進(jìn)制可執(zhí)行文件存放在電腦的硬盤上,此時(shí)它是一個(gè)靜態(tài)的文件,一般稱之為程序。

當(dāng)這個(gè)程序被啟動(dòng)的時(shí)候,操作系統(tǒng)將會(huì)做下面幾件事情:

  1. 把程序的內(nèi)容(代碼段、數(shù)據(jù)段)從硬盤復(fù)制到內(nèi)存中;
  2. 創(chuàng)建一個(gè)數(shù)據(jù)結(jié)構(gòu)PCB(進(jìn)程控制塊),來描述這個(gè)程序的各種信息(例如:使用的資源,打開的文件描述符...);
  3. 在代碼段中定位到入口函數(shù)的地址,讓CPU從這個(gè)地址開始執(zhí)行。

當(dāng)程序開始被執(zhí)行時(shí),就變成一個(gè)動(dòng)態(tài)的狀態(tài),一般稱之為進(jìn)程。

內(nèi)存分為:物理內(nèi)存和虛擬內(nèi)存。操作系統(tǒng)對(duì)物理內(nèi)存進(jìn)行管理、包裝,我們開發(fā)者面對(duì)的是操作系統(tǒng)提供的虛擬內(nèi)存。
這2個(gè)概念不妨礙文章的理解,因此就統(tǒng)一稱之為內(nèi)存。

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

那么,我們定義一個(gè)變量之后,這個(gè)變量放在哪里呢?那就是內(nèi)存的數(shù)據(jù)區(qū)。內(nèi)存是一個(gè)很大的存儲(chǔ)區(qū)域,被操作系統(tǒng)劃分為一個(gè)一個(gè)的小空間,操作系統(tǒng)通過地址來管理內(nèi)存。

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

也就是把連續(xù)的4個(gè)字節(jié)的空間畫在一起,這樣就便于表述和理解,特別是深入到代碼對(duì)齊相關(guān)知識(shí)時(shí)更容易理解。(我認(rèn)為根本原因應(yīng)該是:大家都這么畫,已經(jīng)看順眼了~~)

2. 32位與64位系統(tǒng)

我們平時(shí)所說的計(jì)算機(jī)是32位、64位,指的是計(jì)算機(jī)的CPU中寄存器的最大存儲(chǔ)長度,如果寄存器中最大存儲(chǔ)32bit的數(shù)據(jù),就稱之為32位系統(tǒng)。

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

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

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

3. 變量

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

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

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

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

示例:代碼中定義了一個(gè)變量

int a = 20;

類型是int型,值是20。這個(gè)變量在內(nèi)存中的存儲(chǔ)模型為:

我們?cè)诖a中使用變量名a,在程序執(zhí)行的時(shí)候就表示使用0x11223344地址所對(duì)應(yīng)的那個(gè)存儲(chǔ)單元中的數(shù)據(jù)。因此,可以理解為變量名a就等價(jià)于這個(gè)地址0x11223344。換句話說,如果我們可以提前知道編譯器把變量a安排在地址0x11223344這個(gè)單元格中,我們就可以在程序中直接用這個(gè)地址值來操作這個(gè)變量。

在上圖中,變量a的值為20,在內(nèi)存中占據(jù)了4個(gè)格子的空間,也就是4個(gè)字節(jié)。為什么是4個(gè)字節(jié)呢?在C標(biāo)準(zhǔn)中并沒有規(guī)定每種數(shù)據(jù)類型的變量一定要占用幾個(gè)字節(jié),這是與具體的機(jī)器、編譯器有關(guān)。

比如:32位的編譯器中:

char: 1個(gè)字節(jié);
short int: 2個(gè)字節(jié);
int: 4個(gè)字節(jié);
long: 4個(gè)字節(jié)。

比如:64位的編譯器中:

char: 1個(gè)字節(jié);
short int: 2個(gè)字節(jié);
int: 4個(gè)字節(jié);
long: 8個(gè)字節(jié)。

為了方便描述,下面都以32位為例,也就是int型變量在內(nèi)存中占據(jù)4個(gè)字節(jié)。

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

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

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

這句話將會(huì)打印出:&a = 0x11223344。

考慮一下,在32位系統(tǒng)中:指針變量占用幾個(gè)字節(jié)?

4. 指針變量

指針變量可以分2個(gè)層次來理解:

  1. 指針變量首先是一個(gè)變量,所以它擁有變量的所有屬性:類型和值。它的類型就是指針,它的值是其他變量的地址。 既然是一個(gè)變量,那么在內(nèi)存中就需要為這個(gè)變量分配一個(gè)存儲(chǔ)空間。在這個(gè)存儲(chǔ)空間中,存放著其他變量的地址。
  2. 指針變量所指向的數(shù)據(jù)類型,這是在定義指針變量的時(shí)候就確定的。例如:int *p; 意味著指針指向的是一個(gè)int型的數(shù)據(jù)。

首先回答一下剛才那個(gè)問題,在32位系統(tǒng)中,一個(gè)指針變量在內(nèi)存中占據(jù)4個(gè)字節(jié)的空間。因?yàn)镃PU對(duì)內(nèi)存空間尋址時(shí),使用的是32位地址空間(4個(gè)字節(jié)),也就是用4個(gè)字節(jié)就能存儲(chǔ)一個(gè)內(nèi)存單元的地址。而指針變量中的值存儲(chǔ)的就是地址,所以需要4個(gè)字節(jié)的空間來存儲(chǔ)一個(gè)指針變量的值。

示例:

int a = 20;
int *pa;
pa = &a;
printf("value = %d \n", *pa);

在內(nèi)存中的存儲(chǔ)模型如下:

對(duì)于指針變量pa來說,首先它是一個(gè)變量,因此在內(nèi)存中需要有一個(gè)空間來存儲(chǔ)這個(gè)變量,這個(gè)空間的地址就是0x11223348;

其次,這個(gè)內(nèi)存空間中存儲(chǔ)的內(nèi)容是變量a的地址,而a的地址為0x11223344,所以指針變量pa的地址空間中,就存儲(chǔ)了0x11223344這個(gè)值。

這里對(duì)兩個(gè)操作符&和*進(jìn)行說明:

&:取地址操作符,用來獲取一個(gè)變量的地址。上面代碼中&a就是用來獲取變量a在內(nèi)存中的存儲(chǔ)地址,也就是0x11223344。

*:這個(gè)操作符用在2個(gè)場(chǎng)景中:定義一個(gè)指針的時(shí)候,獲取一個(gè)指針?biāo)赶虻淖兞恐档臅r(shí)候。

  1. int pa; 這個(gè)語句中的表示定義的變量pa是一個(gè)指針,前面的int表示pa這個(gè)指針指向的是一個(gè)int類型的變量。不過此時(shí)我們沒有給pa進(jìn)行賦值,也就是說此刻pa對(duì)應(yīng)的存儲(chǔ)單元中的4個(gè)字節(jié)里的值是沒有初始化的,可能是0x00000000,也可能是其他任意的數(shù)字,不確定;

  2. printf語句中的*表示獲取pa指向的那個(gè)int類型變量的值,學(xué)名叫解引用,我們只要記住是獲取指向的變量的值就可以了。

5. 操作指針變量

對(duì)指針變量的操作包括3個(gè)方面:

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

int a = 20;這個(gè)語句是定義變量a,在隨后的代碼中,只要寫下a就表示要操作變量a中存儲(chǔ)的值,操作有兩種:讀和寫。

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

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

printf("pa = %d \n", pa);這個(gè)語句就是要讀取指針變量pa中的值,當(dāng)然是0x11223344;
pa = &a;這個(gè)語句就是要把新的值寫入到指針變量pa中。再次強(qiáng)調(diào)一下,指針變量中存儲(chǔ)的是地址,如果我們可以提前知道變量a的地址是 0x11223344,那么我們也可以這樣來賦值:pa = 0x11223344;

思考一下,如果執(zhí)行這個(gè)語句printf("&pa =0x%x \n", &pa);,打印結(jié)果會(huì)是什么?

上面已經(jīng)說過,操作符&是用來取地址的,那么&pa就表示獲取指針變量pa的地址,上面的內(nèi)存模型中顯示指針變量pa是存儲(chǔ)在0x11223348這個(gè)地址中的,因此打印結(jié)果就是:&pa = 0x11223348。

5.2 獲取指針變量所指向的數(shù)據(jù)

指針變量所指向的數(shù)據(jù)類型是在定義的時(shí)候就明確的,也就是說指針pa指向的數(shù)據(jù)類型就是int型,因此在執(zhí)行printf("value = %d \n", *pa);語句時(shí),首先知道pa是一個(gè)指針,其中存儲(chǔ)了一個(gè)地址(0x11223344),然后通過操作符*來獲取這個(gè)地址(0x11223344)對(duì)應(yīng)的那個(gè)存儲(chǔ)空間中的值;又因?yàn)樵诙xpa時(shí),已經(jīng)指定了它指向的值是一個(gè)int型,所以我們就知道了地址0x11223344中存儲(chǔ)的就是一個(gè)int類型的數(shù)據(jù)。

5.3 以什么樣的數(shù)據(jù)類型來使用/解釋指針變量所指向的內(nèi)容

如下代碼:

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

根據(jù)以上的描述,我們知道printf的打印結(jié)果會(huì)是value = 30000,十進(jìn)制的30000轉(zhuǎn)成十六進(jìn)制是0x00007530,內(nèi)存模型如下:

現(xiàn)在我們做這樣一個(gè)測(cè)試:

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

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

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

以上內(nèi)容,就是指針最根本的心法了。把這個(gè)心法整明白了,剩下的就是多見識(shí)、多練習(xí)的問題了。

三、指針的幾個(gè)相關(guān)概念

1. const屬性

const標(biāo)識(shí)符用來表示一個(gè)對(duì)象的不可變的性質(zhì),例如定義:

const int b = 20;

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

int a = 20;
int b = 20;
int * const p = &a;

內(nèi)存模型如下:

這里的const用來修飾指針變量p,根據(jù)const的性質(zhì)可以得出結(jié)論:p在定義為變量a的地址之后,就固定了,不能再被改變了,也就是說指針變量pa中就只能存儲(chǔ)變量a的地址0x11223344。如果在后面的代碼中寫p = &b;,編譯時(shí)就會(huì)報(bào)錯(cuò),因?yàn)閜是不可改變的,不能再被設(shè)置為變量b的地址。

但是,指針變量p所指向的那個(gè)變量a的值是可以改變的,即:*p = 21;這個(gè)語句是合法的,因?yàn)橹羔榩的值沒有改變(仍然是變量c的地址0x11223344),改變的是變量c中存儲(chǔ)的值。

與下面的代碼區(qū)分一下:

int a = 20;
int b = 20;
const int *p = &a;
p = &b;

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

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

short int getDataCRC(const char *pData, int len)
{
    short int crc = 0x0000;
    // 計(jì)算CRC
    return crc;
}

2. void型指針

關(guān)鍵字void并不是一個(gè)真正的數(shù)據(jù)類型,它體現(xiàn)的是一種抽象,指明不是任何一種類型,一般有2種使用場(chǎng)景:

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

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

int a = 20;
int b = a;
int *p1 = &a;
int *p2 = p1;

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

int a = 20;
int *p1 = &a;
char *p2 = (char *)p1;

內(nèi)存模型如下:

p1指針指向的是int型數(shù)據(jù),現(xiàn)在想把它的值(0x11223344)賦值給p2,但是由于在定義p2指針時(shí)規(guī)定它指向的數(shù)據(jù)類型是char型,因此需要把指針p1進(jìn)行強(qiáng)制類型轉(zhuǎn)換,也就是把地址0x11223344處的數(shù)據(jù)按照char型數(shù)據(jù)來看待,然后才可以賦值給p2指針。

如果我們使用void *p2來定義p2指針,那么在賦值時(shí)就不需要進(jìn)行強(qiáng)制類型轉(zhuǎn)換了,例如:

int a = 20;
int *p1 = &a;
void *p2 = p1;

指針p2是void*型,意味著可以把任意類型的指針賦值給p2,但是不能反過來操作,也就是不能把void*型指針直接賦值給其他確定類型的指針,而必須要強(qiáng)制轉(zhuǎn)換成被賦值指針?biāo)赶虻臄?shù)據(jù)類型,如下代碼,必須把p2指針強(qiáng)制轉(zhuǎn)換成int*型之后,再賦值給p3指針:

int a = 20;
int *p1 = &a;
void *p2 = p1;
int *p3 = (int *)p2;

我們來看一個(gè)系統(tǒng)函數(shù):

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

第一個(gè)參數(shù)類型是void*,這正體現(xiàn)了系統(tǒng)對(duì)內(nèi)存操作的真正意義:它并不關(guān)心用戶傳來的指針具體指向什么數(shù)據(jù)類型,只是把數(shù)據(jù)挨個(gè)存儲(chǔ)到這個(gè)地址對(duì)應(yīng)的空間中。

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

3. 空指針和野指針

一個(gè)指針必須指向一個(gè)有意義的地址之后,才可以對(duì)指針進(jìn)行操作。如果指針中存儲(chǔ)的地址值是一個(gè)隨機(jī)值,或者是一個(gè)已經(jīng)失效的值,此時(shí)操作指針就非常危險(xiǎn)了,一般把這樣的指針稱作野指針,C代碼中很多指針相關(guān)的bug就來源于此。

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

在定義一個(gè)指針變量之后,如果沒有賦值,那么這個(gè)指針變量中存儲(chǔ)的就是一個(gè)隨機(jī)值,有可能指向內(nèi)存中的任何一個(gè)地址空間,此時(shí)萬萬不可以對(duì)這個(gè)指針進(jìn)行寫操作,因?yàn)樗锌赡苤赶騼?nèi)存中的代碼段區(qū)域、也可能指向內(nèi)存中操作系統(tǒng)所在的區(qū)域。

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

#ifdef __cplusplus
     #define NULL    0
#else    
     #define NULL    ((void *)0)
#endif
3.2 野指針:地址已經(jīng)失效的指針

我們都知道,函數(shù)中的局部變量存儲(chǔ)在棧區(qū),通過malloc申請(qǐng)的內(nèi)存空間位于堆區(qū),如下代碼:

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

內(nèi)存模型為:

在堆區(qū)申請(qǐng)了4個(gè)字節(jié)的空間,然后強(qiáng)制類型轉(zhuǎn)換為int*型之后,賦值給指針變量p,然后通過*p設(shè)置這個(gè)地址中的值為14,這是合法的。如果在釋放了p指針指向的空間之后,再使用*p來操作這段地址,那就是非常危險(xiǎn)了,因?yàn)檫@個(gè)地址空間可能已經(jīng)被操作系統(tǒng)分配給其他代碼使用,如果對(duì)這個(gè)地址里的數(shù)據(jù)強(qiáng)行操作,程序立刻崩潰的話,將會(huì)是我們最大的幸運(yùn)!

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

四、指向不同數(shù)據(jù)類型的指針

1. 數(shù)值型指針

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

2. 字符串指針

字符串在內(nèi)存中的表示有2種:

  1. 用一個(gè)數(shù)組來表示,例如:char name1[8] = "zhangsan";
  2. 用一個(gè)char *指針來表示,例如:char *name2 = "zhangsan";

name1在內(nèi)存中占據(jù)8個(gè)字節(jié),其中存儲(chǔ)了8個(gè)字符的ASCII碼值;name2在內(nèi)存中占據(jù)9個(gè)字節(jié),因?yàn)槌舜鎯?chǔ)8個(gè)字符的ASCII碼值,在最后一個(gè)字符'n'的后面還額外存儲(chǔ)了一個(gè)'\0',用來標(biāo)識(shí)字符串結(jié)束。

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

char *name2 = "zhangsan";
char *p = name2;
while (*p != '\0')
{
    printf("%c ", *p);
    p = p + 1;
}

在while的判斷條件中,檢查p指針指向的字符是否為結(jié)束符'\0'。在循環(huán)體重,打印出當(dāng)前指向的字符之后,對(duì)指針比那里進(jìn)行自增操作,因?yàn)橹羔榩所指向的數(shù)據(jù)類型是char,每個(gè)char在內(nèi)存中占據(jù)一個(gè)字節(jié),因此指針p在自增1之后,就指向下一個(gè)存儲(chǔ)空間。

也可以把循環(huán)體中的2條語句寫成1條語句:

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

假如一個(gè)指針指向的數(shù)據(jù)類型為int型,那么執(zhí)行p = p + 1;之后,指針p中存儲(chǔ)的地址值將會(huì)增加4,因?yàn)橐粋€(gè)int型數(shù)據(jù)在內(nèi)存中占據(jù)4個(gè)字節(jié)的空間,如下所示:

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

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

打印結(jié)果如下:

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

說明void*型指針在自增時(shí),是按照一個(gè)字節(jié)的跨度來計(jì)算的。

3. 指針數(shù)組與數(shù)組指針

這2個(gè)說法經(jīng)常會(huì)混淆,至少我是如此,先看下這2條語句:

int *p1[3];   // 指針數(shù)組
int (*p2)[3]; // 數(shù)組指針
3.1 指針數(shù)組

第1條語句中:中括號(hào)[]的優(yōu)先級(jí)高,因此與p1先結(jié)合,表示一個(gè)數(shù)組,這個(gè)數(shù)組中有3個(gè)元素,這3個(gè)元素都是指針,它們指向的是int型數(shù)據(jù)??梢赃@樣來理解:如果有這個(gè)定義char p[3],很容易理解這是一個(gè)有3個(gè)char型元素的數(shù)組,那么把char換成int*,意味著數(shù)組里的元素類型是int*型(指向int型數(shù)據(jù)的指針)。內(nèi)存模型如下(注意:三個(gè)指針指向的地址并不一定是連續(xù)的):

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

int a = 1, b = 2, c = 3;
char *p1[3];
p1[0] = &a;
p1[1] = &b;
p1[2] = &c;
3.2 數(shù)組指針

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

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

  1. 字符串字面量作為右值時(shí),就代表這個(gè)字符串在內(nèi)存中的首地址;
  2. 數(shù)組名就代表這個(gè)數(shù)組的地址,也等于這個(gè)數(shù)組的第一個(gè)元素的地址;
  3. 函數(shù)名就代表這個(gè)函數(shù)的地址。

因此,對(duì)于一下代碼,三個(gè)printf語句的打印結(jié)果是相同的:

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

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

答案是12個(gè)字節(jié)。因?yàn)閜2指向的是一個(gè)數(shù)組,這個(gè)數(shù)組中包含3個(gè)元素,每個(gè)元素占據(jù)4個(gè)字節(jié),那么這個(gè)數(shù)組在內(nèi)存中一共占據(jù)12個(gè)字節(jié),因此p2在加1之后,就跳過12個(gè)字節(jié)。

4. 二維數(shù)組和指針

一維數(shù)組在內(nèi)存中是連續(xù)分布的多個(gè)內(nèi)存單元組成的,而二維數(shù)組在內(nèi)存中也是連續(xù)分布的多個(gè)內(nèi)存單元組成的,從內(nèi)存角度來看,一維數(shù)組和二維數(shù)組沒有本質(zhì)差別。

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

int a[3][3] = {{1,2,3}, {4,5,6}, {7,8,9}}; // 二維數(shù)組
int (*p0)[3] = NULL;   // p0是一個(gè)指針,指向一個(gè)數(shù)組
int (*p1)[3] = NULL;   // p1是一個(gè)指針,指向一個(gè)數(shù)組
int (*p2)[3] = NULL;   // p2是一個(gè)指針,指向一個(gè)數(shù)組
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));

打印結(jié)果是:

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

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

5. 結(jié)構(gòu)體指針

C語言中的基本數(shù)據(jù)類型是預(yù)定義的,結(jié)構(gòu)體是用戶定義的,在指針的使用上可以進(jìn)行類比,唯一有區(qū)別的就是在結(jié)構(gòu)體指針中,需要使用->箭頭操作符來獲取結(jié)構(gòu)體中的成員變量,例如:

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

看起來似乎沒有什么技術(shù)含量,如果是結(jié)構(gòu)體數(shù)組呢?例如:

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

打印結(jié)果是:

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

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

6. 函數(shù)指針

每一個(gè)函數(shù)在經(jīng)過編譯之后,都變成一個(gè)包含多條指令的集合,在程序被加載到內(nèi)存之后,這個(gè)指令集合被放在代碼區(qū),我們?cè)诔绦蛑惺褂煤瘮?shù)名就代表了這個(gè)指令集合的開始地址

函數(shù)指針,本質(zhì)上仍然是一個(gè)指針,只不過這個(gè)指針變量中存儲(chǔ)的是一個(gè)函數(shù)的地址。函數(shù)最重要特性是什么?可以被調(diào)用!因此,當(dāng)定義了一個(gè)函數(shù)指針并把一個(gè)函數(shù)地址賦值給這個(gè)指針時(shí),就可以通過這個(gè)函數(shù)指針來調(diào)用函數(shù)。

如下示例代碼:

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

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

  1. 有2個(gè)int型的參數(shù);
  2. 有一個(gè)int型的返回值。

代碼中的add函數(shù)正好滿足這個(gè)要求,因此,可以把a(bǔ)dd賦值給函數(shù)指針p,此時(shí)p就指向了內(nèi)存中這個(gè)函數(shù)存儲(chǔ)的地址,后面就可以用函數(shù)指針p來調(diào)用這個(gè)函數(shù)了

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

int (*p)(int, int);
int (*p2)(int, int);

這里的參數(shù)比較簡(jiǎn)單,如果函數(shù)很復(fù)雜,這樣的定義方式豈不是要煩死?可以用typedef關(guān)鍵字來定義一個(gè)函數(shù)指針類型

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

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

注意:這里有幾個(gè)小細(xì)節(jié)稍微了解一下:

  1. 在賦值函數(shù)指針時(shí),使用p = &a;也是可以的;
  2. 使用函數(shù)指針調(diào)用時(shí),使用(*p)(a, b);也是可以的。

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

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

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);,先分析中間部分,標(biāo)識(shí)符p與中括號(hào)[]結(jié)合(優(yōu)先級(jí)高),所以p是一個(gè)數(shù)組,數(shù)組中有4個(gè)元素;然后剩下的內(nèi)容表示一個(gè)函數(shù)指針,那么就說明數(shù)組中的元素類型是函數(shù)指針,也就是其他函數(shù)的地址,內(nèi)存模型如下:

如果還是難以理解,那就回到指針的本質(zhì)概念上:指針就是一個(gè)地址!這個(gè)地址中存儲(chǔ)的內(nèi)容是什么根本不重要,重要的是你告訴計(jì)算機(jī)這個(gè)內(nèi)容是什么。如果你告訴它:這個(gè)地址里存放的內(nèi)容是一個(gè)函數(shù),那么計(jì)算機(jī)就去調(diào)用這個(gè)函數(shù)。那么你是如何告訴計(jì)算機(jī)的呢,就是在定義指針變量的時(shí)候,僅此而已!

五、總結(jié)

我已經(jīng)把自己知道的所有指針相關(guān)的概念、語法、使用場(chǎng)景都作了講解,就像一個(gè)小酒館的掌柜,把自己的美酒佳肴都呈現(xiàn)給你,但愿你已經(jīng)酒足飯飽!

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

  1. 指針就是地址,地址就是指針。
  2. 指針就是指向內(nèi)存中的一塊空間,至于如何來解釋/操作這塊空間,由這個(gè)指針的類型來決定。

另外還有一點(diǎn)囑咐,那就是學(xué)習(xí)任何一門編程語言,一定要弄清楚內(nèi)存模型,內(nèi)存模型,內(nèi)存模型!

免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問題,請(qǐng)聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

倫敦2024年8月29日 /美通社/ -- 英國汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時(shí)1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動(dòng) BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時(shí)企業(yè)卻面臨越來越多業(yè)務(wù)中斷的風(fēng)險(xiǎn),如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報(bào)道,騰訊和網(wǎng)易近期正在縮減他們對(duì)日本游戲市場(chǎng)的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)開幕式在貴陽舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國國際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對(duì)環(huán)境變化,經(jīng)營業(yè)績(jī)穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤率延續(xù)升勢(shì) 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競(jìng)爭(zhēng)力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競(jìng)爭(zhēng)優(yōu)勢(shì)...

關(guān)鍵字: 通信 BSP 電信運(yùn)營商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺(tái)與中國電影電視技術(shù)學(xué)會(huì)聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會(huì)上宣布正式成立。 活動(dòng)現(xiàn)場(chǎng) NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會(huì)上,軟通動(dòng)力信息技術(shù)(集團(tuán))股份有限公司(以下簡(jiǎn)稱"軟通動(dòng)力")與長三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉