Lesson 23 动态内存管理

开辟内存的方式

目前已知的是使用变量类型来开辟内存空间:

int a = 10;//在栈区开辟4个字节
char arr[10] = {0};//在栈区开辟10个字节

但这样有些问题:

  1. 空间的大小固定
  2. 数组声明后长度不变。
    当需要的内存空间不定时,以上的方式就没法满足了。因此引入了动态内存开辟,让程序员可以自己申请和释放空间。

malloc和free

malloc

看一下malloc的形式:

void* malloc(size_t size)

malloc有以下几个要点:

  1. malloc会向内存申请一块连续可用的空间,返回这块空间的指针;
  2. 如果开辟失败,返回一个NULL,因此malloc一定要做返回值检查;
  3. 返回类型是void* ,依据使用者的需求来决定具体是什么类型的;
  4. 如果size = 0,malloc行为是没有定义的,取决于编译器。

free

free用于释放动态内存。形式如下:

void free(void* ptr);

free有以下几个要点:

  1. 如果ptr指向的空间不是动态开辟的,free函数的行为未定义;
  2. 如果ptr是空指针,函数什么都不做。

使用举例(malloc & free)

include <stdio.h>
#include <stdlib.h>
int main()
{
	int num = 0;
	scanf("%d", &num);
	int arr[num] = {0};
	int* ptr = NULL;
	ptr = (int*)malloc(num*sizeof(int));
	
	if(NULL != ptr)//判断ptr指针是否为空
	{
		int i = 0;
		for(i=0; i<num; i++)
		{
		*(ptr+i) = 0}
	}
	free(ptr);//释放ptr所指向的动态内存
	ptr = NULL;//是否有必要?
	return 0;
}

稍微分析一下代码在做什么:
首先是建立了一个变长数组arr,然后通过malloc为其分配空间,判断malloc返回值之后对这个数组赋值,使用完成后释放掉ptr,然后把ptr置为NULL。

calloc和realloc

calloc

calloc的原型如下:

void* calloc(size_t num, size_t size);

函数功能是为num个大小为size的元素开辟空间,并且把空间的每个字节都初始化为0。它与malloc的区别仅在于calloc会把申请的空间初始化为0。
看个例子:

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

最终会打印10个0。

realloc

realloc的原型如下:

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

它的功能是调整已经申请的内存空间的大小。使用时注意:

  1. ptr是需要调整的内存地址;
  2. size是调整后的新大小;
  3. 返回值是调整后内存的起始位置;
  4. 这个函数调整大小后,还会将原来内存中的数据移动到新的空间;
  5. realloc调整时有两种情况。当原有空间之后有足够的空间时,返回原地址。如果没有,返回一个新的地址;
  6. 扩容失败还是会返回一个NULL,但旧的空间可用。
    举个例子:
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;
	}
	for (i = 0; i < 5; i++)
	{
		printf("%d ",*(p + i));
	}
	printf("\n");

	int* ptr = (int*)relloc(p, 10 * sizeof(int));
	if (ptr != NULL)
	{
		p = ptr;
		ptr = NULL;//不要free(ptr),这里等价与free(p)
	}
	else
	{
		perror("realloc:");
		return 1;
	}
	//使用
	for (i = 5; i < 10; i++)
	{
		*(p + i) = i + 1;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}


	free(p);
	p = NULL;

	return 0;
}

看一下这段代码做了什么事情:

  1. 使用malloc申请了20个字节的空间给5个整型;
  2. 给这5个整型赋值;
  3. 打印这5个整型;
  4. 扩大内存空间,调整大小为40个字节;
  5. 给新的整型赋值
  6. 打印这块空间内所有的整型;
  7. 释放

常见动态内存的错误

对NULL的解引用

int main()
{
	int*p = (int*)malloc(20);
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i;
	}

	return 0;
}

以上代码中,p是有可能为NULL的。

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

int main()
{
	int*p = (int*)malloc(20);
	if (p == NULL)
	{
		return 1;
	}
	int i = 0;
	for (i = 0; i < 20; i++)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;
	return 0;
}

上面的代码中,malloc申请了20个字节,但赋值的时候赋值了20个整型,越界了。

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

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

这里p指向的空间不是动态开辟的。

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

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 + 1;
		p++;
	}
	//释放
	free(p);//必须是起始位置,这里释放了一部分,error
	p = NULL;

	return 0;
}

这里p指向的位置不是开辟空间的起始位置了。

对同一个空间多次释放

int main()
{
	int *p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc:");
		return 1;
	}

	free(p);
	//....
	free(p);//error,如果没有 p = NULL;

	return 0;
}

第一个free之后没有将p置NULL,因此会报错。

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

void test()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		return;
	}
	//使用
	if (5 == 2 + 3)
	{
		return;//这里返回的时候,没有机会free了
	}
	//释放、
	free(p);
	p = NULL;
}
int main()
{
	test();
	//程序不退出
	//7*24一直在跑

	return 0;
}

