【C语言学习笔记 --- 动态内存管理】

C语言程序设计笔记---029

  • C语言之动态内存管理
    • 1、介绍动态内存管理
    • 2、动态内存函数的介绍
      • 2.1、malloc和free函数
      • 2.2、calloc函数
      • 2.3、realloc函数
    • 3、动态内存管理过程中,一些常见的错误
      • 3.1、对NULL指针的解引用操作
      • 3.2、对动态内存开辟的空间的越界访问
      • 3.3、对非动态开辟内存使用了free函数
      • 3.4、使用free释放一块动态开辟内存的一部分
      • 3.5、对同一块动态内存多次释放
      • 3.6、动态开辟内存,忘记释放(导致内存泄漏)
    • 4、柔性数组介绍
      • 4.1、柔性数组的使用
    • 5、结语

C语言之动态内存管理

前言:
通过C语言自定义类型的知识,这篇将对动态内存管理,进行深入学习和巩固相关的知识。

/知识点汇总/

1、介绍动态内存管理

概念:动态内存管理是指在程序运行时,根据需要动态地分配和释放内存空间的过程。
那么什么时候需要应用到动态分配内存呢?
通常来说,我们经常使用的数组,一般都是定长数组,因为在内存中申请的空间是固定的;
比如,当我明确知道需要采集100个人的成绩的时候,那么就可以使用指明长度的数组 int arr[101];
那么当我不知道采集多少人的时候,长度定1000,可能采集全校学生的信息就太小,又可能采集一个班级又太大,导致分配的空间资源得不到很好的利用,甚至浪费。
所以此时,根据C语言标准,它赋予程序员一种权力:能够动态申请和管理内存空间
目的是,合理的利用存储空间使得效率更高,让我们有能力灵活的运用而得心应手。
值得注意的是:动态内存管理的操作是在 堆区 上进行的操作的

2、动态内存函数的介绍

在C/C++中,动态内存管理主要通过malloc、calloc、realloc和free等函数来实现。
其中,malloc函数用于分配指定大小的内存空间,calloc函数用于分配指定数量和大小的内存空间并初始化为0,realloc函数用于重新分配已经分配的内存空间,而free函数则用于释放已经分配的内存空间。

2.1、malloc和free函数

malloc函数:
原型:void* malloc(size_t size);
头文件
功能:向内存申请一块连续可用的空间,并返回指向这块空间的指针
返回值
如果开辟成功,则返回一个指向开辟好的空间的指针
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要判定检查(否则可能为野指针)
free函数
原型:void free( void memblock );
头文件
功能:释放申请的内存空间,通常与申请内存空间的函数malloc()结合使用,可以释放由 malloc()、calloc()、realloc() 等函数申请的内存空间。
补充
1)、返回值的类型是void
,所以malloc函数并不确定开辟空间的类型,由使用者自己决定。
2)、如果参数size为0,malloc的行为是标准是未定义的,取决于当前的编译器。
3)、当然我们申请空间也存在失败的情况,所以需要考虑各种情况,做到代码的严谨性。
示例如下所示

#include 
#include 
#include //调用INT_MAX需申明的头文件
int main()
{
	//申请一块空间,用来存放10个整型
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");//开辟空间失败,打印错误原因
		return 1;
	}
	//当申请的空间太大或被占用就会出现失败的情况
	int* p2 = (int*)malloc(INT_MAX*4);//INT_MAX开辟一个整型最大值的空间
	if (p2 == NULL)
	{
		perror("malloc");//开辟空间失败,打印错误原因
		return 1;
	}
	return 0;
}

关于动态内存的释放
分为两种释放方式
1.主动释放:通过free函数主动释放空间
2.被动释放:程序退出后,被操作系统回收释放
正常情况下:谁申请谁释放,万一自己不释放,也得交代给别人释放
补充
1)、如果参数ptr指向的空间不是动态开辟的,那么free函数的行为就是C标准未定义的
2)、如果参数ptr是NULL指针,则函数什么事都不做,即无用功

#include 
#include 

int main()
{
	//申请一块空间,用来存放10个整型
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");//开辟空间失败,打印错误原因
		return 1;
	}
	//动态内存的使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	//释放动态内存空间 -- free
	free(p);
	p = NULL;//必须将p指针开辟的空间置为空,避免导致野指针
	return 0;
}

2.2、calloc函数

原型:void* calloc(size_t num,size_t size);
功能
为Num个大小为size的元素开辟一块空间,并且把空间的每一个字节初始化为0;
与malloc函数的区别只在于callloc会返回地址之前把申请的空间的每一个字节初始化为全0
头文件
与malloc函数对比
1)、功能对比:calloc会把每一个字节初始化为全0,而malloc不会初始化
2)、函数原型对比
void* calloc(size_t num,size_t size);
void* malloc(size_t size);
示例如下

#include 
#include 
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//打印
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));//随机值,堆区 --- 不是ccccc
	}
	//释放
	free(p);
	p = NULL;
	int* p2 = (int*)calloc(10 , sizeof(int));
	if (p2 == NULL)
	{
		perror("calloc");
		return 1;
	}
	//打印
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p2 + i));//0,堆区 --- calloc初始化
	}
	//释放
	free(p2);
	p2 = NULL;
	return 0;
}

2.3、realloc函数

原型:void* realloc(void* ptr,size_t size);
功能:使得动态内存管理更灵活,即对动态内存空间进行调整
头文件
返回值
返回的是一个void类型的指针:调用成功。(这就要求在你需要的时候进行强制类型转换)
返回NULL:当需要扩展的大小(第二个参数)为0并且第一个参数不为NULL时。此时原内存变成“free(游离)”的了。
返回NULL:当没有足够的空间可供扩展的时候。此时,原内存空间的大小维持不变。
realloc函数开辟空间的多种情况
1.特殊情况:后续空间可能被占用,不能直接增加
那么realloc函数会找一块新的空间,足够一次性能全方进所有数据的空格,并会将旧空间的数据,一同拷贝到新空间
其次,完成新空间开辟拷贝后,就释放旧空间,最后返回指向新空间的地址/指针
2.正常情况:原有空间之后有足够大的空间,返回调整之后的内存起始地址
示例如下所示

#include 
#include 

int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	int i = 0;
	//使用空间存储数据
	for (i = 0; i < 10; i++)
	{
		p[i] = i;
	}
	//打印
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));//0,堆区 --- calloc初始化
	}
	//空间不够时,调整空间为20个整型的空间
	int* ptr = (int*)realloc(p, 20 * sizeof(int));
	if (ptr != NULL)//开辟空间成功
	{
		p = ptr;
	}
	//释放
	free(p);
	return 0;
}

realloc函数开辟空间的特殊情况:后续空间可能被占用,不能直接增加

#include 
#include 
	int main(int argc, char* argv[])
	{
	char *p,*q;
	p = (char *)malloc(10);
	q = p;
	p = (char *)realloc(p,10);
	printf("p=0x%x/n",p);
	printf("q=0x%x/n",q);
	return 0;
	}

输出结果:realloc后,内存地址发生了变化 p=0x431a70 q=0x431a70

#include 
#include 
int main(int argc, char* argv[])
{
	char *p,*q;
	p = (char *)malloc(10);
	q = p;
	p = (char *)realloc(p,1000);
	printf("p=0x%x/n",p);
	printf("q=0x%x/n",q);
	return 0;
}

输出结果:realloc后,内存地址发生了变化 p=0x351c0 q=0x431a70

realloc函数平替mallloc函数的技巧使用:

#include 
#include 
int main()
{
	int* p = (int*)realloc(NULL, 40);//等价 -- malloc(40);
	if (p == NULL)
	{
		perror("realloc");
		return 1;
	}
	free(p);
	p = NULL;
	return 0;
}

3、动态内存管理过程中,一些常见的错误

在动态内存管理过程中,需要注意避免一些常见的错误
如对NULL指针的解引用操作、对动态开辟的内存进行越界访问、对非动态开辟的内存free、使用free释放动态内存的一部分、对同一块动态内存多次释放等。

3.1、对NULL指针的解引用操作

#include 
int main()
{
	int* p = (int*)malloc(40);
	//当此时不作返回值判断时,则有可能是对空指针进行操作,非法访问
	//野指针
	*p = 20;
	//解决办法:添加判断后执行
	//if (p == NULL)
	//{
	//	//...
	//}
	return 0;
}

3.2、对动态内存开辟的空间的越界访问

