C語言內存-棧與堆使用

C語言程序需要載入內存纔可以運行,其不同的數據保存在不同的區域。所使用的內存可以分成兩類:
一類是靜態存儲區,另一類是動態存儲區。

1 靜態存儲區

靜態存儲區分爲三類:只讀數據區(RO Data)、已初始化讀寫數據區(RW Data)、未初始化讀寫讀寫數據區(BSS)。這三類存儲區都是在程序的編譯-連接階段確定的,且運行過程中是不會變化的,只有當程序退出的時候,靜態存儲區的內存纔會被系統回收。

2 動態存儲區

動態存儲區主要分爲兩類:一類是棧(Stack)內存區域,棧內存是由編譯器管理的;另一類是堆(Heap)內存區域,堆內存由程序調用具體的庫函數來分配的。它們都是程序運行過程中動態分配的。

2.1棧內存區域

2.1.1棧的相關概念
棧內存的使用很大的程度上依賴於處理器的硬件機制。在處理器中,有一個寄存器來表示當前棧指針的位置。通常在內存中分配一塊區域,這塊區域的上界(高內存地址)和下界(低內存地址)之間是可用的棧內存區域。
目前常見的體系結構和編譯系統中,棧大多都是向下增長的。在初始階段,棧指針是指向棧區間的上界,隨着棧使用量的增加,棧指針的值向低地址移動,即棧指針的值將變小。下面來看一段程序:

#include <stdio.h>

int main(void)
{
 int a = 1, b = 2, c = 3;
 
 printf("a = %d, &a = %#x \n", a, (unsigned int)&a);
 printf("b = %d, &b = %#x \n", b, (unsigned int)&b);
 printf("c = %d, &c = %#x \n", c, (unsigned int)&c);
 
 return 0;
}

程序運行結果爲:
在這裏插入圖片描述
可見,變量的存儲是從高地址往低地址的方向存儲。
棧有一個重要的特性:先放入的數據最後才能取出,後放入的數據優先能取出,即先進後出(First In Last Out)原則。放入數據常被稱爲入棧或壓棧(Push),取出數據被稱爲出棧或彈出(Pop)。在運用過程中,棧內存可能出現滿棧和空棧兩種情況,這是由處理器的體系結構決定的。
棧(Stack)可以存放函數參數、局部變量、局部數組等作用範圍在函數內部的數據,它的用途就是完成函數的調用。

2.1.1需要知道的關於棧的問題:
(1)函數在調用完成之後,棧指針將回到函數進入之前的位置。下面的程序通過兩次調用同一個函數印證了這一點:

#include <stdio.h>

void stack_test1(int a, int b, int c);

int main(void)
{
 int a = 1, b = 2, c = 3;
 int a1 = 4, b1 = 5, c1 = 6;
 
 printf("第一次調用stack_test1函數:\n");
 stack_test1(a, b, c);
 printf("第二次調用stack_test1函數:\n");
 stack_test1(a1, b1, c1);
 
 return 0;
}

void stack_test1(int a, int b, int c)
{
 printf("a = %d, &a = %#x \n", a, (unsigned int)&a);
 printf("b = %d, &b = %#x \n", b, (unsigned int)&b);
 printf("c = %d, &c = %#x \n", c, (unsigned int)&c);
}

在這裏插入圖片描述
可見,兩次調用中函數參數使用的棧內存是相同的,即第一次調用函數完成之後,棧指針將回到函數進入之前的位置。

(2)在函數調用的過程中,每增加一個層次,棧空間就會被壓入更多的內容,下面的程序驗證了這一點:

#include <stdio.h>
#include <stdlib.h>
void stack_test1(int a, int b, int c);
void stack_test2(int a, int b, int c);

int main(void)
{
 int a = 1, b = 2, c = 3;
 
 printf("直接調用stack_test1函數:\n");
 stack_test1(a, b, c);
 printf("通過stack_test2函數間接調用stack_test1函數:\n");
 stack_test2(a, b, c);
 
 return 0;
}

void stack_test1(int a, int b, int c)
{
 printf("a = %d, &a = %#x \n", a, (unsigned int)&a);
 printf("b = %d, &b = %#x \n", b, (unsigned int)&b);
 printf("c = %d, &c = %#x \n", c, (unsigned int)&c);
}

