动态内存管理

动态内存分配的意义

我们之前掌握的内存开辟方式:

int val = 10;//创建变量:在栈空间上开辟了4个字节的空间

double arr [10] ={0.00}; //创建数组:在栈空间上申请开辟了80个字节的连续空间

上面开辟空间的方式有两个特点:

  • 空间开辟的大小固定
  • 创建数组时,必须指定数组的长度,数组空间确定后不能再调整

动态内存分配的核心作用

  1. 解决固定大小问题

    • 静态分配(如数组)必须提前确定大小,无法适应运行时变化的需求。

    • 动态分配允许程序运行时按需申请内存(例如用户输入决定数据量)。

  2. 突破栈空间限制

    栈内存(局部变量)大小有限(通常几MB),大内存需求(如处理图像、大型矩阵)必须用堆内存(动态分配)。
  3. 灵活控制生命周期

    静态变量(全局/局部)生命周期固定,动态内存可手动管理(malloc申请后,直到free才释放),适合长期存储数据(如链表节点)。
  4. 节省内存

    避免静态分配“按最大可能”预占内存的浪费(如声明int arr[1000]但只用了10个元素)。

引入动态内存管理,程序员可以自己申请释放资源,比较灵活。

malloc

C语言提供了一个动态内存开辟的函数:

void* malloc (size_t size);

作用:向内存的堆区申请了一块连续可用的空间,如果成功申请,就返回指向这块空间的起始地址。

头文件包含:malloc的使用需要包含头文件<stdlib.h>

参数说明:size表示要申请的空间大小,单位是字节

返回值说明:返回值是一个void*类型的指针,如果想要使用这块空间存储某一类型的数据,可以将其强制类型转换成相应类型的指针。

代码示例

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int n;
	scanf("%d", &n);
	//假设要申请n个大小的整型空间存放整型数据
	int* arr = malloc(sizeof(int) * n);
	if (arr == NULL)//申请失败的检查
	{
		perror("malloc fail");
		return 1;
	}
   //使用动态开辟的空间存放数据:
	for (int i = 0; i < n; i++)
	{
		*(arr + i) = i + 1;
	}
	for (int i = 0; i < n; i++)
	{
		printf("%d ", *(arr + i));
	}

	return 0;
}

注意:

  • 如果开辟成功,则返回这块空间的起始地址。
  • 如果开辟失败,则返回一个 NULL 指针,因此 malloc 的返回值一定要做检查。
  • 返回值的类型是 void* ,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数 size 为 0,malloc 的行为是标准是未定义的,取决于编译器。

free

free函数是专门用来做动态内存的释放和回收的,函数原型:

void free (void* ptr);

参数:ptr表示需要释放的内存块的起始地址。

功能:释放动态开辟的内存。

需包含的头文件:<stdlib.h>

动态申请的内存,我们最好在使用完以后手动将其释放掉,所以上面的代码我们写的还不太完整,现在学习了free函数就来将它完善一下吧:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int n;
	scanf("%d", &n);
	//假设要申请n个大小的整型空间存放整型数据
	int* arr = malloc(sizeof(int) * n);
	if (arr == NULL)//申请失败的检查
	{
		perror("malloc fail");
		return 1;
	}
   //使用动态开辟的空间存放数据:
	for (int i = 0; i < n; i++)
	{
		*(arr + i) = i + 1;
	}
	for (int i = 0; i < n; i++)
	{
		printf("%d ", *(arr + i));
	}
   free(arr);
   arr=NULL;
	return 0;
}

注意:

  • 如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是未定义的
  • 如果ptr是空指针,则函数什么事都不用做
  • 释放空间后应该及时将ptr值为空,否则ptr就是野指针。这是因为利用free将空间释放掉,实际上是将ptr指向的空间还给操作系统,但是还给操作系统以后,当前程序仍然能够通过ptr找到不属于当前程序的空间,如果再对这块空间进行访问,就是非法访问,所以我们要将ptr及时置空。
  • free释放空间时,它的参数一定要是动态申请的空间的起始位置,不能释放空间的一部分
  • 不能对同一块空间进行多次释放

calloc

C语言提供了calloc函数来进行动态内存分配,函数原型:

void* calloc (size_t num, size_t size);
  • 函数功能是为num个大小为size的元素开辟一块连续的空间,并且把空间的每个字节初始化为0。
  • calloc与malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为0

代码示例

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

运行结果:

0 0 0 0 0 0 0 0 0 0

如果我们要对申请的空间进行初始化,可以使用calloc来完成任务。

realloc

函数原型:

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

功能:realloc的出现让动态内存管理更加灵活。它的核心功能是扩展或缩小之前分配的内存块,并尽可能保留原有数据。

        扩展内存

  •  若原内存块后方有足够连续空间,直接扩展,返回原指针。

  • 若空间不足,会:分配新的更大的内存块;自动复制旧数据到新内存;释放旧内存块;返回新指针。

  • 如果空间开辟失败就会返回空指针,但原有空间保留

        缩小内存

  • 减少分配的大小,多余部分被释放
  • 可能返回原指针(取决于实现)

参数说明:ptr指向要调整的内存地址,size表示调整之后的空间的大小,单位是字节

返回值:是调整之后的内存的起始位置。

