C语言——动态内存管理

前言

本篇博客为大家介绍C语言中又一重要的内容——动态内存管理,这一部分的内容在后面学习C++,包括数据结构的学习中都有大量的运用,所以希望大家可以掌握好这里的内容,如果你对此感兴趣,请继续往下阅读,下面进入正文部分。

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

目前阶段,我们可能只有以下两种开辟内存的方式:

但是上述的开辟空间的方式有两个特点:

• 空间开辟大小是固定的。

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

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。 C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。 

2. malloc和free

2.1 malloc

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

2.2 free 

#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i + 1;
	}
	free(p);
	p = NULL;
	return 0;
}

这里大家注意看代码,在我们使用完空间后,我们需要将申请的空间返还给操作系统,这里就需要用到free函数,这里要强调的是free函数的参数是要释放的内存空间的起始地址,然后在释放完空间后我们需要将p置为空指针(因为p是野指针)。

3.calloc和realloc

3.1 calloc

这里为大家说明一下:

calloc函数的功能是为num个大小为size的字节开辟空间并且把空间的每一个字节都初始化为0,这是与malloc最大的区别,大家要清楚这一点。

3.2 realloc 

ptr是要调整的内存的地址;

size是调整后的新大小;

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

在realloc函数进行扩容时,可能遇到两种情况,大家来看下面的图‘

第一种情况,是原本的空间后面还有足够的尚未分配的空间,这时realloc只需要在原来的基础上加上相应的空间,最后返回该空间的起始地址即可;

第二种情况,在原本空间后面未分配空间不足时,realloc就会在堆区上重新开辟一块儿空间来满足足新的空间大小,将原来空间的数据拷贝到新的空间中;然后释放旧的空间,并返回新的内存空间的起始地址。

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

 

大家注意上面的代码,我们在使用realloc申请空间时。一定要用一个临时指针去接受realloc的返回值,因为一旦realloc申请失败,将返回NULL,这个时候如果我们没有使用临时指针,就会对原本存在的数据造成影响;最后大家要记住,使用完后要释放空间,并置为NULL。

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

4.1 对NULL解引用操作

 void test()
 {
 int *p = (int *)malloc(INT_MAX/4);
 *p = 20;//如果p的值是NULL,就会有问题 
 free(p);
 }

这里大家看到,上面的代码就是对NULL进行了解引用的操作,这个代码是无法运行的。在我们开辟完空间后,一定要进行判断或者进行断言,防止后面对NULL进行操作。 

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

void test()
 {
 int i = 0;
 int *p = (int *)malloc(10*sizeof(int));
 if(NULL == p)
 {
 exit(EXIT_FAILURE);
 }
 for(i=0; i<=10; i++)
 {
 *(p+i) = i;//当i是10的时候越界访问 
 }
 free(p);
 }

这里想必大家可以轻松理解,这与我们前面学过的数组越界访问基本上是同理,我们可以将malloc开辟的空间想象成一个数组。

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


void test()
{
 int a = 10;
 int *p = &a;
 free(p);//ok?
 }

这里也是一个很常见的错误,我们一定要清楚free是释放动态开辟的空间,其他的空间是无法进行释放的,这段代码就是一个典型的错误释放,a是静态变量,存放在栈区;而动态开辟的空间是在堆区。

4.4 使用free释放⼀块动态开辟内存的⼀部分 

 void test()
 {
 int *p = (int *)malloc(100);
 p++;
 free(p);//p不再指向动态内存的起始位置 
 }

这里大家要注意,释放空间时,free函数的参数必须是动态内存的起始地址,不能是其他的。

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

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

这个就比较好理解了,对同一个空间只能释放一次;想避免这个问题,其实也有办法,就是在我们释放完后,立马将其置为NULL,这样就算后面我们不小心又释放了一次,其实也无伤大雅,因为给free函数传NULL,它什么事都不会做。 

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

 void test()
 {
 int *p = (int *)malloc(100);
 if(NULL != p)
 {
 *p = 20;
 }
 }
 
 int main()
 {
 test();
 while(1);
 }

忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。

切记:动态开辟的空间⼀定要释放,并且正确释放。

5. 动态内存经典笔试题分析

5.1 题目一

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

请问运行Test函数会得到什么结果? 

我们来看这段代码在VS中的运行结果;

大家看到,程序是无法运行的;证明上面的代码是存在问题的,那么具体有哪些问题呢?下面我们进行分析。

首先,在使用完开辟的空间后并没有进行释放,可能导致内存泄漏,这是一个问题,但是这不是造成程序崩溃的主要原因。

程序崩溃的主要原因在于这段代码对NULL进行了解引用操作,形成非法访问。为什么会这样呢?大家来看传递给GetMemory函数里面的参数,是str,这里传递是指针变量本身,所以这里就相当于是传值调用,那么前面我们学过,形参是实参的一份临时拷贝,p有自己的空间,只是接受了str的内容,这时候malloc申请的空间放到p中是和str没关系的,所以当程序走出GetMemory函数后,p指向的那块儿空间就找不到了,所以strcpy里的str就是NULL,那么这个时候想把"hello world"拷贝到空指针所指向的空间中,就会发生对空指针解引用的操作。

那么有人会问,这段代码能不能改成正确的呢?答案当然是可以,我们无非就是想在malloc开辟的空间里拷贝题目所给字符串。这个时候我们可以进行传址调用,大家来看下面的代码。

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

 

大家可以看到这里我们改进了代码,采用了传址调用,注意str本身就是一级指针,所以&str就是二级指针,那么我们就需要用二级指针去接受,对p解引用就可以得到str的地址。

5.2 题目二 

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
char* GetMemory()
{
	char p[] = "hello world";
	return p;
}
void Test()
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

请问这段代码运行会得到什么结果?

我们先给出运行结果;

 

大家可以发现,这里并不能打印出我们想要的结果,那么为什么会这样呢?下面我们来进行分析;

这里GetMemory里创建了一个字符数组,我们知道其数组名p就代表字符串的首字符地址,所以这里返回的就是字符串首字符地址;那么当程序走出GetMemory函数后,使用str指针去访问p数组就属于非法访问,因为p数组的内存空间已经还给了操作系统,这里的str是野指针。

6. 柔性数组

C99中,结构中的最后⼀个元素允许是未知大小的数组,这就叫做『柔性数组』成员。

这里有两种表示方法:

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

6.1 柔性数组的特点

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

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

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

#include<stdio.h>
typedef struct st_type
{
	int n;
	int a[];//柔性数组成员 
}type_a;
int main()
{
	//printf("%zd", sizeof(type_a));
	struct st_type* ps=(struct st_type*)malloc(sizeof(type_a) + 5 * sizeof(int));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	ps->n = 100;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps->a[i] = i;
	}
	//调整空间
	struct st_type* ptr=(struct st_type*)realloc(ps, sizeof(type_a) + 10 * sizeof(int));
	if (ptr != NULL)
	{
		ps = ptr;
	}
	//...
	free(ps);
	ps = NULL;
	return 0;
}

这里大家可以看到,我们不仅可以用malloc来进行内存的动态分配,还可以用realloc来进行内存分配。而且大家可以发现一个点,realloc可以真正实现柔性数组的“柔”,想大就大,想小就小。

6.2 柔性数组的优势

7.总结

本篇文章为大家介绍了C语言中动态内存管理的内容,主要包括重要的四个函数,以及一些关于动态内存常出现的问题,还拓展了柔性数组的内容,这个知识大家作为了解;最后,希望本篇博客可以为大家带来帮助,感谢阅读!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值