Bootstrap

C语言-动态内存分配讲解

e6580e187d94491f9f9bdf87002464b8.jpeg

目录

✨1.什么是动态内存分配

💕2.动态内存开辟函数 malloc

✨3.malloc函数的检查(两种方法)

  💕4.动态内存释放函数 free

 ✨5.free 函数接收空指针

✨6.为什么要释放动态内存

💕7.动态内存开辟函数calloc

💕8.动态内存调整函数 realloc

✨9.动态内存分配——分配数组

9.1  动态内存分配——一维数组

9.2  动态内存分配——二维数组


春秋蝉在耳边鸣 无人与我并肩行        

永生路上多坎坷 这路一人也走的


  

✨1.什么是动态内存分配

我们在处理内存时,内存区域主要分为三块区域,分别为:栈区,堆区,静态区

而我们经常使用的语句其实都是在栈区开辟空间的,如:

int arr[10];
char str[20];

struct S s[10];

int a;

float b;

char c;

但是使用这样的声明,在栈区开辟空间时是有一定风险的

存在风险:

1. 开辟出来的空间是固定的,不能进行具体字节大小的修改,所以可能浪费掉栈区的空间大

2. 栈区空间开辟太大可能导致栈溢出

因此,我们可以将一些东西存储在堆区中,堆区的空间是本身就存在的,我们要做的就是分配出来并利用这些空间,因此也叫做动态内存分配

动态内存的分配是在堆区完成的,同时,堆区的内存空间几乎于无限大,因此不必担心如栈溢出的问题 


动态内存分配是需要调用动态内存函数实现的,下面介绍四种内存函数

点击超链接可以进行访问函数

💕2.动态内存开辟函数 malloc

动态内存开辟函数访问链接 malloc

#include<stdlib.h>
void* malloc(size_t size)
//以上是malloc函数的头文件以及参数

malloc 函数会返回一个void*类型的指针,这个指针是 malloc开辟空间大小的首地址

size表示空间大小是size个字节

 动态内存函数 malloc 可以在堆区开辟出一定的空间大小供程序员使用

1. malloc如果开辟成功,会返回所开辟空间的首地址

2. malloc如果开辟失败,就会返回空指针(NULL指针) 

我们无法确定malloc函数100%开辟成功,所以在利用malloc函数开辟空间时一定要进行检查

使用效果如下:

5f91c02d22f54fee933b278829890c55.png

在使用malloc函数开辟空间时,因为malloc函数返回的地址是void*类型,因此我们需要根据自己的实际需要,进行地址类型的转换 

如图:我们用malloc在堆区开辟了40个字节的内存空间大小,解引用p一次访问4个字节的大小 

但我们会发现,我们使用malloc函数开辟的空间大小,没有解引用的部分都不会初始化值为0,返回全部都是cd,这是因为malloc函数在开辟空间大小时不会初始化内容


但我们这样使用malloc函数其实是不对的,之前我们强调过,malloc函数的使用一定要进行检查,否则编译器就会出现警告,如下图:

421fe78d67604377b6429170eca88e64.png

那么我们该如何解决呢?


✨3.malloc函数的检查(两种方法)

我们使用完malloc函数开辟内存后,因为无法100%确定开辟成功,所以我们需要对malloc函数是否开辟成功进行检查

检查的两种方法:

1. 利用assert函数进行检查

2. 使用if语句进行检查

1.assert情况检查

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#define NDEBUG
int main() {
	int* p = (int*)malloc(40);
	assert(p != NULL);
	*p = 20;
	printf("%d", *p);
}

malloc函数开辟失败就会返回空指针,因此只需要判断是否为空指针即可,如果为空指针,assert就会报错并停止运行代码(使用assert不用担心 NDEBUG 注释掉)

但使用assert有一点不好,如果我们再使用assert函数断言其他代码的同时,确定完malloc函数开辟成功后不能使用#define NDEBUG 注释掉assert函数,因为无法判断下一次执行代码malloc是否会开辟成功,这样就会影响代码的运行速度。可以使用assert函数,并不推荐用

