动态内存管理

本文探讨了动态内存函数malloc和free的使用及注意事项,包括NULL指针解引用、错误处理和内存释放。同时,介绍了C/C++中的柔性数组及其特点、使用方法和优势,以及如何避免内存泄漏和优化内存利用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

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

动态内存函数

malloc

void *malloc(size_t size);

所需引用头文件:<stdlib.h>或<malloc.h>
返回值:
返回类型是void*(使用时自己决定开辟空间的类型)
如果开辟成功,返回一个指向开辟空间的指针;
如果开辟不成功,返回一个空指针

对NULL指针的解引用操作(error)

int main()
{
	int* p = (int*)malloc(1000);
	if (p == NULL)//当malloc申请失败会返回空指针
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i < 250; i++)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;
	return 0;
}

free

C语言提供了函数free,专门用作动态内存的释放和回收
使用需引用头文件:<stdlib.h>或<malloc.h>

void free(void *ptr);

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

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* ptr = (int*)malloc(40);
	int* p;
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i < 10; i++)
	{
		*p = i;
		p++;
	}
	free(ptr);
	ptr = NULL;//在释放掉地址后尽量让指针置空 防止指针指向其他地方导致程序崩溃
	return 0;
}

对非动态内存使用free释放(error)

运行后会报错。malloc开辟的动态内存使用完后,需要我们手动回收。局部变量出了作用域会主动回收,不需要我们手动。

#include<stdlib.h>
int main()
{
	int a = 10;
	int* p = &a;
	free(p);
	p = NULL;
	return 0;
}

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

释放空间必须在动态内存的起始地址

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*)malloc(100);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	for (int i = 0; i < 10; i++)
	{
		*p = i;
		p++;
	}
	free(p);//这里的p已经不是指向动态内存起始地址的指针了
	p = NULL;
	return 0;
}

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

int main()
{
	int* p = malloc(100);
	if (p == NULL)
	{
		return 1;
	}
	free(p);
	// ……
	free(p);
	p = NULL;
	return 0;
}

动态开辟的内存忘记释放

当我们不释放动态申请的内存时。如果程序结束,动态申请的内存由操作系统自动回收;如果程序不结束,动态内存是不会自动回收的,会形成内存泄漏的问题。

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

calloc

void *calloc(size_t num, size_size);

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

realloc

使用需引用头文件:<stdlib.h>或<malloc.h>

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

prt是要调整的内存地址
size是调整之后的新大小
返回值为调整之后的内存起始地址
realloc函数在调整原内存空间大小的基础上,还会将原来的内存中的数据移动到新的空间,并且原本的空间释放掉 realloc调整内存空间:

  1. 原本空间之后有足够的空间: 直接在原有的内存空间后面追加空间,原来的空间的数据不发生变化
  2. 原本空间之后没有足够的空间 在堆空间上另找一个大小合适的连续空间来使用。如果找不到足够的空间,会返回NULL。

练习

1

运行后会有什么结果:

#include<stdio.h>
#include<string.h>
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被销毁了,str依旧是空指针。strcpy对NULL的解引用操作使程序崩溃
且没有进行动态内存释放,发生了内存泄漏
对于上述代码的修正:

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

2 返回栈空间地址问题

运行后会有什么结果:

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

出了GetMemory函数,p[]被销毁,malloc开辟的空间也还给了操作系统(没有访问权限),但是返回了“hello world”的地址。此时,这个str是野指针。

栈的空间地址出了作用域就销毁了,所以不要轻易返回,容易产生野指针的问题

比如以下代码:

#include<stdio.h>
int* test()
{
	int a = 10;
	return &a;
}
int main()
{
	int* p = test();
	printf("%d\n", *p);
	return 0;
}

虽然上述代码确实打印出了10,但是这里的p是野指针。只是恰好这里原本a的空间地址没有被覆盖掉。
再比如上述代码再打印一次:
在这里插入图片描述
Test函数返回的时候,栈帧的空间没有被覆盖,所以用*p访问的时候还是原来的数据。
*调用printf时候,栈帧空间可能被覆盖了,再通过**p去访问就不一定是原来的数据了

但是返回栈空间的变量时,不会产生野指针的问题:
会先把变量放到寄存器,然后销毁。

#include<stdio.h>
int* test()
{
	int a = 10;
	return a;
}
int main()
{
	int ret = test();
	printf("%d\n", ret);
	return 0;
}

3

运行会有什么结果:

#include<stdio.h>
#include<string.h>
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开辟的动态内存,也没有判断p是否为空指针

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

使用free回收动态内存后,就没有访问权限了。但是str依旧指指向原本开辟的动态内存,且free后面没有把str置为空指针。而后,strcpy复制属于非法访问了

C/C++程序的内存开辟

在这里插入图片描述

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

  1. 栈区:在执行函数时,函数内局部变量的存储单元都可以在栈上创建。函数执行结束时,这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  2. 堆区:一般人为分配释放,如果不手动释放,程序结束时,可能由OS回收
  3. 数据段(静态区):存放全局变量、静态数据。程序结束后,由系统释放
  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码

柔性数组

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

struct S
{
	int num;
	double d;
	int arr[];//柔性数组成员
};
struct SS
{
	int num;
	double d;
	int arr[0];//柔性数组成员   这里的0并不表示0个元素,而表示arr是柔性数组
};

柔性数组特点

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

typedef struct S
{
	int i;
	int arr[0];
}S;
int main()
{
	printf("%d\n", sizeof(S));
	return 0;//这里打印的结果:4
}

柔性数组的使用

#include<stdio.h>
#include<stdlib.h>
struct S
{
	int num;
	int arr[];
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);//动态分配内存
	ps->num = 100;
	for (int i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	struct S3 *pr = (struct S3*)realloc(ps, sizeof(struct S) + 80);//扩容
	if (pr == NULL)
	{
		perror("realloc");
		return 1;
	}
	else
	{
		ps = pr;
	}
	for (int i = 10; i < 20; i++)
	{
		ps->arr[i] = i;
	}
	for (int i = 0; i < 20; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	free(ps);
	ps = NULL;
	return 0;
}

柔性数组的优势

#include<stdlib.h>
struct S
{
	int num;
	int arr[];
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
	ps->num = 10;
	for (int i = 0; i < 10; i++)
	{
		ps->arr[i] = i;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", ps->arr[i]);
	}
	free(ps);
	ps = NULL;
	return 0;
}

上述代码也可以这样设计:

#include<stdlib.h>
struct S
{
	int num;
	int *pa;
};
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	ps->num = 40;
	ps->pa = (int*)malloc(10 * sizeof(int));
	for (int i = 0; i < 10; i++)
	{
		ps->pa[i] = i;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", ps->pa[i]);
	}
	free(ps->pa);
	ps->pa = NULL;
	free(ps);
	ps = NULL;
	return 0;
}

上述两个代码,前者对于后者而言,有两个好处

  1. 方便内存释放
  2. 有利于访问速度
    连续的内存有益于提高访问速度,也有益于减少内存碎片
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值