这里由于函数中提前返回了,因此p是没有释放的。造成了内存泄露。

动态内存管理经典笔试题

Prob 1

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

运行Test会有什么问题?
分析一下Test的行为。调用GetMemory的时候,实际是传值调用。那么*p得到的就是str的一份拷贝,因此p得到的空间仅在GetMemory内部,当函数执行结束后,函数栈帧会销毁,所以其实str并没有被开辟空间,因此在strcpy的时候,实际是在往NULL里拷贝。并且GetMemory并没有释放空间。那么应该如何修改这两个函数呢?
传值调用不行,那么就传址调用。

void GetMemory(char** p)
{
	*p = (char*)malloc(100);
	if (*p == NULL)
	{
		return;
	}
}
void Test(void)
{
	char* str = NULL; //
	GetMemory(&str);//
	strcpy(str, "hello world");
	printf(str);//打印没问题
	free(str);
	str = NULL;
}

另一种改造方法:

char* GetMemory()
{
	char* p = (char*)malloc(100);
	return p;
}

void Test(void)
{
	char* str = NULL; //
	str = GetMemory();//传参没什么意义
	strcpy(str, "hello world");
	printf(str);//打印没问题
	free(str);
	str = NULL;
}

Prob 2

char* Getmemory(void)
{
	char p[] = "hello world";
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = Getmemory();
	printf(str);//非法访问了,p已经被销毁
}

这里的问题时返回了栈空间的地址。栈空间在函数执行结束之后就会销毁,这时候p就成了野指针(指向了已经销毁的空间)。

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

这个代码的问题很简单,就是str使用结束之后没有释放。

Prob 4

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

这里代码的问题是当str被释放后,没有置NULL。free之后的指针就是野指针,那么就一定要记住加一句str = NULL。虽然可以打印,但仍然属于非法访问。

柔性数组

C99中,结构体的最后一个元素允许是未知大小的,这就是柔性数组的成员。柔性数组有下面几个要点:

  1. 结构体
  2. 最后一个成员(前面必须有别的成员)
  3. 未知大小
    柔性数组一般和动态内存管理的几个函数一起使用。

柔性数组的声明

struct st_type
{
	int i;
	int a[0];//0可以不写
};

柔性数组的大小

使用sizeof来确认柔性数组大小的时候,其中的柔性数组时不包含在内的。sizeof仅返回除柔性数组之外的大小。例如:

struct st_type
{
	int i;
	int a[0];
};
int main()
{
	printf("%d\n", sizeof(struct st_type));
}

这个结果是4。

柔性数组的使用

包含柔性数组成员的结构需要使用malloc分配内存,而且分配的内存应该大于结构体的大小,以适应柔性数组的大小。
看个例子:

struct st_type
{
	int i;
	int a[0];
};

int main()
{
	struct st_type* p = (struct st_type*)malloc(sizeof(struct st_type) + 10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc:");
		return 1;
	}
	int i = 0;
	p->i = 100;
	for (i = 0; i < 10; i++)
	{
		p->a[i] = i;
	}
}

这里就是一个应用。程序首先使用malloc创建了44个字节的空间,然后给结构体内的成员都赋值。
如果希望修改a的大小:

struct st_type
{
	int i;
	int a[0];//柔性数组成员//0可以不写
};
int main()
{
	struct st_type* p = (struct st_type*)malloc(sizeof(struct st_type) + 10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc:");
		return 1;
	}
	int i = 0;
	p->i = 100;
	for (i = 0; i < 10; i++)
	{
		p->a[i] = i;
	}
	//希望a数组变成60个字节
	struct st_type* ptr = realloc(p,sizeof(struct st_type) + 15 * sizeof(int));
	if (ptr != NULL)
	{
		p = ptr;
		ptr = NULL;
	}
	else
	{
		perror("realloc:");
		return 1;
	}
	//使用
	//..
	//释放
	free(p);
	p = NULL;

	return 0;
}

柔性数组的优势

先看一下代码:

struct st_type
{
	int i;
	int* a;
};

int main()
{
	struct st_type* p = (struct st_type*)malloc(sizeof(struct st_type));
	if (p == NULL)
	{
		perror("malloc:");
		return 1;
	}
	p->i = 100;
	p->a = malloc(10*sizeof(int));
	if (p->a == NULL)
	{
		perror("malloc:");
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p->a[i] = i;
	}
	//希望a指向的空间变为60个字节:
	struct st_type* ptr = (struct st_type*)realloc(p->a, sizeof(struct st_type) + 15 * sizeof(int));
	if (ptr == NULL)
	{
		perror("realloc:");
		return 1;
	}
	else
	{
		p = ptr;
		ptr = NULL;
	}
	//使用

	//释放
	free(p->a);//必须先释放p->a
	p->a = NULL;
	free(p);
	p = NULL;

	return 0;
}

这里使用指针实现了和柔性数组类似的功能。但对比一下不难发现:

  1. 柔性数组方便内存释放
  2. 柔性数组有利于提高运行速度(虽然不大)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值