动态内存管理与柔性数组总结--超详细

在这里插入图片描述

这里是think的博客

希望可以一起交流知识,一起think

今天我们来学习动态内存管理

一起来think

为什么要有动态内存分配

举例:如果一个班级的学生的人数是36人,但是我事先不知道具体由多少人,所以提供了100个空间,所以导致空间开多了,
那么如果是数组的话,就无法修改了,但是我们需要在开辟空间开多了的情况下,修改空间大小,这个需求数组无法满足(变长数组也是无法满足的),所以动态内存分配就来了。

malloc和free

malloc

void* malloc (size_t size);

功能: malloc 用于在堆区申请一块指定大小的连续可用内存,并返回这块内存起始位置的地址。

参数: size 表示需要申请的内存大小,单位为字节。

返回值

  • 申请成功时,返回指向这块内存起始位置的指针;
  • 申请失败时(如可用内存不足),返回 NULL,因此使用 malloc 后通常需要检查返回值是否为空;
  • 返回值类型为 void*。这是因为 malloc 只负责分配原始内存,而不知道调用者打算将这块内存解释成什么类型的数据(如 int、double、结构体等),因此返回通用指针 void*,具体的数据类型由使用者在后续使用时决定。

要注意的是:

当 size 为 0 时,malloc 的行为由实现定义,不同编译器或运行库可能返回 NULL,也可能返回一个不可解引用的非空指针。

实际使用:int* p = (int*)malloc(10 * sizeof(int));

free

void free (void* ptr);

功能: free 用于释放之前通过动态内存分配函数(如 malloc、calloc、realloc)申请的堆内存,使这块内存重新变为可用状态。

参数: ptr 是指向待释放内存块的指针,必须是动态内存分配函数返回的地址。

返回值
free 没有返回值,其返回类型为 void。

要注意的是

  • ptr 必须指向动态分配的内存,否则调用 free 的行为未定义。
  • 同一块动态内存只能释放一次,重复释放(double free)会导致未定义行为。
  • 当 ptr 为 NULL 时,free 什么也不做,因此无需在调用前专门判断是否为 NULL。
  • free 释放的是指针所指向的内存空间,而不是指针变量本身。释放后,指针仍然存在,只是变成了悬空指针(wild pointer),通常应立即将其置为 NULL。
  • 释放成功后,程序不能再通过该指针访问原来的内存,否则会产生未定义行为。
#include <stdio.h>
#include <stdlib.h>
int main()
{
	int num = 0;
	scanf("%d", &num);
	int arr[num];//变长数组
	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}
	}
	else
	{
		perror("malloc");
		return 1;
	}
	free(ptr);//释放ptr所指向的动态内存
	ptr = NULL;
	return 0;
}

变长数组和malloc开辟的函数在现阶段好像功能是一样的,都是在运行期间才确定大小,但是在使用realloc的时候就会发现后者可以继续改变大小,而前者不可以,这就动态开辟灵活的地方。

calloc和realloc

calloc

void* calloc (size_t num, size_t size);
功能: calloc 用于在堆区动态申请内存。它会为 num 个大小为 size 字节的元素分配一块连续的内存空间,并在返回之前将这块空间的所有字节初始化为 0。

参数

  • num:需要分配的元素个数;
  • size:每个元素的大小,单位为字节。

返回值

  • 分配成功时,返回指向所申请内存块起始位置的指针;
  • 分配失败时,返回 NULL;
  • 返回类型为 void*,因为 calloc 只负责分配内存,而不知道这块内存将被解释为哪种数据类型,具体类型由使用者决定。

特点

  • 实际申请的内存大小为 num × size 字节;
  • calloc 会自动将申请到的内存全部初始化为 0;
  • calloc 申请的内存同样需要使用 free 释放。

与 malloc 的区别

  • malloc(size) 只负责分配内存,不会对内存内容进行初始化;
  • calloc(num, size) 不仅分配 num × size 字节的内存,还会将这块内存的所有字节初始化为 0。
  • 可以认为calloc==malloc+memset

realloc

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