代码示例

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int n;
	scanf("%d", &n);
	//假设要申请n个大小的整型空间存放整型数据
	int* arr = malloc(sizeof(int) * n);
	if (arr == NULL)//申请失败的检查
	{
		perror("malloc fail");
		return 1;
	}
	for (int i = 0; i < n; i++)
	{
		*(arr + i) = i + 1;
	}
	for (int i = 0; i < n; i++)
	{
		printf("%d ", *(arr + i));
	}
	//扩展空间,注意,不要直接写成:arr=realloc(arr, 2 * n * sizeof(int));
	//因为如果空间开辟失败,arr就会变成NULL,就无法找到原有的空间
	//所以我们最好使用一个临时指针来接受返回值,进行检查后再让arr=tmp
	int* tmp = realloc(arr, 2 * n * sizeof(int));
	if (tmp == NULL)//realloc失败
	{
		perror("realloc fail");
		return 1;
	}
	arr = tmp;
//一系列操作……
    free(arr);
     arr=NULL;
	return 0;
}

realloc函数也可以直接用来开辟空间:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* arr = (int*) realloc(NULL, sizeof(int) * 10);//等价于malloc(sizeof(int)*10)
//其他操作…………
	return 0;
}

注意哦,当某个函数中使用动态申请的函数申请空间时, 当函数栈桢销毁的时候这块动态申请的空间并没有被销毁,仍属于当前程序。
只有当整个项目/工程结束的时候,或者我们手动使用free函数将这块空间释放的时候
这块空间才会还给操作系统
 

练习题

  • 练习1:下面代码的执行结果:
#include<stdio.h>
#include<stdlib.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;
}

解析:

程序崩溃:str传递给getmemory函数的时候,采用的值传递,形参变量p是str的一份拷贝
当我们把malloc申请的空间的起始地址放在p中时,我们不会修改str,str仍然是NULL
所以当getmemory函数返回后,再去调用strcpy函数,将“hello world"拷贝到str指向的空间
时,程序崩溃
另外,由于malloc申请的空间没有free释放,造成内存泄漏

那么,如何修改代码?

//修改程序:将传值调用改为传址调用,形参的改变可以影响实参
#include<stdio.h>
#include<stdlib.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);
	free(str);
	str = NULL;
}

int main()
{
	Test();
	return 0;
}
  • 题目2:下面代码的执行结果?
#include<stdio.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返回到了Test函数中
 并让str接收返回值,那就能通过str找到这块不属于当前程序的空间,就属于非法访问了
 而这块空间中存放的值也是随机值,所以运行结果是随机值
 

如何修改代码:

//修改代码:
#include<stdio.h>
#include<stdlib.h>
char* GetMemory()
{
	static char p[] = "hello world";//static修饰的变量是放在静态区中的
	//所以函数栈桢销毁的时候,这块空间仍然保留
	return p;
}
void Test()
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

 柔性数组


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

例如:

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

有些编译器会报错无法编译可以改成:

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

柔性数组的特点

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

用代码来说明特点2:

#include<stdio.h>
struct S1
{
	int i;
	int arr[];
};
struct S2
{
	int i;
};
int main()
{
	printf("%zu\n", sizeof(struct S1));
	printf("%zu\n", sizeof(struct S2));
	return 0;
}

运行结果:

所以,结构体的大小并不会包括柔性数组的大小,这也就意味着,我们在使用含有柔性数组的结构体时,不会按照下面的方法创建变量:

struct S
{
	int i;
	int arr[];
};
#include<stdio.h>
int main()
{
	struct S s;//一般不会这么写:这样写的话并不会为数组分配空间
	return 0;
}

那应该怎样写?如下:

#include<stdlib.h>
#include<stdio.h>
struct S
{
	int i;
	int arr[];
};
int main()
{
	
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));
	//后面的10*sizeof(int)表示为柔性数组开辟的空间
	if (ps == NULL)
	{
		perror("malloc fail");
		return 1;
	}
	ps->i = 1;
	for (int i = 0; i < 10; i++)
	{
		ps->arr[i] = i + 1;
	}
	//假设要调整柔性数组的空间:
	struct S* tmp =(struct S* )realloc(ps , sizeof(struct S) + 20 * sizeof(int));
	if (tmp == NULL)
	{
		perror("realloc fail");
		return 1;
	}
	ps = tmp;

	//释放空间
	free(ps);
	return 0;
}

其实,如果我们不使用柔性数组,却想达到相同的效果,可以像下面这么写:

//如果不使用柔性数组可以利用指针达到同样的效果:
struct S
{
	int i;
	int* arr;
};
#include<stdio.h>
#include<stdlib.h>
int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
	{
		perror("malloc fail");
		return 1;
	}
	ps->i = 1;
	ps->arr = (int*)malloc(sizeof(int) * 10);
	if (ps->arr == NULL)
	{
		perror("malloc fail2");
		return 2;
	}
	//使用动态开辟的数组:
	for (int i = 0; i < 10; i++)
	{
		ps->arr[i] = i + 1;
	}
	//如果想要扩容:
	int* tmp = (int*)realloc(ps->arr, sizeof(int) * 20);
	if (tmp == NULL)
	{
		perror("realloc fail");
		return 0;
	}
	ps->arr = tmp;
	//继续使用空间…………

	//释放空间:注意顺序不能乱,如果先释放ps,那就不能通过ps找到arr所指向的空间了
	free(ps->arr);
	free(ps);
	return 0;
}

使用柔性数组(第一个代码)的好处是:方便内存释放

如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用 free 可以释放结构体,但是用户并不知道这个结构体内的成员也需要 free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次 free 就可以把所有的内存也给释放掉。

第二个代码是:这样有利于访问速度

连续的内存有益于提高访问速度,也有益于减少内存碎片。(内存碎片(Memory Fragmentation)是指内存空间被分割成许多小块,导致虽有足够总空闲内存,但无法分配连续大块内存的现象。)

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值