首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >#C语言——学习攻略:攻克 动态内存分配、柔性数组,根本不在话下!

#C语言——学习攻略:攻克 动态内存分配、柔性数组,根本不在话下!

作者头像
晨非辰Tong
发布2025-12-23 14:57:29
发布2025-12-23 14:57:29
570
举报
文章被收录于专栏:C++C++

1. 动态内存分配的作用

--已经掌握的内存开辟方式:

代码语言:javascript
复制
int a = 20;//在栈空间中开辟4个字节
char arr[10] = { 0 };//在占空间开辟10个字节

--上述方式特点:

  • 开辟空间的大小是固定的;
  • 数组在声明需指定数组长度,空间大小无法再修改;

--有时,空间大小只能在程序运行时确定,所以以上方式不适合——引进了动态内存开辟,允许自己申请和释放空间。


2. malloc 和 free 函数

2.1 malloc 函数

--介绍提供的动态内存开辟函数:

代码语言:javascript
复制
void* malloc(size_t size);

--介绍:

--包含 stdlib.h头文件 功能:向内存在堆区申请一块连续的空间; 参数:size ,要申请空间的大小(例如:要输入n个 int 型数据,大小为 n * sizeof(int) ); 返回值:指向开辟好空间的指针(地址起始位置);

--注意:

--开辟成功,返回指向开辟好空间的指针; --开辟失败:返回 NULL指针,一定要检查mallco的返回值!; --返回类型void* ,具体类型看需要; --size函数若为0,行为是未定义的。

2.2 free 函数

--专门用来释放动态内存。

代码语言:javascript
复制
void free(void* ptr);

--介绍:

--包含 stdlib.h头文件 功能:函数用于释放之前通过 malloc() 等函数动态分配的内存; 参数 ptr是一个指向先前分配的内存块的指针,指针必须是通过 malloc()等 返回的指针值(起始位置); 返回值void,没有返回值。

--注意:

--参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的; --若参数 ptr 是NULL指针,函数无事干; --每一个 malloc()等开辟内存函数的调用,必须最终对应一个 free() 的调用; --禁止重复释放,建议释放后立即置空。

2.3 malloc 和 free 函数的应用

代码语言:javascript
复制
#include<stdlib.h>
int main()
{
	int n = 0;
	printf("输入申请个数:");

	//输入个数
	scanf("%d", &n);
	//申请空间,存放数据

	int* ptr = (int*)malloc(n * sizeof(int));
	if (p == NULL)//判断非空
	{
		perror("malloc");
		return 1;
	}

	//使用申请的内存空间
	for (int i = 0;i < n;i++)
	{
		p[i] = i + 1;
		printf("%d ", p[i]);
	}

	free(ptr);//用完就释放
	ptr = NULL;//释放就置空
	return 0;
}

--补充:

--对于使用malloc函数时:要输入 int 型数据,就用int型指针变量接收;因函数返回类型为void*,需要强转为 int*来使用。


3. calloc 和 realloc

3.1 calloc 函数

代码语言:javascript
复制
void* calloc(size_t num, size_t size);

--介绍:

--包含头文件 <stdlib.h> ; 功能:在内存的区动态地分配一块连续的内存空间,并将所有字节初始化为零; 参数:num -- 需要分配的元素个数、size -- 每个元素的大小(以字节为单位); 返回值:分配成功,返回指向这块内存起始地址void* 指针;分配失败(如内存不足),则返回 NULL

--注意:

--检查返回值:和 malloc() 一样,检查 calloc() 的返回值是否为NILL -- 与 malloc() 的核心区别:calloc() 会进行初始化,而 malloc() 不会; --需要类型转换。

--演示 malloc 与 calloc函数的区别:

代码语言:javascript
复制
int main()
{
	int n = 0;
	printf("输入申请个数:");

	//输入个数
	scanf("%d", &n);
	//申请空间存放数据

	int* p = (int*)calloc(n, sizeof(int));
	if (p == NULL)//判断是否为空
	{
		perror("malloc");
		return 1;
	}

	//使用内存空间,看是否初始化为0
	for (int i = 0;i < n;i++)
	{
		printf("%d ", p[i]);
	}
	free(p);//用完就释放
	p = NULL;//释放就置空
	return 0;
}

