C语言-动态内存管理

发布于:2025-03-22 ⋅ 阅读:(18) ⋅ 点赞:(0)

1.为什么要有动态内存分配

我们现如今已经掌握的内存开辟方式有

int main()
{
	int a = 0;
	int arr[30] = { 0 };
	return 0;
}

这两种方式,但是这种开辟空间的方式有两个特点:

1.空间开辟大小是固定的

2.数组在申明的时候,必须指定数组的长度,数组空间一旦确定了大小就不可更改了

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,这样的话数组编译时开辟的空间可能就会不够用

于是C语言就引入了动态内存开辟,让我们自己可以申请和释放空间,提高了空间利用的灵活性

2.malloc和free

2.1malloc

C语言提供了一个动态内存开辟的函数

头文件是stdble

void* malloc(size_t size);

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

-如果开辟成功,则返回一个指向开辟好的内存的指针

-如果开辟失败,则返回一个空指针(NULL)

-返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,需要我们根据需要进行转换

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

#include<stdlib.h>
int main()
{
	int * p = (int *)malloc(10 * sizeof(int));
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
		printf("%d ", *(p + i));
	}
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	return 0;
}

上述是关于用malloc创建10个整型空间

malloc申请的空间与数组空间的区别:

1.动态内存的大小是可以调整的

2.存放的位置不一样

2.2free

C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的

头文件:stdlib.h

函数原型:

void free (void* ptr);

代码应用实例:

	free(p);//p变成了野指针
	//p指向的空间不属于当前程序
	//但是还是可以找到原来的空间(空有地址无法使用)
	p = NULL;
	//为了更彻底,把p这个野指针用空指针拴住

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

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

例子:

int main()
{
	int arr[20] = { 0 };
	free(arr);
	return 0;
}

-如果参数ptr是空指针,那么函数什么都不做

例子:

malloc和free一般都是一块使用

3.calloc和realloc

3.1calloc

C语言提供了一个函数叫calloc,calloc函数也用来动态内存分配

头文件:stdlib.h

函数模型:

void* calloc (size_t num, size_t size);

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

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

calloc函数的应用例子:

#include<stdlib.h>
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}
	if (p == NULL)
	{
		return 1;
	}
	return 0;
}

运行结果为

2.2realloc

realloc函数使得动态内存管理更加灵活

有时候我们会发现申请的内存空间太小了,有时候又会觉得太大了,这时候我们就需要realloc函数对内存空间进行灵活的调整

头文件:stdlib.h

函数模型:

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

-ptr是要调整的内存

-size是调整之后的新大小

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

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

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

1.原有空间之后有足够大的空间:

直接在旧空间之后开辟空间,返回原来的空间地址

2.原有空间之后没有足够大的空间:

realloc函数直接在内存的堆区找一块新的满足大小的空间,并且将旧的数据拷贝到新的空间,然后释放旧的空间,返回新的地址

realloc函数使用的例子

#include<stdlib.h>
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	//如果空间不够
	int* ptr = (int*)realloc(p, 20 * sizeof(int));
	if (ptr == NULL)
	{
		perror("realloc");
		return 1;
	}
	else
	{
		p = ptr;
	}
	free(p);
	p = NULL;
	return 0;
}

tips:

realloc不仅可以调整内存空间,还可以想malloc和calloc一样创建动态内存空间

int main()
{
	int* p = (int*)realloc(NULL, 40);//==malloc(40);
	return 0;
}

4.常见的动态内存的错误

4.1对NULL指针的解引用操作

不注意p是否为空指针的话就会出现上述代码中的错误

加上判断的话编译器就不会报错了

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

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

//对非动态空间进行释放
int main()
{
	int arr[3] = { 0 };
	int* p = arr;
	free(p);
	p = NULL;
	return 0;
}

运行结果:

4.4使用free释放一部分动态内存空间

int main()
{
	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的地址移动了,不再是原来的p
	}
	free(p);
	p = NULL;
	return 0;
}

运行结果:

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

int main()
{
	int* p = (int*)malloc(10*sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 40/*这里越界了*/; i++)
	{
		p[i] = i;
	}
	free(p);
	
	//......
	free(p);//多次释放并且第一次没有将p赋为NULL
	p = NULL;
	return 0;
}

运行结果:

但如果第一次内存释放已经将p变成空指针NULL,那么多次释放也没事

因为p是空指针,所以函数free什么也不做

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

void test()
{
	int flag = 1;
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return;
	}

	//使用......

	if (flag == 0)
		return;

	free(p);//看似进行了释放
	//但是前面的条件有一个符合就会提前结束函数,便会漏掉释放内存
	p = NULL;
}