2.if语句判断

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
#include<stdlib.h>
int main() {
	int* p = (int*)malloc(40);
	if (p == NULL) {
		perror("malloc");
		return 1;
	}
	*p = 20;
	printf("%d", *p);
}

 使用 if 语句,只需要判断是否为空指针即可,如果为空指针,我们可以使用perror函数打印出错误信息,并及时结束代码返回1,推荐使用 if 语句


  💕4.动态内存释放函数 free

我们在堆区开辟完空间后,我们可以使用 free 函数释放掉开辟的内存,并将这块内存空间还给操作系统

动态内存释放函数访问 free

#include<stdlib.h>
void free(void* ptr)

free函数接收一个因动态内存函数所开辟内存空间的首地址

注意:

1. free函数接受的地址一定是动态内存在堆区开辟空间的地址,不能用栈区的地址

2.free函数所接受的地址一定是动态内存开辟堆区地址的首地址

以上两种情况,如果实现任意一种,代码直接崩

75536386ad974c5bb6485a6045810fb6.png45cbc587b02c482285a428a667e69812.png


 free函数正确示例如下:

free 释放前的内存

f86364fbe7d149a9b900991f319e82f8.png

free释放后的内存

fac472dfe03446119856fda64127c36d.png

注意:释放空间之后,free函数接收的指针(也就是因动态内存分配出的空间的首地址)就没有任何意义,但 free 不会自动给它置成空指针,此时他就成为一个野指针,所以我们要即及时将其置成空指针,如下:

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
#include<stdlib.h>
int main() {
	int* p = (int*)malloc(40);

	if (p == NULL) {
		return 1;
	}

	free(p);
	p == NULL;
    //初始化为空指针
    //野指针在使用时是非常危险的,因为你不知道它指向哪里,所以要及时定义为空指针
}

 ✨5.free 函数接收空指针

free函数在接受空指针时会进行摆烂状态,什么也不做,如下:

650c5fba0426455d8a7c8d1637965746.png  


✨6.为什么要释放动态内存

在我们利用动态内存分配出来的空间后,这些分配出来的空间只有两种方法可以释放掉

1. 利用free函数,将动态内存分配出来的空间释放掉

2. 等待程序自己结束,程序结束时会将这些空间释放掉

所以及时的释放掉堆区内存是非常重要的


💕7.动态内存开辟函数calloc

calloc函数与malloc函数都为开辟动态内存的函数,两者极其相似却有不同,接下来进行讲解

动态内存开辟函数访问 calloc

#include<stdlib.h>
void* calloc (size_t num, size_t size);

calloc函数的第一个参数 num 为元素的个数

                    第二个参数 size 为每个元素字节的大小

calloc函数与malloc函数相同点:

如果开辟成功,会返回所开辟空间中较小空间的地址

如果开辟失败,就会返回空指针(NULL指针) 

我们无法确定calloc函数100%开辟成功,所以在利用malloc函数开辟空间时一定要进行检查

calloc函数与malloc函数不同点

1. 参数不同

2. calloc函数在开辟完内存空间后,会将内存空间中每个字节全部初始化为0

效果如下:

b87326281fd541e89ce49860eda3f32a.png

462d408589a24b4b969985a59756b400.png

可见,calloc与malloc函数相似,开辟的所有内存,每一个字节都会初始化为0

在free方面用法相同


💕8.动态内存调整函数 realloc

动态内存调整函数 realloc

虽然我们能通过 malloc 或 calloc 函数动态内存分配一块空间,但这块空间分配完成后大小也是固定的,如果空间满了需要扩容或空间多了需要缩减,这时 realloc 函数就登场了

realloc 的作用是对已经动态分配的一块空间再次分配。

#include<stdlib.h>
void* realloc (void* ptr, size_t size);

它有两个参数:

  1. ptr:要调整的内存地址,这块内存是动态内存分配得到的,必须是首地址,如果不是首地址代码运行就会崩
  2. size:以字节为单位的新大小

它会返回调整之后的内存起始位置。

关于调整之后的内存起始位置会出现以下两种情况:

 1. 与原来内存的起始位置相同
 2. 与原来内存的起始位置不同


当要缩小原有的内存时,说明原来的内存空间已经足够,此时它的返回值就是原来内存的起始位置。


