【小記】與指針和二維數組過幾招

来源:https://www.cnblogs.com/somebottle/archive/2022/05/10/Pointers_and_2D_Arrays.html
-Advertisement-
Play Games

在C/C++中有個叫指針的玩意存在感極其強烈,而說到指針又不得不提到記憶體管理。現在時不時能聽到一些朋友說指針很難,實際上說的是記憶體操作和管理方面的難。(這篇筆記咱也會結合自己的理解簡述一些相關的記憶體知識) 最近在寫C程式使用指針的時候遇到了幾個讓我印象深刻的地方,這裡記錄一下,以便今後回顧。 “經一 ...


在C/C++中有個叫指針的玩意存在感極其強烈,而說到指針又不得不提到記憶體管理。現在時不時能聽到一些朋友說指針很難,實際上說的是記憶體操作和管理方面的難。(這篇筆記咱也會結合自己的理解簡述一些相關的記憶體知識)

最近在寫C程式使用指針的時候遇到了幾個讓我印象深刻的地方,這裡記錄一下,以便今後回顧。

embarrassed-2022-05-06

“經一蹶者長一智,今日之失,未必不為後日之得。” - 王陽明《與薛尚謙書》

指針和二級指針

簡述下指針的概念。

指針

一個指針可以理解為一條記憶體地址

pointer-2022-05-06

這裡先定義了一個整型變數test,接著用取址運算符&取得這個變數的記憶體地址並列印出來。
可以看到該變數的記憶體地址是000000000061FE1C

指針變數

指針變數就是存放指針(也就是存放記憶體地址)的變數,使用數據類型* 變數名進行定義。

值得註意的是指針變數內儲存的指針(記憶體地址)所代表的變數的數據類型,比如int*定義的指針變數就只能指向int類型的變數。

int test = 233;
int* ptr = &test;

test變數的類型是整型int,所以test存放的就是一個整形數據。
ptr變數的類型是整型指針類型int*,存放則的是整性變數test的指針(記憶體地址)。

二級指針

二級指針指的是一級指針變數的地址。

int main() {
    int test = 233;
    printf("%p\n", &test);
    int *ptr = &test;
    printf("%p", &ptr);
    return 0;
}
/* stdout
000000000061FE1C
000000000061FE10
*/

這個例子中二級指針就是ptr變數的地址000000000061FE10

二級指針變數

二級指針變數就是存放二級指針(二級指針的地址)的變數,使用數據類型** 變數名進行定義。

int main() {
    int test = 233;
    int *ptr = &test;
    int **ptr2 = &ptr;
    return 0;
}

ptr變數的類型是整型指針類型int*,存放的是整性(int)變數test的指針(記憶體地址),
ptr2變數的類型是二級整型指針類型int**,存放的是整性指針(int*)變數ptr的記憶體地址。

doublePointerGraph-2022-05-06

多級指針變數

雖然二級以上的指針變數相對來說不太常用,但我覺得基本的辨別方法還是得會的:

通過觀察發現,指針變數的數據類型定義其實就是在其所指向的數據類型名後加一個星號
比如說:

  • 指針ptr指向整型變數int test,那麼它的定義寫法就是int* ptr。(數據類型在int後加了一個星號)

  • 指針ptr2指向一級指針變數int* ptr,那麼它的定義寫法就是int** ptr2。(數據類型在int*後加了一個星號)

再三級指針變數int*** ptr3,乍一看星號這麼多,實際上“剝”一層下來就真相大白了:

(int**)*

實際上三級指針變數指向的就是二級指針變數的地址

008-2022-05-06

其他更多級的指針變數可以依此類推。

棧記憶體和堆記憶體

指針和記憶體操作關係緊密,提到指針總是令人情不自禁地想起記憶體。

程式運行時占用的記憶體空間會被劃分為幾個區域,其中和這篇筆記息息相關的便是棧區(Stack)堆區(Heap)

棧區 (Stack)

棧區的操作方式正如數據結構中的棧,是LIFO後進先出的。這種操作模式的一個很經典的應用就是遞歸函數了。

每個函數被調用時需要從棧區劃分出一塊棧記憶體用來存放調用相關的信息,這塊棧記憶體被稱為函數的棧幀


