一文带你了解C语言中的动态内存管理函数

目录
  • 1.什么是动态内存管理
  • 2.为什么要有动态内存管理
  • 3.如何进行动态内存管理
    • 3.1 malloc
    • 3.2 free
    • 3.3 calloc
    • 3.4 realloc
  • 总结

1.什么是动态内存管理

平时我们写代码,一种非常常见的写法是:

int a = 0; // 创建一个变量
int arr[10] = {0}; // 创建一个数组

当你创建变量a的时候,其实是向内存中申请了4个字节的空间来存放一个整数。而当你创建数组arr的时候,是向内存中申请了40个字节的空间来存放10个整数。当你这么写了之后,从a和arr创建开始,一直到程序结束,它们都只会占用4个和40个字节的空间,不会多也不会少!换句话说,它们的大小并不会变化,是静态的。

与之相反,如果你向内存中申请一块空间,这块空间可以一会大,一会小,你觉得不够了就扩容,你觉得空间太多了就缩容,你想要多少空间就来多少空间,这块空间的大小是会变化的,所以我们认为它是动态的。对这样可以改变大小的内存空间的管理,称之为动态内存管理。

你可能会想:这么神奇?我还能自己操纵向内存申请的空间大小?这是怎么做到的呢?别急,听我慢慢道来。

当你创建一个局部变量时,比如int a = 0;,变量a是存储在栈区上的。一般来说,如果你想要改变栈区上申请的空间的大小,是非常困难的,因为这块空间是编译器提前帮你算好的,一旦进入到函数内部就自动帮你开辟好了,我们作为程序员并没有太多的操作空间。但是内存中有一块空间,专门用来给我们做动态内存管理,这块空间就是堆区。如果我们在堆区上申请空间,就可以做到想要多少空间就来多少空间,不够了还可以继续申请,非常灵活。

2.为什么要有动态内存管理

静态的内存管理,某些情况下会显得很死板。比如,我要实现一个通讯录,是不是要创建一个数组来存储联系人的信息?那好,我要开辟多大的空间?如果容量是100人,假设我要存储200个人的信息呢?你可能会说,那好,给我1w个空间!那我如果只存3个人的信息呢?这么多的空间是不是就浪费了?

所以,静态的内存管理有2个硬伤:

  • 开辟空间太少,不够用。
  • 开辟空间太多,造成空间浪费。

而这2个问题可以用动态内存管理的方式完美解决。空间太少,咱再多来点!空间太多,我还可以缩容,减少空间消耗。

3.如何进行动态内存管理

这可能是大家最关心的问题。我应该怎么样进行动态内存管理呢?C语言提供了4个函数,专门用来进行动态内存管理。这四个函数分别是malloc, free, calloc, realloc。使用这几个函数都需要引用头文件stdlib.h。

接下来,我将详细介绍这几个函数的用法。

3.1 malloc

如果你想要40个字节的空间来存储10个int,你可以跟内存说:“喂,内存!给我40个字节的空间!我要用!”也可以直接使用malloc函数。malloc函数的声明如下:

void* malloc(size_t size);

其实你只需要给malloc传你需要的空间大小就行了。比如你想申请40个字节,就直接malloc(40);,此时malloc就会屁颠屁颠的跑去内存那里,申请40个字节的空间,然后返回这块空间的起始地址。有了前面“什么是动态内存管理”的讲解,你应该很清楚,这40个字节的空间是在内存的堆区上的。

注意,malloc返回空间的地址,类型是void*。啥是void*呢?其实,void*的意思是,malloc不知道返回的地址是什么类型的,这要你来告诉他。比如,你想用这40个字节的空间来存储10个int,你当然希望这个指针是int*类型的,此时你就会用一个int*类型的指针来接收。由于void*和int*毕竟是两种不同的类型,所以需要强制类型转换。具体的写法如下:

int* ptr = (int*)malloc(40);