输入申请个数:10
输出:0 0 0 0 0 0 0 0 0 0

--上面可知:calloc函数“先置零,再使用”,不信任未初始化的内存,主动为自己创造一个安全可控的环境后再开始工作。优势所在!!

3.2 realloc 函数

代码语言:javascript
复制
void* realloc(void* ptr, size_t size);

--介绍:调整原内存空间大小基础上,会将原内存中数据移到新空间。

功能:用于重新分配之前通过 malloc(), calloc(), 或 realloc() 分配的内存块的大小 -- 扩大或缩小; 参数:ptr -- 指向先前分配的内存块的指针。如果 ptrNULL,则 realloc() 的行为等同于 malloc(new_size)(开辟空间); size -- 这是新的内存块大小,以字节为单位。如果 size0,并且 ptr 不为 NULL,那么 realloc 的行为就等同于 free(ptr),并且返回 NULL; 返回值:函数返回一个指向新分配内存的指针。这个指针可能与 ptr 相同,也可能不同。如果分配失败,则返回 NULL,并且原来的内存块不会被释放或修改

--注意:

--不要直接将返回值赋给原指针:因为若返回NULL就会覆盖原地址,导致旧数据丢失--使用临时指针在赋值;

--realloc函数调整内存空间的情况:

情况1--原有空间后有足够大的空间进行调整:直接进行操作,保留原数据; 情况2--原有空间后没有足够大的空间进行调整:在堆空间寻找合适大小的连续空间,并且返回新地址;

--代码+图示:

代码语言:javascript
复制
int main()
{
	//申请一块连续空间,存放数据
	int* p = (int*)malloc(5 * sizeof(int));//

	//判断是否为空
	if (p == NULL)
	{
		perror("malloc");
		return 1;//错误结束
	}

	//使用空间
	//先存放1~5
	for (int i = 0; i < 5; i++)
	{
		p[i] = i + 1;
	}

	//扩大空间,继续存放6~10
	int* p2 = (int*)realloc(p, 10*sizeof(int));
	if (p2 == NULL)
	{
		perror("realloc");
		return 1;
	}

	p = p2;//仍然使用p指向空间地址

	//存放
	for (int i = 5; i < 10; i++)
	{
		p[i] = i + 1;
	}

	//打印空间内的数据
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}

	free(p);//释放空间
	p = NULL;//置空,防止指向无效地址成为野指针
	return 0;
}

x64环境:

x86环境:

--注意在调试时(vs2022),现将p的地址显出来后在添加p2进行显示地址!


4. 常见的的动态内存错误

4.1 对NULL指针的解引用

代码语言:javascript
复制
int main()
{
	int* p = (int*)malloc(INT_MAX / 4);
	/*if (p == NULL)
	{
		return 1;
	}*/
	//如果p的值是NULL,就会有问题
	for (int i = 0; i < 10; i++)
	{
		p[i] = i + 1;
	 }
	free(p);
	p = NULL;
	return 0;
}

--注意一定要判断内存开辟函数的返回值!!

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

代码语言:javascript
复制
int main()
{
	//申请空间
	int* p = (int*)malloc(5 * sizeof(int));

	//判断返回值
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}

	//使用空间
	for (int i = 0; i < 10; i++)
	{
		p[i] = i + 1;//注意i值,i > 5超过了动态内存空间大小,越界
		printf("%d ", p[i]);
	}

	//释放、置空
	free(p);
	p = NULL;
	return 0;
}

--虽然最后打印出1~10 侥幸成功,但是这样是错误的!

4.3 对非动态开辟的内存进行free释放

代码语言:javascript
复制
int main()
{
	int arr[10] = { 0 }; 数组 栈区的空间	非动态
    int* p = arr;
	//.....
	//使用p
	//.....

	free(p);  释放动态空间 堆区
	p = NULL;
	return 0;
}

--free函数用于释放之前通过 malloc() 等函数动态分配的内存;对于栈区的内存,会自动销毁!!

4.4 free函数释放动态内存的一部分