棧幀存放的內容主要是(按入棧次序由先至後):

  1. 返回地址,也就是函數被調用處的下一條指令的記憶體地址(記憶體中專門有代碼區用於存放),用於函數調用結束返回時能接著原來的位置執行下去。

  2. 函數調用時的參數值

  3. 函數調用過程中定義的局部變數的值。

  4. and so on...

由LIFO後進先出可知一次函數調用完畢後相較而言局部變數先出棧,接著是參數值,最後棧頂指針指向返回地址,函數返回,接著下一條指令執行下去。


棧區的特性:

  1. 交由系統(C語言這兒就是編譯器參與實現)自動分配和釋放,這點在函數調用中體現的很明顯。

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

  3. 相對來說空間較小,如果申請的空間大於棧剩餘的記憶體空間,會引發棧溢出問題。(棧記憶體大小限制因操作系統而異)

    比如遞歸函數控制不當就會導致棧溢出問題,因為每層函數調用都會形成新的棧幀“壓到”棧上,如果遞歸函數層數過高,棧幀遲遲得不到“彈出”,就很容易擠爆棧記憶體。

  4. 棧記憶體占用大小隨著函數調用層級升高而增大,隨著函數調用結束逐層返回而減小;也隨著局部變數的定義而增大,隨著局部變數的銷毀而減小。

    棧記憶體中儲存的數據的生命周期很清晰明確。

  5. 棧區是一片連續的記憶體區域。


堆區 (Heap)

堆記憶體就真的是“一堆”記憶體,值得一提的是,這裡的堆和數據結構中的堆沒有關係

相對棧區來說,堆區可以說是一個更加靈活的大記憶體區,支持按需進行動態分配。


堆區的特性:

  1. 交由程式員或者垃圾回收機制進行管理,如果不加以回收,在整個程式沒有運行完前,分配的堆記憶體會一直存在。(這也是容易造成記憶體泄漏的地方)

    在C/C++中,堆記憶體需要程式員手動申請分配和回收

  2. 分配速度較慢,系統需要依照演算法搜索(鏈表)足夠的記憶體區域以分配。

  3. 堆區空間比較大,只要還有可用的物理記憶體就可以持續申請。

  4. 堆區是不連續(離散)的記憶體區域。(大概是依賴鏈表來進行分配操作的)

  5. 現代操作系統中,在程式運行完後會回收掉所有的堆記憶體。

    要養成不用就釋放的習慣,不然運行過程中進程占用記憶體可能越來越大。


簡述C中堆記憶體的分配與釋放

分配

這裡咱就直接報菜名吧!

alloc-2022-05-07

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

  1. void* malloc(size_t size)

    用於請求系統從堆區中分配一段連續的記憶體塊

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

    在和malloc一樣申請到連續的記憶體塊後,將所有分配的記憶體全部初始化為0

  3. void* realloc(void* block, size_t size)

    修改已經分配的記憶體塊的大小(具體實現是重新分配),可以放大也可以縮小。

malloc可以記成Memory Allocate 分配記憶體
calloc可以記成Clear and Allocate 分配並設置記憶體為0
realloc可以記成Re-Allocate 重分配記憶體


簡單來說原理大概是這樣:

  • malloc記憶體分配依賴的數據結構是鏈表。簡單說來就是所有空閑的記憶體塊會被組織成一個空閑記憶體塊鏈表

  • 當要使用malloc分配記憶體時,它首先會依據演算法掃描這個鏈表,直到找到一個大小滿足需求的空閑記憶體塊為止,然後將這個空閑記憶體塊傳遞給用戶(通過指針)。
    (如果這塊的大小大於用戶所請求的記憶體大小,則將多餘部分“切出來”接回鏈表中)。

  • 在不斷的分配與釋放過程中,由於記憶體塊的“切割”,大塊的記憶體可能逐漸被切成許多小塊記憶體存在鏈表中,這些便是記憶體碎片。當malloc找不到合適大小的記憶體塊時便會嘗試合併這些記憶體碎片以獲得大塊空閑的記憶體。

  • 實在找不到空閑記憶體塊的情況下,malloc會返回NULL指針。


釋放

釋放手動分配的堆記憶體需要用到free函數:

void free(void* block)

只需要傳入指向分配記憶體始址的指針變數作為實參傳入即可。

C/C++中,對於手動申請分配的堆記憶體在使用完後一定要及時釋放,
不然在運行過程中進程占用記憶體可能會越來越大,也就是所謂的記憶體泄漏。

