【C语言】动态内存管理

目录

1.为什么存在动态内存分配?

2.动态内存函数

2.1malloc函数

2.2free函数

2.3calloc函数

2.4realloc函数

3.常见的动态内存错误

3.1对NULL指针的解引用操作

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

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

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

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

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

4.常见错误代码分析

5.柔性数组

4.1柔性数组的特点 

4.2柔性数组的使用

4.3柔性数组的优点 


1.为什么存在动态内存分配?

  1. 空间开辟大小是固定的
  2. 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配,但对于空间的需求,不仅仅是上述情况。有时我们需要空间的大小在程序运行的时候才能知道,这时数组在编译时开辟空间的方式就不满足了,我们需要进行动态内存开辟

我们平时创建的局部变量,形式参数等都是在栈区开辟空间,一旦创建大小固定

动态内存函数malloc,free,calloc,realloc是在堆区申请空间,大小可以进行修改 

2.动态内存函数

2.1malloc函数

void* malloc (size_t size);

  1. malloc函数可以在内存中开辟size个字节的空间,并返回这块空间的起始地址;当size为0,即要求动态开辟0字节的空间时,返回值可能是空指针也可能不是空指针,这时返回的指针不能进行解引用操作。
  2. 注意size的类型为size_t(unsigned int),函数返回的指针类型为void*,void*类型的指针不能直接进行解引用操作,需要先强制类型转换为所需数据类型
  3. malloc函数分配的内存块未初始化,其中存放的是一些随机值
  4. 使用malloc函数必须进行指针检查:即检查malloc函数返回的指针是否为空指针,如果其返回空指针,则内存申请失败

malloc函数的使用 

#include<stdio.h>
#include<stdlib.h>//动态内存函数所需头文件
#include<string.h>
#include<errno.h>
int main()
{
	int arr[10] = { 0 };
    //使用malloc开辟40字节的空间
	int* p = (int*)malloc(40);
    //指针检查
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));//输出错误信息
		return 1;//异常返回
	}
	//内存开辟成功,正常使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}

📍 使用malloc函数时,需要对其返回值进行检查,如果开辟失败则进行拦截

2.2free函数

void free (void* ptr);

  1. free可以释放由malloc,calloc,realloc调用分配的内存块
  2. 如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的
  3. 如果ptr == NULL,函数不会执行任何的操作
  4. 动态内存开辟函数和free函数成对使用

free函数的使用 

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
	int arr[10] = { 0 };
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));//输出错误信息
		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);//free之后内存空间释放,但p的地址还存在
	p = NULL;//p赋为空指针
	return 0;
}

2.3calloc函数

void* calloc (size_t num, size_t size);

  1. calloc函数可以为一个元素数组分配一个内存块,元素的数目是num,每个元素大小为size字节;当size为0,即要求动态开辟0字节的空间时,返回值可能是空指针也可能不是空指针,这时返回的指针不能进行解引用操作。
  2. 注意size的类型为size_t(unsigned int),函数返回的指针类型为void*,void*类型的指针不能直接进行解引用操作,需要先强制类型转换为所需数据类型
  3. calloc函数分配的内存块会零初始化,即其开辟的内存都被初始化为0
  4. 使用calloc函数必须进行指针检查:即检查calloc函数返回的指针是否为空指针,如果其返回空指针,则内存申请失败

calloc函数的使用 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>//动态内存函数所需头文件
#include<string.h>
#include<errno.h>

int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	//指针检查
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	//开辟成功
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	//使用结束,释放空间
	free(p);
	p = NULL;

	return 0;
}

 可以发现,calloc函数开辟的空间都被初始化为0

2.4realloc函数

void* realloc (void* ptr, size_t size);
  1. calloc函数可以对指定的空间重新分配一个内存块,ptr指向要更改的内存块(这块内存是先前由malloc,calloc,realloc动态分配的内存块);如果ptr是空指针,则realloc函数的操作类似于malloc函数,分配一个为size字节的新内存块并返回其起始地址。
  2. 注意size的类型为size_t(unsigned int),函数返回的指针类型为void*,void*类型的指针不能直接进行解引用操作,需要先强制类型转换为所需数据类型
  3. 使用realloc函数必须进行指针检查:即检查calloc函数返回的指针是否为空指针,如果其返回空指针,则内存申请失败,但并不会释放ptr指向的内存块,且其指向的内存块中内容不变
  4. 该函数的内存分配有两种情况

🔅追加

可以看到malloc分配的内存块的地址与realloc函数扩容后的内存块的地址相同,这种情况是在原内存块后的内存未被使用,可以继续追加,如下图

🔅重新分配

可以看到malloc分配的内存块的地址与realloc函数扩容后的内存块的地址不同,这种情况是在原内存块后的内存已经被使用,不可以继续追加,需要重新寻找一块满足要求的内存块,返回其起始地址,并释放原内存块,如下图

malloc函数的使用 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<string.h>
#include<errno.h>
#include<stdlib.h>

int main()
{
	int* p = (int*)malloc(40);
	//指针检查
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	//开辟成功,正常使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i + 1;
	}
	//扩容
	int* ptr = (int*)realloc(p, 48);
	if (ptr != NULL)
	{
		p = ptr;//扩容成功
	}
	//使用
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	//释放
	free(p);
	p = NULL;
	return 0;
}