功能:

  • realloc 函数用于调整动态内存空间的大小,使动态内存管理更加灵活。

  • 在实际开发中,最初申请的空间可能过小,也可能过大。为了更合理地使用内存,可以使用 realloc 对已经申请的内存进行扩容或缩容。

参数:

  • ptr:指向之前动态分配内存块的起始地址。
    • 如果 ptr == NULL,则 realloc 的行为与 malloc(size) 类似,都可以认为是申请一块内存。
  • size:调整后的内存大小,单位为字节。
    • 当 size == 0 时,与 malloc(0)、calloc(0, size) 一样属于特殊情况,其具体行为由实现决定(实现定义)。但由于 realloc 还涉及原内存块的处理,因此通常不建议依赖 realloc(ptr, 0) 的具体行为,需要释放内存时应直接使用 free(ptr)。

返回值:

  • 调整成功:返回调整后内存块的起始地址(类型为 void*)。
  • 调整失败:返回 NULL,且原来的内存块仍然保持有效,不会被释放。

当 realloc 调整内存大小时,通常有两种情况

情况1:原空间后面有足够的连续空闲内存
如果原内存块后方存在足够大的空闲区域,则直接在原空间基础上扩展(或缩小)内存。

特点
原有数据保持不变;
不需要搬移数据;
返回地址与原地址相同。
原空间
[ 已使用 ][ 空闲空间 ]
扩容后
[ 已使用 ][ 新增空间 ]

情况2:原空间后面没有足够的连续空闲内存
此时无法直接扩容,realloc 会:
在堆区寻找一块满足要求的新空间;
将原空间中的数据复制到新空间;
释放原来的内存块;
返回新空间的起始地址。

特点:
原有数据会被保留;
返回地址发生变化;
应使用临时指针接收返回值,在成功后再更新原指针,否则可能丢失原内存地址。

旧空间到新空间的步骤:

  1. 申请新空间
  2. 复制数据
  3. 释放旧空间

注意点:
由于 realloc 可能返回新的地址,因此不要直接将返回值赋给原指针:
ptr = realloc(ptr, new_size); // 不推荐
如果扩容失败,返回 NULL,原地址会丢失,导致内存泄漏。

更安全的写法:

int* tmp = realloc(ptr, new_size);
if (tmp != NULL)
{
    ptr = tmp;
}

这样即使 realloc 失败,原来的内存空间仍然能够通过 ptr 访问和释放。

使用动态内存时的注意点

  1. 对NULL指针的解引用操作
void test()
{
	int *p = (int *)malloc(INT_MAX);
	*p = 20;//如果p的值是NULL,就会有问题
	free(p);
	p = NULL;
}

改法:在动态内存申请完成以后加上判断即可。

void test()
{
    int* p = (int*)malloc(INT_MAX);
    if (p == NULL)
    {
        perror("malloc");
        exit(1);
    }
    *p = 20;
    free(p);
    p = NULL;
}
  1. 对动态开辟空间的越界访问
void test()
{
	int i = 0;
	int *p = (int *)malloc(10*sizeof(int));
	if(p == NULL)
	{
		exit(1);
	}
	for(i = 0; i <= 10; i++)
	{
		*(p+i) = i;//当i是10的时候越界访问
	}
	free(p);
	p = NULL;
}
  1. 对非动态开辟内存使用free释放
void test()
{
	int a = 10;
	int *p = &a;
	free(p);
}
  1. 使用free释放一块动态开辟内存的一部分
void test()
{
	int *p = (int *)malloc(100);
	p++;
	free(p);//p不再指向动态内存的起始位置
}
  1. 对同一块动态内存多次释放
void test()
{
	int *p = (int *)malloc(100);
	free(p);
	free(p);//重复释放
}
  1. 动态开辟内存忘记释放(内存泄漏)
void test()
{
	int *p = (int *)malloc(100);
	if(NULL != p)
	{
		*p = 20;
	}
}
int main()
{
	test();
	while(1);
	return 0;
}