void stack_test2(int a, int b, int c)
{
 stack_test1(a, b, c);
}

在這裏插入圖片描述
可見,在程序中兩次調用stack_test1函數,第一次是直接調用,第二次是通過stack_test2函數間接調用。從運行結果來看,通過stack_test2函數間接調用stack_test1函數的棧指針的值變小了,說明是由於棧中壓入了更多的內容。

(3)函數調用結束後,函數棧上的內容不能被其他函數使用。例如,下面是一種錯誤的用法:

int *stack_test3(void)
{
 int a;
 /* ...... */
 return (&a);
}

return(&a)將自動變量a的值返回,這種寫法不會發生編譯錯誤(又可能出現警告),但是其邏輯是不正確的。此時,調用者可以得到stack_test3運行時a的地址,但是由於變量a是建立在棧上,函數退出後,棧區域已經釋放,這個地址已經指向無效的內存,因此不應該再被程序使用。

2.2 堆內存區域

2.2.1堆的相關概念
在一般的編譯系統中,堆內存的分配方向和棧內存是相反的。棧內存利用的是處理器的硬件機制,而堆內存的處理使用的是庫函數。堆內存的分配形式如下圖:
在這裏插入圖片描述
可見,堆內存與棧內存的區別:棧內存只有一個入口點,就是棧指針,棧內存壓棧和出棧都只能通過棧指針及其偏移量;而堆內存有多個入口點,每次分配得到的指針就是訪問內存的入口,每個分配內存區域都可以被單獨釋放。
當頻繁的分配和釋放內存的過程中,將會出現如下情況:在兩塊已經分配的內存之間可能出現較小的未分配的內存區域,這些內存理論上可以被使用。但是由於它們的空間較小,不夠連續內存的分配,因此當分配內存的時候,它們經常不能被使用。這種較小的內存就是內存碎片。

2.2.2關於堆空間的使用及其一些問題:
(1)庫文件:stdlib.h
實現堆內存分配和釋放的4個主要函數爲:

/* 分配內存空間 */
void *malloc(size_t size);
/* 釋放內存空間 */
void free(void *ptr);
/* 分配內存空間 */
void *calloc(size_t num, size_t size);
/* 重新分配內存空間 */
void *realloc(void *ptr, size_t size);

(2)malloc和free的簡單應用

//malloc和free的簡單應用
void heap_test1(void)
{
 int *pa;
 
 pa = (int*)malloc(sizeof(int));
 if ( NULL != pa )
 {
   *pa = 0x1234;
   printf("pa = %#x, *pa = %x\n", (unsigned int)pa, *pa);
   free(pa);
 }
 
 return;
}

在malloc分配完內存之後,可以用得到的指針值是否爲NULL來判斷內存是否分配成功。按照C語言內存分配規則,如果內存分配成功,返回的是內存的地址;如果內存分配不成功,將返回NULL(0x0),表示一個無效的地址。

(3)malloc在分配內存的時候,是從低地址至高地址方向。但是,先分配的內存地址不一定比後分配的內存地址小。下面的程序驗證了這一點:
//後分配內存地址反而更小

void heap_test2(void)
{
 void *pa;
 void *pb;
 void *pc;
 void *pd;
 pa = (int*)malloc(1024);
 printf("pa = %#x \n", (unsigned int)pa);
 pb = (int*)malloc(1024);
 printf("pb = %#x \n", (unsigned int)pb);
 pc = (int*)malloc(1024);
 printf("pc = %#x \n", (unsigned int)pc);
 free(pb);
 pd = (int*)malloc(1024);
 printf("pd = %#x \n", (unsigned int)pd);

 free(pa);
 free(pc);
 free(pd);
 
 return;
}

程序運行結果:
在這裏插入圖片描述
可見,在該程序中,首先3次分配1024字節的堆上內存,然後再將第二次分配的內存釋放,再次分配內存時,將利用了這一塊空間。
(4)calloc()和malloc()很類似,主要區別是calloc()可以將分配好的內存區域的初始值全部設置爲0,以下程序驗證了這一點:

//calloc和malloc的主要區別
void heap_test3(void)
{
 unsigned int *pa;
 int i;
 
 pa = (unsigned int*)calloc(sizeof(unsigned int), 5);
 if ( NULL != pa )
 {
   printf("<< colloc pa = %#x >>\n", (unsigned int)pa);
   for ( i = 0; i < 5; i++ )
   {
     printf("pa[%d] = %d \n", i, pa[i]);
   }
   free(pa);
 }
 
 return;
}