不過在現代操作系統中,程式運行完畢後OS會自動回收對應進程的記憶體,包括泄露的記憶體。記憶體泄露指的是在程式運行過程中無法操作的記憶體


free為什麼知道申請的記憶體塊大小?

allocatedMem-2022-05-07

簡單來說,就是在malloc進行記憶體分配時會把記憶體大小分配地略大一點,多餘的記憶體部分用於儲存一些頭部數據(這塊記憶體塊的信息),這塊頭部數據內就包括分配的記憶體的長度

但是在返回指針的時候,malloc會將其往後移動,使得指針代表的是用戶請求的記憶體塊的起始地址

頭部數據占用的大小通常是固定的(網上查了一下有一種說法是16位元組,也有說是sizeof(size_t)的),在將指針傳入free後,free會將指針向前移動指定長度以獲得頭部數據,讀取到分配的記憶體長度,然後連同頭部數據和所分配長度的記憶體一併釋放掉

記憶體釋放可以理解為這塊記憶體被重新接到了空閑鏈表上,以備後面的分配。
(實際上記憶體釋放後的情況其實挺複雜的,得要看具體的演算法實現和運行環境)


二維數組

定義和初始化

C語言中二維數組的定義:

數據類型 數組名[行數][列數];

初始化則可以使用大括弧

int a[3][4]={
    {1,2,3,4},
    {5,6,7,8},
    {9,10,11,12}
};

int b[3][4]={ // 內層不要大括弧也是可以的,具體為什麼後面再說
    1,2,3,4,
    5,6,7,8,
    9,10,11,12  
};

char str[2][6]={
    "Hello",
    "World"
};

此外,在有初始化值的情況下,定義二維數組時的一維長度(行數)是可以省略的:

int a[][4]={ // 如果沒有初始化,則一維長度不可省略
    1,2,3,4,
    5,6,7,8,
    9,10,11,12  
}

在記憶體中

按上述語句定義的數組,在進程記憶體中一般儲存於:

  1. 棧區 - 在函數內部定義的局部數組變數。

  2. 靜態儲存區 - 當用static修飾數組變數或者在全局作用域中定義數組。

數組在記憶體中是連續且呈線性儲存的二維數組也是不例外的

雖然在使用過程中二維數組發揮的是“二維”的功能,但其在記憶體中是被映射為一維線性結構進行儲存的。

實踐驗證一下:

int i, j;
int a[][4] = { // 如果沒有初始化,則一維長度不可省略
        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");
}

輸出:

continuousArr-2022-05-08

第一維有3行,第二維有4列

一個int類型數據占用4個位元組,從上面的圖可以看出來:

  • [1]000000000061FDD0 -> [2]000000000061FDD4 相隔4位元組,說明這兩個數組元素相鄰,同一行中數組元素儲存連續。

  • [4]000000000061FDDC -> [5]000000000061FDE0 同樣相隔4位元組,這兩個數組元素在記憶體中也是相鄰的。

  • [1]000000000061FDD0[12]000000000061FDFC正好相差44個位元組,整個二維數組元素在記憶體中是連續儲存的。


這樣一看,為什麼定義並初始化的時候二維數組的第一維可以省略已經不言而喻了:

在初始化的時候編譯器通過數組第二維的大小對元素進行“分組”,每一組可以看作是一個一維數組,這些一維數組在記憶體中從低地址到高地址連續排列儲存形成二維數組:

memOf2DArr-2022-05-08

在上面例子中大括弧中的元素{1,2,3,4,5,6,7,8,9,10,11,12}被按第二維長度4劃分成了{1,2,3,4}{5,6,7,8}{9,10,11,12}三組,這樣程式也能知道第一維數組長度為3了。

二維數組名代表的地址

一維數組名代表的是數組的起始地址(也是第一個元素的地址)。

二維數組在記憶體中也是映射為一維進行連續儲存的,
既然如此,二維數組名代表的地址其實也是整個二維數組的起始地址,在上面的例子中相當於a[0][0]的地址。

在上面的示例最後加一行:

printf("Arr address: %p", a);  

列印出來的地址和a[0][0]的地址完全一致,是000000000061FDD0

二維數組和二級指針

二維數組不等於二級指針

首先要明確一點:二維數組 ≠ 二級指針

