C/C++ 指针详解
指針詳解
參考視頻:https://www.bilibili.com/video/BV1bo4y1Z7xf/,感謝Bilibili@fengmuzi2003的搬運翻譯及后續(xù)勘誤,也感謝已故原作者Harsha Suryanarayana的講解,RIP。
學(xué)習(xí)完之后,回看找特定的知識點,善用目錄 —>
筆者親測實驗編譯器版本:
gcc版本
gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
Copyright ? 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
指針的基本介紹
數(shù)據(jù)在內(nèi)存中的存儲與訪問
在內(nèi)存中,每一字節(jié)(8位)有一個地址。假設(shè)圖中最下面的內(nèi)存地址位0,內(nèi)存地址向上生長,圖中標(biāo)識出的(下面)第一個字節(jié)的地址位201,地址向上生長一直到圖中最上面的地址208。
當(dāng)我們在程序中聲明一個變量時,如int a,系統(tǒng)會為這個變量分配一些內(nèi)存空間,具體分配多少空間則取決于該變量的數(shù)據(jù)類型和具體的編譯器。常見的有int類型4字節(jié),char類型1字節(jié),float類型4字節(jié)等。其他的內(nèi)建數(shù)據(jù)類型或用戶定義的結(jié)構(gòu)體和類的大小,可通過sizeof來查看。
我們聲明兩個變量:
int a; char c;假如他們分別被分配到內(nèi)存的204-207字節(jié)和209字節(jié)。則在程序中會有一張查找表(圖中右側(cè)),表中記錄的各個條目是變量名,變量類型和變量的首地址。
當(dāng)我們?yōu)樽兞抠x值時,如a = 5,程序就會先查到 a 的類型及其首地址,然后到這個地址把其中存放的值寫為 5。
指針概念
我們能不能在程序中直接查看或者訪問內(nèi)存地址呢?當(dāng)然是可以的,這就用到我們今天的主角——指針。
指針是一個變量,它存放的是另一個變量的地址。
- 指針與它指向的變量 假設(shè)我們現(xiàn)在有一個整型變量a=4存放在內(nèi)存中的204地址處(實際上應(yīng)該是204-207四個字節(jié)中,這里我們用首地址204表示)。在內(nèi)存中另外的某個地址處,我們有另外一個變量 p,它的類型是“指向整型的指針”,它的值為204,即整型變量a的地址,這里的 p 就是指向整型變量 a 的指針。
- 指針?biāo)嫉膬?nèi)存空間 指針作為一種變量也需要占據(jù)一定的內(nèi)存空間。由于指針的值是一個內(nèi)存地址,所以指針?biāo)紦?jù)的內(nèi)存空間的大小與其指向的數(shù)據(jù)類型無關(guān),而與當(dāng)前機器類型所能尋址的位數(shù)有關(guān)。具體來說,在32位的機器上,一個指針(指向任意類型)的大小為4個字節(jié),在64位的機器上則為8個字節(jié)。
- 指針的修改 我們可以通過修改指針p的值,來使它指向其他的內(nèi)存地址。比如我們將 p 的值修改為 208,則可以使它指向存放在208地址處的另一個整型變量 b。
- 指向用戶定義的數(shù)據(jù)類型 除了內(nèi)建的數(shù)據(jù)類型之外,指針也可以指向用戶定義的結(jié)構(gòu)體或者類。
指針的聲明和引用
-
指針的聲明 在C語言中,我們通過 * 來聲明一個指向某種數(shù)據(jù)類型的指針:int *p。這個聲明的含義即:聲明一個指針變量 p,它指向一個整型變量。換句話說,p 是一個可以存放整型變量的地址的變量。
-
取地址 如果我們想指定 p 指向某一個具體的整型變量 a,我們可以:p = &a。其中用到了取地址運算符 &,它得到的是一個變量的地址,我們把這個地址賦值給 p,即使得 p 指向該地址。
這時,如果我們打印p, &a, &p的值會得到什么呢?不難理解,應(yīng)該分別是204,204,64。
-
解引用 如果我們想得到一個指針變量所指向的地址存放的值,該怎么辦呢?還是用 *放在指針變量 p 前面,即 *p 注意這里的 * 就不再是聲明指針的意思了,而稱為 解引用,即把 p 所指向的對象的值讀出來。 所以如果我們打印 *p,則會得到其所指向的整型變量 a 的值:5。
實際上,我們還可以通過解引用直接改變某個地址的值。比如 *p = 8,我們就將204地址處的整型變量的值賦為8。此時再打印*p或者a,則會得到8。
關(guān)于*,&兩個運算符的使用,可參考博客:指針(*)、取地址(&)、解引用(*)與引用(&)。
指針代碼示例
指針的算術(shù)運算
實際上,指針的唯一算術(shù)運算就是以整數(shù)值大小增加或減少指針值。如p+1、p-2等
示例程序
考慮以下程序:
#include <stdio.h>int main(){int a = 10;int* p;p = &a;printf("%d\n", p);printf("%d\n", p+1);return 0; }初學(xué)者可能會好奇,指針p 不是一個常規(guī)意義上的數(shù)字,而是一個內(nèi)存地址,它能夠直接被加1嗎?答案是可以的,但是結(jié)果可能會和整數(shù)的加1結(jié)果不太一樣。
輸出:
358010748 358010752可以看到p+1比p大了4,而不是我們加的1。
指針的加1
這是因為指針 p 是一個指向整型變量的指針,而一個整型變量在內(nèi)存中占4個字節(jié), 對 p 執(zhí)行加1,應(yīng)該得到的是下一個整型數(shù)據(jù)的地址,即在地址的數(shù)值上面應(yīng)該加4。
相應(yīng)地,如果是p+2的話,則打印出的地址的數(shù)值應(yīng)該加8。
危險
可能會造成危險的是,C/C++并不會為我們訪問的地址進行檢查,也就是說,我們可能通過指針訪問一塊未分配的內(nèi)存,但是沒有任何報錯。這可能會造成我們不知不覺地弄錯了一些數(shù)值。
比如,接著上面的例子,我們試圖打印 p 和 p+1 所指向的地址所存放的值:
#include <stdio.h>int main(){int a = 10;int* p;p = &a;printf("Addresses:\n");printf("%d\n", p);printf("%d\n", p+1);printf("Values:\n");printf("%d\n", *p);printf("%d\n", *(p+1));return 0; }輸出:
Addresses: -428690420 -428690416 Values: 10 -428690420可以看到,對指針進行加法,訪問 p+1 所指向的地址的值是沒有意義的,但是C/C++并不會禁止我們這么做,這可能會帶來一些難以察覺的錯誤。
指針的類型
明確指針的類型
首先要明確的是,指針是強類型的,即:我們需要特定類型的指針來指向特定類型的變量的存放地址。如int*、char*等或者指向自定義結(jié)構(gòu)體和類的指針。
指針不是只存放一個地址嗎?為什么指針必須要明確其指向的數(shù)據(jù)類型呢?為什么不能有一個通用類型的指針來指向任意數(shù)據(jù)類型呢?那樣不是很方便嗎?
原因是我們不僅僅是用指針來存儲內(nèi)存地址,同時也使用它來解引用這些內(nèi)存地址的內(nèi)容。而不同的數(shù)據(jù)類型在所占的內(nèi)存大小是不一樣的,更關(guān)鍵的是,除了大小之外,不同的數(shù)據(jù)類型在存儲信息的方式上也是不同的(如整型和浮點型)。
示例程序
考慮一下程序:
#include <stdio.h>int main(){int a = 1025;int *p;p = &a;printf("Size of integer is %d bytes\n", sizeof(int));printf("p\t Address = %d, Value=%d\n", p, *p);printf("p+1\t Address = %d, Value=%d\n", p+1, *(p+1));char*p0;p0 = (char*)p; // 強制類型轉(zhuǎn)換printf("Size of char is %d bytes\n", sizeof(char));printf("p0\t Address = %d, Value=%d\n", p0, *p0);printf("p0+1\t Address = %d, Value=%d\n", p0+1, *(p0+1));return 0;// 1025 == 0000 0000 0100 0001 }輸出:
Size of integer is 4 bytes p Address = 1241147588, Value=1025 p+1 Address = 1241147592, Value=1241147588 Size of char is 1 bytes p0 Address = 1241147588, Value=1 p0+1 Address = 1241147589, Value=4我們可以通過強制類型轉(zhuǎn)換,將指向整型的指針p 轉(zhuǎn)為指向字符型的p0。由于指向了字符型,p0在被解引用時只會找該地址一個字節(jié)的內(nèi)容,而整型1025的第一個字節(jié)的內(nèi)容為0001,第二個字節(jié)內(nèi)容為0100,所以會有上面程序的打印行為。
可以參考筆者畫的內(nèi)存示意圖來理解這段測試程序,其中v表示將該段內(nèi)存解釋為%d的值。
需要指出的是這里的指針的強制類型轉(zhuǎn)換,看似只會添亂,毫無用處,但是它實際上是有一些有用使用場景的,會在后面介紹。
void *
我們這里首先對通用指針類型void *的一些基本特性做出說明,后面會介紹一些具體的使用場景。
void *時通用指針類型,它不針對某個特定的指針類型。在使用時將其賦值為指向某種特定的數(shù)據(jù)類型的指針時不需要做強制類型轉(zhuǎn)換。
由于不知道它指向的類型,因此不能直接對其進行解引用*p,也不能對其進行算數(shù)運算p+1。
指向指針的指針
我們之所以能夠把整型變量 x 的地址存入 p 是因為 p 是一個指向整型變量的指針int*。那如果想要把指針的地址也存儲到一個變量中,這個變量就是一個指向指針的指針,即int**。
這個邏輯說起來時挺清楚的,在實際程序中,則有可能會暈掉。我們來看一個示例程序,開始套娃:
#include <stdio.h>int main(){int x;int* p = &x;*p = 6;int** q = &p;int*** r = &q;printf("%d\n", *p);printf("%d\n", *q);printf("%d\n", **q);printf("%d\n", **r);printf("%d\n", ***r);return 0; }在這里我們不按編譯器實際輸出的地址值來進行分析,因為這個地址值是不固定的且通常較大。筆者在這里畫了一小段內(nèi)存,我們按圖中的地址值來分析打印輸出的內(nèi)容。在圖中,紅色字體是地址值,青色塊是該變量占據(jù)的地址空間,其中的黑色字體是該變量的值。假設(shè)我們在32位機中,即一個指針占4個字節(jié)。
在程序中,x是整型變量,p指向x,q指向p,r指向q,這樣x, p, q, r的數(shù)據(jù)類型分別是:int, int*, int**, int***。
我們編譯運行該程序,得到的輸出是:
6 -1672706964 6 -1672706964 6和我們分析的結(jié)果一致。
大家可以自己設(shè)計一些這種小示例程序,試著分析一下,再來查看程序運行的結(jié)果是否與預(yù)期一致。
函數(shù)傳值 vs. 傳引用
在執(zhí)行一個C語言程序時,此程序?qū)碛形ㄒ坏摹皟?nèi)存四區(qū)”——棧區(qū)、堆區(qū)、全局區(qū)、代碼區(qū).
具體過程為:操作系統(tǒng)把硬盤中的數(shù)據(jù)下載到內(nèi)存,并將內(nèi)存劃分成四個區(qū)域,由操作系統(tǒng)找到main入口開始執(zhí)行程序。
內(nèi)存四區(qū)
- 堆區(qū)(heap):一般由程序員手動分配釋放(動態(tài)內(nèi)存申請與釋放),若程序員不釋放,程序結(jié)束時可能由操作系統(tǒng)回收。
- 棧區(qū)(stack):由編譯器自動分配釋放,存放函數(shù)的形參、局部變量等。當(dāng)函數(shù)執(zhí)行完畢時自動釋放。
- 全局區(qū)(global / stack):用于存放全局變量和靜態(tài)變量, 里面細分有一個常量區(qū),一些常量存放在此。該區(qū)域是在程序結(jié)束后由操作系統(tǒng)釋放。
- 代碼區(qū)(code / text):用于存放程序代碼,字符串常量也存放于此。
函數(shù)調(diào)用
在程序未執(zhí)行結(jié)束時,main()函數(shù)里分配的空間均可以被其他自定義函數(shù)訪問。
自定義函數(shù)若在堆區(qū)(malloc動態(tài)分配內(nèi)存等)或全局區(qū)(常量等)分配的內(nèi)存,即便此函數(shù)結(jié)束,這些內(nèi)存空間也不會被系統(tǒng)回收,內(nèi)存中的內(nèi)容可以被其他自定義函數(shù)和main()函數(shù)使用。
函數(shù)傳值 call by value
假設(shè)新手程序員Albert剛剛學(xué)習(xí)了關(guān)于函數(shù)的用法,寫了這樣的程序:
#include <stdio.h>void Incremnet(int a){a = a + 1; }int main(){int a;a = 10;Incremnet(a);printf("a = %d\n", a);return 0; }在該程序中,Albert期望通過Increment()函數(shù)將a的值加1,然后打印出a = 11,但是,程序的實際運行結(jié)果卻是a = 10。問題出在哪里呢?
實際上,這種函數(shù)調(diào)用的方式稱為值傳遞call by value,這樣在Increment()函數(shù)中,臨時變量local variable a,會在該函數(shù)結(jié)束后立刻釋放掉。也就是說Increment()函數(shù)中的a ,和main() 函數(shù)中的 a 并不是同一個變量。我們可以分別在Increment()和main()兩個函數(shù)內(nèi)打印變量a的地址:
printf("Address of a in Increment: %d", &a); printf("Address of a in main: %d", &a); // 將這兩句分別放在Increment函數(shù)和main函數(shù)中輸出:
Address of a in Increment: 2063177884 Address of a in main: 2063177908這里兩個地址的具體值不重要,重要的是他們是不一樣的,也就是說我們在兩個函數(shù)中操作的a變量并不是同一個,所以程序輸出的是沒有加1過的a的值。
筆者這里還是根據(jù)原視頻作者的講解,通過畫出內(nèi)存的形式來分析值傳遞。
程序會為每個函數(shù)創(chuàng)造屬于這個函數(shù)的棧幀,我們首先調(diào)用main()函數(shù),其中的變量a一直存儲在main()函數(shù)自己的棧幀中。在我們調(diào)用Increment()函數(shù)的時候,會單獨為其創(chuàng)造一個屬于它的棧幀,然后main()函數(shù)將實參a=10傳給Increment()作為形參,a會在其中加1,但是并沒有被返回。在Increment()函數(shù)調(diào)用結(jié)束后,它的棧幀被釋放掉,main()函數(shù)并不知道它做了什么,main()自己的變量值一直是10,然后調(diào)用printf()函數(shù),將該值打印出來。
可以看到,局部變量的值的生命周期隨著被調(diào)用函數(shù)Increment()的結(jié)束而結(jié)束了,而由于main()中的a和Incremet()中的a并不是同一個變量(剛才已經(jīng)看到,二者并不在同一地址),因此最終打印出的值還是10。
傳引用 call by reference
那怎樣才能實現(xiàn)Albert的預(yù)期呢?我們剛才已經(jīng)看到,之所以最終在main()中打印的值沒有加1,就是因為加1的變量和最終打印的變量不是同一個變量。那我們只要使得最終打印的變量就是在Increment()中加過1的變量就可以了。這要怎么實現(xiàn)呢?我們剛剛學(xué)過,通過指針可以指向某個特定的變量,并可以通過解引用的方式對該變量再進行賦值,而又由于在程序未執(zhí)行結(jié)束時,main()函數(shù)里分配的空間均可以被其他自定義函數(shù)訪問。因此我們可以將main()中的變量地址傳給Increment(),在其中對該地址的值進行加一,這樣最終打印的變量就會是加過1的了。
實現(xiàn)如下:
#include <stdio.h>void Incremnet(int* p){*p = *p + 1; }int main(){int a;a = 10;Incremnet(&a);printf("a = %d\n", a);return 0; }這種傳地址的方式我們稱之為call by reference。
它可以在原地直接修改傳入的參數(shù)值。另外,由于傳的參數(shù)是一個指針,無論被指向的對象有多么大,這個指針也只占4個字節(jié)(32位機),因此,這種方式也可以大大提高傳參的效率。
指針與數(shù)組
指針和數(shù)組常常一起出現(xiàn),二者之間有著很強的聯(lián)系。
數(shù)組的聲明
當(dāng)我們聲明一個整型數(shù)組int A[5]時,就會有五個整型變量:A[0] - A[4],被連續(xù)地存儲在內(nèi)存空間中。
數(shù)組與指針?biāo)阈g(shù)運算
還記得我們在前面介紹過指針的算術(shù)運算時,提到過指針的算術(shù)運算可能會導(dǎo)致訪問到未知的內(nèi)存,因為我們定義一個指針時,它指向的位置的鄰居通常是未知的。而在數(shù)組中,我們沒有這個問題,因為數(shù)組是一整塊連續(xù)的內(nèi)存空間,我們確定旁邊也存放著一些相同類型的變量。
int A[5]; int* p; p = A;printf("%d\n", p); // 200 printf("%d\n", *p); // 2 printf("%d\n", p+1); // 204 printf("%d\n", *(p+1)); // 4 // ...在數(shù)組中,指針的算術(shù)運算就很有意義了。因為相鄰位置的變量都是已知的,我們可以通過這種偏移量的方式去訪問它們。
數(shù)組名和指針
數(shù)組與指針的另一個聯(lián)系是:數(shù)組名就是指向數(shù)組首元素的指針。數(shù)組的首元素的地址,也被稱為數(shù)組的基地址。
(這里還有一個要注意的小點:數(shù)組名不能直接自加,即不可A++,但是可以將其賦值給一個指針,指針可以自加:p++)
比如上面例程中我們寫的p = A。這樣,考慮以下例程的打印輸出:
printf("%d\n", A); // 200 printf("%d\n", *A); // 2 printf("%d\n", A+3); // 212 printf("%d\n", *(A+3)); // 8數(shù)組/指針 取值/取地址
對于第 iii 個元素:
- 取地址:&A[i] or (A+i)
- 取值: A[i] or *(A+i)
關(guān)于C/C++中指針與數(shù)組的關(guān)系可參考博客:C++中數(shù)組和指針的關(guān)系(區(qū)別)詳解,筆者已將全文重要的一些知識點都總結(jié)好,放在文章開頭。
數(shù)組作為函數(shù)參數(shù)
注意我們可以通過sizeof函數(shù)獲取到數(shù)組的元素個數(shù):sizeof(A) / sizeof(A[0]),即用整個數(shù)組的大小除以首元素的大小,由于我們的數(shù)組中存儲的元素都是相同的數(shù)據(jù)類型,因此可以通過此法獲得數(shù)組的元素個數(shù)。
例程1
我們現(xiàn)在定義一個SumOfElements()函數(shù),用來計算傳入的數(shù)組的元素求和,該函數(shù)還需要傳入?yún)?shù)size作為數(shù)組的元素個數(shù)。在main()函數(shù)中新建一個數(shù)組,并通過sizeof來求得該數(shù)組的元素個數(shù),調(diào)用該函數(shù)求和。
#include <stdio.h>int SumOfElements(int A[], int size){int i, sum = 0;for (i=0; i<size; i++){sum += A[i];}return sum; }int main(){int A[] = {1, 2, 3, 4, 5};int size = sizeof(A) / sizeof(A[0]);int total = SumOfElements(A, size);printf("Sum of elements = %d\n", total);return 0; }打印出的結(jié)果如我們所料,為15:
Sum of elements = 15例程2
有人可能回想,既然我們已經(jīng)將數(shù)組傳入函數(shù)了,能不能進行進一步的封裝,將數(shù)組元素個數(shù)的計算也放到調(diào)用函數(shù)內(nèi)來進行呢?于是有了如下實現(xiàn):
#include <stdio.h>int SumOfElements(int A[]){int i, sum = 0;int size = sizeof(A) / sizeof(A[0]);for (i=0; i<size; i++){sum += A[i];}return sum; }int main(){int A[] = {1, 2, 3, 4, 5};int total = SumOfElements(A);printf("Sum of elements = %d\n", total);return 0; }結(jié)果好像除了億點點問題(筆者注:這里筆者的測試結(jié)果與原視頻作者不同(原結(jié)果1),是由于筆者是在64位機上進行的測試,一個指針大小為8字節(jié),而原作者使用的是32位機,指針占4字節(jié),這在接下來的測試程序中也有體現(xiàn)):
Sum of elements = 3為了測試問題出在哪里,讓我們在main()和SumOfElements()函數(shù)中打印如下信息:
printf("Main - Size of A = %d, Size of A[0] = %d\n", sizeof(A), sizeof(A[0])); printf("SOE - Size of A = %d, Size of A[0] = %d\n", sizeof(A), sizeof(A[0])); // 將這兩行分別添加到main和SumOfElements輸出結(jié)果:
SOE - Size of A = 8, Size of A[0] = 4 Main - Size of A = 20, Size of A[0] = 4 Sum of elements = 3果然,在SOE中傳入的數(shù)組A的大小僅有8字節(jié),即一個指針的大小。
實際上,在編譯例程2時,編譯器會給我們一定的提示W(wǎng)arning:
pointer.c: In function ‘SumOfElements’: pointer.c:5:22: warning: ‘sizeof’ on array function parameter ‘A’ will return size of ‘int *’ [-Wsizeof-array-argument]int size = sizeof(A) / sizeof(A[0]);可以看到,還是比較準(zhǔn)確地指出了可能存在的問題,在被調(diào)函數(shù)中直接對數(shù)組名使用sizeof,會返回指針的大小。
分析
我們還是要畫出棧區(qū)來進行分析:
我們期望的是向左邊那樣,在main()函數(shù)將數(shù)組A作為參數(shù)傳給SOE()之后,會在SOE()的棧幀上拷貝一份完全相同的20字節(jié)的數(shù)組。但是在實際上,編譯器卻并不是這么做的,而是只把數(shù)組A的首地址賦值給一個指針,作為SOE()的形參。也就是說,SOE()的函數(shù)簽名SumOfElements(int A[]) 其實是相當(dāng)于SumOfElements(int* A)。這也就解釋了為什么我們在其內(nèi)部計算A的大小時,得到的會是一個指針的大小。結(jié)合我們之前介紹過的值傳遞和地址傳遞的知識。可以這樣講:數(shù)組作為函數(shù)參數(shù)時是傳引用(call by reference),而非我們預(yù)期的值傳遞。
需要指出的是,編譯器的這種做法其實是合理的。因為通常來講,數(shù)組會是一個很長,占內(nèi)存空間很大的變量,如果每次傳參都按照值傳遞完整地拷貝一份的話,效率極其低下。而如果采用傳引用的方式,需要傳遞的只有一個指針的大小。
指針與數(shù)組辨析
這里視頻原作者做了許多解釋,筆者認(rèn)為有一句話可以概括二者關(guān)系的本質(zhì):數(shù)組名稱和指針變量的唯一區(qū)別是,不能改變數(shù)組名稱指向的地址,即數(shù)組名稱可視為一個指向數(shù)組首元素地址的指針常量。也就是說數(shù)組名指針是定死在數(shù)組首元素地址的,其指向不能被改變。比如數(shù)組名不允許自加A++,因為這會它是一個不可改變的指針常量,而一般指針允許自加p++;還有常量不能被賦值,即若有數(shù)組名 A,指針 p,則A = p是非法的。詳見博客:C++中數(shù)組和指針的關(guān)系(區(qū)別)詳解。
指針與字符數(shù)組
當(dāng)我們在C語言中談?wù)撟址麛?shù)組時,通常就是在談?wù)撟址?/p>
C語言中字符串的存儲
在C語言中,我們通常以字符數(shù)組的形式來存儲字符串。對于一個有 nnn 個字符組成的字符串,我們需要一個長度至少為 n+1n+1n+1 的字符數(shù)組。例如要存儲字符串JOHN,我們需要一個長度至少為 5 的字符數(shù)組。
之所以字符數(shù)組的長度要比字符串中字符的個數(shù)至少多一個,是因為我們需要符號來標(biāo)志字符串的結(jié)束。在C語言中,我們通過在字符數(shù)組的最后添加一個 \0 來標(biāo)志字符串的結(jié)束。如下圖。
在這個圖中,我們?yōu)榱舜鎯ψ址甁OHN,我們使用了字符數(shù)組中的5個元素,其中最后一個字符 \0,用來標(biāo)識字符串的結(jié)束。倘若沒有這個標(biāo)識的話,程序就不知道這個字符串到哪里結(jié)束,就可能會訪問到5,6中一些未知的內(nèi)容。
示例程序:
#include <stdio.h>int main(){char C[4];C[0] = 'J';C[1] = 'O';C[2] = 'H';C[3] = 'N';printf("%s", C);return 0; }這里原作者給出了這樣一個示例程序,并且測試得到的輸出結(jié)果是JOHN+幾個亂碼,這是合理的,因為如前所述,沒有設(shè)置 \0 來標(biāo)識字符串的結(jié)束。
但是筆者在自己的機器上親測(編譯器為gcc 7.5.0)的時候打印輸出是正常的JOHN字符串,這是由于有些編譯器會自動的為你補全\0。筆者也嘗試了通過調(diào)整gcc的-O參數(shù)嘗試了各種優(yōu)化等級,都可以正常打印字符串。
而通過 char C[20] = "JOHN" 這種方式定義的字符串,編譯器一定會在其末尾添加一個 \0。 原作者強調(diào)這里編譯器會強制要求聲明的數(shù)組長度大于等于5,也就是說char C[4] = "JOHN"是無法通過編譯的。但在筆者測試時,這也是可行的,但是 3 就肯定不行了哈。
通過引入頭文件string.h,可以使用strlen()函數(shù)獲取到字符串的長度,無論我們聲明的字符數(shù)組有多長(比如上面這個20),該函數(shù)會找到第一個 \0,并返回之前的元素個數(shù),也就是我們實際的字符串長度。有以下例程:
#include <stdio.h> #include <string.h>int main(){char C[20] = "JOHN";int len = strlen(C);printf("%d", len);return 0; }輸出會是 4,我們實際的字符串JOHN的長度。
字符串常量與常量指針
char[] 和 char*
char C[20] = "JOHN"; // 字符串就會儲存在分配給這個數(shù)組的內(nèi)存空間中,這種情形下它會被分配在棧上當(dāng)向上面一樣使用字符數(shù)組進行初始化時,字符串就會儲存在分配給這個數(shù)組的內(nèi)存空間中,這種情形下它會被分配在棧上。
而當(dāng)使用 char* 的形式聲明一個字符串時(如下),它會是一個字符串常量,通常會被存放在代碼區(qū)。
char* C = "JOHN"; // 如此聲明則為字符串常量,存放在代碼區(qū),其值不能被修改既然叫做常量,那它的值肯定是不能更改的了,即*C[0]='A' 是非法的操作。
常量指針
還記得我們之前提到過,即數(shù)組名稱可視為一個指向數(shù)組首元素地址的指針常量。指針常量的含義是指針的指向不能被修改,如數(shù)組名看作指針時不能自加,因為這會修改它的指向。
而本小節(jié)提到的常量指針則是指指針指向的值不能被修改。常量指針通常用在引用傳參時,如果某個函數(shù)要進行一些只讀的操作(如打印),為了避免在函數(shù)體內(nèi)部對數(shù)據(jù)進行了寫操作,而又因為是傳引用,則會破壞原數(shù)據(jù)。如以下打印字符串的函數(shù),由于打印字符串不需要改動原來的數(shù)據(jù),故可以在函數(shù)簽名中加上const關(guān)鍵字,來使得 C 是一個常量指針,保證其指向的值不會被誤操作修改。注意此處的 C 是常量指針,而非指針常量,即其指向可以改變,因此函數(shù)體中的C++是合法的操作。
void printString(const char* C){while (*C != '\0'){printf("%c", *C);C++;}printf("\n"); }指針與多維數(shù)組
指針與二維數(shù)組
二維數(shù)組概念
我們可以聲明一個二維數(shù)組:int B[2][3],實際上,這相當(dāng)于聲明了一個數(shù)組的數(shù)組。如此例中,B[0], B[1] 都是包含3個整型數(shù)據(jù)的一維數(shù)組。
如前所述,數(shù)組名相當(dāng)于是指向數(shù)組首元素地址的指針常量。在這里,首元素不在是一個整型變量,而是一個包含3個整型變量的一維數(shù)組。這時int* p = B就是非法的了,因為數(shù)組名B是一個指向一維數(shù)組的指針,而非一個指向整型變量的指針。正確的寫法應(yīng)該是:int (*p)[3] = B。
而B[0]就相當(dāng)于是一個一維數(shù)組名(就像前幾章的A),也相當(dāng)于一個指向整型的指針常量。
例程
我們通過一個例程來幫助自己分析理解二維數(shù)組和指針,與上面的元素設(shè)定一致,也假設(shè)地址就按上方藍色字體,每一組有虛線分隔開來,每一組之內(nèi)的含義是一樣的。大家可以先不看注釋中的解釋與答案,自己試著分析一下每一組是什么含義。后面會給出筆者的分析。
#include <stdio.h>int main(){int B[2][3] = {2, 3, 6, 4, 5, 8};printf("-----------------------\n"); // 指向一維數(shù)組的指針 400printf("%d\n", B);printf("%d\n", &B[0]);printf("-----------------------\n"); // 指向整型的指針 400printf("%d\n", *B);printf("%d\n", B[0]);printf("%d\n", &B[0][0]);printf("-----------------------\n"); // 指向一維數(shù)組的指針 412printf("%d\n", B+1); printf("%d\n", &B[1]);printf("-----------------------\n"); // 指向整型的指針 412printf("%d\n", *(B+1));printf("%d\n", B[1]);printf("%d\n", &B[1][0]);printf("-----------------------\n"); // 指向整型的指針 420 printf("%d\n", *(B+1)+2);printf("%d\n", B[1]+2); printf("%d\n", &B[1][2]); printf("-----------------------\n"); // 整型 3printf("%d\n", *(*B+1)); printf("-----------------------\n"); return 0; }第一組 (B,&B[0]):數(shù)組名B是一個指針,其指向的元素是一個一維數(shù)組,即二維數(shù)組第一個元素(第一個一維數(shù)組)的首地址。而B[0]就是二維數(shù)組的第一個元素,即二維數(shù)組的第一個一維數(shù)組,對其進行取地址運算,故&B[0]就是第一個一維數(shù)組的地址,也即第一個指向第一個一位數(shù)組的指針。
所以說第一組是指向一位數(shù)組的指針,其值為 400。
第二組:(*B,B[0],&B[0][0]):對數(shù)組名B進行解引用,得到的是其第一個元素(第一個一維數(shù)組)的值,也就是一個一維數(shù)組名B[0](相當(dāng)于前面幾章的一維數(shù)組名A),這個一維數(shù)組名就相當(dāng)于是一個指向整型數(shù)據(jù)的指針常量。而B[0][0]是一個整型數(shù)據(jù)2,對其進行取地址運算,得到的是一個指向整型變量的指針。
所以說第二組是指向整型變量的指針,其值也為400,但與第一組指向的元素不同,注意體會。
第三、四組與第一、二組類似,關(guān)鍵區(qū)別在于加入了指針運算。這里需要注意的是對什么類型的指針進行運算,是對指向一維數(shù)組的指針(+12),還是對指向整型的指針(+4)。在這兩組中都是對指向一維數(shù)組的指針(如二維數(shù)組名B)進行運算,所以地址要偏移12個字節(jié)。
第五組中開始有了對不同的指針類型進行指針運算的情況。在這一組中的,+1都是對指向一維數(shù)組的指針進行運算,要+(1*12),而+2都是對指向整型變量的指針進行運算,要+(2*4),故最終結(jié)果是420。
最后一組只有一個值。但需要一步一步仔細分析。首先*B是對二位數(shù)組名進行解引用,得到的是一個一位數(shù)組名,也就是一個指向整型的指針常量。對其加1,需要偏移4個字節(jié),即(*B+1)是一個指向地址404處的整型變量的指針,對其進行解引用,直接拿出404地址處的值,得到3。
大家可以考慮一下,如果加一個括號*(*(B+1))的值會是多少呢?
小公式
對于二位數(shù)組和指針、指針運算的關(guān)系,原作者給出了這樣一個公式,筆者同樣寫在下面供大家參考。希望大家不要死記硬背,而是試著去理解它。
B[i][j] == *(B[i]+j) == *(*(B+i)+j)
指針與高維數(shù)組
前面我們已經(jīng)看到多維數(shù)組的本質(zhì)其實就是數(shù)組的數(shù)組。如果你已經(jīng)對上一小節(jié)例程中的幾組值得含義都已經(jīng)完全搞清楚了,那么理解高維數(shù)組也不難了。
以下我們以三維數(shù)組為例進行介紹,開始套娃。
三維數(shù)組概念
我們可以這樣聲明一個三維數(shù)組:int C[3][2][2]。三維數(shù)組中的每個元素都是二維數(shù)組, 具體來說,它是由三個二維數(shù)組組成的,每個二維數(shù)組是由兩個一維數(shù)組組成的,每個一維數(shù)組含有兩個整型變量。圖示如下:
類似地,如果我們想將三維數(shù)組名C賦值給一個指針的話,應(yīng)該這樣聲明:int (*p)[2][2] = C。
小公式
同樣給出三維數(shù)組的小公式如下:
C[i][j][k] == *(C[i][j]+k) == *(*(C[i]+j)+k) == *(*(*(C+i)+j)+k)
這里筆者只簡單分析一下。首先,要明確,在本例中,一個整型變量占4個字節(jié),一個一維數(shù)組占2*4=8個字節(jié),一個二維數(shù)組占2*2*4=16個字節(jié),而整個三維數(shù)組占3*2*2*4=48個字節(jié)。
從右向左、從里向外看:
C是三維數(shù)組名,其值是三維數(shù)組中的第一個元素(即第一個二維數(shù)組)的起始地址,800,相當(dāng)于指向二維數(shù)組的指針常量,C+i是對指向二維數(shù)組的指針進行運算,因此應(yīng)該偏移+i*16個字節(jié),而對其進行解引用*(C+i),得到的就是起始地址為800+i*16處的那個二維數(shù)組,其名為C[i](相當(dāng)于B);
而二維數(shù)組名是一個指向一位數(shù)組的指針常量,然后C[i]+j是對指向一維數(shù)組的指針進行運算,偏移+j*8個字節(jié),而對其進行解引用*(C[i]+j),得到的是起始地址為800+i*16+j*8處的一維數(shù)組,其名為C[i][j](相當(dāng)于A);
而一維數(shù)組名是一個指向整型變量的指針常量,C[i][j]+k是對指向整型變量的指針進行運算,應(yīng)該偏移+k*4個字節(jié),而對其進行解引用*(C[i][j]+k),得到的是起始地址為800+i*16+j*8+k*4處的那個整型變量的值,即C[i][j][k]。
大家可以試著分析一下*(C[1]+1)和*(C[0][1]+1)分別是多少,這時作者給出的兩個小測試題,答案是824和9。
多位數(shù)組作為函數(shù)參數(shù)
一維數(shù)組作為參數(shù)需要注意是傳引用,另外在函數(shù)體內(nèi)不修改數(shù)據(jù)時,注意在函數(shù)簽名中將數(shù)組名指針聲明為常量指針。
二維數(shù)組做參數(shù):
注意事項
-
注意:多維數(shù)組做函數(shù)參數(shù)時,數(shù)組的第一個維度可以省略,但是其他維度必須指定。所以說,對一個需要接收二維數(shù)組的參數(shù),將函數(shù)簽名聲明為void func(int **A) 是不可行的,因為這樣沒有指定任何數(shù)組維度。
-
注意:在調(diào)用時要正確地傳遞參數(shù)數(shù)組的類型。比如下面這樣就是不可行的:
void func1(int Arr[][3]){} void func2(int Arr[][2][2]){}int main(){int A[2][2];int B[2][3];int C[3][2][2];int D[3][2][3];func1(A); // 錯誤func1(B); // 正確func2(C); // 正確func2(D); // 錯誤 }
指針與動態(tài)內(nèi)存
內(nèi)存四區(qū)簡介
內(nèi)存被分為四個區(qū),分別是代碼區(qū),靜態(tài)/全局區(qū),棧區(qū)和堆區(qū)。
- 代碼區(qū):存放指令。
- 靜態(tài)區(qū) / 全局區(qū):存放靜態(tài)或全局變量,也就是不再函數(shù)中聲明的變量,它們的生命周期貫穿整個應(yīng)用程序。
- 棧區(qū):用來存放函數(shù)調(diào)用的所有信息,和所有局部變量。
- 堆區(qū):大小不固定,可以由程序員自由地分配和釋放(動態(tài)內(nèi)存申請與釋放)。
在整個程序運行期間,代碼區(qū),靜態(tài)/全局區(qū),棧區(qū)的大小是不會增長的。
有一個小點要說明一下:有堆、棧這兩種數(shù)據(jù)結(jié)構(gòu),也有堆、棧這兩個內(nèi)存分區(qū),內(nèi)存中的棧基本是由數(shù)據(jù)結(jié)構(gòu)中的棧實現(xiàn)的,而內(nèi)存中的堆和數(shù)據(jù)結(jié)構(gòu)中的堆毫無關(guān)系。堆可以簡單理解為一塊大的、可供自由分配釋放的內(nèi)存空間。
之前我們已經(jīng)介紹過在程序運行過程中,代碼區(qū)、靜態(tài)/全局區(qū)和棧區(qū)是怎樣運作的了,特別是函數(shù)調(diào)用時棧區(qū)的工作方式,我們特別進行了說明。
C/C++中的動態(tài)內(nèi)存分配
- 在C中,我們需要使用四個函數(shù)進行動態(tài)內(nèi)存分配:malloc(),calloc(),realloc(),free()。
- 在C++中,我們需要使用兩個操作符:new,delete。另外,由于C++是C的超集,兼容C,故也可以用以上4個函數(shù)來進行動態(tài)內(nèi)存分配。
malloc 和 free
#include <stdio.h> #include <stdlib.h>int main(){int a;int* p;p = (int*)malloc(sizeof(int));*p = 10;free(p);return 0; }malloc函數(shù)從堆上找到一塊給定大小的空閑的內(nèi)存,并將指向起始地址的void *指針返回給程序,程序員應(yīng)當(dāng)根據(jù)需要做適當(dāng)?shù)闹羔様?shù)據(jù)類型的轉(zhuǎn)換。
向堆上寫值的唯一方法就是使用解引用,因為malloc返回的總是一個指針。如果malloc無法在堆區(qū)上找到足夠大小的空閑內(nèi)存,則會返回NULL。
程序員用malloc在堆上申請的內(nèi)存空間不會被程序自動釋放,因此程序員在堆上申請內(nèi)存后,一定要記得自己手動free釋放。
free接收一個指向堆區(qū)某地址的指針作為參數(shù),并將對應(yīng)的堆區(qū)的內(nèi)存空間釋放。
new 和 delete
在C++中,程序員們通常使用new,delete操作符來進行動態(tài)內(nèi)存的分配和釋放。以下是整型變量和整型數(shù)組的分配和釋放例程。
p = new int; *p = 10; delete p;p = new int[20] delete[] p;注意數(shù)組delete時要有[]。
在C++中,不需要做指針數(shù)據(jù)類型的轉(zhuǎn)換,new和delete是類型安全的。它們是帶類型的,返回特定類型的指針。
malloc、calloc、realloc、free
malloc
-
函數(shù)簽名:void* malloc(size_t size)。函數(shù)接收一個參數(shù)size,返回的void*指針指向了分配給我們的內(nèi)存塊中的第一個字節(jié)的地址。
-
void*類型的指針只能指明地址值,但是無法用于解引用,所以通常我們需要對返回的指針做強制類型轉(zhuǎn)換,轉(zhuǎn)換成我們需要的指針類型。
-
通常我們不顯式地給出參數(shù)size的值,而是通過sizeof,再乘上我們需要的元素個數(shù),計算出我們需要的內(nèi)存空間的大小。
-
典型用法:
int* p = (int*)malloc(3 * sizeof(int)); *p = 10; *(p+1) = 3; p[2] = 2; // 之前學(xué)過的數(shù)組的形式
calloc
-
函數(shù)簽名:void* calloc(size_t num, size_t size)。函數(shù)接收兩個參數(shù)num,size,分別表示特定類型的元素的數(shù)量,和類型的大小。同樣返回一個void*類型的指針。
-
典型用法:
int *p = (int*)calloc(3, sizeof(int)); -
calloc與malloc的另一個區(qū)別是:malloc分配完內(nèi)存后不會對其進行初始化,calloc分配完內(nèi)存后會將值初始化位0。
realloc
-
函數(shù)簽名:void* realloc(void* ptr, size_t size)。函數(shù)接收兩個參數(shù),第一個是指向已經(jīng)分配的內(nèi)存的起始地址的指針,第二個是要新分配的內(nèi)存大小。返回void*指針。可能擴展原來的內(nèi)存塊,也可能另找一塊大內(nèi)存拷貝過去,如果是縮小的話,就會是原地縮小。
-
如果縮小,或者拷貝到新的內(nèi)存地址,總之只要是由原來分配的內(nèi)存地址不會再被用到,realloc函數(shù)自己會將這些不被用到的地址釋放掉。
-
以下這種情況使用realloc相當(dāng)于free:
int* B = (int*)realloc(A, 0);以下這種情況使用realloc相當(dāng)于malloc:
int* B = (int*)realloc(NULL, sizeof(int));
free
在堆區(qū)動態(tài)分配的內(nèi)存會一直占據(jù)著內(nèi)存空間,如果程序員不將其顯式地釋放,程序是不會自動將其釋放的,直到整個程序結(jié)束。 已經(jīng)沒有用的堆區(qū)內(nèi)存如果不進行手動釋放會造成內(nèi)存泄漏,因此,使用上面三個函數(shù)在動態(tài)分配的堆區(qū)內(nèi)存的使命結(jié)束后,程序員有責(zé)任記得將它們釋放。
在C中,我們使用free函數(shù)來進行堆區(qū)內(nèi)存的釋放。只需將要釋放的內(nèi)存的其實地址傳入即可:free(p)。
使用場景
當(dāng)我們想要根據(jù)用戶的輸入來分配一個合適大小的數(shù)組,如果寫成如下這樣:
#include <stdio.h> #include <stdlib.h>int main(){int n;printf("Please Enter the Size of Array You Want:\n");scanf("%d", &n);int* A[n];return 0; }作者將這樣在運行時才知道數(shù)組的大小是不行的。但是筆者實驗過發(fā)現(xiàn)是可以的,這應(yīng)該是C99支持的特性變長數(shù)組。
但是這并不妨礙我們試著練習(xí)用動態(tài)分配內(nèi)存的方式來新建一個數(shù)組,我們可以這樣做:
int* A = (int*)malloc(n * sizeof(int));或者用calloc,會自動將初始值賦為0:
int* A = (int*)calloc(n, sizeof(int));別忘了手動釋放堆區(qū)內(nèi)存。
free(A);注意
在C程序中,只要我們知道某個內(nèi)存的地址,我們就能訪問它,C語言并不禁止我們的這種行為。但我們應(yīng)當(dāng)注意,不要去試圖讀寫未知的內(nèi)存,因為這將使我們的程序的行為不可預(yù)測,可能某個存在非法讀寫的程序在某個機器上運行正常,但是到了另一個環(huán)境、另一個機器上就會崩潰。最好的方法是:只去讀寫為我們分配的內(nèi)存,而不要試圖訪問未知的內(nèi)存。
內(nèi)存泄漏
動態(tài)分配內(nèi)存使用結(jié)束后不進行釋放的行為可能會造成內(nèi)存泄漏。乍看之下,好像不進行內(nèi)存釋放”只是“多占了一些內(nèi)存空間而已,為什么會被稱為內(nèi)存泄漏呢?而又為什么只有堆區(qū)的動態(tài)內(nèi)存未被釋放會造成內(nèi)存泄漏呢?本小節(jié)將介紹相關(guān)內(nèi)容。
#include <stdio.h> #include <stdlib.h>void allocate_stack(){int Arr[10000]; }void allocate_heap(){int* Arr = (int*)malloc(10000 * sizeof(int)); }int main(){int c;while (1) {printf("'0' to break, '1' to continue\n");scanf("%d", &c);if (!c) break;else {int i = 0;for (i=0; i<100; i++){allocate_heap();allocate_stack();}}}return 0; }我們有在主函數(shù)上調(diào)用allocate_stack或者allocate_heap,兩者的區(qū)別是一個在棧上開辟一個數(shù)組并直接返回,另一個在堆區(qū)開辟一個數(shù)組并且不釋放返回。在主函數(shù)中死循環(huán)詢問是否繼續(xù)開辟數(shù)組,得到繼續(xù)開辟數(shù)組的命令后開辟100個數(shù)組。我們可以通過top命令清晰地看到allocate_stack的內(nèi)存占用在每次開辟數(shù)組后驟增,然后掉下去,而allocate_heap的內(nèi)存占用每次驟增后也不掉下去,直到內(nèi)存占用過大被操作系統(tǒng)kill掉。
allocate_stack
對于調(diào)用allocate_stack的程序,在allocate_stack函數(shù)調(diào)用時,每次將數(shù)組創(chuàng)建在棧區(qū),然后再函數(shù)返回時,程序自動將其棧幀釋放,數(shù)組也被釋放掉,不會占用內(nèi)存。
allocate_heap
對于調(diào)用allocate_heap的程序:每次調(diào)用allocate_heap在堆區(qū)開辟一個數(shù)組Arr,在棧上只創(chuàng)建了一個指針p來指向這個堆區(qū)數(shù)組,但是堆區(qū)數(shù)組沒有釋放,這樣在allocate_heap函數(shù)返回之后,函數(shù)棧幀上的p也被程序釋放掉,就再也沒有辦法去釋放堆區(qū)的數(shù)組Arr。這樣隨著函數(shù)調(diào)用次數(shù)越來越多,這些堆區(qū)的數(shù)組都處于已分配但無法引用也無法使用的狀態(tài)。而堆區(qū)大小又是不固定的,可以一直向操作系統(tǒng)申請,終有一天,會超過內(nèi)存上限,被系統(tǒng)這就是內(nèi)存泄漏。
函數(shù)返回指針
指針本質(zhì)上也是一種數(shù)據(jù)類型(就像int、char),其中存儲了另外一個數(shù)據(jù)的地址,因此將一個指針作為返回類型是完全可行的。但是,需要考慮的是,在什么情況下,我們會需要函數(shù)返回一個指針類型呢?
示例程序
考慮這樣一個程序:
#include <stdio.h> #include <stdlib.h>int Add(int a, int b){int c = a + b;return c; }int main(){int x = 2, y = 4;int z = Add(x, y);printf("Sum = %d\n", z); }我們定義了一個加和函數(shù)Add,它從main函數(shù)中接收兩個參數(shù),并將二者求和的值返回給main。需要注意的是,就像我們之前提到的那樣,這里的x,y,z都是棧區(qū)main函數(shù)棧幀里的局部變量,而a, b 則都是棧區(qū)上Add函數(shù)棧幀中的局部變量。
并且這種函數(shù)傳參的方式我們之前已經(jīng)講過,成為值傳遞。
要將函數(shù)的傳參方式改為地址傳遞,只需改為以下程序:
#include <stdio.h> #include <stdlib.h>int Add(int* a, int* b){int c = (*a) + (*b);return c; }int main(){int x = 2, y = 4;int z = Add(&x, &y);printf("Sum = %d\n", z); }函數(shù)返回指針
上面關(guān)于傳值和傳引用的做法我們已經(jīng)在前面介紹過了,我們這一小節(jié)的重點是看看怎樣讓函數(shù)返回一個指針,我們的第一個版本可能會是這樣的:
#include <stdio.h> #include <stdlib.h>int* Add(int* a, int* b){int c = (*a) + (*b);return &c; }void printHello(){printf("Hello\n"); }int main(){int x = 2, y = 4;int* ptr = Add(&x, &y);// printHello();printf("Sum = %d\n", *ptr); }這個版本在作者測試時是正常的,但是如果在打印結(jié)果之前再多調(diào)用一個函數(shù)printHello,則會導(dǎo)致輸出錯誤。這究竟是怎么回事呢?我們還是要借助棧區(qū)內(nèi)存分析。
我們再次劃出這個程序運行時的內(nèi)存,這里沒有用到堆區(qū),就暫時先不畫出來了。
我們看到,在調(diào)用Add是,有Add自己的棧幀,其中存放兩個傳入的指向整型的指針a,b,指向main函數(shù)棧幀中的我們想要加和的兩個整型變量x,y,Add的棧幀中還有一個整型變量c,是我們的計算結(jié)果,按照上面的程序?qū)懛?#xff0c;Add函數(shù)返回一個整型指針指向變量c,即main中的ptr。
問題來了,在Add函數(shù)返回之后,它在棧區(qū)上的棧幀也被程序自動釋放了,這個時候,原來存放整型變量c的150這個內(nèi)存地址中的值就已經(jīng)是未知的了,我們之前說過,訪問未知的內(nèi)存是極其危險的。
如果在Add函數(shù)返回之后,沒有調(diào)用其他任何函數(shù),直接對150解引用,有可能能夠打印出正確的結(jié)果,如果編譯器沒有對釋放的棧幀進行其他處理。但是如果調(diào)用了其他函數(shù),如printHello,即使該函數(shù)中沒有任何我們看得到的參數(shù),但也需要保存一些返回地址、寄存器現(xiàn)場等參數(shù),因此也會在棧區(qū)占用一塊作為自己的棧幀。這時,內(nèi)存位置150幾乎是肯定被重寫了,這時無法得到預(yù)期的結(jié)果。無論如何,訪問未知的內(nèi)存地址是一定要杜絕的,不能寄希望于偶然的正確結(jié)果。
另外,需要指出的是,main函數(shù)也通過傳引用的形式將地址傳遞給了Add函數(shù),但這是沒問題的,因為Add函數(shù)調(diào)用時,main函數(shù)的棧幀還是在原來的內(nèi)存位置,這是已知的,我們可以進行訪問。即棧底向上傳參數(shù)是可以的,從棧底向上傳一個局部變量或者一個局部變量的地址是可以的。但是棧頂向下傳參數(shù)是不可以的,從棧頂向下傳一個局部變量或者一個局部變量的地址是不可以的。可想而知,C/C++中的main函數(shù)是可以自由地向被調(diào)函數(shù)傳引用的。
筆者自己在親測這個程序,編譯時會報警告:
pointer.c: In function ‘Add’: pointer.c:6:12: warning: function returns address of local variable [-Wreturn-local-addr]return &c;^~而運行時則會直接Core Dumped。應(yīng)該是新版本的編譯器直接禁止了這種返回已釋放的棧區(qū)指針的行為。
使用場景
可以見到,返回被調(diào)函數(shù)在棧區(qū)的局部變量的指針是危險的。通常,我們可以安全地返回堆區(qū)或者全局區(qū)的內(nèi)存指針,因為它們不會被程序自動地釋放。
我們嘗試在堆區(qū)分配內(nèi)存,將上面的程序中的Add函數(shù)改為:
int* Add(int* a, int* b){int* c = (int*)malloc(sizeof(int));*c = (*a) + (*b);return c; }這樣,程序就可以正常地工作了。
這樣,Add返回的指針?biāo)赶虻牡刂反娣旁诙褏^(qū),不像棧區(qū)一樣在函數(shù)返回之后被程序自動釋放,可以在main函數(shù)中正常地進行訪問。堆區(qū)內(nèi)存使用結(jié)束之后不要忘記釋放。
函數(shù)指針
代碼段
函數(shù)指針,就像名字所描述的那樣,是用來存儲函數(shù)的地址的。之前我們介紹的指針都是指向數(shù)據(jù)的,本章我們將討論指向函數(shù)的指針。我們可以使用函數(shù)指針來解引用和執(zhí)行函數(shù)。
我們已經(jīng)不止一次提過內(nèi)存四區(qū)或者內(nèi)存四區(qū)的某一部分了。但是有一部分我們在之前的講述中一直沒有提到過,那就是代碼區(qū)(Code)。我們知道,雖然我們編寫程序源代碼時使用的大多是C、C++等高級語言,但是機器要真正的運行程序,必須是運行二進制的機器代碼。從C到機器代碼的這個過程(包括預(yù)處理、編譯、匯編、鏈接)是由編譯器替我們完成的,得到的二進制代碼將被存放在可執(zhí)行文件中。
應(yīng)用程序的代碼段,就是用來存放從可執(zhí)行文件拷貝過來(到內(nèi)存代碼段)的機器碼或指令的。下面我們來仔細討論一下代碼區(qū)。
如上圖所示,假設(shè)一條指令占4個字節(jié),在內(nèi)存中,一個函數(shù)就是一塊連續(xù)的內(nèi)存(其中存放的不是數(shù)據(jù),而是指令)。指令通常都是順序執(zhí)行的,直到發(fā)生跳轉(zhuǎn)(如函數(shù)調(diào)用,函數(shù)返回),會根據(jù)指令調(diào)到指定的地址執(zhí)行。假設(shè)圖中藍色區(qū)域(指令02 - 指令 05,地址208 - 220)是一個函數(shù),指令00是一條跳轉(zhuǎn)指令,調(diào)用了藍色區(qū)域的函數(shù),程序就會從200跳轉(zhuǎn)到208執(zhí)行。函數(shù)的起始地址(比如208),被稱為函數(shù)的入口點,它是函數(shù)的第一條指令的地址。
函數(shù)指針的定義和使用
下面這個程序定義和使用了一個函數(shù)指針:
#include <stdio.h>int Add(int a, int b){return a + b; }int main(){int c;int (*p)(int, int);p = &Add;c = (*p)(2, 3);printf("%d\n", c); }聲明函數(shù)指針的語法是:int (*p)(int, int),這條語句聲明了一個接收兩個整型變量作為參數(shù),并且返回一個整型變量的函數(shù)指針。注意函數(shù)指針可以指向一類函數(shù),即可以說,指針p指向的類型是輸入兩整型,輸出一整型的這一類函數(shù),即所有滿足這個簽名的函數(shù),都可以賦值給p這個函數(shù)指針。
另外,要注意指針要加括號。否則int *p(int, int),是聲明一個函數(shù)名為p,接收兩個整型,并返回一個整型指針的函數(shù)。
函數(shù)指針賦值:p = &Add,將函數(shù)名為Add的函數(shù)指針賦值給p。同樣注意只要滿足p聲明時的函數(shù)簽名的函數(shù)名都可以賦值給p。
函數(shù)指針的使用:int c = (*p)(2, 3),先對p解引用得到函數(shù)Add,然后正常傳參和返回即可。
還有一點,在為函數(shù)指針賦值時,可以不用取地址符號&,僅用函數(shù)名同樣會返回正確的函數(shù)地址。與之匹配的,在調(diào)用函數(shù)的時候也不需要再解引用。這種用法更加常見。
int (*p)(int, int); p = Add; c = p(2, 3);再強調(diào)一下:注意函數(shù)指針可以指向一類函數(shù),即可以說,指針p指向的類型是輸入兩整型,輸出一整型的這一類函數(shù),即所有滿足這個簽名的函數(shù),都可以賦值給p這個函數(shù)指針。用不同的函數(shù)簽名來聲明的函數(shù)指針不能指向這個函數(shù)。
如以下這些函數(shù)指針的聲明都是不能指向Add函數(shù)的:
void (*p)(int, int); int (*p)(int); int (*p)(int, char);函數(shù)指針的使用案例(回調(diào)函數(shù))
回調(diào)函數(shù)的概念
這里使用函數(shù)指針的案例都圍繞著這么一個概念:函數(shù)指針可以用作函數(shù)參數(shù),而接收函數(shù)指針作為參數(shù)的這個函數(shù),可以回調(diào)函數(shù)指針?biāo)赶虻哪莻€函數(shù)。
#include <stdio.h>void A(){printf("Hello !\n"); }void B(void (*ptr)()){ptr(); }int main(){void (*p)() = A;B(p);B(A); return 0; }或者我們可以直接在主函數(shù)中B(A),而不需要上面那寫兩句先復(fù)制給p,再調(diào)用p。
在上面的例程中,將函數(shù)A()的函數(shù)指針傳給B(),B()在函數(shù)體內(nèi)直接通過傳入的函數(shù)指針調(diào)用函數(shù)A(),這個過程成為回調(diào)。這里函數(shù)指針被傳入另一個函數(shù),再被用函數(shù)指針進行回調(diào)的函數(shù)A()成為回調(diào)函數(shù)。
回調(diào)函數(shù)的實際使用場景
#include <stdio.h> #include <math.h>void BubbleSort(int A[], int size){int i, j, temp;for (i=0; i<size; i++){for (j=0; j<size-1; j++){if (A[j] > A[j+1]){temp = A[j];A[j] = A[j+1];A[j+1] = temp;}}} }int main(){int A[] = {2, -4, -1, 3, 9, -5, 7};int size = sizeof(A) / sizeof(A[0]);// BubbleSort(A, size, greater);BubbleSort(A, size, abs_greater);int i = 0;for (i=0; i<size; i++){printf("%d ", A[i]);}printf("\n"); }輸出排序結(jié)果:
-5 -4 -1 2 3 7 9對于這個排序函數(shù),我們可能有時需要升序排序有時需要降序排序,即我們可能會根據(jù)具體使用場景有不同的排序規(guī)則。而由于實現(xiàn)不同的排序函數(shù)時,整個算法的邏輯是不變的,只有排序的規(guī)則會不同,總不至于為了不同的排序規(guī)則都單獨寫一個函數(shù),這時我們就可以借助函數(shù)指針作為參數(shù)來實現(xiàn)不同的排序規(guī)則的切換。
即實現(xiàn)如下:
#include <stdio.h> #include <math.h>void BubbleSort(int A[], int size, int (*compare)(int, int)){int i, j, temp;for (i=0; i<size; i++){for (j=0; j<size-1; j++){if (compare(A[j], A[j+1]) > 0){temp = A[j];A[j] = A[j+1];A[j+1] = temp;}}} }int greater(int a, int b){if (a > b) return 1;else return -1; }int abs_greater(int a, int b){if (abs(a) > abs(b)) return 1;else return -1; }int main(){int A[] = {2, -4, -1, 3, 9, -5, 7};int size = sizeof(A) / sizeof(A[0]);BubbleSort(A, size);int i = 0;for (i=0; i<size; i++){printf("%d ", A[i]);}printf("\n"); }我們在排序函數(shù)中接收一個函數(shù)指針Compare作為參數(shù),用整個參數(shù)來指示排序的規(guī)則。這樣我們就利用回調(diào)函數(shù)實現(xiàn)了這一想法。我們可以寫不同的排序規(guī)則作為回調(diào)函數(shù),比如筆者這里又寫了一個按照絕對值比較大小的回調(diào)函數(shù)abs_greater,
輸出:
-1 2 3 -4 -5 7 9另外回調(diào)函數(shù)還有更多有趣的應(yīng)用,比如事件回調(diào)函數(shù)等。
Ref:
https://blog.csdn.net/helloyurenjie/article/details/79795059
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎勵來咯,堅持創(chuàng)作打卡瓜分現(xiàn)金大獎總結(jié)
以上是生活随笔為你收集整理的C/C++ 指针详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 科普 | 单精度、双精度、多精度和混合精
- 下一篇: s3c2440移植MQTT