程序運行結果:
在這裏插入圖片描述
除此之外,calloc()和malloc()另外一個不同之處在於參數的個數,malloc只有一個參數,即要分配的內存字節數;calloc有兩個參數,第一個是分配單元的大小,第二個是要分配的數目。從本質上,calloc使用兩個參數和malloc使用一個並沒有區別。

(5)realloc的應用。realloc函數具有兩個參數,一個是指向內存的地址指針,另一個是重新分配內存的大小,而返回值是指向所分配內存的指針。基本應用代碼如下:

//realloc的應用
void heap_test4(void)
{
 int *pa;
 int i;
 
 pa = (int*)malloc(sizeof(int)*6);
 if ( NULL != pa ){
   for ( i = 0; i < 6; i++ ){
     *(pa + i) = i;
   }
   for ( i = 0; i < 6; i++ ){
     printf("pa[%d] = %d \n", i, pa[i]);
   }
 }
 printf("relloc重新分配內存\n");
 pa = (int*)realloc(pa, sizeof(int)*10);
 if ( NULL != pa ){
   for ( i = 0; i < 10; i++ ){
     printf("pa[%d] = %d\n", i, pa[i]);
   }
   free(pa);
 }
 
 return;
}

程序運行結果:+在這裏插入圖片描述
除此之外,realloc還具有兩種功能:一是當指針爲NULL的時候,作爲malloc使用,分配內存;二是當重新分配內存大小爲0的時候,作爲free使用,釋放內存。

(6)再堆內存的管理上,容易出現以下幾個問題:
開闢的內存沒有釋放,造成內存泄漏

//內存泄漏例子
void heap_test6(void)
{
 char *pa;
 pa = (char*)malloc(sizeof(char)*20);
 /* ...... */
 
 return;
}

在函數heap_test6中,使用malloc開闢了20個字節的內存區域,但是使用結束後該函數沒有釋放這塊區域,也沒有通過任何返回值或者參數的手段將這塊內存區域的地址告訴其它函數。此時,這20個字節的內存不會被任何程序釋放,因此再調用該函數的時候,就會導致內存泄漏。

  • 野指針被使用或者釋放
    野指針是一個已經被釋放的內存指針,它指向的位置已經被free或者realloc釋放了,此時再使用該指針,就會導致程序的錯誤。野指針例子:
//野指針例子
void heap_test6(void)
{
 char *pa;
 pa = (char*)malloc(sizeof(char)*20);
 /* ...... */
 free(pa);
 /* ...... */
 printf("pa = %s \n",pa); //野指針被使用
 
 return;
}

在此程序中,調用free函數已經釋放了pa指針,但後面還在繼續使用pa,這就是一個錯誤的程序。

  • 非法釋放指針

1) 非法釋放靜態存儲區的內存,示例如下:

//非法釋放靜態存儲區的內存
void heap_test7(void)
{
 /* ...... */
 /* 錯誤釋放只讀數據區指針 */
 free(ro_data);
 /* 錯誤釋放已初始化讀寫數據區指針 */
 free(rw_data);
 /* 錯誤釋放未初始化讀寫數據區指針 */
 free(bss_data);
 
 /* 錯誤釋放代碼區指針 */
 free(heap_test7);
 /* ...... */
 return;
}

2) 非法釋放棧上的內存,示例如下:

//非法釋放堆上的內存---2
void heap_test9(void)
{
 char *pa;
 /* ...... */
 pa = (char*)malloc(sizeof(char)*20);
 free(pa);
 free(pa);  //錯誤釋放堆內存
 /* ...... */
 return;
}

第一次釋放之後,該地址已經變成了未被分配的堆上的內存了,free函數不能釋放未分配的堆內存。

3) 非法釋放棧上的內存,示例如下:

//非法釋放堆上的內存---3
void heap_test10(void)
{
 char *pa;
 char *pb;
 /* ...... */
 pa = (char*)malloc(sizeof(char)*20);
 pb = pa++;
 free(pb);  //錯誤釋放堆內存
 /* ...... */
 return;
}

釋放內存pb是非法的內存釋放,由於這個指針並不是從malloc分配出來的,而是一箇中間的指針值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章