此时你就有了一个指针ptr,指向了动态开辟的40个字节的连续空间了,你就可以在这块空间里为所欲为了。接下来,你还需要掌握一些细节。

malloc函数申请空间一定会成功吗?那可不见得!如果你一次申请的空间太多了,比如malloc(INT_MAX);,INT_MAX是整形变量能够存储的最大值,你一次申请这么多字节的空间,那很有可能失败呀!或者还有一种可能,你的电脑已经运行了几百个进程了,内存已经快被耗干了,哪怕你申请的空间不是很多,也有可能申请失败。

malloc函数在申请空间失败的时候,会返回NULL指针。每次使用malloc函数的时候,都必须要检验返回值是不是NULL,否则后面在使用这个指针的时候,有可能会有对NULL指针的解引用操作,这是非常危险的!检查的示例代码如下:

if (ptr == NULL)
{
    // 错误处理
    perror("malloc()");
    exit(-1);
}

错误处理的代码应该根据实际情况来写,我这里只是给了一个常见的处理方式:用perror函数报个错,接着直接用exit函数结束进程,非常简单粗暴。当你验完货,发现货的质量不对,直接扭头就走,不玩了,exit掉。而当你觉得这货的质量还可以,就可以使用了。

使用起来非常简单,ptr指向了这块空间的起始地址,由于类型是int*,+1就跳过一个int,+2就跳过2个int,以此类推。每次解引用,就可以访问这块空间了,比如把1~10放进去:

for (int i = 0; i < 10; i++)
{
    *(ptr + i) = i + 1;
}

由于在C语言中,*(a+b)就等价于a[b],所以上面的代码也可以这么写:

for (int i = 0; i < 10; i++)
{
    ptr[i] = i + 1;
}

有没有发现,ptr就像一个数组一样,这个数组的容量是10个int。

3.2 free

注意,动态内存管理的空间需要程序员手动释放!前面我们用malloc开辟了一块空间,并把这块空间的起始地址交给ptr指针来看管,最后,我们还要使用free函数来释放这块空间。使用方式非常简单,你想释放谁就free谁,比如:

free(ptr);

传给free函数的指针必须指向一块动态开辟空间的起始位置!言外之意就是,你malloc出来一块空间交给ptr管理后,你还能修改ptr吗?答案是:不行。你一旦把ptr给改了,就找不到这块空间的起始位置了,就没办法把它free掉了。就相当于,警察局有一个卧底,这个卧底和一个领导是单线联系的,如果领导被干掉了,就没有人知道这个卧底的身份了。ptr就是这个领导,malloc开辟出来的这块空间就是这个卧底。

你可能会想:如果我不free,会怎么样呢?事实上,如果程序员没有手动回收动态申请的空间,当程序结束时,这块空间会自动还给操作系统;如果程序一直不结束,这块空间就一直不会被回收,就一直搁那,占据资源,浪费内存,此时我们称,造成了内存泄漏。

free函数会把ptr指针置成NULL吗?答案是:不会。free函数没有这个能力。如果你函数这个章节学的不错,你应该知道,C语言调用函数时是值传递,函数内部的形参是实参的一份临时拷贝,改变形参不会影响实参。也就是说,free函数内部会有另一个指针拷贝ptr的值,free函数会把这个指针指向的内存空间还给操作系统,但是没有能力影响外面的ptr指针。

既然ptr指针没有被置成NULL,也就是说,ptr的还是指向原来malloc申请的那块空间,但是这块空间已经还给操作系统了!如果还使用ptr指针访问这块空间,就造成了内存的非法访问,因为此时ptr是一个野指针!这是很危险的一件事。所以,在free掉这个指针之后,最好把它置为NULL,就像这样:

free(ptr);
ptr = NULL;

最后还有一个细节,如果传给free函数的是NULL指针,free函数什么也不做。free(NULL);相当于什么也没发生。

来看看一段完整的代码,来演示malloc和free:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	// 开辟空间
	int* ptr = (int*)malloc(10 * sizeof(int));
	// 检验是否开辟成功
	if (NULL == ptr)
	{
		perror("malloc");
		return 1;
	}

	// 把1~10放到这块空间里
	for (int i = 0; i < 10; i++)
	{
		ptr[i] = i + 1;
	}

	// 打印这块空间里的值
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", ptr[i]);
	}

	// 释放这块空间,别忘了把ptr置NULL
	free(ptr);
	ptr = NULL;

	return 0;
}

输出结果:

3.3 calloc

calloc函数的使用和malloc函数几乎完全一样,只有2个微小的区别:

  • calloc函数有2个参数,分别表示开辟空间要存储的元素个数和每个元素的大小。malloc函数只有一个参数,表示要开辟的空间的总大小。
  • calloc函数会把开辟的空间初始化成0。malloc函数并不会做任何处理,所以空间内存储的是随机值。也就是说,calloc由于会对开辟的空间进行初始化,效率会低一点,相当于malloc+memset。

由于使用上就这点区别,大家看个例子就懂了。我只是把上面的代码改一下:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	// 开辟空间(40个字节,即10个int)
	int* ptr = (int*)calloc(10, sizeof(int));
	// 检验是否开辟成功
	if (NULL == ptr)
	{
		perror("calloc");
		return 1;
	}

	// 打印这块空间里的值
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", ptr[i]);
	}

	// 释放这块空间,别忘了把ptr置NULL
	free(ptr);
	ptr = NULL;

	return 0;
}

输出结果:

输出结果侧面验证了,calloc会把开辟的空间全部初始化成0。

3.4 realloc

刚刚讲了3个函数了,也只是把空间开辟出来,然后再还给操作系统,似乎也没有什么新鲜的?其实,最后一个函数,realloc,会让你大开眼界。

realloc函数可以帮你改变开辟的空间的大小,你可以让这块空间任意的变大变小,非常灵活!

realloc函数的声明如下:

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

第一个参数是你想扩容的空间的起始地址,也就是一开始接收malloc和calloc返回值的指针,第二个参数是你想把空间改变的新大小。注意:不是相对大小!是新大小!

比如,一开始先malloc出40个字节的空间,这块空间能存10个int:

int* ptr = (int*)malloc(40);
if (NULL == ptr)
{
    // 错误处理
    // ...
}

如果这块空间存满了,你还想再存10个int进去,此时新空间的大小就是20个int,即80个字节,比原空间多出40个字节。再次强调:realloc的第二个参数是新空间的总大小!是最终的大小,而不是相对的大小。所以如果把原来的40个字节的空间翻一倍,应该写realloc(ptr, 80);而不是realloc(ptr, 40);,后一种写法相当于空间大小没有改变。

realloc函数会返回新空间的起始地址,如果扩容失败就会返回NULL。那能不能这么写?

ptr = (int*)realloc(ptr, 80);

答案是:不行!你想想,如果扩容失败,返回NULL给ptr,相当于既没有扩容成功,还把原来的空间的地址给弄丢了,那不是赔了夫人又折兵吗?所以,还是那句话,得先验货,再使用。比如:

int* tmp = (int*)realloc(ptr, 80);
if (tmp == NULL)
{
    // 扩容失败
    perror("realloc");
    exit(-1);
}
else
{
    // 扩容成功
    ptr = tmp;
}

那realloc函数具体是如何做到改变动态开辟空间的大小的呢?

realloc函数会先看一眼,原来空间后面的空间够不够。比如上面的例子中,realloc函数会去看,原来的空间再往后数40个字节的空间有没有被占用,如果没有,realloc会直接把原来空间的后40个字节的空间给申请到,再把原来空间的起始地址给返回,此时是原地扩容。

但是如果原来的空间的后面的空间被占用了呢?那就得另外找一块80个字节的空间,把原来的40个字节的数据拷贝过去,接着释放掉原来的空间,返回新空间的起始地址。这种扩容方式称为异地扩容。

还有一点,realloc函数也可以像malloc函数一样使用。当第一个参数是NULL时,realloc函数的表现和malloc一样。也就是说,malloc(40);和realloc(NULL, 40);等价。

我们把最初的程序改造一下,如下:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	// 开辟空间
	int* ptr = (int*)malloc(10 * sizeof(int));
	// 检验是否开辟成功
	if (NULL == ptr)
	{
		perror("malloc");
		return 1;
	}

	// 把1~10放到这块空间里
	for (int i = 0; i < 10; i++)
	{
		ptr[i] = i + 1;
	}

	// 打印这块空间里的值
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", ptr[i]);
	}
	printf("\n");

	// 扩容
	int* tmp = (int*)realloc(ptr, 20 * sizeof(int));
	if (NULL == tmp)
	{
		// 扩容失败
		perror("realloc");
		return 1;
	}
	else
	{
		// 扩容成功
		ptr = tmp;
	}

	// 把11~20也放进去
	for (int i = 10; i < 20; i++)
	{
		ptr[i] = i + 1;
	}

	// 打印这块空间的所有值
	for (int i = 0; i < 20; i++)
	{
		printf("%d ", ptr[i]);
	}

	// 释放这块空间,别忘了把ptr置NULL
	free(ptr);
	ptr = NULL;

	return 0;
}

输出结果如下:

总结

1.有时我们需要进行动态的内存管理,如果是静态内存管理,空间给少了不够用,给多了又浪费。动态内存管理是在堆区上进行的。

2.动态内存管理需要我们掌握4个函数,分别是malloc, free, calloc和realloc,它们分别扮演着重要的角色。

3.malloc和calloc负责开辟空间,并返回空间的起始地址。如果开辟空间失败会返回NULL。

4.malloc函数只有一个参数,表示开辟空间的总大小;calloc函数有两个参数,分别表示开辟空间能存储的元素个数和每个元素的大小。

5.malloc函数不会对空间进行处理,calloc函数会把空间初始化成全0。

6.free函数可以释放一块动态内存,传参时必须传这块空间的起始地址。释放完后最好置NULL,防止出现野指针。free(NULL)这种写法free函数什么都不会做。

7.realloc函数可以动态调整开辟空间的大小,传递参数时,第一个参数是动态内存的起始地址,第二个参数是新空间的总大小。如果第一个参数是NULL,realloc的表现和malloc一样。

8.使用malloc, calloc, realloc都需要判断返回值是不是NULL,并对相应情况进行处理。

