欧美一区二区三区老妇人-欧美做爰猛烈大尺度电-99久久夜色精品国产亚洲a-亚洲福利视频一区二区

【小記】與指針和二維數(shù)組過幾招

在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)明《與薛尚謙書》

指針和二級(jí)指針

簡(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í)指針指的是一級(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í)指針(二級(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í)指針變量

雖然二級(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)存和堆內(nèi)存

指針和內(nèi)存操作關(guān)系緊密,提到指針總是令人情不自禁地想起內(nèi)存。

程序運(yùn)行時(shí)占用的內(nèi)存空間會(huì)被劃分為幾個(gè)區(qū)域,其中和這篇筆記息息相關(guān)的便是棧區(qū)(Stack)堆區(qū)(Heap)

棧區(qū) (Stack)

棧區(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)容主要是(按入棧次序由先至后):

  1. 返回地址,也就是函數(shù)被調(diào)用處的下一條指令的內(nèi)存地址(內(nèi)存中專門有代碼區(qū)用于存放),用于函數(shù)調(diào)用結(jié)束返回時(shí)能接著原來(lái)的位置執(zhí)行下去。

  2. 函數(shù)調(diào)用時(shí)的參數(shù)值

  3. 函數(shù)調(diào)用過程中定義的局部變量的值。

  4. and so on...

由LIFO后進(jìn)先出可知一次函數(shù)調(diào)用完畢后相較而言局部變量先出棧,接著是參數(shù)值,最后棧頂指針指向返回地址,函數(shù)返回,接著下一條指令執(zhí)行下去。


棧區(qū)的特性:

  1. 交由系統(tǒng)(C語(yǔ)言這兒就是編譯器參與實(shí)現(xiàn))自動(dòng)分配和釋放,這點(diǎn)在函數(shù)調(diào)用中體現(xiàn)的很明顯。

  2. 分配速度較快,但并不受程序員控制。

  3. 相對(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)存。

  4. 棧內(nèi)存占用大小隨著函數(shù)調(diào)用層級(jí)升高而增大,隨著函數(shù)調(diào)用結(jié)束逐層返回而減小;也隨著局部變量的定義而增大,隨著局部變量的銷毀而減小。

    棧內(nèi)存中儲(chǔ)存的數(shù)據(jù)的生命周期很清晰明確。

  5. 棧區(qū)是一片連續(xù)的內(nèi)存區(qū)域。


堆區(qū) (Heap)

堆內(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ū)的特性:

  1. 交由程序員或者垃圾回收機(jī)制進(jìn)行管理,如果不加以回收,在整個(gè)程序沒有運(yùn)行完前,分配的堆內(nèi)存會(huì)一直存在。(這也是容易造成內(nèi)存泄漏的地方)

    在C/C++中,堆內(nèi)存需要程序員手動(dòng)申請(qǐng)分配和回收

  2. 分配速度較慢,系統(tǒng)需要依照算法搜索(鏈表)足夠的內(nèi)存區(qū)域以分配。

  3. 堆區(qū)空間比較大,只要還有可用的物理內(nèi)存就可以持續(xù)申請(qǐng)。

  4. 堆區(qū)是不連續(xù)(離散)的內(nèi)存區(qū)域。(大概是依賴鏈表來(lái)進(jìn)行分配操作的)

  5. 現(xiàn)代操作系統(tǒng)中,在程序運(yùn)行完后會(huì)回收掉所有的堆內(nèi)存。

    要養(yǎng)成不用就釋放的習(xí)慣,不然運(yùn)行過程中進(jìn)程占用內(nèi)存可能越來(lái)越大。


簡(jiǎn)述C中堆內(nèi)存的分配與釋放

分配

這里咱就直接報(bào)菜名吧!

這一部分的函數(shù)的原型都定義在頭文件stdlib.h中。

  1. void* malloc(size_t size)

    用于請(qǐng)求系統(tǒng)從堆區(qū)中分配一段連續(xù)的內(nèi)存塊。

  2. void* calloc(size_t n, size_t size);

    在和malloc一樣申請(qǐng)到連續(xù)的內(nèi)存塊后,將所有分配的內(nèi)存全部初始化為0。

  3. 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)境)


二維數(shù)組

定義和初始化

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  
}