剛接觸C語言時我總是想當然地把這兩個搞混了,實際上根本不是一回事兒。

  • 二級指針變數儲存的是一級指針變數地址

  • 二維數組是記憶體中連續儲存的一組數據,二維數組名相當於一個一級指針(二維數組的起始地址)。

int arr[][4]={
    {1,2},{1},{3},{4,5}
};
int** ptr=arr; // 這樣寫肯定是不行的!,ptr儲存的是一級指針變數的地址
int* ptr=arr; // 這樣寫是可以的,但是不建議
int* ptr=&arr[0][0]; // 這樣非常ok, ptr儲存的是數組起始地址(也就是首個變數的地址)

可以把之前二維數組的例子改一下:

int i;
int a[][4] = { // 如果沒有初始化,則一維長度不可省略
        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; // 整個二維數組的長度
int *ptr = &a[0][0]; // ptr指向二維數組首地址
for (i = 0; i < totalLen; 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);

輸出結果和之前遍歷二維數組的是一模一樣的。

指針數組

實現“二維數組”

既然二級指針變數不能直接指向二維數組,那能不能依賴二級指針來實現一個類似的結構呢?當然是可以的啦!

整型變數存放著整型int數據,整型數組int a[]中存放了整型數據;

如果是用申請堆記憶體來實現的整型數組:

int* arr = (int*)malloc(sizeof(int) * 3);

指針int*變數arr此時指向的是連續存放整型(int)數據的記憶體的起始地址,相當於一個一維數組的起始地址。


代碼實現

二級指針int**變數存放著一級指針變數的地址,那麼就可以構建二級指針數組來存放二級指針數據(也就是每個元素都是一級指針變數的地址)。

pointerArray1-2022-05-08

具體代碼實現:

int rows = 3; // 行數/一維長度
int cols = 4; // 列數/二維長度
int **ptr = (int **) malloc(rows * sizeof(int *));
// 分配一段連續的記憶體,儲存int*類型的數據
int i, j, num = 1;
for (i = 0; i < rows; i++) {
    ptr[i] = (int *) malloc(cols * sizeof(int));
    // 再分配一段連續的記憶體,儲存int類型的數據
    for (j = 0; j < cols; j++)
        ptr[i][j] = num++; // 儲存一個整型數據1-12
}

其中
ptr[i] = (int *) malloc(cols * sizeof(int));
這一行,等同於
*(ptr+i) = ...
也就是利用間接訪問符*讓一級指針變數指向在堆記憶體中分配的一段連續整形數據,這裡相當於初始化了第二維。

而在給整型元素賦值時和二維數組一樣用了中括弧進行訪問:
ptr[i][j] = i * j;
其實就等同於:
*(*(ptr+i)+j) = i * j;

  1. 第一次訪問第一維元素,用第一維起始地址ptr加上第一維下標i,取出對應的一級指針變數存放的地址*(ptr+i)
    這個地址是第二維中一段連續記憶體的起始地址。

  2. 第二次訪問第二維元素,用1中取到的地址*(ptr+i)加上第二維下標j,再用間接訪問符*訪問對應的元素,並賦值。


在記憶體中的存放

指針數組在記憶體中的存放不同於普通定義的二維數組,它的每一個維度是連續儲存的,但是維度和維度之間在記憶體中的存放是離散的。

用一個迴圈列印一下每個元素的地址:

for (i = 0; i < rows; i++) {
    for (j = 0; j < cols; j++)
        printf(" [%d]%p ", ptr[i][j], *(ptr + i) + j);
    printf("\n");
}

輸出:

pointerArrAddress-2022-05-09

可以看到第二維度的地址是連續的,但是第二維度“數組”之間並不是連續的。比如元素4和元素5的地址相差了20個位元組,並不是四個位元組。

pointerArray2-2022-05-09

其在記憶體中的存放結構大致如上,並無法保證*(ptr+0)+3*(ptr+1)的地址相鄰,也無法保證*(ptr+1)+3*(ptr+2)的地址相鄰。

這種非連續的存放方式可以說是和二維數組相比很大的一個不同點了。


釋放對應的堆記憶體

通常指針數組實現的“二維數組”是在堆記憶體中進行存放的,既然申請了堆記憶體,咱也應該養成好習慣,使用完畢後將其釋放掉:

for (i = 0; i < rows; i++)
    free(ptr[i]);
free(ptr);

先利用一個迴圈釋放掉每一個一級指針變數指向的連續記憶體塊(儲存整型數據),最後再把二級指針變數指向的連續記憶體塊(儲存的是一級指針變數的地址)釋放掉。


sizeof的事兒

sizeof()是C語言中非常常用的一個運算符,而二級指針二維數組的區別在這裡也可以很好地展現出來。

對於直接定義的數組

對於非變數長度定義的數組,sizeof編譯階段就會完成求值運算,被替換為對應數據的大小的常量值。

int arr[n]; 這種定義時數組長度為變數的即為變數長度數組(C99標準開始支持),不過還是不太推薦這種寫法。

直接固定長度定義二維數組時,編譯器是知道這個變數是數組的,比如:

int arr[3][4];
size_t arrSize = sizeof(arr);  

在編譯階段,編譯器知道數組arr是一個整型int二維數組:

  1. 每個第二維數組包含四個int數據,長度為sizeof(int)*4=16個位元組。

  2. 第一維數組包含三個第二維數組,每個第二維數組長度為16位元組,整個二維數組總長度為16*3=48個位元組。

sizeof(arr) = 48


對於指針數組

指針變數儲存的是指針,也就是一個地址。記憶體地址在運算的時候會存放在CPU的整數寄存器中。

64位電腦中整數寄存器寬度有64bit(位),而指針數據要能存放在這裡。

目前來說 1 位元組(Byte) = 8 位(bit),那麼64位就是8個位元組,
所以64位系統中指針變數的長度是8位元組。

int rows = 3; // 行數/一維長度
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

雖然上面咱通過申請分配堆記憶體實現了二維數組(用二級指針變數ptr指向了指針數組起址),

但其實在編譯器眼中,ptr單純是一個二級指針變數,占用位元組數為8 Bytes(64位),儲存著一個地址,因此在這裡是無法通過sizeof獲得這塊連續記憶體的長度的。

通過上面的例子很容易能觀察出來:

sizeof(指針變數) = 8 Bytes (64位電腦)

無論指針變數指向的是什麼數據的地址,它儲存的單純只是一個記憶體地址,所以所有指針變數的占用位元組數是一樣的


函數傳參與返回

得先明確一點:C語言中不存在所謂的數組參數,通常讓函數接受一個數組的數據需要通過指針變數參數傳遞。

傳參時數組發生退化

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

在上面這個例子中test函數的定義中聲明瞭“看上去像數組的”形參newArr,然而sizeof的運算結果是8

實際上這裡的形參聲明是等同於int* newArr的,因為把數組作為參數進行傳遞的時候,實際上傳遞的是數組的首地址(因為數組名就代表數組的首地址)。

這種情況下就發生了數組指針的退化。

在編譯器的眼中,newArr此時就被當作了一個指針變數,指向arr數組的首地址,因此聲明中數組的長度怎麼寫都行:int newArr[5]int newArr[]都可以。

為了讓代碼更加清晰,我覺得最好還是聲明為int* newArr,這樣一目瞭然能知道這是一個指針變數!


函數內運算涉及到數組長度時

當函數內運算涉及到數組長度時,就需要在函數定義的時候另聲明一個形參來接受數組長度:

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) // 每個第二維數組元素列印完後換行
            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;
}

