C语言:动态内存分配

栈区与堆区初探

C程序会对内存进行分区,主要分为5个区域:

  • 栈区(Stack)
  • 堆区(Heap)
  • 全局/静态区
  • 常量区(Constant)
  • 代码区

我们先主要了解前两个:

栈内存 由编译器自动分配和释放,我们不需要操心。每调用一个函数,都会在栈区为该函数分配一块内存区域,这块区域就叫做函数栈帧。其中主要存放一些非静态的局部变量、函数参数等。

例如,下面代码中的函数形参 b、定义的局部变量 a 所用到的内存,都会由编译器自动开辟,开辟的方式是静态内存开辟 。当 add 函数执行完毕返回时,对应的函数栈帧就会被销毁,自然这些占用的内存会被编译器自动回收。

c 复制代码
int add(int b) {
	int a = 10;
	return a + b;
}

堆内存 由我们程序员手动分配(malloccalloc)和释放(free)。

malloccalloc 的区别在于:malloc 分配的内存存储的都是未初始化的随机垃圾值,而 calloc 会自动将分配的内存全部初始化为 0。

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
	// 申请了32MB的内存
	int* arr = (int*) malloc(8 * 1024 * 1024 * sizeof(int)); // 返回值类型是void*,表示无类型指针,我们可以强转赋予它类型
	
	// 每次动态分配内存后,都要检查返回值是否为 NULL
	if (arr == NULL) {
		// 防止操作到空指针,导致程序崩溃
		printf("Memory allocation failed!\n");
		return -1; 
	}

	// 释放内存        
	free(arr);
	// 置空,防止野指针
	arr = NULL;
	
	return 0;
}

堆内存的开辟方式是动态内存开辟,这些内存不会自动回收,如果不手动回收,就会造成内存泄漏。

此外,栈空间通常很小(1MB),堆空间则很大,和系统可用的内存有关。

运行时决定内存大小

动态内存开辟的使用场景有很多:数据长度只在运行时才确定、栈空间不满足需求、需要延长变量的生命周期、内存大小需要动态改变等。

我们以第一种场景为例:运行时由用户输入决定人员的数量。

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
	// 用户输入
	int num = 0;
	printf("Please enter the number of people.\n");
	scanf_s("%d", &num);

	// 开辟对应大小的空间
	int* arr = (int*)malloc(num * sizeof(int));
	
	// 检查内存是否开辟成功
	if (arr == NULL) {
		printf("Memory allocation failed!\n");
		return -1; // 退出程序
	}

	for (int i = 0; i < num; i++)
	{
		// 输入年龄
		int age = 0;
		printf("Please enter the age of the %d member at this position.\n", i + 1);
		scanf_s("%d", &age);
		arr[i] = age;
	}

	// 输出每个人的年龄
	for (int i = 0; i < num; i++)
	{
		printf("The age of the %d member is %d\n", i + 1, arr[i]);
	}

	// 释放并置空
	free(arr);
	arr = NULL;
	
	return 0;
}

scanf_s 是 Visual Studio 环境下特有的安全函数,在非 VS 环境中请使用 scanf

运行结果:

realloc 的扩容机制与暗坑

再来看看第四种场景,普通的数组一旦定义后,长度就固定了,而动态内存的大小可以使用 realloc 进行重新调整,根据自己的需要扩容或缩容。

使用 realloc 进行扩容时,有两种情况:

  • 原地扩容:如果原位置后有足够的连续内存空间,它会直接在原地址后追加空间,返回的地址和原地址相同。

  • 异地扩容 :如果原位置所需的连续地址空间不足,它会尝试在堆区找到一块合适的内存空间,将之前的数据拷贝到新位置,并自动释放之前的旧内存,最后返回指向这块新内存空间的指针。

因为异地扩容很常见,所以我们应该总是要使用新的指针去接收返回值。同时,如果发生后了异地扩容,原来的指针就变为了野指针,应该置为空。

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
	// 初始可以存储8个整型
	int* p = (int*)malloc(8 * sizeof(int));
	if (p == NULL) {
		printf("Initial memory allocation failed.\n");
		return -1;
	}

	for (int i = 0; i < 8; i++)
	{
		p[i] = i + 1;
	}

	printf("Before capacity expansion\n");
	for (int i = 0; i < 8; i++)
	{
		printf("%d ", p[i]);
	}

	// 扩容至16
	printf("\nAfter capacity expansion\n");
	
	// 使用新指针变量接收,防止因扩容失败导致原内存地址 p 丢失
	int* new_p = (int*)realloc(p, 16 * sizeof(int));
	
	if (new_p == NULL) {
		printf("\nFailed to allocate memory for expansion.\n");
		// 扩容失败,旧内存 p 依然有效,程序结束前记得释放
		free(p);
		p = NULL;
		return -1;
	}

	// 扩容成功,原指针 p 可能已在异地扩容中被自动释放而失效,为防止误用,我们将其置空
	p = NULL; 

	for (int i = 8; i < 16; i++)
	{
		new_p[i] = i + 1;
	}
	for (int i = 0; i < 16; i++)
	{
		printf("%d ", new_p[i]);
	}

	// 此时由 new_p 管理这块空间,我们只需释放 new_p
	free(new_p);
	new_p = NULL;

	return 0;
}

如果 realloc 扩容失败,它将返回 NULL 空指针,但旧内存不会被释放,我们需要手动处理。

c 复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
	int* p = (int*)malloc(8 * sizeof(int));
	if (p == NULL) return -1;
	
	// 尝试申请一块非常大的内存,模拟失败的情况
	int* new_p = (int*)realloc(p, 8 * 1024LL * 1024 * 1024 * sizeof(int));

	if (new_p == NULL)
	{
		printf("Failed to allocate memory.\n");
		// 虽然申请新内存失败,但是旧内存块 p 依然存在,需要由我们手动释放
		free(p);
		p = NULL;
	}
	else 
	{
		printf("Success to allocate memory.\n");
		// 如果成功,释放新指针 new_p 即可
		free(new_p);
		new_p = NULL;
		p = NULL; // 置空防误用
	} 
	
	return 0;
}

注意:永远不要多次释放同一块内存,可能会导致程序崩溃。

相关推荐
Android-Flutter1 小时前
android compose 自定义Painter绘制图形 使用
android·kotlin·compose
我是一颗柠檬2 小时前
【Java项目技术亮点】覆盖索引与索引下推优化
android·java·开发语言
vigor5123 小时前
MySQL通过Mango实现分库分表
android·数据库·mysql
阿pin6 小时前
Android随笔-Zygote中fork究竟是什么?
android·zygote·fork
Go-higher6 小时前
DriverTest 驾考知识卡片学习助手 —— 一款基于 Jetpack Compose 的现代 Android 学习APP
android·学习
安卓修改大师7 小时前
安卓修改大师APK控件修改实战教程
android
阿pin7 小时前
Android随笔-Zygote是什么?
android·zygote
小虎牙0077 小时前
Android kotlin图片库Coil源码详解
android·前端