当要扩大原有的内存时,又有两种情况:
1. 原有空间之后有足够大的内存时,直接在原内存的基础上再开辟后边几个连续的空间,此时它的返回值是原来内存的起始位置;
2. 原有空间之后没有足够大的内存进行扩容时,此时会在堆空间上另找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址,与原来内存的起始位置不同。这时会拷贝原来空间的内容到新的空间的相应位置,原空间就被释放掉了。

虽然情况这么多,但其实很好更改与理解

所以,在使用 realloc 函数时就要注意用一个临时指针接收,当内存调整成功后再将临时指针赋给原指针

代码如下:

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
#include<stdlib.h>
int main() {
	int* p = (int*)malloc(50);
	if (p == NULL) {
		return 1;
	}

	int * ptr = (int*)realloc(p, 30);

	if (ptr != NULL) {
		p = ptr;
	}

	free(p);
	p = NULL;
}

realloc函数调整前:

8fa86ce350bc496d9385cceb0e58aaf6.png

realloc函数调整后: 

a437c3be48324b8b9b10eecc070f1a83.png

realloc函数在有一种情况下等价于malloc

代码如下: 

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

这种情况下,就相当于开辟了40个字节的空间大小


✨9.动态内存分配——分配数组

9.1  动态内存分配——一维数组

利用动态内存分配出一维数组是较容易的,我们知道一维数组在内存中存储是连续的,我们可以利用这一点,动态内存分配出连续的空间,然后进行赋值利用,就实现了动态内存分配一维数组

代码如下:

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
#include<stdlib.h>
int main() {
	int* p = (int*)malloc(40);

	if (p == NULL) {
		return 1;
	}

	for (int i = 0; i < 10; i++) {
		p[i] = i;
	}

	for (int i = 0; i < 10; i++) {
		printf("%d ", p[i]);
	}

	free(p);
	p = NULL;
}

可能不懂的点:

1. p[ i ] = *(p+i)


9.2  动态内存分配——二维数组

 使用动态内存分配出二维数组,我们有三种方法

第一种:利用二维数组其实是一堆一维数组的原理,开辟足够多的一维数组,并当作二维数组使用

d02a9b3a49e4460585c2cea05bba2042.png

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#define row 8
#define line 5
int main()
{
	
	int * p = (int*)malloc(row*line*sizeof(int));
	//或者int * p = (int*)malloc(160*sizeof(char));
    //开辟40个int类型的空间大小

	int count = 1;
	assert(p!=NULL);
	for(int i = 0;i<row;i++){

		for(int j = 0;j<line;j++){
			*(p+(i*line)+j) = count++;
		}
	}
	
		for(int i = 0;i<row;i++){
		for(int j = 0;j<line;j++){
			printf("%d ",	*(p+(i*line)+j));
		}
		printf("\n");
	}
	free(p);
	p = NULL;

}	

第二种:开辟4个 int* 类型的空间大小,让每一个 int* 都指向 5个int 的首地址 

 d2aad90a230e4e88a5558ecf7d43d764.png

#define _CRT_SECURE_NO_WARNINGS 
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
int main()
{
	int** p = (int**)malloc(4 * sizeof(int*));
	assert(p != NULL);
	for (int i = 0; i < 4; i++) {
		*(p + i) = (int*)malloc(5 * sizeof(int));
		assert(p + i != NULL);
	}
	int count = 1;
	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 5; j++) {
			assert((*(p + i)) + j != NULL);
			//*(*(p + i) + j * sizeof(char)) = count++;
			*((*(p + i)) + j) = count++;

		}
	}

	for (int i = 0; i < 4; i++) {
		for (int j = 0; j < 5; j++) {
			assert((*(p + i)) + j != NULL);
			//*(*(p + i) + j * sizeof(char)) = count++;
			printf("%d ", * ((*(p + i)) + j) );

		}
		printf("\n");
	}


	//释放每个int*所代表的5个int
	for (int i = 0; i < 4; i++) {
		free(*(p + i));
		*(p + i) = NULL;
	}

	//释放一级指针
	free(p);
	p = NULL;
}

这里一定要都释放,只释放 int* 的空间并不会释放 int 的空间

3e4fc21fb6644a9eaea23095a00d0f34.png

;