輸出:

printFuncOutput-2022-05-09

這個例子中test函數就多接受了二維數組的一維長度rowLen和二維長度colLen,以對二維數組元素進行遍歷列印。


返回“數組”

經常有應用場景需要函數返回一個“數組”,說是數組,實際上函數並無法返回一個局部定義的數組,哪怕是其指針(在下麵一節有寫為什麼)。

取而代之地,常常會返回一個指針指向分配好的一塊連續的堆記憶體
(在演算法題中就經常能遇到要求返回指針的情況)

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

這個示例中,test函數的返回類型是整型指針。當調用了test函數,傳入要分配的連續記憶體長度後,其在函數內部定義了一個局部指針變數,指向分配好的記憶體,在記憶體中存放數據後將該指針返回。

在主函數中,test返回的整型指針被賦給了指針變數allocated,所以接下來可以通過一個迴圈列印出這塊連續記憶體中的數據。

再次提醒,申請堆記憶體並使用完後,一定要記得使用free進行釋放

生疏易犯-函數返回局部變數

錯誤示例

記得初學C語言的時候,我曾經犯過一個錯誤:將函數內定義的數組的數組名作為返回值

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

這個例子中直到for迴圈前進程仍然正常運行,但是一旦嘗試使用*運算符取出記憶體中的數據*(allocated + i),進程立馬接收到了系統發來的異常信號SIGSEGV,進而終止執行。