3.常见的动态内存错误

3.1对NULL指针的解引用操作

int main()
{
	int* p = (int*)malloc(40);
	*p = 20;

	return 0;
}

这里并不确定p是否为空指针,直接对p进行了解引用操作,编译器会对这种行为报警告,检查严格的编译器里这段代码会直接报错

解决方法:对动态内存开辟函数的返回值进行有效性检查

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

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	int i = 0;
	for (i = 0; i <= 10; i++)
	{
		p[i] = i;
	}
	free(p);
	p = NULL;

	return 0;
}

 应当注意数组的下标,避免越界访问

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

int main()
{
	int a = 10;
	int* p = &a;
	free(p);

	return 0;
}

注意free函数只能释放由动态内存开辟函数开辟的内存块,对于非动态开辟的空间,free函数没有操作权限

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

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 1;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p = i;
		p++;
	}
	free(p);
	p = NULL;
	return 0;
}

 ++和--操作符会改变变量自身的值,执行free(p)语句时,p已经不指向malloc函数开辟的空间的起始地址,执行free(p)就相当于释放一块动态开辟内存的一部分,这种操作是非法的

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

这种情况一般出现在代码较长情况下,忘记之前已经释放

解决方法:free(p);与p=NULL;搭配使用,free(NULL)不执行任何操作

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

动态内存函数使用三步:指针有效性检查,使用,释放 

4.常见错误代码分析

题一: 

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

这段代码是动态开辟一块空间,然后将"hello world"存到这块空间中

GetMemory函数可以动态开辟一块空间,p是一个局部变量,存放这块动态开辟空间的起始地址,当GetMemory函数调用结束时局部变量p销毁,但malloc函数开辟的内存仍然存在,导致了内存泄漏;并且GetMemory函数是传值调用,str并没有指向开辟的动态空间,所以并不能实现最终拷贝的功能

修改:

传址调用:GetMemory(&str);str中存放动态开辟的100byte空间的起始地址

使用完后,释放str指向的空间

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

	return 0;
}

 题二:

int* f1(void)
{
	int x = 10;
	return (&x);
}
int* f2(void)
{
	int* ptr;
	*ptr = 10;
	return ptr;
}
int main()
{
	int* ret1 = f1();
	int* ret2 = f2();
	printf("%p %p\n", ret1, ret2);

	return 0;
}

分析:

f1函数:x是局部变量,出了f1函数的作用域x销毁,&x为野指针

f2函数:指针ptr未初始化,是野指针

题三: 

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

	return 0;
}

分析:数组p是一个局部数组,作用域为GetMemory函数,出了GetMemory函数p销毁,p是一个野指针,其指向的空间及空间中的内容是未知的,所以str指向的空间也是未知的

同理还有以下代码: 

int* test(void)
{
	int a = 10;
	return &a;
}

int main()
{
	int* p = test();
	printf("%d\n", *p);

	return 0;
}

可以发现printf函数可以成功打印10,原因如下

test函数调用需要创建栈帧

 test函数调用结束后,栈帧销毁,printf函数调用又重新开辟栈帧,变量a未被覆盖,p指向的那块空间里仍存放的是变量a,所以可以打印10

题四:

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指向malloc动态开辟的100byte的空间,strcpy(str, "hello");将"hello"拷贝到str指向的空间中,free(str)释放了这块空间,str的地址存在且不变,但是str指向的空间未知,str就是一个野指针,strcpy(str, "world");就是非法访问

所以,free(p)和p=NULL一起使用

5.柔性数组

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

如下结构中a就是柔性数组成员

typedef struct st_type
{
	int i;
	int a[0];//也可以写成int a[]
}type_a;

4.1柔性数组的特点 

  1. 结构中的柔性数组成员前面必须至少有一个其他成员
  2. sizeof返回的这种结构大小不包括柔性数组的内存
  3. 包含柔性数组成员的结构用malloc函数进行内存的动态分配时,分配的内存大小应该大于结构的大小,以适应柔性数组的预期大小

4.2柔性数组的使用

为含有柔性数组的结构开辟空间有两种方法

🔅malloc一次

🔅malloc俩次(可能导致内存碎片化,使内存利用率下降)

#include<stdlib.h>

struct S
{
	int n;
	int arr[];
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
	if (ps == NULL)
	{
		return;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	//扩容
	struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 80);
	if (ptr != NULL)
	{
		ps = ptr;
	}
	free(ps);
	ps = NULL;

	return 0;
}
#include<stdlib.h>

struct S
{
	int n;
	int* arr;
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
	if (ps == NULL)
	{
		return 1;
	}
	ps->n = 100;
	ps->arr = (int*)malloc(40);
	if (ps->arr == NULL)
	{
		return 1;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}

	//扩容
	struct S* ptr = (struct S*)realloc(ps->arr, 80);
	if (ptr != NULL)
	{
		ps->arr = ptr;
	}
	//释放
	free(ps->arr);
	free(ps);
	ps = NULL;

	return 0;
}

第二种方法中,结构成员是一个整型变量和一个整形指针,此时数组的大小是可变的

这个结构类型的创建在堆区上

4.3柔性数组的优点 

  • 方便内存释放
  • 有利于提升访问速度
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值