int main()
{
	test();//如果提前释放,那么就会发生内存泄漏
	//malloc创建的动态内存空间无法释放,也无法被找到
	//无法被找到是因为p只在test函数中使用,test函数也没有返回地址
	return 0;
}

总的来说,动态内存管理是一把双刃剑,既能提供灵活的内存管理方式,又会具有一定的风险

5.动态内存题目练习

5.1

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

运行结果:程序会崩溃

解析:(注释)

void GetMemory(char* p)
{
	p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(str);//无返回值,创建malloc动态内存后也没有释放,也没有地址
	//造成内存泄露
	strcpy(str, "hello world");
	//这里对str这个NULL空指针进行了解引用
	//所以程序会崩溃
	printf(str);
}
int main()
{
	Test();
	return 0;
}

正确代码:

或者不用二级指针,只是将函数GetMemory加个返回值,返回malloc动态空间的地址

5.2

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

运行结果:

解析:(注释)

char* GetMemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();//str的值变成了GM函数中p的地址
	printf(str);//printf可以通过str找到原来char p[] 的空间地址
	//但是没有访问权限,char p []只在GM函数中有效
	//所以str相当于野指针,无法访问,所以打印出来了这些
}
int main()
{
	Test();
	return 0;
}

str的使用是个返回栈空间的问题

5.3

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

运行结果:

看似没有问题,hello也可以打印出来,但是遗漏了一个重要的问题

malloc这个动态内存没有进行释放,使得内存泄漏

改正:

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;
}

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

5.4

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

运行结果:

看似没问题,但是str使用了未初始化的内存,意思是str是个野指针,这里是野指针的非法访问,代码是错误的

改正:

void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	str = NULL;
	if (str != NULL)//本意应该是为检测str是否转化为NULL
	{
		strcpy(str, "world");
		printf(str);
	}
}
int main()
{
	Test();
	

6.柔性数组

C99中,结构体中的最后一个元素允许是未知大小的数组

两种写法:(有些编译器只能由其中一个写法)


struct S
{
	int a;
	float b;
	char c;
	int arr[];//未知数组
};

struct S2
{
	int a;
	float b;
	int arr[0];
};

6.1柔性数组的特点

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

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

3.-包括柔性数组成员的结构用malloc()函数进行内存分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小

struct S
{
	int n;
	int arr[];
};
int main()
{
	struct S s;
	//这个只创建了有int的结构体,不包含柔性数组的内存空间
	struct S* pf = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
	//这才是创建有柔性数组的结构体的代码
	return 0;
}

应用:

struct S
{
	int n;
	int arr[];
};
int main()
{
	struct S* pf = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
	if (pf == NULL)
	{
		perror("malloc");
		return 1;
	}
	pf->n = 10;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		pf->arr[i] = i;
		printf("%d\n", pf->arr[i]);
	}
	return 0;
}

运行结果:

由于是malloc创建的动态内存空间,所以可以用realloc来对内存进行调整

struct S
{
	int n;
	int arr[];
};
int main()
{
	struct S* pf = (struct S*)malloc(sizeof(struct S) + 20 * sizeof(int));
	if (pf == NULL)
	{
		perror("malloc");
		return 1;
	}
	pf->n = 10;
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		pf->arr[i] = i;
		printf("%d\n", pf->arr[i]);
	}

	//调整空间
	struct S* ptr = (struct S*)realloc(pf, (sizeof(struct S) + 40 * sizeof(int)));
	if (ptr != NULL)
	{
		pf = ptr;
		ptr = NULL;
	}
	else
	{
		return 1;
	}
	//使用


	//释放空间
	free(pf);
	pf = NULL;
	return 0;
}

6.2柔性数组的优势

还有一种类型也可以有柔性数组的作用

就是将柔性数组变成指向malloc创建动态内存的指针

struct S
{
	int n;
	int* arr;
};
int main()
{
	struct S* s = (struct S*)malloc(sizeof(struct S));
	int* pl = (int*)malloc(20 * sizeof(int));
	if (pl == NULL)
	{
		perror("malloc");
		return 1;
	}
	else
	{
		s->arr = pl;
	}
	//use
	//......

	//调整内存
	int* ptr = (int*)realloc(pl, 40 * sizeof(int));
	if (ptr != NULL)
	{
		pl = ptr;
	}
	else
	{
		perror("realloc");
		return 1;
	}

	//use......

	//ending
	free(s->arr);
	free(pl);
	pl = NULL;
	return 0;
}

两者对比我们可以发现

柔性数组既方便内存的释放(释放一次即可),并且有利于提高访问速度