在C/C++中有個(gè)叫指針的玩意存在感極其強(qiáng)烈,而說到指針又不得不提到內(nèi)存管理?,F(xiàn)在時(shí)不時(shí)能聽到一些朋友說指針很難,實(shí)際上說的是內(nèi)存操作和管理方面的難。(這篇筆記咱也會(huì)結(jié)合自己的理解簡(jiǎn)述一些相關(guān)的內(nèi)存知識(shí))
創(chuàng)新互聯(lián)公司專注于湖州網(wǎng)站建設(shè)服務(wù)及定制,我們擁有豐富的企業(yè)做網(wǎng)站經(jīng)驗(yàn)。 熱誠(chéng)為您提供湖州營(yíng)銷型網(wǎng)站建設(shè),湖州網(wǎng)站制作、湖州網(wǎng)頁(yè)設(shè)計(jì)、湖州網(wǎng)站官網(wǎng)定制、微信小程序開發(fā)服務(wù),打造湖州網(wǎng)絡(luò)公司原創(chuàng)品牌,更為您提供湖州網(wǎng)站排名全網(wǎng)營(yíng)銷落地服務(wù)。
最近在寫C程序使用指針的時(shí)候遇到了幾個(gè)讓我印象深刻的地方,這里記錄一下,以便今后回顧。
“經(jīng)一蹶者長(zhǎng)一智,今日之失,未必不為后日之得。” - 王陽(yáng)明《與薛尚謙書》
簡(jiǎn)述下指針的概念。
一個(gè)指針可以理解為一條內(nèi)存地址。
這里先定義了一個(gè)整型變量
test
,接著用取址運(yùn)算符&
取得這個(gè)變量的內(nèi)存地址并打印出來(lái)。
可以看到該變量的內(nèi)存地址是000000000061FE1C
指針變量就是存放指針(也就是存放內(nèi)存地址)的變量,使用數(shù)據(jù)類型* 變量名
進(jìn)行定義。
值得注意的是指針變量?jī)?nèi)儲(chǔ)存的指針(內(nèi)存地址)所代表的變量的數(shù)據(jù)類型,比如int*
定義的指針變量就只能指向int
類型的變量。
int test = 233;
int* ptr = &test;
test
變量的類型是整型int
,所以test
存放的就是一個(gè)整形數(shù)據(jù)。
而ptr
變量的類型是整型指針類型int*
,存放則的是整性變量test
的指針(內(nèi)存地址)。
二級(jí)指針指的是一級(jí)指針變量的地址。
int main() {
int test = 233;
printf("%p\n", &test);
int *ptr = &test;
printf("%p", &ptr);
return 0;
}
/* stdout
000000000061FE1C
000000000061FE10
*/
這個(gè)例子中二級(jí)指針就是
ptr
變量的地址000000000061FE10
。
二級(jí)指針變量就是存放二級(jí)指針(二級(jí)指針的地址)的變量,使用數(shù)據(jù)類型** 變量名
進(jìn)行定義。
int main() {
int test = 233;
int *ptr = &test;
int **ptr2 = &ptr;
return 0;
}
ptr
變量的類型是整型指針類型int*
,存放的是整性(int
)變量test
的指針(內(nèi)存地址),
ptr2
變量的類型是二級(jí)整型指針類型int**
,存放的是整性指針(int*
)變量ptr
的內(nèi)存地址。
雖然二級(jí)以上的指針變量相對(duì)來(lái)說不太常用,但我覺得基本的辨別方法還是得會(huì)的:
通過觀察發(fā)現(xiàn),指針變量的數(shù)據(jù)類型定義其實(shí)就是在其所指向的數(shù)據(jù)類型名后加一個(gè)星號(hào),
比如說:
指針ptr
指向整型變量int test
,那么它的定義寫法就是int* ptr
。(數(shù)據(jù)類型在int
后加了一個(gè)星號(hào))
指針ptr2
指向一級(jí)指針變量int* ptr
,那么它的定義寫法就是int** ptr2
。(數(shù)據(jù)類型在int*
后加了一個(gè)星號(hào))
再三級(jí)指針變量int*** ptr3
,乍一看星號(hào)這么多,實(shí)際上“剝”一層下來(lái)就真相大白了:
(int**)*
實(shí)際上三級(jí)指針變量指向的就是二級(jí)指針變量的地址。
其他更多級(jí)的指針變量可以依此類推。
指針和內(nèi)存操作關(guān)系緊密,提到指針總是令人情不自禁地想起內(nèi)存。
程序運(yùn)行時(shí)占用的內(nèi)存空間會(huì)被劃分為幾個(gè)區(qū)域,其中和這篇筆記息息相關(guān)的便是棧區(qū)(Stack)和堆區(qū)(Heap)。
棧區(qū)的操作方式正如數(shù)據(jù)結(jié)構(gòu)中的棧,是LIFO后進(jìn)先出的。這種操作模式的一個(gè)很經(jīng)典的應(yīng)用就是遞歸函數(shù)了。
每個(gè)函數(shù)被調(diào)用時(shí)需要從棧區(qū)劃分出一塊棧內(nèi)存用來(lái)存放調(diào)用相關(guān)的信息,這塊棧內(nèi)存被稱為函數(shù)的棧幀。
棧幀存放的內(nèi)容主要是(按入棧次序由先至后):
返回地址,也就是函數(shù)被調(diào)用處的下一條指令的內(nèi)存地址(內(nèi)存中專門有代碼區(qū)用于存放),用于函數(shù)調(diào)用結(jié)束返回時(shí)能接著原來(lái)的位置執(zhí)行下去。
函數(shù)調(diào)用時(shí)的參數(shù)值。
函數(shù)調(diào)用過程中定義的局部變量的值。
and so on...
由LIFO后進(jìn)先出可知一次函數(shù)調(diào)用完畢后相較而言局部變量先出棧,接著是參數(shù)值,最后棧頂指針指向返回地址,函數(shù)返回,接著下一條指令執(zhí)行下去。
棧區(qū)的特性:
交由系統(tǒng)(C語(yǔ)言這兒就是編譯器參與實(shí)現(xiàn))自動(dòng)分配和釋放,這點(diǎn)在函數(shù)調(diào)用中體現(xiàn)的很明顯。
分配速度較快,但并不受程序員控制。
相對(duì)來(lái)說空間較小,如果申請(qǐng)的空間大于棧剩余的內(nèi)存空間,會(huì)引發(fā)棧溢出問題。(棧內(nèi)存大小限制因操作系統(tǒng)而異)
比如遞歸函數(shù)控制不當(dāng)就會(huì)導(dǎo)致棧溢出問題,因?yàn)槊繉雍瘮?shù)調(diào)用都會(huì)形成新的棧幀“壓到”棧上,如果遞歸函數(shù)層數(shù)過高,棧幀遲遲得不到“彈出”,就很容易擠爆棧內(nèi)存。
棧內(nèi)存占用大小隨著函數(shù)調(diào)用層級(jí)升高而增大,隨著函數(shù)調(diào)用結(jié)束逐層返回而減小;也隨著局部變量的定義而增大,隨著局部變量的銷毀而減小。
棧內(nèi)存中儲(chǔ)存的數(shù)據(jù)的生命周期很清晰明確。
棧區(qū)是一片連續(xù)的內(nèi)存區(qū)域。
堆內(nèi)存就真的是“一堆”內(nèi)存,值得一提的是,這里的堆和數(shù)據(jù)結(jié)構(gòu)中的堆沒有關(guān)系。
相對(duì)棧區(qū)來(lái)說,堆區(qū)可以說是一個(gè)更加靈活的大內(nèi)存區(qū),支持按需進(jìn)行動(dòng)態(tài)分配。
堆區(qū)的特性:
交由程序員或者垃圾回收機(jī)制進(jìn)行管理,如果不加以回收,在整個(gè)程序沒有運(yùn)行完前,分配的堆內(nèi)存會(huì)一直存在。(這也是容易造成內(nèi)存泄漏的地方)
在C/C++中,堆內(nèi)存需要程序員手動(dòng)申請(qǐng)分配和回收。
分配速度較慢,系統(tǒng)需要依照算法搜索(鏈表)足夠的內(nèi)存區(qū)域以分配。
堆區(qū)空間比較大,只要還有可用的物理內(nèi)存就可以持續(xù)申請(qǐng)。
堆區(qū)是不連續(xù)(離散)的內(nèi)存區(qū)域。(大概是依賴鏈表來(lái)進(jìn)行分配操作的)
現(xiàn)代操作系統(tǒng)中,在程序運(yùn)行完后會(huì)回收掉所有的堆內(nèi)存。
要養(yǎng)成不用就釋放的習(xí)慣,不然運(yùn)行過程中進(jìn)程占用內(nèi)存可能越來(lái)越大。
這里咱就直接報(bào)菜名吧!
這一部分的函數(shù)的原型都定義在頭文件stdlib.h
中。
void* malloc(size_t size)
用于請(qǐng)求系統(tǒng)從堆區(qū)中分配一段連續(xù)的內(nèi)存塊。
void* calloc(size_t n, size_t size);
在和malloc
一樣申請(qǐng)到連續(xù)的內(nèi)存塊后,將所有分配的內(nèi)存全部初始化為0。
void* realloc(void* block, size_t size)
修改已經(jīng)分配的內(nèi)存塊的大小(具體實(shí)現(xiàn)是重新分配),可以放大也可以縮小。
malloc
可以記成Memory Allocate 分配內(nèi)存
;
calloc
可以記成Clear and Allocate 分配并設(shè)置內(nèi)存為0
;
realloc
可以記成Re-Allocate 重分配內(nèi)存
。
簡(jiǎn)單來(lái)說原理大概是這樣:
malloc
內(nèi)存分配依賴的數(shù)據(jù)結(jié)構(gòu)是鏈表。簡(jiǎn)單說來(lái)就是所有空閑的內(nèi)存塊會(huì)被組織成一個(gè)空閑內(nèi)存塊鏈表。
當(dāng)要使用malloc
分配內(nèi)存時(shí),它首先會(huì)依據(jù)算法掃描這個(gè)鏈表,直到找到一個(gè)大小滿足需求的空閑內(nèi)存塊為止,然后將這個(gè)空閑內(nèi)存塊傳遞給用戶(通過指針)。
(如果這塊的大小大于用戶所請(qǐng)求的內(nèi)存大小,則將多余部分“切出來(lái)”接回鏈表中)。
在不斷的分配與釋放過程中,由于內(nèi)存塊的“切割”,大塊的內(nèi)存可能逐漸被切成許多小塊內(nèi)存存在鏈表中,這些便是內(nèi)存碎片。當(dāng)malloc
找不到合適大小的內(nèi)存塊時(shí)便會(huì)嘗試合并這些內(nèi)存碎片以獲得大塊空閑的內(nèi)存。
實(shí)在找不到空閑內(nèi)存塊的情況下,malloc
會(huì)返回NULL
指針。
釋放手動(dòng)分配的堆內(nèi)存需要用到free
函數(shù):
void free(void* block)
只需要傳入指向分配內(nèi)存始址的指針變量作為實(shí)參傳入即可。
在
C/C++
中,對(duì)于手動(dòng)申請(qǐng)分配的堆內(nèi)存在使用完后一定要及時(shí)釋放,
不然在運(yùn)行過程中進(jìn)程占用內(nèi)存可能會(huì)越來(lái)越大,也就是所謂的內(nèi)存泄漏。
不過在現(xiàn)代操作系統(tǒng)中,程序運(yùn)行完畢后OS會(huì)自動(dòng)回收對(duì)應(yīng)進(jìn)程的內(nèi)存,包括泄露的內(nèi)存。內(nèi)存泄露指的是在程序運(yùn)行過程中無(wú)法操作的內(nèi)存。
free
為什么知道申請(qǐng)的內(nèi)存塊大?。?/p>
簡(jiǎn)單來(lái)說,就是在malloc
進(jìn)行內(nèi)存分配時(shí)會(huì)把內(nèi)存大小分配地略大一點(diǎn),多余的內(nèi)存部分用于儲(chǔ)存一些頭部數(shù)據(jù)(這塊內(nèi)存塊的信息),這塊頭部數(shù)據(jù)內(nèi)就包括分配的內(nèi)存的長(zhǎng)度。
但是在返回指針的時(shí)候,malloc
會(huì)將其往后移動(dòng),使得指針代表的是用戶請(qǐng)求的內(nèi)存塊的起始地址。
頭部數(shù)據(jù)占用的大小通常是固定的(網(wǎng)上查了一下有一種說法是16
字節(jié),也有說是sizeof(size_t)
的),在將指針傳入free
后,free
會(huì)將指針向前移動(dòng)指定長(zhǎng)度以獲得頭部數(shù)據(jù),讀取到分配的內(nèi)存長(zhǎng)度,然后連同頭部數(shù)據(jù)和所分配長(zhǎng)度的內(nèi)存一并釋放掉。
內(nèi)存釋放可以理解為這塊內(nèi)存被重新接到了空閑鏈表上,以備后面的分配。
(實(shí)際上內(nèi)存釋放后的情況其實(shí)挺復(fù)雜的,得要看具體的算法實(shí)現(xiàn)和運(yùn)行環(huán)境)
C語(yǔ)言中二維數(shù)組的定義:
數(shù)據(jù)類型 數(shù)組名[行數(shù)][列數(shù)];
初始化則可以使用大括號(hào):
int a[3][4]={
{1,2,3,4},
{5,6,7,8},
{9,10,11,12}
};
int b[3][4]={ // 內(nèi)層不要大括號(hào)也是可以的,具體為什么后面再說
1,2,3,4,
5,6,7,8,
9,10,11,12
};
char str[2][6]={
"Hello",
"World"
};
此外,在有初始化值的情況下,定義二維數(shù)組時(shí)的一維長(zhǎng)度(行數(shù))是可以省略的:
int a[][4]={ // 如果沒有初始化,則一維長(zhǎng)度不可省略
1,2,3,4,
5,6,7,8,
9,10,11,12
}
按上述語(yǔ)句定義的數(shù)組,在進(jìn)程內(nèi)存中一般儲(chǔ)存于:
棧區(qū) - 在函數(shù)內(nèi)部定義的局部數(shù)組變量。
靜態(tài)儲(chǔ)存區(qū) - 當(dāng)用static
修飾數(shù)組變量或者在全局作用域中定義數(shù)組。
數(shù)組在內(nèi)存中是連續(xù)且呈線性儲(chǔ)存的,二維數(shù)組也是不例外的。
雖然在使用過程中二維數(shù)組發(fā)揮的是“二維”的功能,但其在內(nèi)存中是被映射為一維線性結(jié)構(gòu)進(jìn)行儲(chǔ)存的。
實(shí)踐驗(yàn)證一下:
int i, j;
int a[][4] = { // 如果沒有初始化,則一維長(zhǎng)度不可省略
1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12
};
size_t len1 = sizeof(a) / sizeof(a[0]);
size_t len2 = sizeof(a[0]) / sizeof(a[0][0]);
for (i = 0; i < len1; i++) {
for (j = 0; j < len2; j++)
printf(" [%d]%p ", a[i][j], &a[i][j]);
printf("\n");
}
輸出:
第一維有3行,第二維有4列。
一個(gè)int
類型數(shù)據(jù)占用4
個(gè)字節(jié),從上面的圖可以看出來(lái):
[1]000000000061FDD0
-> [2]000000000061FDD4
相隔4字節(jié),說明這兩個(gè)數(shù)組元素相鄰,同一行中數(shù)組元素儲(chǔ)存連續(xù)。
[4]000000000061FDDC
-> [5]000000000061FDE0
同樣相隔4字節(jié),這兩個(gè)數(shù)組元素在內(nèi)存中也是相鄰的。
從[1]000000000061FDD0
到[12]000000000061FDFC
正好相差44
個(gè)字節(jié),整個(gè)二維數(shù)組元素在內(nèi)存中是連續(xù)儲(chǔ)存的。
這樣一看,為什么定義并初始化的時(shí)候二維數(shù)組的第一維可以省略已經(jīng)不言而喻了:
在初始化的時(shí)候編譯器通過數(shù)組第二維的大小對(duì)元素進(jìn)行“分組”,每一組可以看作是一個(gè)一維數(shù)組,這些一維數(shù)組在內(nèi)存中從低地址到高地址連續(xù)排列儲(chǔ)存形成二維數(shù)組:
在上面例子中大括號(hào)中的元素
{1,2,3,4,5,6,7,8,9,10,11,12}
被按第二維長(zhǎng)度4
劃分成了{1,2,3,4}
,{5,6,7,8}
,{9,10,11,12}
三組,這樣程序也能知道第一維數(shù)組長(zhǎng)度為3
了。
一維數(shù)組名代表的是數(shù)組的起始地址(也是第一個(gè)元素的地址)。
二維數(shù)組在內(nèi)存中也是映射為一維進(jìn)行連續(xù)儲(chǔ)存的,
既然如此,二維數(shù)組名代表的地址其實(shí)也是整個(gè)二維數(shù)組的起始地址,在上面的例子中相當(dāng)于a[0][0]
的地址。
在上面的示例最后加一行:
printf("Arr address: %p", a);
打印出來(lái)的地址和a[0][0]
的地址完全一致,是000000000061FDD0
。
首先要明確一點(diǎn):二維數(shù)組 ≠ 二級(jí)指針
剛接觸C語(yǔ)言時(shí)我總是想當(dāng)然地把這兩個(gè)搞混了,實(shí)際上根本不是一回事兒。
二級(jí)指針變量儲(chǔ)存的是一級(jí)指針變量的地址。
二維數(shù)組是內(nèi)存中連續(xù)儲(chǔ)存的一組數(shù)據(jù),二維數(shù)組名相當(dāng)于一個(gè)一級(jí)指針(二維數(shù)組的起始地址)。
int arr[][4]={
{1,2},{1},{3},{4,5}
};
int** ptr=arr; // 這樣寫肯定是不行的!,ptr儲(chǔ)存的是一級(jí)指針變量的地址
int* ptr=arr; // 這樣寫是可以的,但是不建議
int* ptr=&arr[0][0]; // 這樣非常ok, ptr儲(chǔ)存的是數(shù)組起始地址(也就是首個(gè)變量的地址)
可以把之前二維數(shù)組的例子改一下:
int i;
int a[][4] = { // 如果沒有初始化,則一維長(zhǎng)度不可省略
1, 2, 3, 4,
5, 6, 7, 8,
9, 10, 11, 12
};
size_t len1 = sizeof(a) / sizeof(a[0]);
size_t len2 = sizeof(a[0]) / sizeof(a[0][0]);
size_t totalLen = len1 * len2; // 整個(gè)二維數(shù)組的長(zhǎng)度
int *ptr = &a[0][0]; // ptr指向二維數(shù)組首地址
for (i = 0; i < totalLen; i++) {
// 一維指針操作就是基于一維的,所以整個(gè)二維數(shù)組此時(shí)會(huì)被當(dāng)作一條連續(xù)的內(nèi)存
printf(" [%d]%p ", ptr[i], &ptr[i]);
// printf(" [%d]%p ", *(ptr + i), ptr + i);
if (i % len2 == 3) // 換行
printf("\n");
}
printf("Arr address: %p", ptr);
輸出結(jié)果和之前遍歷二維數(shù)組的是一模一樣的。
既然二級(jí)指針變量不能直接指向二維數(shù)組,那能不能依賴二級(jí)指針來(lái)實(shí)現(xiàn)一個(gè)類似的結(jié)構(gòu)呢?當(dāng)然是可以的啦!
整型變量存放著整型int
數(shù)據(jù),整型數(shù)組int a[]
中存放了整型數(shù)據(jù);
如果是用申請(qǐng)堆內(nèi)存來(lái)實(shí)現(xiàn)的整型數(shù)組:
int* arr = (int*)malloc(sizeof(int) * 3);
指針int*
變量arr
此時(shí)指向的是連續(xù)存放整型(int
)數(shù)據(jù)的內(nèi)存的起始地址,相當(dāng)于一個(gè)一維數(shù)組的起始地址。
二級(jí)指針int**
變量存放著一級(jí)指針變量的地址,那么就可以構(gòu)建二級(jí)指針數(shù)組來(lái)存放二級(jí)指針數(shù)據(jù)(也就是每個(gè)元素都是一級(jí)指針變量的地址)。
具體代碼實(shí)現(xiàn):
int rows = 3; // 行數(shù)/一維長(zhǎng)度
int cols = 4; // 列數(shù)/二維長(zhǎng)度
int **ptr = (int **) malloc(rows * sizeof(int *));
// 分配一段連續(xù)的內(nèi)存,儲(chǔ)存int*類型的數(shù)據(jù)
int i, j, num = 1;
for (i = 0; i < rows; i++) {
ptr[i] = (int *) malloc(cols * sizeof(int));
// 再分配一段連續(xù)的內(nèi)存,儲(chǔ)存int類型的數(shù)據(jù)
for (j = 0; j < cols; j++)
ptr[i][j] = num++; // 儲(chǔ)存一個(gè)整型數(shù)據(jù)1-12
}
其中
ptr[i] = (int *) malloc(cols * sizeof(int));
這一行,等同于
*(ptr+i) = ...
也就是利用間接訪問符*
讓一級(jí)指針變量指向在堆內(nèi)存中分配的一段連續(xù)整形數(shù)據(jù),這里相當(dāng)于初始化了第二維。
而在給整型元素賦值時(shí)和二維數(shù)組一樣用了中括號(hào)進(jìn)行訪問:
ptr[i][j] = i * j;
其實(shí)就等同于:
*(*(ptr+i)+j) = i * j;
第一次訪問第一維元素,用第一維起始地址ptr
加上第一維下標(biāo)i
,取出對(duì)應(yīng)的一級(jí)指針變量中存放的地址:*(ptr+i)
這個(gè)地址是第二維中一段連續(xù)內(nèi)存的起始地址。
第二次訪問第二維元素,用1中取到的地址*(ptr+i)
加上第二維下標(biāo)j
,再用間接訪問符*
訪問對(duì)應(yīng)的元素,并賦值。
指針數(shù)組在內(nèi)存中的存放不同于普通定義的二維數(shù)組,它的每一個(gè)維度是連續(xù)儲(chǔ)存的,但是維度和維度之間在內(nèi)存中的存放是離散的。
用一個(gè)循環(huán)打印一下每個(gè)元素的地址:
for (i = 0; i < rows; i++) {
for (j = 0; j < cols; j++)
printf(" [%d]%p ", ptr[i][j], *(ptr + i) + j);
printf("\n");
}
輸出:
可以看到第二維度的地址是連續(xù)的,但是第二維度“數(shù)組”之間并不是連續(xù)的。比如元素4
和元素5
的地址相差了20
個(gè)字節(jié),并不是四個(gè)字節(jié)。
其在內(nèi)存中的存放結(jié)構(gòu)大致如上,并無(wú)法保證*(ptr+0)+3
和*(ptr+1)
的地址相鄰,也無(wú)法保證*(ptr+1)+3
和*(ptr+2)
的地址相鄰。
這種非連續(xù)的存放方式可以說是和二維數(shù)組相比很大的一個(gè)不同點(diǎn)了。
通常指針數(shù)組實(shí)現(xiàn)的“二維數(shù)組”是在堆內(nèi)存中進(jìn)行存放的,既然申請(qǐng)了堆內(nèi)存,咱也應(yīng)該養(yǎng)成好習(xí)慣,使用完畢后將其釋放掉:
for (i = 0; i < rows; i++)
free(ptr[i]);
free(ptr);
先利用一個(gè)循環(huán)釋放掉每一個(gè)一級(jí)指針變量指向的連續(xù)內(nèi)存塊(儲(chǔ)存整型數(shù)據(jù)),最后再把二級(jí)指針變量指向的連續(xù)內(nèi)存塊(儲(chǔ)存的是一級(jí)指針變量的地址)釋放掉。
sizeof()
是C語(yǔ)言中非常常用的一個(gè)運(yùn)算符,而二級(jí)指針和二維數(shù)組的區(qū)別在這里也可以很好地展現(xiàn)出來(lái)。
對(duì)于非變量長(zhǎng)度定義的數(shù)組,sizeof
在編譯階段就會(huì)完成求值運(yùn)算,被替換為對(duì)應(yīng)數(shù)據(jù)的大小的常量值。
int arr[n];
這種定義時(shí)數(shù)組長(zhǎng)度為變量的即為變量長(zhǎng)度數(shù)組(C99標(biāo)準(zhǔn)開始支持),不過還是不太推薦這種寫法。
直接固定長(zhǎng)度定義二維數(shù)組時(shí),編譯器是知道這個(gè)變量是數(shù)組的,比如:
int arr[3][4];
size_t arrSize = sizeof(arr);
在編譯階段,編譯器知道數(shù)組arr
是一個(gè)整型int
二維數(shù)組:
每個(gè)第二維數(shù)組包含四個(gè)int
數(shù)據(jù),長(zhǎng)度為sizeof(int)*4=16
個(gè)字節(jié)。
第一維數(shù)組包含三個(gè)第二維數(shù)組,每個(gè)第二維數(shù)組長(zhǎng)度為16
字節(jié),整個(gè)二維數(shù)組總長(zhǎng)度為16*3=48
個(gè)字節(jié)。
即sizeof(arr) = 48
。
指針變量?jī)?chǔ)存的是指針,也就是一個(gè)地址。內(nèi)存地址在運(yùn)算的時(shí)候會(huì)存放在CPU的整數(shù)寄存器中。
64位計(jì)算機(jī)中整數(shù)寄存器寬度有64
bit(位),而指針數(shù)據(jù)要能存放在這里。
目前來(lái)說 1
字節(jié)(Byte) = 8
位(bit),那么64
位就是8
個(gè)字節(jié),
所以64位系統(tǒng)中指針變量的長(zhǎng)度是8
字節(jié)。
int rows = 3; // 行數(shù)/一維長(zhǎng)度
int **ptr = (int **) malloc(rows * sizeof(int *));
size_t ptrSize = sizeof(ptr); // 8 Bytes
size_t ptrSize2 = sizeof(int **); // 8 Bytes
size_t ptrSize3 = sizeof(int *); // 8 Bytes
size_t ptrSize4 = sizeof(char *); // 8 Bytes
雖然上面咱通過申請(qǐng)分配堆內(nèi)存實(shí)現(xiàn)了二維數(shù)組(用二級(jí)指針變量ptr
指向了指針數(shù)組起址),
但其實(shí)在編譯器眼中,ptr
就單純是一個(gè)二級(jí)指針變量,占用字節(jié)數(shù)為8 Bytes
(64位),儲(chǔ)存著一個(gè)地址,因此在這里是無(wú)法通過sizeof獲得這塊連續(xù)內(nèi)存的長(zhǎng)度的。
通過上面的例子很容易能觀察出來(lái):
sizeof(指針變量) = 8 Bytes
(64位計(jì)算機(jī))
無(wú)論指針變量指向的是什么數(shù)據(jù)的地址,它儲(chǔ)存的單純只是一個(gè)內(nèi)存地址,所以所有指針變量的占用字節(jié)數(shù)是一樣的。
得先明確一點(diǎn):C語(yǔ)言中不存在所謂的數(shù)組參數(shù),通常讓函數(shù)接受一個(gè)數(shù)組的數(shù)據(jù)需要通過指針變量參數(shù)傳遞。
int test(int newArr[2]) {
printf(" %d ", sizeof(newArr)); // 8
return 0;
}
int main() {
int arr[5] = {1, 2, 3, 4, 5};
test(arr);
return 0;
}
在上面這個(gè)例子中test
函數(shù)的定義中聲明了“看上去像數(shù)組的”形參newArr
,然而sizeof
的運(yùn)算結(jié)果是8
。
實(shí)際上這里的形參聲明是等同于int* newArr
的,因?yàn)榘褦?shù)組作為參數(shù)進(jìn)行傳遞的時(shí)候,實(shí)際上傳遞的是數(shù)組的首地址(因?yàn)閿?shù)組名就代表數(shù)組的首地址)。
這種情況下就發(fā)生了數(shù)組到指針的退化。
在編譯器的眼中,newArr
此時(shí)就被當(dāng)作了一個(gè)指針變量,指向arr
數(shù)組的首地址,因此聲明中數(shù)組的長(zhǎng)度怎么寫都行:int newArr[5]
,int newArr[]
都可以。
為了讓代碼更加清晰,我覺得最好還是聲明為int* newArr
,這樣一目了然能知道這是一個(gè)指針變量!
當(dāng)函數(shù)內(nèi)運(yùn)算涉及到數(shù)組長(zhǎng)度時(shí),就需要在函數(shù)定義的時(shí)候另聲明一個(gè)形參來(lái)接受數(shù)組長(zhǎng)度:
int test(int *arr, size_t rowLen, size_t colLen) {
int i;
size_t totalLen = rowLen * colLen;
for (i = 0; i < totalLen; i++) {
printf(" %d ", arr[i]);
if (i % colLen == colLen - 1) // 每個(gè)第二維數(shù)組元素打印完后換行
printf("\n");
}
return 0;
}
int main() {
int arr[3][3] = {
1, 2, 3,
4, 5, 6,
7, 8, 9
};
test(arr, sizeof(arr) / sizeof(arr[0]), sizeof(arr[0]) / sizeof(arr[0][0]));
return 0;
}
輸出:
這個(gè)例子中test
函數(shù)就多接受了二維數(shù)組的一維長(zhǎng)度rowLen
和二維長(zhǎng)度colLen
,以對(duì)二維數(shù)組元素進(jìn)行遍歷打印。
經(jīng)常有應(yīng)用場(chǎng)景需要函數(shù)返回一個(gè)“數(shù)組”,說是數(shù)組,實(shí)際上函數(shù)并無(wú)法返回一個(gè)局部定義的數(shù)組,哪怕是其指針(在下面一節(jié)有寫為什么)。
取而代之地,常常會(huì)返回一個(gè)指針指向分配好的一塊連續(xù)的堆內(nèi)存。
(在算法題中就經(jīng)常能遇到要求返回指針的情況)
int *test(size_t len) {
int i;
int *arr = (int *) malloc(len * sizeof(int));
for (i = 0; i < len; i++)
arr[i] = i + 1;
return arr;
}
int main() {
int i = 0;
int *allocated = test(5);
for (; i < 5; i++)
printf(" %d ", allocated[i]);
free(allocated); // 一定要記得釋放!
return 0;
}
這個(gè)示例中,test
函數(shù)的返回類型是整型指針。當(dāng)調(diào)用了test
函數(shù),傳入要分配的連續(xù)內(nèi)存長(zhǎng)度后,其在函數(shù)內(nèi)部定義了一個(gè)局部指針變量,指向分配好的內(nèi)存,在內(nèi)存中存放數(shù)據(jù)后將該指針返回。
在主函數(shù)中,test
返回的整型指針被賦給了指針變量allocated
,所以接下來(lái)可以通過一個(gè)循環(huán)打印出這塊連續(xù)內(nèi)存中的數(shù)據(jù)。
再次提醒,申請(qǐng)堆內(nèi)存并使用完后,一定要記得使用free
進(jìn)行釋放!
記得初學(xué)C語(yǔ)言的時(shí)候,我曾經(jīng)犯過一個(gè)錯(cuò)誤:將函數(shù)內(nèi)定義的數(shù)組的數(shù)組名作為返回值:
int *test() {
int arr[4] = {1, 2, 3, 4};
return arr;
}
int main() {
int i = 0;
int *allocated = test();
for (; i < 4; i++)
printf(" %d ", *(allocated + i));
return 0;
}
這個(gè)例子中直到for循環(huán)前進(jìn)程仍然正常運(yùn)行,但是一旦嘗試使用*
運(yùn)算符取出內(nèi)存中的數(shù)據(jù)*(allocated + i)
,進(jìn)程立馬接收到了系統(tǒng)發(fā)來(lái)的異常信號(hào)SIGSEGV
,進(jìn)而終止執(zhí)行。
SIGSEGV
是比較常見的一種異常信號(hào),代表Signal Segmentation Violation
,也就是內(nèi)存分段沖突
造成異常的原因通常是進(jìn)程 試圖訪問一段沒有分配給它的內(nèi)存,“野指針”總是伴隨著這個(gè)異常出現(xiàn)。
上面簡(jiǎn)述棧區(qū)的時(shí)候提到了棧幀,每次調(diào)用函數(shù)時(shí)會(huì)在棧上給函數(shù)分配一個(gè)棧幀用來(lái)儲(chǔ)存函數(shù)調(diào)用相關(guān)信息。
函數(shù)調(diào)用完成后,先把運(yùn)算出來(lái)的返回值存入寄存器中,接著會(huì)在棧幀上進(jìn)行彈棧操作,在這個(gè)過程中分配的局部變量就會(huì)被回收。
最后,程序在棧頂中取到函數(shù)的返回地址,返回上層函數(shù)繼續(xù)執(zhí)行余下的指令。棧幀銷毀,此時(shí)局部變量相關(guān)的棧內(nèi)存已經(jīng)被回收了。
然而此時(shí)寄存器中仍存著函數(shù)的返回值,是一個(gè)內(nèi)存地址,但是內(nèi)存地址代表的內(nèi)存部分已經(jīng)被回收了。
當(dāng)將返回值賦給一個(gè)指針變量時(shí),野指針就產(chǎn)生了——此時(shí)這個(gè)指針變量指向一片未知的內(nèi)存。
所以當(dāng)進(jìn)程試圖訪問這一片不確定的內(nèi)存時(shí),就容易引用到無(wú)效的內(nèi)存,此時(shí)系統(tǒng)就會(huì)發(fā)送SIGSEGV
信號(hào)讓進(jìn)程終止執(zhí)行。
教訓(xùn)總結(jié)成一句話就是:
延伸:返回靜態(tài)局部變量是可以的,因?yàn)殪o態(tài)局部變量是儲(chǔ)存在靜態(tài)儲(chǔ)存區(qū)的。
int *test() {
static int arr[4] = {1, 2, 3, 4};
return arr;
}
???? 如果之前例子中的test
函數(shù)內(nèi)這個(gè)局部數(shù)組變量聲明為局部的靜態(tài)變量,程序就可以正常執(zhí)行了。
用一個(gè)擁有指針變量的結(jié)構(gòu)體作為實(shí)參傳入函數(shù):
struct Hello {
int num;
int *ptr;
};
int test(struct Hello testStruct) {
printf(" [test]testStruct-Ptr: %p \n", ++testStruct.ptr);
*testStruct.ptr = 2;
return 1;
}
int main() {
int *testPtr = (int *) calloc(4, sizeof(int));
struct Hello testStruct = {
.num=5,
.ptr=testPtr
};
printf(" [main]testStruct-Ptr: %p \n\tptr[1]=%d\n", testStruct.ptr, testStruct.ptr[1]);
test(testStruct);
printf(" [main]testStruct-Ptr: %p \n\tptr[1]=%d\n", testStruct.ptr, testStruct.ptr[1]);
free(testPtr);
return 0;
}
輸出:
[main]testStruct-Ptr: 0000000000A
ptr[1]=0
[test]testStruct-Ptr: 0000000000A
[main]testStruct-Ptr: 0000000000A
ptr[1]=2
在test
函數(shù)中,通過自增操作和*
運(yùn)算符給testStruct.ptr
指向的下一個(gè)元素賦值為2
。
通過輸出可以看到,test
函數(shù)內(nèi)結(jié)構(gòu)體中指針變量的自增操作并沒有影響到main
函數(shù)中結(jié)構(gòu)體的指針變量,這是因?yàn)?strong>結(jié)構(gòu)體作為參數(shù)傳入時(shí)實(shí)際上是被拷貝了一份作為局部變量以供操作。
之所以能賦值是因?yàn)?code>testStruct.ptr是指針變量,存放著一個(gè)內(nèi)存地址。無(wú)論怎么拷貝,變量?jī)?chǔ)存的內(nèi)存地址是沒有變的,所以通過*
運(yùn)算符仍然能直接對(duì)相應(yīng)數(shù)據(jù)進(jìn)行賦值。
如果要在test
函數(shù)中改變?cè)Y(jié)構(gòu)體中指針變量的指向,就需要把原結(jié)構(gòu)體的地址傳入函數(shù):
int test(struct Hello *testStruct) {
printf(" [test]testStruct-Ptr: %p \n", ++testStruct->ptr);
*testStruct->ptr = 2;
return 1;
}
int main() {
int *testPtr = (int *) calloc(4, sizeof(int));
struct Hello testStruct = {
.num=5,
.ptr=testPtr
};
printf(" [main]testStruct-Ptr: %p \n\t*ptr=%d\n", testStruct.ptr, *testStruct.ptr);
test(&testStruct);
printf(" [main]testStruct-Ptr: %p \n\t*ptr=%d\n", testStruct.ptr, *testStruct.ptr);
free(testPtr);
return 0;
}
輸出:
[main]testStruct-Ptr: 00000000001A1420
*ptr=0
[test]testStruct-Ptr: 00000000001A1424
[main]testStruct-Ptr: 00000000001A1424
*ptr=2
可以看到通過在函數(shù)內(nèi)通過地址訪問到對(duì)應(yīng)結(jié)構(gòu)體,能直接修改結(jié)構(gòu)體中指針變量的指向。這個(gè)例子中通過自增運(yùn)算符讓指針變量指向的內(nèi)存地址后移了一個(gè)int
的長(zhǎng)度。
通過指針訪問結(jié)構(gòu)體時(shí)使用箭頭運(yùn)算符
->
獲取屬性。
最近寫的一個(gè)小工具中有個(gè)自動(dòng)擴(kuò)大堆內(nèi)存以容納數(shù)據(jù)的需求,最開始我寫成了這個(gè)樣:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define SIZE_PER_ALLOC 10
void extend(int *arr, int arrPtr, int *arrMax) {
*arrMax += SIZE_PER_ALLOC; // 新分配這么多
arr = (int *) realloc(arr, (*arrMax) * sizeof(int));
memset(arr + arrPtr, 0, SIZE_PER_ALLOC * sizeof(int)); // 將新分配的部分初始化為0
}
int main() {
int i;
int arrPtr = 0;
int arrMax = 10; // 當(dāng)前最多能容納多少元素
int *flexible = (int *) calloc(arrMax, sizeof(int));
for (i = 0; i < 95; i++) { // 模擬push 95 個(gè)元素
flexible[arrPtr++] = i + 1;
if (arrPtr >= arrMax) // 數(shù)組要容納不下了,多分配一點(diǎn)
extend(flexible, arrPtr, &arrMax);
}
for (i = 0; i < 95; i++) // 打印所有元素
printf("%d ", flexible[i]);
return 0;
}
本來(lái)預(yù)期是95
個(gè)元素能順利推入flexible
這個(gè)“數(shù)組”,“數(shù)組”大小也會(huì)擴(kuò)展為足夠容納100
個(gè)元素。
然而程序運(yùn)行未半而中道崩殂,這個(gè)例子中系統(tǒng)送來(lái)了SIGSEGV
信號(hào)(調(diào)試器Debugger可能會(huì)顯示因?yàn)?code>SIGTRAP而終止進(jìn)程)。根據(jù)上面寫到的SIGSEGV
產(chǎn)生原因,很明顯我又訪問到了未分配給進(jìn)程的無(wú)效內(nèi)存(產(chǎn)生了野指針)。
觀察一下函數(shù)的聲明和調(diào)用時(shí)的傳參:
void extend(int *arr, int arrPtr, int *arrMax);
extend(flexible, arrPtr, &arrMax);
后面的arrPtr
整型變量參數(shù)接受main
函數(shù)傳入的arrPtr
的值,用以確定當(dāng)前“數(shù)組”的下標(biāo)指向哪;而arrMax
指針變量參數(shù)接受main
函數(shù)傳入的arrMax
的地址,用以修改當(dāng)前“數(shù)組”的大小。這兩個(gè)參數(shù)沒有引發(fā)任何問題。
很明顯了,問題就出現(xiàn)在arr
參數(shù)這兒!
實(shí)際上,當(dāng)我將指針變量flexible
作為參數(shù)傳入時(shí)也只是傳入了一個(gè)地址,而不是指針本身。因此在extend
里調(diào)用realloc
重分配內(nèi)存后,新的內(nèi)存塊的地址會(huì)被賦給局部變量arr
,此時(shí)外部的指針變量flexible
的指向沒有任何改變。
realloc()
在重分配內(nèi)存時(shí),會(huì)盡量在原有的內(nèi)存塊上進(jìn)行擴(kuò)展/縮減,盡量不移動(dòng)數(shù)據(jù),這種時(shí)候返回的地址和原來(lái)一樣。
但是一旦原有內(nèi)存塊及其后方相鄰的空閑內(nèi)存不足以提供分配,就會(huì)找到一塊足夠大的新內(nèi)存塊,并將原內(nèi)存塊的數(shù)據(jù)“移動(dòng)”過去,此時(shí)realloc()
返回的地址和原來(lái)的不同,并且原來(lái)的地址所代表的內(nèi)存已經(jīng)被回收。
也就是當(dāng)realloc()
移動(dòng)了數(shù)據(jù)在內(nèi)存中的位置時(shí),外面的flexible
指針變量還指向著原來(lái)的地址,原來(lái)地址代表的內(nèi)存已經(jīng)被回收了。
因此,extend
函數(shù)調(diào)用結(jié)束后的flexible
指針變量就變成了野指針,指向了一片無(wú)效內(nèi)存,所以試圖訪問這片內(nèi)存時(shí),就導(dǎo)致了SIGSEGV
異常。
根本原因在于我傳入函數(shù)的是一個(gè)地址而不是指針變量本身,所以把指針變量的地址傳入就能解決了!
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#define SIZE_PER_ALLOC 10
void extend(int **arr, int arrPtr, int *arrMax) {
*arrMax += SIZE_PER_ALLOC; // 多分配這么多
*arr = (int *) realloc(*arr, (*arrMax) * sizeof(int));
memset(*arr + arrPtr, 0, SIZE_PER_ALLOC * sizeof(int)); // 將新分配的部分初始化為0
}
int main() {
int i;
int arrPtr = 0;
int arrMax = 10; // 當(dāng)前最多能容納多少元素
int *flexible = (int *) calloc(arrMax, sizeof(int));
for (i = 0; i < 95; i++) { // 模擬push 95 個(gè)元素
flexible[arrPtr++] = i + 1;
if (arrPtr >= arrMax) // 數(shù)組要容納不下了,多分配一點(diǎn)
extend(&flexible, arrPtr, &arrMax);
}
for (i = 0; i < 95; i++) // 打印所有元素
printf("%d ", flexible[i]);
free(flexible);
return 0;
}
因?yàn)?strong>二級(jí)指針變量存放一級(jí)指針變量的地址,所以在聲明形參arr
的時(shí)候需要聲明為二級(jí)指針:
void extend(int **arr, int arrPtr, int *arrMax);
調(diào)用函數(shù)的時(shí)候,將指針變量flexible
的地址傳入:
extend(&flexible, arrPtr, &arrMax);
接下來(lái)在函數(shù)extend
內(nèi)部通過*
運(yùn)算符訪問指針變量flexible
以做出修改即可。
這樣一來(lái)程序就能成功運(yùn)行完成了,輸出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
說到最開始遇到這個(gè)問題的時(shí)候,我真的是找了半天都沒找著,因?yàn)閜ush元素和數(shù)組擴(kuò)展我分開寫在了兩個(gè)源文件中,而這個(gè)部分又涉及到其他內(nèi)存分配的代碼。我甚至查了realloc
是怎么導(dǎo)致SIGSEGV
的,結(jié)果就...打斷點(diǎn)調(diào)試了好多次才發(fā)現(xiàn)是這個(gè)問題。
涉及到指針變量和內(nèi)存操作的時(shí)候,一定要牢記指針變量的指向,也一定要步步謹(jǐn)慎,不然一旦出現(xiàn)問題,很可能難以定位。
C語(yǔ)言的內(nèi)存管理很靈活,但正是因?yàn)殪`活,在編寫相關(guān)操作的時(shí)候要十分小心。
在接觸這類和底層接壤的編程語(yǔ)言時(shí)對(duì)基礎(chǔ)知識(shí)的要求真的很高...感覺咱還有超長(zhǎng)的路要走呢。
那么就是這樣,感謝你看到這里,也希望這篇筆記能對(duì)你有些幫助!再會(huì)~
【C語(yǔ)言】二十二步了解函數(shù)棧幀(壓棧、傳參、返回、彈棧)
逆向基礎(chǔ)筆記:匯編二維數(shù)組 - 52pojie論壇 <--- 這個(gè)筆記系列超棒的說!
網(wǎng)站標(biāo)題:【小記】與指針和二維數(shù)組過幾招
網(wǎng)站鏈接:http://chinadenli.net/article26/dsoijcg.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供ChatGPT、網(wǎng)站設(shè)計(jì)公司、全網(wǎng)營(yíng)銷推廣、App開發(fā)、網(wǎng)站建設(shè)、網(wǎng)站設(shè)計(jì)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)