在內(nèi)存中

按上述語(yǔ)句定義的數(shù)組,在進(jìn)程內(nèi)存中一般儲(chǔ)存于:

  1. 棧區(qū) - 在函數(shù)內(nèi)部定義的局部數(shù)組變量。

  2. 靜態(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ù)組名代表的是數(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。

二維數(shù)組和二級(jí)指針

二維數(shù)組不等于二級(jí)指針

首先要明確一點(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ù)組的是一模一樣的。

指針數(shù)組

實(shí)現(xiàn)“二維數(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ù)組的起始地址。


代碼實(shí)現(xiàn)

二級(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;

  1. 第一次訪問第一維元素,用第一維起始地址ptr加上第一維下標(biāo)i,取出對(duì)應(yīng)的一級(jí)指針變量存放的地址*(ptr+i)
    這個(gè)地址是第二維中一段連續(xù)內(nèi)存的起始地址。

  2. 第二次訪問第二維元素,用1中取到的地址*(ptr+i)加上第二維下標(biāo)j,再用間接訪問符*訪問對(duì)應(yīng)的元素,并賦值。


在內(nèi)存中的存放

指針數(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)了。


釋放對(duì)應(yīng)的堆內(nèi)存

通常指針數(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的事兒

sizeof()是C語(yǔ)言中非常常用的一個(gè)運(yùn)算符,而二級(jí)指針二維數(shù)組的區(qū)別在這里也可以很好地展現(xiàn)出來(lái)。

對(duì)于直接定義的數(shù)組

對(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ù)組:

  1. 每個(gè)第二維數(shù)組包含四個(gè)int數(shù)據(jù),長(zhǎng)度為sizeof(int)*4=16個(gè)字節(jié)。

  2. 第一維數(shù)組包含三個(gè)第二維數(shù)組,每個(gè)第二維數(shù)組長(zhǎng)度為16字節(jié),整個(gè)二維數(shù)組總長(zhǎng)度為16*3=48個(gè)字節(jié)。

sizeof(arr) = 48


對(duì)于指針數(shù)組

指針變量?jī)?chǔ)存的是指針,也就是一個(gè)地址。內(nèi)存地址在運(yùn)算的時(shí)候會(huì)存放在CPU的整數(shù)寄存器中。

64位計(jì)算機(jī)中整數(shù)寄存器寬度有64bit(位),而指針數(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ù)是一樣的。


函數(shù)傳參與返回

得先明確一點(diǎn):C語(yǔ)言中不存在所謂的數(shù)組參數(shù),通常讓函數(shù)接受一個(gè)數(shù)組的數(shù)據(jù)需要通過指針變量參數(shù)傳遞。

傳參時(shí)數(shù)組發(fā)生退化

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è)指針變量!


函數(shù)內(nèi)運(yùn)算涉及到數(shù)組長(zhǎng)度時(shí)

當(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)行遍歷打印。


返回“數(shù)組”

經(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)行釋放

生疏易犯-函數(shù)返回局部變量

錯(cuò)誤示例

記得初學(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í)行。

原因簡(jiǎn)述

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)

教訓(xùn)總結(jié)成一句話就是:

  • 程序中請(qǐng)不要讓函數(shù)返回代表棧內(nèi)存局部變量的地址。

延伸:返回靜態(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í)行了。

實(shí)參結(jié)構(gòu)體中的指針

改變指針變量指向的變量

用一個(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)行賦值。

改變?cè)Y(jié)構(gòu)體的指針變量指向

如果要在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)算符-> 獲取屬性。

最近摔了一跤的地方

被自己繞進(jì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 

教訓(xùn)

說到最開始遇到這個(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)問題,很可能難以定位。

總結(jié)

C語(yǔ)言的內(nèi)存管理很靈活,但正是因?yàn)殪`活,在編寫相關(guān)操作的時(shí)候要十分小心。

在接觸這類和底層接壤的編程語(yǔ)言時(shí)對(duì)基礎(chǔ)知識(shí)的要求真的很高...感覺咱還有超長(zhǎng)的路要走呢。

那么就是這樣,感謝你看到這里,也希望這篇筆記能對(duì)你有些幫助!再會(huì)~

相關(guān)文章

  • 【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)

成都app開發(fā)公司