C語言編程動態內存分配常見錯誤全面分析

前言:為什麼存在動態內存分配?

我們已經掌握的內存開辟方式如下

int a=10;//在棧空間上開辟4字節
char arr[10]={0};//在棧空間上開辟10字節連續空間

以上開辟空間的方法有兩個缺點:
1.空間開辟的大小是固定的。
2.數組在聲明的時候,必須指定數組長度,它所需要的內存在編譯時進行分配。
但是對於空間的需求,不僅僅是上述的情況。有時我們需要的空間大小在程序運行時才能知道,這時上述的方法就不能滿足需要瞭,我們來介紹一種解決方案:動態內存分配

一、動態內存函數

在這裡插入圖片描述

(圖片來自比特就業課)

計算機在使用時會有三個區:常見的有棧區——用來存放局部變量、函數形式參數;靜態區——用來存放靜態區和全局變量;最後一個堆區則是我們用來動態內存分配的,學習動態內存分配必須掌握以下4種函數:

1.malloc和free函數

malloc函數聲明:

void*malloc(size_t size);//size_t即unsigned int

該函數向內存申請一塊連續可用的空間,並返回指向這塊空間的指針。註意點如下:
1.如果開辟成功,返回一個指向開辟空間的指針
2.如果開辟失敗,返回一個NULL指針
(由於是NULL指針,所以對於malloc函數常用assert進行檢查)
3.返回值類型為void*,所以malloc函數開辟空間的類型需要使用者自己進行決定
4.如果size=0,malloc的行為取決具體編譯器

free函數聲明

void free(void*ptr);

free函數用來釋放動態開辟的空間,註意點如下:
1.如果參數ptr指向的空間不是動態開辟的,那free函數的行為是未知的
2.如果ptr是空指針,free函數什麼也不做
實戰舉例:

#include<stdio.h>
#include<stdlib.h>//malloc和free函數頭文件
int main()
{
	int*p=(int*)malloc(40);//malloc出來的空間是不確定類型的,需要你自己強轉一個類型
	//這裡我們把它轉換成int*,由整型指針對這塊空間進行維護
	//那這裡會是4個字節看成一個元素,40個字節,可填充10個int元素
	if (p == NULL)
	{
		return -1;
	}//如果沒有返回值則說明開辟成功
	int i = 0;
	for (i = 0;i < 10;i++)
	{
		*(p + i)=i;//對開辟空間進行賦值操作
	}
	free(p);//用完之後不需要瞭,用free函數釋放p所指向的空間(40個字節全部釋放掉,還給操作系統)
	p = NULL;//由於p存儲的是開辟空間的地址,即使空間還給瞭操作形態,但p還是可以找到這塊空間,這是非常危險的,所以我們用一個空指針賦給p
	return 0;
}

2.calloc函數

calloc函數也是用來動態內存分配的,函數聲明如下

void*calloc(size_t num,size_t size)

該函數功能是為num個大小為size的元素開辟一塊空間,並且把空間的每個字節都初始化為0
它與malloc函數的區別隻是在返回地址前會把申請空間裡的每個字節都初始化為0

代碼如下(示例):

#include<stdio.h>
#include<stdlib.h>//calloc函數頭文件
#include<string.h>//strerror函數頭文件
#include<errno.h>//errno頭文件
int main()
{
	int*p = (int*)calloc(10, sizeof(int));//開辟10個大小為int型的空間
	if (p == NULL)//calloc函數也有可能開辟空間失敗,需要進行檢驗
	{
		printf("%s\n", strerror(errno));//errno是錯誤碼,strerror會把錯誤碼轉換成相應的錯誤信息
		return -1;
	}
	int i = 0;
	for (i = 0;i < 10;i++)
	{
		printf("%d ", *(p + i));//會打印10個0(calloc會自己初始化為0)
	}
	free(p);
	p = NULL;
	return 0;
}

3.realloc函數

realloc函數是在已有空間不夠的情況下,進行追加申請空間的函數,其聲明如下:

void*realloc(void*memblock,size_t size);

realloc函數是為瞭讓動態內存管理更加靈活,有時候我們發現之前申請的空間過少瞭,或者過大瞭,我們可以用realloc函數來進行增減,它的註意點如下:
1.ptr是要調整的內存地址
2.size是調整之後新的大小
3.返回值是調整後的內存起始位置
4.該函數在調整原先內存大小的基礎上,還會將原來內存的數據復制到新空間
5.realloc函數在調整內存空間存在兩種情況
關於4和5解釋如下:
假設我們現在追加1倍空間

在這裡插入圖片描述

第一種:如上圖,紅色是我們開辟的空間,藍色是其他程序正在使用的空間,又因為我們要開辟空間肯定是物理上連續的,我們又不能使用藍色部分,中間的空白空間又完全不夠原先空間的兩倍,那怎麼辦?

在這裡插入圖片描述

我們會另外找一塊足夠空間的地方進行原空間兩倍的開辟,並且把原空間內數據進行復制到新空間,函數返回新空間的地址。

第二種:這種就比較簡單瞭,原先空間後面就足夠追加開辟一塊1倍的空間,我們直接進行開辟即可。

在這裡插入圖片描述

在這裡插入圖片描述

函數直接會返回原先空間的首地址。
當然瞭,realloc函數同malloc和calloc函數一樣,也有可能開辟空間失敗,所以依然需要檢驗是否返回的是空指針

