內存管理
C語言中,棧內存(局部變量)自動分配/釋放,靜態區(全局、靜態變量)編譯時分配;堆內存需手動分配/釋放,核心函數有3個:
?malloc
函數
- 原型:
void* malloc(size_t size);
- 功能:在堆上分配連續的
size
字節內存,不初始化內存內容。 - 返回值:成功則返回指向內存首地址的
void*
指針;失敗返回NULL
(如內存不足)。 - 用法示例:
int *p = (int*)malloc(5 * sizeof(int)); // 分配能存5個int的內存(需強轉) if (p == NULL) { // 檢查分配失敗 printf("malloc failed\n"); return -1; }
?calloc
函數
- 原型:
void* calloc(size_t num, size_t size);
- 功能:在堆上分配**
num
個大小為size
的連續內存塊**,且自動初始化為0。 - 返回值:成功返回內存首地址
void*
;失敗返回NULL
。 - 與
malloc
的區別:calloc
會初始化內存為0,且參數是“個數+單個大小”(malloc
是總字節數)。 - 用法示例:
int *q = (int*)calloc(5, sizeof(int)); // 分配5個int,每個初始化為0 if (q == NULL) { /* 錯誤處理 */ }
?realloc
函數
- 原型:
void* realloc(void* ptr, size_t new_size);
- 功能:調整已分配內存塊的大小(基于
ptr
指向的原內存)。 - 調整邏輯:
- 若原內存后有足夠空間,直接擴展,返回原地址;
- 若空間不足,重新分配新內存塊,復制原數據到新塊,釋放原內存,返回新地址;
- 若
ptr
為NULL
,等價于malloc(new_size)
;若new_size
為0,等價于free(ptr)
(不同實現有差異)。
- 返回值:成功返回新內存首地址;失敗返回
NULL
(原ptr
指向的內存不變)。 - 用法示例:
p = realloc(p, 10 * sizeof(int)); // 將p的內存從5個int擴容到10個int if (p == NULL) { /* 錯誤處理(原p內存仍有效) */ }
內存釋放函數free
- 原型:
void free(void* ptr);
- 功能:將
ptr
指向的動態分配的堆內存歸還給系統,避免內存泄漏。 - 注意點:
ptr
必須是malloc
/calloc
/realloc
返回的指針(否則行為未定義,如野指針、棧內存指針);- 釋放后
ptr
變為懸空指針(指向無效內存),需手動置NULL
避免誤用; - 不可重復釋放同一指針(未定義行為,可能崩潰)。
- 用法示例:
free(p); p = NULL; // 釋放后置空,避免懸空指針
常見問題與注意事項
- 分配失敗檢查:調用
malloc
/calloc
/realloc
后,必須判斷返回值是否為NULL
,否則后續操作空指針會崩潰。 - 內存泄漏:動態分配的內存未用
free
釋放,程序結束前不會自動回收,長期運行會耗盡內存。 - 懸空指針:
free
后指針未置NULL
,后續若誤操作(如解引用、再次free)會觸發未定義行為。 - 內存越界:訪問分配內存范圍外的地址(如
p[5]
但只分配了5個int,索引0~4有效),會破壞內存結構,導致程序崩潰或數據錯亂。 - realloc的風險:若
realloc
失敗返回NULL
,原ptr
仍有效,需單獨處理(如備份原指針)。
C語言動態內存管理核心是malloc
/calloc
/realloc
分配堆內存,free
釋放內存。需牢記分配必檢查、釋放要置空、避免越界/重復釋放,才能安全管理內存。
指針
一、指針的基本概念
定義與聲明
指針是存儲另一個變量的地址的變量。其聲明形式如下:
數據類型 *指針名;
其中
數據類型
是指針指向的數據類型的說明符,而*
表示這是一個指針變量。示例:
int *p; // p 是一個指向整數的指針
取地址運算符(&)和解引用運算符(*)
&
:取地址運算符,用于獲取變量的地址。int a = 5; int *p = &a; // p 現在保存了 a 的地址
*
:解引用運算符,訪問指針所指向的值。int value = *p; // value 將會是 5,即 p 所指向位置的值
二、指針與數組
在C語言中,數組名實際上是一個指向數組第一個元素的常量指針。這意味著你可以使用指針來遍歷數組。
示例:
int arr[5] = {1, 2, 3, 4, 5};
int *p = arr; // 等價于 int *p = &arr[0];
for (int i = 0; i < 5; ++i) {printf("%d ", *(p + i)); // 使用指針訪問數組元素
}
三、指針與函數
傳遞指針作為參數
通過傳遞指針給函數,可以在函數內部修改調用者提供的變量的值。
示例:
void increment(int *n) {(*n)++; }int main() {int number = 10;increment(&number);// number 現在是 11 }
返回指針
函數可以返回一個指針,但需要小心處理動態分配的內存以避免內存泄漏。
示例:
int* createArray(int size) {return malloc(size * sizeof(int)); }
四、指針的算術運算
指針支持加法和減法操作,允許你移動指針指向不同的內存位置。
指針加法:將指針向前移動若干個元素的位置。
int arr[] = {1, 2, 3}; int *p = arr; p++; // p 現在指向 arr[1]
指針減法:將指針向后移動若干個元素的位置或計算兩個指針之間的距離。
五、多級指針
指針本身也可以有地址,即指向指針的指針或多級指針。
示例:
int a = 10;
int *p = &a; // p 是一個指向 int 的指針
int **pp = &p; // pp 是一個指向 int* 類型指針的指針
六、void指針
void*
是一種特殊類型的指針,它可以指向任何數據類型的變量,但它不能直接解引用,因為編譯器不知道它實際指向的數據類型。
示例:
void* ptr;
int a = 10;
ptr = &a;
int *intptr = (int*)ptr; // 需要強制轉換為具體類型才能解引用
七、指針的安全注意事項
空指針檢查:在使用指針前,應該檢查它是否為
NULL
。if (ptr != NULL) {// 安全使用指針 }
避免懸空指針:當指針指向的內存被釋放后,該指針變成懸空指針。訪問懸空指針會導致未定義行為。
正確管理動態內存:使用
malloc
,calloc
,realloc
分配內存,并確保使用free
釋放不再使用的內存以避免內存泄漏。
函數指針
在C語言中,指針不僅可以指向變量,還可以指向函數。函數指針是一種特殊的指針類型,它存儲的是函數的起始地址,可以通過這個指針來調用函數。
一、定義和聲明函數指針
定義一個函數指針需要指定其指向的函數的返回類型以及參數列表。其基本語法如下:
返回類型 (*指針名)(參數類型列表);
例如,假設有一個返回類型為int
,接受兩個int
類型參數的函數,那么對應的函數指針可以這樣定義:
int add(int a, int b) {return a + b;
}int (*funcPtr)(int, int); // 定義一個函數指針
二、初始化函數指針
你可以將函數的名字賦值給函數指針,因為函數名本質上是指向函數入口點的指針。例如:
funcPtr = add; // 將add函數的地址賦給funcPtr
或者直接在聲明時初始化:
int (*funcPtr)(int, int) = add;
三、通過函數指針調用函數
一旦函數指針被初始化,就可以像普通函數那樣使用它來調用函數:
int result = funcPtr(3, 4); // 相當于調用add(3, 4)
printf("%d\n", result); // 輸出7
四、函數指針作為參數傳遞
函數指針可以作為參數傳遞給其他函數,這在實現回調機制時非常有用。例如:
void executeOperation(int (*operation)(int, int), int a, int b) {printf("Result: %d\n", operation(a, b));
}int main() {executeOperation(add, 5, 3); // 傳遞add函數的指針return 0;
}
五、函數指針數組
你也可以創建一個函數指針數組,用于存儲多個函數指針。這對于實現類似多態的行為很有幫助。
int subtract(int a, int b) {return a - b;
}int main() {int (*operations[2])(int, int) = {add, subtract}; // 函數指針數組int result1 = operations[0](10, 5); // 調用addint result2 = operations[1](10, 5); // 調用subtractprintf("Add: %d, Subtract: %d\n", result1, result2);return 0;
}
六、注意事項
- 類型匹配:函數指針的類型必須與它指向的函數的簽名完全匹配(包括返回類型和參數列表)。
- 空指針檢查:如同其他類型的指針一樣,使用前應確保函數指針不是NULL,避免未定義行為。
- 函數指針與函數本身的區別:雖然函數名可以直接賦值給函數指針,但它們并不完全相同。函數名是編譯時常量,而函數指針是一個變量,可以在運行時改變其所指向的函數。