代码语言:javascript
复制
int main()
{
	//申请一块空间
	int* p = (int*)malloc(5 * sizeof(int));
	if (p == NULL)//判断
	{
		perror("malloc");
		return 1;
	}
	//使用空间
	for (int i = 0;i < 5;i++)
	{
		*p = 1 + i;
		p++;
	}
	//循环结束,p指向了5以后的空间
	
	//free释放空间,一定要给空间的起始位置
	free(p);
	p = NULL;
	return 0;
}

-- free 函数参数指针必须是通过 malloc()等 返回的指针值(地址起始位置);对于数据存放用 - - p[ i ]= i + 1、*(p+i) = i+1,使用不要使用p++......

4.5 对同一块内存的重复释放

代码语言:javascript
复制
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}

	//使用内存空间
	//…………

	free(p);
	
	//…………
	
	free(p);
	p = NULL;
	return 0;
}

4.6 动态开辟内存忘记释放(内存泄漏,但程序仍会运行)

代码语言:javascript
复制
void test()
{
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}

	//最后没有释放
}
int main()
{
	test();
	//test() 函数结束时,栈帧被回收,p不存在,丢失内存的地址,内存空间仍然存在
	
	//…………
	//…………

	return 0;
}

--忘记释放不再使用的动态开辟的空间会造成内存泄漏; --注意:动态开辟的空间⼀定要释放,并正确释放。

  • 谁申请的空间,谁释放;
  • 不使用的空间,及时释放;
  • 自家(函数1)不方便释放的空间,要告诉别人(函数2)释放;

--在绝大多数情况下,即使忘记 free 当程序正常结束时,操作系统会自动回收动态分配的所有内存。


5. 动态内存经典笔试题分析

5.1 试题1

代码语言:javascript
复制
//错误示例
#include<stdio.h>
#include<stdlib.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;
}

--问题:

  1. 首先一个很显然的问题,函数调用 malloc 函数后没有对空间进行 free 释放,后续也无法释放,导致空间泄露
  2. 程序崩溃--主函数调用函数将str传给函数,但是二者之间是传值调用,实参、形参是两个独立个体,函数对形参进行操作,对实参没有任何改变。后续将str置空,由于strcpy函数无法对空指针进行操作(无效地址),错误进行。

--修改:

代码语言:javascript
复制
//修改1
#include<stdio.h>
#include<stdlib.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);
	free(str);//释放
}
int main()
{
	Test();
	return 0;
}
代码语言:javascript
复制
//修改2
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

char* GetMemory(char* p)//返回类型根据返回值进行修改
{
	p = (char*)malloc(100);
	return p;//复制一份出来
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory(str);//变量进行接收
	strcpy(str, "hello world");
	printf(str);
	free(str);//释放
}
int main()
{
	Test();
	return 0;
}

5.2 试题2

代码语言:javascript
复制
//错误示例
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

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

--修改:

代码语言:javascript
复制
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

char* GetMemory(void)
{
	static char p[] = "hello world";//变为静态变量,改变生命周期
	return p;
}
void Test(void)
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

--可能注意到,为什么 试题1_修改2 就是retuen形参可以呢?? --因为malloc开辟的动态内存只能被手动释放,函数返回时不会被销毁;而本题数组存在栈区,返回时销毁,且涉及到地址,函数最会返回的是数组首地址,但是原空间不存在,所以错误。

5.3 试题3

代码语言:javascript
复制
//错误示例
#include<stdio.h>
#include<stdlib.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;
}

--修改:

代码语言:javascript
复制
#include<stdio.h>
#include<stdlib.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);
	free(str);//用完就释放
	str = NULL;//释放就置空
}
int main()
{
	Test();
	return 0;
}

5.4 试题4

代码语言:javascript
复制
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
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;
}

--修改:

代码语言:javascript
复制
void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	str = NULL;//及时置空,防止成为野指针致非法访问

    //或者最后在进行释放、置空

	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}
int main()
{
	Test();
	return 0;
}

6. 柔性数组

--柔性数组:C99中,结构体中最后一个成员可以是未知大小的数组,称为柔性数组成员;可以自由调整大小。

代码语言:javascript
复制
struct st_type
{
	int i;
	int a[];//柔性数组成员--[]中填0看编译器选择
};