#include<stdio.h>
#include<stdlib.h>//malloc和free函數頭文件
int main()
{
	int*p=(int*)malloc(40);//開辟10個int大小的空間
	int i = 0;
	for (i = 0;i < 10;i++)
	{
		*(p + i)=i;//對開辟空間進行賦值操作
	}
	int*ptr = realloc(p, 20 * sizeof(int));//註意!這裡20*sizeof(int)是新的大小
	//比如我現在隻有10個,我需要20個,差10個。但這裡不是寫10,而是寫新的大小20
	if (ptr != NULL)
	{
		p = ptr;
	}
	for (i = 10;i < 20;i++)//對新開辟空間賦值
	{
		*(p + i) = i;
	}
	for (i = 0;i < 20;i++)
	{
		printf("%d ", *(p + i));//打印0-19
	}
	free(p);
	p = NULL;
	return 0;
}

二、常見錯誤

1.對NULL指針解引用

代碼如下(示例):

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int*p = (int*)malloc(20);
	*p = 0;//對p這個地址解引用並賦值為0
	free(p);
	return 0;
}

malloc等等函數在開辟空間時都是有可能開辟失敗的,萬一失敗,就是返回空指針,你直接對空指針解引用並賦值肯定是有問題的

所以我們這裡還是要進行指針檢驗
代碼如下(示例):

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int*p = (int*)malloc(20);
	if(p==NULL)
	{
	return -1;
	//如果返回,開辟失敗結束程序,如果沒有返回則可進行下面的操作
	}
	*p = 0;//對p這個地址解引用並賦值為0
	free(p);
	return 0;
}

2.對動態開辟空間的越界訪問

代碼如下(示例):

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int*p = (int*)malloc(200);//200個字節也就是50個int型
	if (p == NULL)
	{
		return -1;
	}
	int i = 0;
	for (i = 0;i < 60;i++)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;
	return 0;
}

這個代碼乍一看上去沒有問題,但是仔細看的話就會發現端倪,malloc開辟200字節空間也就是50個int型,你for循環賦值最多循環次數也隻能是50次啊,你循環60次肯定是越界訪問瞭,這裡也是妥妥的會報錯。

3.對非動態開辟使用free函數

代碼如下(示例):

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int a = 10;
	int*p = &a;
	free(p);
	p = NULL;
}

我們這裡int創建瞭a,然後把a的地址賦給瞭int*類型的p,再然後free掉p。這種操作也是鐵定會報錯的,p這個局部變量是在棧上的,而free函數針對的是堆區

4.使用free釋放一塊動態內存開辟內存的一部分

代碼如下(示例):

//使用free釋放一塊動態內存開辟內存的一部分
#include<stdio.h>
#include<stdlib.h>
int main()
{
	int*p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		return -1;
	}
	//使用
	int i = 0;
	for (i = 0;i < 5;i++)
	{
		*p++ = i;
	}
	//釋放
	free(p);
	p = NULL;
}

這裡的代碼有什麼問題呢?我們畫一個圖就一目瞭然瞭

在這裡插入圖片描述

一開始p在上圖位置,然而隨著for循環,p++這個操作,p指向的位置不斷往後,一直到下圖位置

在這裡插入圖片描述

這時p已經不指向原先開辟空間的位置瞭,你這時候去用free釋放掉顯然是不合適的

5.對同一塊空間多次釋放

我們先來看2段代碼:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int*p = (int*)malloc(40);
	if (p == NULL)
	{
		return -1;
	}
	free(p);
	free(p);
}
#include<stdio.h>
#include<stdlib.h>
int main()
{
	int*p = (int*)malloc(40);
	if (p == NULL)
	{
		return -1;
	}
	free(p);
	p = NULL;
	free(p);
}

兩段代碼都是對同一塊空間多次釋放,但第一段代碼會報錯,第二段不會。
解釋如下:
第一段代碼你已經釋放掉p所指向的空間瞭,空間裡什麼也沒有瞭,但p仍然指向那塊空間,所以你再次釋放不屬於你的空間肯定會報錯。
第二段代碼你釋放掉p所指向空間,然後用空指針給p賦值,再去釋放空指針,我們知道,free空指針是什麼也不做,所以不會報錯。

6.動態開辟內存忘記釋放

對於動態開辟內存忘記釋放,在堆區上申請的空間有2種回收方式:
1.你自己free掉
2.程序退出時,系統自動回收
我們先來看一段代碼

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int*p = (int*)malloc(40);
	if (p == NULL)
	{
		return -1;
	}
	getchar();
	return 0;
}

該代碼我們沒有自己使用free來釋放內存,而中間又有getchar一直在等待接收字符,打個比方:假如你中途去上廁所或者幹其他事情瞭,getchar一直沒有接收到字符,程序就一直沒有結束,那我們用p開辟的空間在你上廁所期間就一直被占用,那塊空間系統沒辦法去做別的有意義的事情。而上升到將來公司層面:我們寫的程序可能一天24h都在跑,那遇到這種情況,你沒有free掉內存,你不用又不回收,整體效率的影響是非常大的。

總結

今天介紹瞭動態內存分配函數和一些常見的動態內存分配的錯誤,希望讀者學習有所收獲,祝讀者學業有成,萬事順心!

更多關於C語言動態內存分配的資料請關註WalkonNet其它相關文章!

推薦閱讀: