【C语言】动态内存管理(详细!!!)

目录

1、动态内存分配的重要性

1.0 动态内存管理的概念

1.1 为何需要动态内存分配

2、malloc和free

2.0 malloc

2.0.0 malloc 申请的空间和数组的空间有什么区别

2.1 free

3. calloc和realloc

3.0 calloc

3.1 realloc

4、动态内存管理中的常见错误

4.0 对NULL指针的解引用操作

4.1 对动态开辟空间的越界访问

4.2 对非动态开辟内存使用free释放

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

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

4.5 动态开辟内存忘记释放(内存泄漏)

5、动态内存经典笔试题解析

6、柔性数组(Flexible Array Member)

6.0 柔性数组概念

6.1 柔性数组特点

6.2 柔性数组的优势

7、C/C++程序内存区域划分总结


1、动态内存分配的重要性

1.0 动态内存管理的概念

动态内存管理,也称为动态内存开辟,是程序在运行时根据需要动态地分配和释放内存空间的过程。
在C语言中,动态内存分配通常通过系统提供的库函数来实现,如malloc、calloc和realloc等; free函数用于释放之前通过malloc、calloc或realloc分配的内存空间。需要注意的是,被释放的内存空间的值将变得不确定,且不应再被访问。

1.1 为何需要动态内存分配

已知:

int a = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间

从上面例子看出,上述的开辟空间的方式有两个特点

空间开辟大小固定。

数组在申明时必须指定数组的长度,数组空间一旦确定了大小不能调整。

所以C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活。

2、malloc和free

2.0 malloc

函数原型如下: 

 void* malloc (size_t size);

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

• 如果开辟成功,则返回一个指向开辟好空间的指针。

• 如果开辟失败,则返回一个 NULL 指针,因此malloc的返回值一定要做检查。

• 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。

• 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。

比如:我们想向内存申请10个整型内存空间,由于我们知道申请空间的数据是整型,所以我们将malloc的返回值进行了强制类型转换,本来,返回值应是void * 类型,但是我们将其强转为了int *

2.0.0 malloc 申请的空间和数组的空间有什么区别

它们在内存分配方式、开辟空间的位置、内存管理和灵活性等方面有所区别。


内存分配方式:

• malloc函数动态地在堆(heap)上分配内存。它用于申请一块连续的指定大小的内存块区域,并以void*类型返回分配的内存区域地址。如果分配成功,则返回指向被分配内存的指针;如果分配失败,则返回空指针NULL。

• 数组通常是在栈(stack)上分配的(对于局部数组而言),其大小在编译时就已确定,并且是连续分配的。全局数组或静态数组则是在程序的静态存储区分配空间,与程序的寿命一样长。
灵活性:动态内存大小是可以调整的,更灵活。

2.1 free

当不再需要某个内存空间时,程序应该通过动态内存释放将该空间归还给系统,以便其他部分可以使用。free函数就是专门是用来做动态内存的释放和回收的。

函数原型如下:  

void free (void* ptr);

free函数用来释放动态开辟的内存。

• 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。

• 如果参数 ptr 是NULL指针,则函数什么事都不做。 

⛳总代码:

注意:我们最后通过代码手动释放内存,如果不释放的话,程序结束后也会被操作系统自动回收,但这不是好习惯。而且如果后面还有代码(程序没有结束)的话,可能会造成空间浪费。

#include<stdio.h>
#include<stdlib.h>

int main()
{
	// 申请10 个内存你空间
	int* p = (int*)malloc(10*sizeof(int));
	if (p == NULL)
	{
		// 空间申请失败
		perror("malloc");
		return 1;
	}
	// 申请成功,可以使用这40个字节空间
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i + 1;
	}
	// 释放
	free(p);
	p = NULL;

	return 0;
}

3. calloc和realloc

3.0 calloc

calloc 函数也用来动态内存分配。

函数原型如下: 

void* calloc (size_t num, size_t size);

• 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。

• 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全 0。

换句话说,如果您需要一块已清零的内存空间来存储num个大小为size字节的元素,那么使用calloc 是一个方便且高效的选择,因为它同时完成了内存分配和初始化两个步骤。

⛳例子:

int main()
{
	// 申请10 个内存空间
	/*int* p = (int*)malloc(10 * sizeof(int));*/
	  
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		// 空间申请失败
		perror("calloc");
		return 1;
	}
	// 申请成功,可以使用这40个字节空间
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ",p[i]);// *(p+i)
	}
	free(p);
	p = NULL;

	return 0;
}

运行结果:

3.1 realloc

realloc函数的出现让动态内存管理更加灵活。

• 为了更加灵活地管理动态分配的内存大小,当发现原先分配的空间不再满足需求(无论是太小还是过大)时,那 realloc 函数就可以做到对动态开辟内存大小的调整

函数原型如下: 

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

ptr 是要调整的内存地址,size 是调整之后的新大小

返回值为调整之后的内存起始位置。

这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

• realloc在调整内存空间的是存在两种情况:

◦ 情况1:原有空间之后有足够大的空间

◦ 情况2:原有空间之后没有足够大的空间

 当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。

 当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:

1)realloc 函数直接在内存堆区找一块新的满足大小的空间。

(2)将旧的数据拷贝到新的空间。

(3)释放旧的空间。

(4)返回新的地址。

⛳代码示例: 

int main()
{
	// 申请10个整型空间
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ",p[i]);// *(p+i)
	}
	// 调整空间,希望变为20个整型空间
    // 先将realloc函数的返回值放在ptr中,不为NULL,在放回p中
	int* ptr = (int*)realloc(p, 20 * sizeof(int));
	if (ptr != NULL)
	{
		p = ptr;
	}
	// 使用
	//...
	//释放
	free(p);
	p = NULL;

	return 0;
}

4、动态内存管理中的常见错误

4.0 对NULL指针的解引用操作

void test()
{
	int* p = (int*)malloc(INT_MAX);// 值比较大,返回NULL
	*p = 20;//如果p的值是NULL,对空指针解引用就会有问题
	free(p);
}

4.1 对动态开辟空间的越界访问

int main()
{
	// 申请10 个内存空间
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		// 空间申请失败
		perror("malloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 50; i++) // 越界访问
	{
		*(p + i) = i + 1;
	}
	free(p);
	p = NULL;

	return 0;
}

4.2 对非动态开辟内存使用free释放

int main()
{
	int a = 10;
	int* p = &a;
	// ...
	free(p);//erro
	p = NULL;

}

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

int main()
{
	// 申请10 个内存空间
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		// 空间申请失败
		perror("malloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 5; i++) 
	{
		*p = i;
		p++;// //p不再指向动态内存的起始位置
	}
	free(p);
	p = NULL;

	return 0;
}

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

void test()
 {
 int *p = (int *)malloc(100);
 free(p);
// ...
 free(p);//重复释放
 }

4.5 动态开辟内存忘记释放(内存泄漏)

在 if (flag) 判断后,如果 flag 为真(在代码中总是为真,因为 flag 被初始化为 1),则 return 语句会导致函数提前返回,而之前分配的内存 p 没有被 free 释放。这会导致内存泄漏。

void test()
{
	int flag = 1;
	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		return;
	}
	// 使用
	if (flag) 
		return;

	free(p);
	p = NULL;
}

int main()
{
	test();
	// ...
	return 0;
}

5、动态内存经典笔试题解析

💡题一:

void GetMemory(char* p)
{
	p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}

int main()
{
	Test();
	return 0;
}

🍊解析:

GetMemory 函数中,p 是一个局部变量,它被初始化为指向 malloc 分配的内存。然而,这个地址仅在这个函数内部有效,因为它是按值传递给 GetMemory 的。一旦 GetMemory 函数结束,这个局部变量 p 将被销毁,而原始指针 str 在 Test 函数中仍然指向 NULL。我们要拷贝“hello world”就要对空指针解引用,程序会崩溃。

✅修正:将 str 的地址传给 p,用char** 来接收,这使得它可以修改 Test 函数中的 str 指针。

void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");// ok
	printf(str);//ok
	free(str);
	str = NULL;
}

int main()
{
	Test();
	return 0;
}

或者:

调用 GetMemory 函数,并将返回的指针赋给 str;而GetMemory这个函数分配了100字节的内存空间,并将这块内存的地址赋给指针 p并返回这个指针 p;然后 strcpy 函数将字符串 "hello world" 复制到 str 指向的内存中(也就是str 指向的内存足够大的100字节)。

-> 注意的是,我们自己写代码时最好检查一下这里 malloc 是否成功分配了内存。

-> 还有就是这里的printf 直接传递了 str,其实就是拿到这个有效字符串第一个元素地址,也能打印出字符串;但这不是 printf 的标准用法。更安全的做法是使用 %s 格式说明符,如 printf("%s", str) ;

 

#include<stdio.h>
#include<string.h>
char* GetMemory()
{
	char *p = (char*)malloc(100);
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	strcpy(str, "hello world");// ok
	printf(str);//ok
	free(str);
	str = NULL;
}

int main()
{
	Test();
	return 0;
}

💡题二:涉及返回栈空间地址问题知识

char *GetMemory(void)
{
 char p[] = "hello world";
 return p;
}
void Test(void)
{
 char *str = NULL;
 str = GetMemory();
 printf(str);
}

运行Test 函数会有什么样的结果?一堆乱码

🍊解析如下:

GetMemory 函数中定义了一个局部字符数组 p,并返回这个局部数组的地址。然而,当GetMemory 函数返回时,局部变量 p 的作用域结束,其内存被释放(它不再有效,因为栈帧被销毁),但返回的指针 str 仍然指向那个已经无效的内存区域,这时str 是野指针了。

💡题三:

void GetMemory(char **p, int num)
 {
 *p = (char *)malloc(num);
 }
void Test(void)
 {
 char *str = NULL;
 GetMemory(&str, 100);
 strcpy(str, "hello");
 printf(str);
 }

🍊解析:示例中分配的内存从未被释放,内存泄漏。

✅更正:

void GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
   // 释放
	free(str);
	str = NULL;
}

 💡题四:

void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

🍊解析:

在调用 free(str); 之后,str 指向的内存块已经被释放回堆中,无法继续使用了,并且其内容是未定义的。此时,str 已经是野指针了,任何对 str 的解引用(包括尝试写入或读取)都是未定义行为。后面拷贝“world”就是非法访问了。

 ✅更正:

void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	str = NULL;// 将指针置为 NULL,避免野指针 
	if (str != NULL) // 使用不了了
	{
		strcpy(str, "world");
		printf(str);
	}
}

6、柔性数组(Flexible Array Member)

6.0 柔性数组概念

柔性数组也称为变长数组,是C99标准引入的一种特性,结构中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

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

有些编译器会报错无法编译可以改成:

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

6.1 柔性数组特点

• 结构中的柔性数组成员前面必须至少一个其他成员

• sizeof 返回的这种结构大小不包括柔性数组的内存

• 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

例如: 

typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;
int main()
{
	printf("%d\n", sizeof(type_a));//输出的是4
	return 0;
}

🍀柔性数组使用:

通过malloc函数为struct S类型的指针ps分配了足够的内存。分配的内存大小是结构体本身的大小(sizeof(struct S))加上一个包含20个整数的数组的大小(20 * sizeof(int))。这种方式确保了结构体实例ps拥有一个足够大的柔性数组a来存储20个整数。

#include <stdio.h>
#include <stdlib.h>
struct S
{
	int i;
	int a[];//柔性数组成员
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
	if (ps == NULL)
	{
		printf("malloc()");
		return 1;
	}
	// 使用这些空间
	ps->i = 100;
	int n = 0;
	for (n = 0; n < 20; n++)
	{
		ps->a[n] = n + 1; 
	}
    free(ps);
	return 0;
}

6.2 柔性数组的优势

第一种方式:

struct S
{
	int i;
	int a[];//柔性数组成员
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
	if (ps == NULL)
	{
		printf("malloc()");
		return 1;
	}
	// 使用这些空间
	ps->i = 100;
	int n = 0;
	for (n = 0; n < 20; n++)
	{
		ps->a[n] = n + 1; 
	}

	// 调整ps 指向空间的大小
	struct S* ptr = (struct S*)realloc(ps,sizeof(struct S) + 40 * sizeof(int));
	if (ptr != NULL)
	{
		ps = ptr;
        ptr = NULL;
	}
    else
       return 1;
	// 使用
	for (n = 0; n < 40; n++)
	{
		printf("%d ", ps->a[n]);
	}
    // 释放
    free(ps);
    ps = NULL;
	return 0;
}

 打印结果:只有前20元素为整型,后面打印随机值了

第二种方式:

#include<stdio.h>
#include<stdlib.h>

struct S
{
	int n;
	int* arr;
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	int*tmp = (int*)malloc(20*sizeof(int));
	if (tmp != NULL)
	{
		ps->arr = tmp;
	}
	else
	{
		return 1;
	}
	ps->n = 100;
	int i = 0;
	//给arr中的20个元素赋值为1~20
	for (i = 0; i < 20; i++)
	{
		ps->arr[i] = i + 1;
	}
	//调整空间
	tmp = (int*)realloc(ps->arr, 40*sizeof(int));
	if (tmp != NULL)
	{
		ps->arr = tmp;
	}
	else
	{
		perror("realloc");
		return 1;
	}
	for (i = 0; i < 40; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	//释放
	free(ps->arr);
	ps->arr = NULL;

	free(ps);
	ps = NULL;

	return 0;
}

 🍊方式二的解析:

1、定义了一个结构体S,它包含一个整型成员n和一个指向整型的指针arr 。

2、通过malloc为结构体S分配内存,并检查是否成功。

3、为整型数组分配了20个元素的内存,并将ps->arr指向这块内存。

4、设置结构体成员n的值为100,并将数组arr的前20个元素初始化为1到20。

5、使用realloc将arr数组的大小从20个元素增加到40个元素。如果realloc成功,它将返回指向新内存块的指针,否则返回NULL。注意,这里通过临时变量tmp来接收realloc的返回值,以防止在realloc失败时丢失对原始内存块的引用。

6、循环遍历并打印调整大小后的数组arr的所有元素

7、首先释放arr指向的内存,然后将ps->arr设置为NULL以防止野指针。接着释放ps指向的结构体内存,并将ps设置为NULL。

 方式一和方式二可以完成同样的功能,但是 方式一 的实现有两个好处:

方便内存释放。

• 这样有利于访问速度. 连续的内存有益于提高访问速度,也有益于减少内存碎片。

7、C/C++程序内存区域划分总结

如图:

C/C++程序内存分配的几个区域:

1. 栈区(stack):用于存放函数的参数值、局部变量的值等。这部分内存由编译器自动分配和释放,函数的调用过程就是通过栈这种数据结构实现的。

2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。

3. 数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放。

4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。这部分内存区域通常是只读的,以防止程序意外地修改其指令。

喜欢记得

⛳ 点赞☀收藏 ⭐ 关注!!

如有不足欢迎评论区指出~

  • 18
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值