6.1 柔性数组特点

  • 结构体中的柔性数组成员前面必须至少⼀个其他成员;
  • sizeof 返回的这种结构大小不包括柔性数组的内存;
  • 含柔性数组成员的结构体用malloc ()函数进行内存动态分配--因为之前的方法无法给柔性数组分配空间,且分配的内存应该大于结构体大小,以满足柔性数组预期大小。
代码语言:javascript
复制
int main()
{
	struct s
	{
		int i;//4
		int a[];//...
	};
	printf("%zu\n", sizeof(struct s));
	return 0;
}
输出:4 //不包含柔性数组大小

--验证了第三条特点。

6.2 使用柔性数组(使用动态内存函数)

代码语言:javascript
复制
#include <stdlib.h>
struct s
{
	int n;
	int arr[];
};
int main()
{
	struct s* ps = (struct s*)malloc(sizeof(struct s) + 10 * sizeof(int));
	//计算结构体大小-n的大小,后面10*sizeof(int)是为柔性数组申请的空间

	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}

	//直接使用空间
	ps->n = 100;
	int i = 0;
	for (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");
		return 1;
	}
	ps = tmp;

	//继续使用空间
	//...

	//释放
	free(ps);
	ps = NULL;
	return 0;
}

6.3 柔性数组的优势

代码语言:javascript
复制
struct s
{
	int n;
	int* arr;
};
int main()
{
	struct s* ps = (struct s*)malloc(sizeof(struct s));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	ps->n = 100;
	int* ptr = (int*)malloc(10 * sizeof(int));
	if (ps == NULL)
	{
		perror("malloc2");
		return 1;
	}
	ps->arr = ptr;

	//存放1~10
	int i = 0;
	for (i = 0;i < 10;i++)
	{
		ps->arr[i] = i + 1;
	}
	//调整空间
	ptr = realloc(ps->arr, 20 * sizeof(int));
	if (ptr == NULL)
	{
		perror("realloc");
		return 1;
	}
	ps->arr = ptr;
	//继续使用空间
	//…………

	//释放
	free(ps->arr);
	free(ps);
	ps = NULL;
	return 0;

--对比上面代码,同样可以完成任务,但是使用柔性数组有两个好处:

  • 方便内存释放,如果代码是在⼀个给别⼈用的函数中,在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给用户⼀个结构体指针,⼀次free就把所有的内存释放掉;(free次数越多,发生错误的概率越大!
  • 这样有利于访问速度,连续的内存有益于提高i访问速度,也有益于减少内存碎片,但是提升也不会很大,你还是要用做偏移量的加法来寻址。

7. 总结C/C++中程序内存区域划分

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

  • 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数分配的局部变量、函数参数、返回数据、返回地址等
  • 堆区(heap):⼀般由程序员分配释放, 若程序员不释放,程序结束时可能由OS(操作系统) 回收 ,分配方式类似于链表。存放通过 malloc() 等动态分配的内存;
  • 数据段(静态区):(static)存放全局变量、静态数据。程序结束后由系统释放;
  • 代码段:存放函数体(类成员函数和全局函数)的二进制代码;
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-09-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 动态内存分配的作用
  • 2. malloc 和 free 函数
    • 2.1 malloc 函数
    • 2.2 free 函数
    • 2.3 malloc 和 free 函数的应用
  • 3. calloc 和 realloc
    • 3.1 calloc 函数
    • 3.2 realloc 函数
  • 4. 常见的的动态内存错误
    • 4.1 对NULL指针的解引用
    • 4.2 对动态开辟空间的越界访问
    • 4.3 对非动态开辟的内存进行free释放
    • 4.4 free函数释放动态内存的一部分
    • 4.5 对同一块内存的重复释放
    • 4.6 动态开辟内存忘记释放(内存泄漏,但程序仍会运行)
  • 5. 动态内存经典笔试题分析
    • 5.1 试题1
    • 5.2 试题2
    • 5.3 试题3
    • 5.4 试题4
  • 6. 柔性数组
    • 6.1 柔性数组特点
    • 6.2 使用柔性数组(使用动态内存函数)
    • 6.3 柔性数组的优势
  • 7. 总结C/C++中程序内存区域划分
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档