到此这篇关于一文带你了解C语言中的动态内存管理函数的文章就介绍到这了,更多相关C语言动态内存管理函数内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 基于C语言实现计算生辰八字五行的示例详解

    本文介绍生辰八字和八字五行的一种算法.站内有人在查询生辰八字的算法,此题本人也感兴趣.故以此文以续貂尾. 生辰八字计算要点是节气日,年柱以立春起,月柱以是月节气日起,故先要计算月首的节气日.本节气算法的节气时刻精度差些,但确定节气日是可以的.程序启动时先计算干支表和对应的五行表.具体的计算方法参阅程序的注释.算法很简单,一看就明白.要注意的是,八字的时柱先输出的是起时,生日时辰十二个时辰对应下面的起时表计算.程序列示文本打印输出和图片显示输出二种方法供参考. 本文主要介绍生辰八字的算法,没有计算

  • 使用C语言实现珠玑妙算Mastermind小游戏

    引言 最近玩到过一款十分好玩的益智类桌游——珠玑妙算-Mastermind,这款游戏也出现在热片<拆弹专家2>里,该款游戏就是有四个槽位,而要将6种颜色依次放入槽位之中,然后由出题人反馈正确位置及错误位置正确颜色数,再通过逻辑推理,推出正确的颜色及位置.因为这种游戏为多人游戏,一个人不能自己出题.判断及推理,我在手机上搜找相关游戏却没有找到相应游戏,于是,萌生自主编写的想法. ( Mastermind(珠玑妙算)是一种可供两名玩家使用的密码破译棋盘游戏.在1970年由Mordecai Meir

  • C语言中的switch语句基本用法

    switch语句: 实际生活中,需要做出很多选择,大家都知道做选择可以使用if语句,但是如果选择太多,if语句使用起来就会很繁琐,这个时候就需要一个能将代码简化的语句,也就是我们今天的主角switch语句. switch语句是一个多分支选择语句,并且可以支持嵌套. switch语句的基本格式 switch(表达式) { case 常量1:语句1 case 常量2:语句2 default:语句n break; } switch语句通过将表达式的值与常量值进行比对,如果相等则执行后面的语句,如果不相

  • C语言实现大数值金额大写转换的方法详解

    关于大数值金额大写转换,在财务管理的应用方面没什么意义.一般来说,千亿级,万亿级的数值就够了.因为在国家级层面是以亿为单位的,也就表达为千万亿,万万亿.在企业层面数值金额转换设置到千亿.万亿就行了.大的集团级企业扩大到万万亿也就行了.做企业应用软件的可根据需要设置.至于再大的数值就是天文数字,有另外的表达方法. 本人喜欢探索各种算法.前些天写了15位数值的金额大写转换.今再尝试写一个更多位数值的换算大写转换.提供给需要的同道参考. 金额大写应用在很多方面,如支票.发票.各种单据,各种财务凭证,合

  • C语言学习之柔性数组详解

    目录 一.前言 二.柔性数组的用法 三.柔性数组的内存分布 四.柔性数组的优势 五.总结 一.前言 仔细观察下面的代码,有没有看出哪里不对劲? struct S { int i; double d; char c; int arr[]; }; 还有另外一种写法: struct S { int i; double d; char c; int arr[0]; }; 你应该一眼就看到了,结构体的最后一个成员数组的写法是int arr[];或者是int arr[0],这两种写法是等价的,意思是这个数组

  • 一文带你了解C语言中的动态内存管理函数

    目录 1.什么是动态内存管理 2.为什么要有动态内存管理 3.如何进行动态内存管理 3.1 malloc 3.2 free 3.3 calloc 3.4 realloc 总结 1.什么是动态内存管理 平时我们写代码,一种非常常见的写法是: int a = 0; // 创建一个变量 int arr[10] = {0}; // 创建一个数组 当你创建变量a的时候,其实是向内存中申请了4个字节的空间来存放一个整数.而当你创建数组arr的时候,是向内存中申请了40个字节的空间来存放10个整数.当你这么写

  • 详解C语言中的动态内存管理

    目录 一.动态内存管理 1.1为什么要有动态内存管理 1.2动态内存介绍 1.3常见的动态内存错误 一.动态内存管理 1.1为什么要有动态内存管理 1.1.1  在c语言中我们普通的内存开辟是直接在栈上进行开辟的 int i = 20;//在栈空间上开辟四个字节 int arr[10]={0}; //在栈中连续开辟四十个字节 这样开辟的特点是: (1)他所开辟的空间是固定的 (2)数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配 但对于空间的需求,我们有的时候并不知道,有可能空间

  • 一文带你了解Go语言中的单元测试

    目录 基本概念 示例一:取整函数基本测试 示例二:Fail()函数 示例三:FailNow函数 实例四:Log和Fetal函数 基本概念 上一节提到,代码完成的标准之一还包含了单元测试,这部分也是很多开发流程中不规范的地方.写过单元测试的开发人员应该理解,单元测试最核心的价值是为了证明:为什么我写的代码是正确的?也就是从逻辑角度帮你检查你的代码.但是另外一方面,如果从单元测试覆盖率角度来看,单元测试也是非常耗时的,几乎是三倍于你代码的开发时间,所以在很多迭代速度非常快的项目中,单元测试就几乎没人

  • 一文带你入门Go语言中定时任务库Cron的使用

    目录 前言 快速开始 安装 导入 Demo Cron表达式格式 标准格式 预定义时间表 常用的方法介绍 new() AddJob() AddFunc() Start() 相关推荐 Go第三方库之cronexpr——解析 crontab 表达式 总结 前言 在平时的开发需求中,我们经常会有一些重复执行的操作需要触发执行,和系统约个时间,在几点几分几秒或者每隔几分钟跑一个任务,说白了就是定时任务,,想必大家第一反应都是linux的Crontab.其实定时任务不止使用系统自带的Crontab,在Go语

  • 一文带你了解Go语言中的类型断言和类型转换

    目录 类型断言 类型判断 为什么需要断言 类型转换 什么时候使用类型转换 类型为什么称为转换 类型结论 在Go中,类型断言和类型转换是一个令人困惑的事情,他们似乎都在做同样的事情. 下面是一个类型断言的例子: var greeting interface{} = "hello world" greetingStr := greeting.(string) 接着看一个类型转换的例子: greeting := []byte("hello world") greeting

  • 一文带你了解Go语言中的指针和结构体

    目录 前言 指针 指针的定义 获取和修改指针所指向变量的值 结构体 结构体定义 结构体的创建方式 小结 前言 前面的两篇文章对 Go 语言的基础语法和基本数据类型以及几个复合数据类型进行介绍,本文将对 Go 里面的指针和结构体进行介绍,也为后续文章做铺垫. 指针 在 Go 语言中,指针可以简单理解是一个地址,指针类型是依托于某一个类型而存在的,例如 Go 里面的基本数据类型 int.float64.string 等,它们所对应的指针类型为 *int.*float64.*string等. 指针的定

  • 一文带你了解Go语言中接口的使用

    目录 接口 接口的实现 接口类型变量 空接口 类型断言 类型断言变种 type switch 小结 接口 在 Go 语言中,接口是一种抽象的类型,是一组方法的集合.接口存在的目的是定义规范,而规范的细节由其他对象去实现.我们来看一个例子: import "fmt" type Person struct { Name string } func main() { person := Person{Name: "cmy"} fmt.Println(person) //

  • 一文带你掌握Go语言中的文件读取操作

    目录 os 包 和 bufio 包 os.Open 与 os.OpenFile 以及 File.Read 读取文件操作 bufio.NewReader 和 Reader.ReadString 读取文件操作 小结 os 包 和 bufio 包 Go 标准库的 os 包,为我们提供很多操作文件的函数,如 Open(name) 打开文件.Create(name) 创建文件等函数,与之对应的是 bufio 包,os 包是直接对磁盘进行操作的,而 bufio 包则是带有缓冲的操作,不用每次都去操作磁盘.

  • 一文带你了解Go语言中方法的调用

    目录 前言 方法 方法的调用 Receiver 参数类型的选择 方法的约束 小结 前言 在前面的 一文熟悉 Go 函数 文章中,介绍了 Go 函数的声明,函数的几种形式如匿名函数.闭包.基于函数的自定义类型和函数参数详解等,而本文将对方法进行介绍,方法的本质就是函数,介绍方法的同时也会顺带对比其与函数的不同之处. 方法 在 Go 中,我们可以为任何的数据类型定义方法(指针或接口除外),现在让我们看一看方法的声明和组成部分以及与函数有什么不同之处. type Person struct { age

  • 一文带你熟悉Go语言中函数的使用

    目录 函数 函数的声明 Go 函数支持变长参数 匿名函数 闭包 init 函数 函数参数详解 形式参数与实际参数 值传递 函数是一种数据类型 小结 函数 函数的英文单词是 Function,这个单词还有着功能的意思.在 Go 语言中,函数是实现某一特定功能的代码块.函数代表着某个功能,可以在同一个地方多次使用,也可以在不同地方使用.因此使用函数,可以提高代码的复用性,减少代码的冗余. 函数的声明 通过案例了解函数的声明有哪几部分: 定义一个函数,实现两个数相加的功能,并将相加之后的结果返回. f

随机推荐