原因簡述

SIGSEGV是比較常見的一種異常信號,代表Signal Segmentation Violation,也就是記憶體分段衝突
造成異常的原因通常是進程 試圖訪問一段沒有分配給它的記憶體,“野指針”總是伴隨著這個異常出現。

上面簡述棧區的時候提到了棧幀,每次調用函數時會在棧上給函數分配一個棧幀用來儲存函數調用相關信息

函數調用完成後,先把運算出來的返回值存入寄存器中,接著會在棧幀上進行彈棧操作,在這個過程中分配的局部變數就會被回收

最後,程式在棧頂中取到函數的返回地址,返回上層函數繼續執行餘下的指令。棧幀銷毀,此時局部變數相關的棧記憶體已經被回收了

然而此時寄存器中仍存著函數的返回值,是一個記憶體地址,但是記憶體地址代表的記憶體部分已經被回收了

當將返回值賦給一個指針變數時,野指針就產生了——此時這個指針變數指向一片未知的記憶體

所以當進程試圖訪問這一片不確定的記憶體時,就容易引用到無效的記憶體,此時系統就會發送SIGSEGV信號讓進程終止執行。


教訓

教訓總結成一句話就是:

  • 程式中請不要讓函數返回代表棧記憶體局部變數的地址

延伸:返回靜態局部變數是可以的,因為靜態局部變數是儲存在靜態儲存區的

int *test() {
    static int arr[4] = {1, 2, 3, 4};
    return arr;
}


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1.salesforce是什麼? salesforce致力於在銷售,服務,營銷,分析和 客戶聯繫方面為用戶提供幫助。 通過使用標準產品和功能(standard products and features),可以管理潛在客戶(prospects )和客戶(customers)的關係,與員工和合作伙伴協 ...
  • 算數運算符 <?php $x=10; $y=6; echo ($x + $y); // 加 echo '<br>'; // 換行 echo ($x - $y); // 減 echo '<br>'; // 換行 echo ($x * $y); // 乘 echo '<br>'; // 換行 echo ...
  • Spring Bean 的創建過程介紹了FactoryBean 的創建方式,那麼接下來介紹不是FactoryBean的創建方式,在創建過程中,又會分為單例的Bean的創建,原型類型的Bean的創建等。一般來說在Spring中幾乎所有對象都是單例創建的,除非有其他業務需要設置為其他作用域的Bean,所 ...
  • 動態規劃 [P1216 USACO1.5][IOI1994]數字三角形 Number Triangles - 洛谷 | 電腦科學教育新生態 (luogu.com.cn) 題目描述 觀察下麵的數字金字塔。 寫一個程式來查找從最高點到底部任意處結束的路徑,使路徑經過數字的和最大。每一步可以走到左下方的 ...
  • 前言 作為目前全世界最大的視頻網站,它幾乎全是用Python來寫的 該網站當前行業內線上視頻服務提供商,該網站的系統每天要處理上千萬個視頻片段,為全球成千上萬的用戶提供高水平的視頻上傳、分發、展示、瀏覽服務。 2015年2月,央視首次把春晚推送到該網站。 今天,我們就要用Python來快速批量下載該 ...
  • 在開發中,遇到這樣一個需求,在介質資料新增時,需要生成一個介質編號,格式為"JZ+yyyyMMDD+4位遞增數字" 先是使用百度找尋解決方法。解決方法 裡面的查詢緩存的方法在我這項目里沒有,我也不會寫。就自己想了個折中的方法,再請求這個介面的時候,先去資料庫查詢MAX(id),如果有,就在此基礎上+ ...
  • 前言 當前的主瀏覽器都支持直接打開pdf文件,從而實現文件預覽。如果是其他格式文件則得下載,因此用openOffice實現文件轉pdf格式。 一、 openOffice的安裝 下載地址:http://www.openoffice.org/ 安裝教程可參考:openOffice下載和安裝 進入安裝目錄 ...
  • 本文只是通過一個實例來講述如何獲得python中所有的單字元的字母表,不僅僅是局限於英文的abcd,可能還有其他語言如ᵝᵞᵟᵠ等。在實際寫python的過程中可能不一定用得到,但是不失為一個挺有趣的功能探索。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...