动态开辟的内存最终会被释放,释放方式通常有两种:

  1. 程序员主动调用 free 释放;
  2. 程序结束时,由操作系统回收该进程占用的全部内存资源。
  • 对于一些运行时间较短的程序,即使忘记释放动态内存,程序结束后操作系统也会将其回收。但在实际开发中,很多程序(例如服务器程序、数据库程序等)需要长期甚至持续运行,不会轻易退出。

  • 因此,当一块动态申请的内存不再使用时,应及时调用 free 将其释放。否则,这部分内存会一直被当前进程占用,无法被程序中的其他功能再次利用,从而造成内存泄漏(Memory Leak)。随着泄漏的内存越来越多,程序可用内存会不断减少,严重时甚至可能导致程序运行缓慢、申请内存失败,甚至崩溃。

柔性数组

你可能从没听过柔性数组(flexible array),但它是真实存在的语法特性。
在 C99 标准里,结构体的最后一个成员允许定义长度未知的数组,这种数组就被称作柔性数组成员。

示例写法一:

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

部分编译器使用 a[0] 会编译报错,遇到这种情况可以换另一种写法:

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

柔性数组的特点

柔性数组的特点

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

要注意的是如果直接用类型创建变量,创建在栈区的话,柔性数组是无法使用的,只能使用除了柔性数组外的其他成员变量。

typedef struct st_type
{
	int i;
	int a[0];//柔性数组成员
}type_a;
int main()
{
	printf("%d\n", sizeof(type_a));//输出的是4
	return 0;
}

柔性数组的使用

//代码1
#include <stdio.h>
#include <stdlib.h>

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

int main()
{
	int i = 0;
	type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
	p->i = 100;
	for(i=0; i<100; i++)
	{
		p->a[i] = i;
	}
	free(p);
	return 0;
}

这样柔性数组成员a,相当于获得了100个整型元素的连续空间。
在这里插入图片描述

柔性数组的优势

前面介绍的柔性数组结构(代码1)与下面这种“结构体 + 指针成员”的写法(代码2)都能够实现动态存储数据的功能。

//代码2
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
	int i;
	int *p_a;
}type_a;
int main()
{
	type_a *p = (type_a *)malloc(sizeof(type_a));
	p->i = 100;
	p->p_a = (int *)malloc(p->i*sizeof(int));
	//业务处理
	for(i=0; i<100; i++)
	{
		p->p_a[i] = i;
	}
	//释放空间
	free(p->p_a);
	p->p_a = NULL;
	free(p);
	p = NULL;
	return 0;
}

1. 便于内存管理

对于代码2:

type_a* p = malloc(sizeof(type_a));
p->p_a = malloc(100 * sizeof(int));

实际上进行了两次动态内存申请:
一次为结构体对象申请空间;
一次为数据成员申请空间。
因此释放时也必须对应地进行两次释放:
free(p->p_a);
free(p);
在这里插入图片描述
要注意的是如果该结构体是在某个库函数内部创建并返回给用户的,用户可能只知道释放结构体本身:
free(p);
而忽略释放 p->p_a 指向的内存,从而造成内存泄漏。

而柔性数组通常采用一次性分配:
struct S* p = malloc(sizeof(struct S) + n * sizeof(int));
柔性数组是结构体的一个成员,柔性数组元素与其他结构体成员共同存储在同一块连续内存空间中。因此整个结构体对象只需一次动态内存申请,也只需一次 free 即可完成释放,因此释放时只需:
free(p);
即可释放全部空间,降低了内存泄漏的风险,也使内存管理更加简单。

2. 有利于提高访问效率

  • 柔性数组的数据与其他结构体成员存放在同一块连续内存中。

  • 而指针方案通常需要分别申请内存:
    结构体 -------> 数据区
    两块内存的位置可能相距较远。

连续存储具有以下优点:

  • 更符合 CPU 缓存(Cache)的工作方式;
  • 减少缓存未命中的概率;
  • 提高数据访问效率;
  • 减少内存碎片的产生。

因此,在需要频繁访问大量数据时,柔性数组通常具有更好的性能表现。

柔性数组优点总结

柔性数组相比于“结构体 + 指针成员”的方案,主要有两个优点:

  • 一次申请、一次释放,内存管理更加简单。
  • 数据连续存储,缓存友好,访问效率更高,同时能减少内存碎片。

因此,在需要存储变长数据且数据生命周期与结构体一致时,柔性数组往往是更优的选择。

总结

谢谢观看!

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值