#include 
#include 
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	int i = 0;
	//越界访问,对开辟的空间越界访问了
	for (i = 0; i <= 10; i++)
	{
		p[i] = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

3.3、对非动态开辟内存使用了free函数

#include 
#include 
int main()
{
	int a = 10;
	int* p = &a;
	//对非动态内存空间释放空间 -- error
	free(p);
	p = NULL;
	return 0;
}

3.4、使用free释放一块动态开辟内存的一部分

#include 
#include 
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*p = i;
		p++;
	}
	// 0 1 2 3 4 0 0 0 0  ---- error
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);//只释放了一部分动态开辟的内存空间
	//更好的办法是引用,另一个指针变量,使得始终保存p的起始地址
	p = NULL;
	return 0;
}

3.5、对同一块动态内存多次释放

#include 
#include 
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//...
	free(p);//error
	//解决办法:始终与free搭配释放后置为空指针
	//p = NULL;//有了空指针,以免后面再次释放时,及时报错提醒
	//....
	free(p);
	p = NULL;
	return 0;
}

3.6、动态开辟内存,忘记释放(导致内存泄漏)

#include 
#include 
void test()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//不释放 -- error
	//....
	//解决办法1:谁申请谁及时释放
	//free(p);
	//p = NULL;
}
int main()
{
	test();//在函数内开辟的动态内存,不释放
	//解决办法2:使void* test 设置返回值--->int* test(),返回p
	//用指针变量接收p
	//int* ret = test();
	// free(ret);
	// ....
	while (1);
	return 0;
}

4、柔性数组介绍

概念:C99标准中,规定结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员;柔性数组是结构体的成员
比如

typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}st_type;

4.1、柔性数组的使用

方法一:使用数组的方法

#include 
#include 
struct S
{
	char c;//1
	int i;//4
	int arr[];//未知大小的数组 -- 柔性数组成员
};
int main()
{
	//printf("%zd\n", sizeof(struct S));//8,sizeof返回的这种结构大小不包括柔性数组的内存
	//即,不包括柔性数组的内存大小。不做计算
	struct S* ps = (struct s*)malloc(sizeof(struct S) + 20);
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	ps->c = 'w';
	ps->i = 100;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps->arr[i] = i;
	}
	//打印
	for (i = 0; i < 5; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	//如果空间不够,就可使用柔性数组和内存函数搭配,灵活使用动态内存
	struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 40);
	if (ptr != NULL)
	{
		ps = ptr;
	}
	else
	{
		perror("realloc");
		return 1;
	}
	//释放
	free(ps);
	ps = NULL;

	return 0;
}

方法二:使用指针的方法

#include 
#include 
struct S
{
	char c;//1
	int i;//4
	int* data;
};
int main()
{
	struct S* ps = (struct s*)malloc(sizeof(struct S) + 20);
	if (ps == NULL)
	{
		perror("malloc1");
		return 1;
	}
	ps->c = 'w';
	ps->i = 100;
	ps->data = (int*)malloc(20);
	if (ps->data == NULL)
	{
		perror("malloc2");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps->data[i] = i;
	}
	//打印
	for (i = 0; i < 5; i++)
	{
		printf("%d ", ps->data[i]);
	}
	//如果空间不够,就可使用柔性数组和内存函数搭配,灵活使用动态内存
	int* ptr = (int*)realloc(ps->data, 40);
	if (ptr == NULL)
	{
		perror("realloc");
		return;
	}
	//释放
	free(ps->data);
	ps->data = NULL;
	free(ps);
	ps = NULL;
	return 0;
}

小结
1.结构中的柔性数组成员前面必须至少一个其他成员
2.sizeof返回的这种结构大小不包括柔性数组的内存
3.包含柔性数组成员的结构用malloc函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
对比两种方法
柔性数组加动态内存与相比指针加动态内存的方法指针的方法更好
1.方便内存释放
2.有利于访问速度

5、结语

掌握动态内存管理用于利于我们把知识嚼碎的消化,从而更好地理解和控制内存,提高程序的效率和性能,减少内存碎片和错误,增强程序的灵活性和适应性。同时,也要避免错误和内存泄漏的可能性。
名人名言分享:
“我愿意投入时间和精力去学习,因为我知道这将使我在未来的生活中受益。” — 爱因斯坦
“生活就像海洋,只有意志坚强的人,才能到达彼岸。” — 马克思
“只有那些敢于相信自己内心有某种比生存更重要的东西的人,才能过上所想象的那种充满快乐的生活。” — 契诃夫

你可能感兴趣的:(C语言基础,c语言,笔记,柔性数组,动态内存管理,malloc函数,calloc函数,realloc函数)