初识C语言·动态内存开辟

news2024/11/28 4:28:45

目录

1 为什么要有动态内存开辟

2 malloc函数的使用

3 free函数的使用

4 calloc函数的使用

5 realloc函数的使用

6 常见的动态内存开辟的错误

1)对空指针的解引用

2)对动态内存开辟空间的越界访问

我们使用了calloc函数开辟了10个整型空间,使用的时候循环次数是11次,那么最后一次循环就会越界访问到未开辟的空间,系统就会报错。3)  对非动态内存开辟的空间释放

4)free释放一部分动态内存开辟的空间

5)对同一块空间多次释放

6)动态内存开辟的空间未释放

7 动态内存开辟函数题目解析

代码1:

代码2:

代码3:

 代码4:

8 柔性数组


1 为什么要有动态内存开辟

int a = 10;
int arr[10] = { 0 };

上述定义了一个整型,开辟了4个字节,定义了一个整型数组,开辟了40个字节,但是是固定开辟的,面对灵活多变的实际问题的时候可能就有点鸡肋,这种开辟空间的特点是:

i) 开辟好空间之后不能改变

ii) 开辟的空间大小是固定的

那么为了解决实际问题就引入了动态内存开辟,可以根据实际需要进行内存开辟。

那么引用了4个函数,分别是malloc calloc free realloc,而动态开辟函数开辟的空间都是在堆区开辟的,不是栈区!


2 malloc函数的使用

void* malloc (size_t size);

这是malloc函数的原型,需要引用的头文件是stdlib,返回的是void*指针,返回void*指针的原因是因为实际工作中开辟的空间类型是根据实际需要确定的,所以开辟好空间之后需要进行强制类型转化,开辟空间的单位是字节,参数表示的是开辟多少个字节,最后返回的地址是开辟的字节的首地址。

那么开辟空间的话也是分为是否开辟成功的,如果开辟成功了,返回的就是那块空间的首地址,如果开辟失败了,返回的就是空指针,比如我开辟几百亿个字节,一般情况下返回的就是NULL,所以我们用指针接收了地址后第一件事就是判断一下是不是空指针,不然是会有警告的。

 这是正常使用的情况,那如果size_ t size是0呢?这时候malloc的行为标准是未定义的,操作就取决于编译器了。

代码1:

int main()
{
	int* p = (int*)malloc(40);
    assert(p);
	for (int i = 0; i < 10; i++)
	{
		*p = i;
        p++;
	}	
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}

问运行结果是什么?
答案是报错,你可以把内存开辟的返回的地址理解成数组名,数组名哪里能自增自减呢?理解成数组名的缘由是因为free,动态内存函数开辟了空间之后,使用完空间是要被释放的,而free的参数是开辟的空间的首地址,所以p不能自增自减。防止后面释放空间释放错了。


3 free函数的使用

上面提到了返回的地址不能自增自减,因为free( 头文件依然是stdlib)要出场了,free,免费,释放,在C语言里面就是专门用来释放动态内存开辟的空间的,当使用完之后都是要free的。

int main()
{
	int* p = (int*)malloc(40);
	assert(p);
	for (int i = 0; i < 10; i++)
	{
		*(p + i) = i;
	}	
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

free的参数是动态内存开辟的地址,可能让人疑问的点就是为什么最后要给一个NULL,这是因为我们传的是值,不是地址,free的形参是值,所以释放空间的时候,那块空间确实是释放了没错,但是p还仍然保留着原来的数据,所以需要手动置为空指针。

使用free的时候需要注意的:

·free的参数一定是动态开辟的地址,如果不是,那么free的行为是未定义的

·free的参数如果是NULL,那么该函数什么事都不做


4 calloc函数的使用

void* calloc (size_t num, size_t size);

calloc函数的原型如上,头文件依然是stdlib,作用是开辟num个大小为size字节的空间,它和malloc是极其相似的,唯一的不同就是calloc会自动初始化空间为0,malloc函数则不会初始化, 举个例子:

int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	assert(p);
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

最后的运行结果应该全是0。


5 realloc函数的使用

realloc函数才是动态内存开辟函数的老大,因为点啥呢,因为它可以扩大空间,比如你写代码到一半发现空间不够了,这时候就需要realloc函数来操作了,它可以扩大空间

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

realloc函数的原型如上,头文件依然是stdlib,第一个参数一般都是动态开辟的地址,第二个是表示扩大到多少字节,所以一般情况下,使用该函数之前一般都是已经动态开辟了空间的,那么什么是特殊情况呢?

如下:

int main()
{
	int* p = (int*)realloc(NULL, 40);
	assert(p);
	for (int i = 0; i < 10; i++) 
	{
		*(p + i) = i;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

这种情况realloc函数就是malloc函数,也就是说,如果realloc函数的第一个参数是空指针的话,那么就是从内存中随机开辟空间,此时的realloc函数就是malloc函数。

那么realloc函数一般用法就是用来扩大空间的,如果你想实验一下缩短空间也是可以试试的,只不过这个时候realloc函数的行为是未定义的。

realloc函数扩大空间有两种情况:

1 原地址的后面有足够的空间用于扩容 

2 原地址的后面没有足够的空间用于扩容

第一种情况没什么好说的,原有空间变大而已,第二种情况,realloc会在堆区重新找一个符合需要的空间,如果没有找到就会返回空指针,如果找到了,那么原有的数据会赋值到新空间,且原有空间会被释放,所以realloc函数开辟完空间之后,如果是重新找空间开辟的,就会释放原来的空间,那么实际写代码的时候,我们就会重新用一个指针来接收新空间,判断完是不是空指针后,再决定要不要赋给原来的地址,

realloc函数和其他函数最不一样的地方就是在于它会自己释放空间,这点需要注意,realloc函数举个例子:
 

int main()
{
	int* p = (int*)calloc(25, sizeof(int));
	assert(p);
	int* pa = realloc(p, 20 * sizeof(int));//两种情况 所以用另一个指针接收
	assert(pa);
	if (pa != NULL)
	{
		p = pa;
	}
	free(*p);
	free(*pa);
	p = NULL;
	pa = NULL;
	return 0;
}

Tips:所有动态内存开辟的空间是不会自己释放的,释放的空间都是需要自己释放的,要么就是程序结束由操作系统来释放。

int* Test()
{
	int* p = *(int*)malloc(40);
	return p;
}
int main()
{
	//操作
	return 0;
}

这样的p只要不释放,空间都是可以使用的。


6 常见的动态内存开辟的错误

1)对空指针的解引用

//对空指针的解引用
int main()
{
	int* p = (int*)malloc(INT_MAX * INT_MAX);;
	*p = 20;
	free(p);
    p = NULL;
	return 0;
}

这里内存是开辟不了这么多的空间的,所以p是空指针,那么解引用之后,自然就会报错

2)对动态内存开辟空间的越界访问

//对动态内存开辟空间的越界访问
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	assert(p);
	for (int i = 0; i <= 10; i++)
	{
		*(p + i) = i;
	}	
	for (int i = 0; i <= 10; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

我们使用了calloc函数开辟了10个整型空间,使用的时候循环次数是11次,那么最后一次循环就会越界访问到未开辟的空间,系统就会报错。
3)  对非动态内存开辟的空间释放

//对非动态开辟的空间进行释放
int main()
{
	int a = 10;
	int* pa = &a;
	free(pa);
	pa = NULL;
	return 0;
}

前面提到free函数只能释放动态内存开辟的空间,因为局部变量 全局变量是在栈区 静态区的,而free适用于堆区的动态内存开辟,所以使用free释放非动态内存开辟的空间的时候系统就会报错。

4)free释放一部分动态内存开辟的空间

//使用free释放一部分动态内存开辟的空间
int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	assert(p);
	p++;
	free(p);
	p = NULL;
	return 0;
}

前面提及我们可以把动态内存开辟返回的地址当作数组名,这样可以避免我们给该地址自增自减,因为free释放都是释放的一整块空间,那么自增之后,free的参数不是起始地址,就会导致释放过多,也会导致越界访问,系统就会报错。

5)对同一块空间多次释放

//对同一块空间多次释放
int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	assert(p);
	/* 
		操作
	*/
	free(p);
	//p = NULL;
	/*
		操作
	*/
	free(p);
	p = NULL;
	return 0;
}

对同一块空间多次释放后,free再去访问那个地址, 自然就会报错,但是如果前面释放了空间之后,并且置于0,是没有问题的,因为free的参数如果是空指针的话,就不会有任何行为。

6)动态内存开辟的空间未释放

//动态内存开辟的空间未释放
int main()
{
	int* p = (int*)malloc(40);
	/*
		操作
	*/
	return 0;
}

如果开辟的空间没有进行释放,那么内存中这块空间的状态就是一直被占用的情况,就会导致内存泄露,假如这种情况多了的话,说不定某一天你的系统内存就被占满了,然后你一重启,就会发现,欸对了,所以动态内存开辟的空间一定要正确释放。


7 动态内存开辟函数题目解析

代码1:

void GetMemory(char* p)
{
	p = (char*)malloc(100);
}
int main()
{
	char* str = NULL;
	Getmemory(str);
	strcpy(str, "hello world");
	printf(str);
	return 0;
}

程序运行到strcpy的时候就会报错,最开始str是空指针,那么这里的传参方式是传值调用,所以即使p的地址已经指向了malloc开辟的100字节,str仍然是空指针,还有一个问题是出了函数GeiMemory的时候,p就会被销毁了,也会导致内存泄漏,并且打印出错

Tips:printf这里是没有问题的,可以自行实验一下,比如pirntf("abcdefg\n"); 实际上传的也是该字符串的地址。

void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}
int main()
{
	char* str = NULL;
	GetMemory(&str);
	strcpy(str, "hello world");
	printf(str);
    free(str);
    str = NULL;
	return 0;
}

这是修正后的代码。

代码2:

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

如果经过调试,我们会发现str指向的确实是常量字符串的地址,实际打印的效果却是一串乱码,其实这是因为出了Getmemory函数的作用域,导致常量字符串被销毁,但是返回的地址是没问题的,是数据没了,此时的p就是野指针了。

修改的方式也很简单,只需要加一个static就行了。

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

代码3:

void Getmemory(char** p, size_t num)
{
	*p = (char*)malloc(num);
}
int main()
{
	char* str = NULL;
	Getmemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
	return 0;
}

程序看起来是没有问题的,确实是打印了hello,但是还是存在问题,内存泄漏,没有释放空间。

 代码4:

int main()
{
	char* str = (char*)malloc(100);
	assert(str);
	strcpy(str, "hello");
	free(str);
	if (str != NULL);
	{
		strcpy(str, "world");
		printf(str);
	}
	return 0;
}

这串代码的运行结果是打印world,虽然free了动态开辟的空间,但是str仍然指向了那块空间,str先是赋值了hello,然后判断不是空指针,再次复制了world,所以最后的结果是world,这个str就是野指针了,虽然运行结果是对的,但是程序仍然是有问题的。


8 柔性数组

 在C99标准中,允许柔性数组的存在,比如:

struct St
{
	int i;
	int arr[0];
};

其中arr就是柔性数组,但有的编译器可能无法通过,所以有时候0是没有加的。

柔性数组的特点是:
i)柔性数组一定是最后一个成员

ii)sizeof计算大小的时候不包括柔性数组的大小

iii)使用malloc的时候开辟的空间应该大于结构体前面成员大小的总和,以此来符合预期

int main()
{
	struct St
	{
		char i;
		int j;
		int arr[0];
	};
	printf("%zd\n",sizeof(struct St));
	return 0;
}

结合内存对齐,柔性数组的特点,最后的结果是8。

柔性数组的使用:

struct St
{
	char i;
	int j;
	int arr[0];
};
int main()
{
	struct St* ps = (struct St*)malloc(sizeof(struct St) + 10 * sizeof(int));
	assert(ps);
	ps->i = 'w';
	ps->j = 520;
	//使用柔性数组
	for (int m = 0; m < 10; m++)
	{
		ps->arr[m] = m;
	}
	//空间不够
	struct St* pt = (struct St*)realloc(ps,sizeof(struct St) + 15 * sizeof(int));
	assert(pt);
	ps = pt;
	printf("%c\n", ps->i);
	printf("%d\n", ps->j);
	for (int n = 10; n < 15; n++)
	{
		ps->arr[n] = n;
	}
	for (int m = 0; m < 15; m++)
	{
		printf("%d ", ps->arr[m]);
	}
	free(pt);
	ps = NULL;
	pt = NULL;
	return 0;
}

常规结构体在栈区开辟的空间,有了柔性数组,我们就需要用到malloc函数,那么结构体就是在堆区开辟的空间,在使用的时候需要注意最后释放只需要释放一个指针,因为realloc函数是会自己释放上一块空间的,在开辟空间的时候:

struct St* ps = (struct St*)malloc(sizeof(struct St) + 10 * sizeof(int));

这种写法是为了更直观的看到开辟的空间比整个结构体都大,这样柔性数组才有自己的空间

那么还有一种模拟柔性数组的写法,如下:
 

struct St
{
	char i;
	int j;
	int* arr;
};
int main()
{
	struct St* ps = (struct St*)malloc(sizeof(struct St));
	assert(ps);
	ps->i = 'w';
	ps->j = 520;
	ps->arr = (int*)malloc(10*sizeof(int));
	assert(ps->arr);
	//使用柔性数组
	for (int i = 0; i < 10; i++)
	{
		*(ps->arr + i) = i;
	}
	//空间不够
	int* pt = (int*)realloc(ps->arr,15 * sizeof(int));
	assert(pt);
	ps->arr = pt;
	printf("%c\n", ps->i);
	printf("%d\n", ps->j);
	for (int n = 10; n < 15; n++)
	{
		*(ps->arr + n) = n;
	}
	for (int m = 0,n = 0; m < 15; m++,n++)
	{
		printf("%d ",*(ps->arr + m));
	}
	free(pt);
	free(ps);
	ps->arr = NULL;
	pt = NULL;
	ps = NULL;
	return 0;
}

结构体的最后一个成员是int*,也可以是其他类型的指针,这种写法是先为结构体的其他成员开辟空间,再给int*开一个单间,需要用的时候给个malloc,空间不够给个realloc,这样的话,也是类似于柔性数组的,它与上面的写法不同的是多次开辟空间,多次释放,略显繁琐,但是访问速度快点,上面的优点就是内存方便释放,两种写法各有优势。


感谢阅读!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1425584.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【MyBatis】MyBatis是什么?作用?怎么实现?

一、MyBatis是什么 MyBatis 是一款优秀的持久层框架&#xff0c;它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO&#xff08;Plain …

spring-boot-admin的介绍和使用

概述 Spring Boot 有一个非常好用的监控和管理的源软件&#xff0c;这个软件就是 Spring Boot Admin。该软件能够将 Actuator 中的信息进行界面化的展示&#xff0c;也可以监控所有 Spring Boot 应用的健康状况&#xff0c;提供实时警报功能。 主要的功能点有&#xff1a; 显…

排序(4)——快速排序

五、快速排序 1.简介 快速排序是Hoare于1962年提出的&#xff0c;主要采取了分治的思想。快速排序首先确定一个基准值&#xff0c;然后以这个选出的基准值为标准&#xff0c;将整个数组进行按大小进行区分&#xff0c;使得小于该基准值的位于其一侧&#xff0c;大于基准值的位…

超越GPT4 Turbo?科大讯飞发布星火认知大模型3.5版本

简介 1月30日&#xff0c;科大讯飞举行星火认知大模型V3.5升级发布会。科大讯飞董事长刘庆峰、研究院院长刘聪正式发布基于首个全国产算力训练的讯飞星火V3.5&#xff0c;七大核心能力全面提升。 功能展示多模交互 多模理解&#xff1a;上传图片素材&#xff0c;大模型完成识…

C++/数据结构:二叉搜索树的实现与应用

目录 一、二叉搜索树简介 二、二叉搜索树的结构与实现 2.1二叉树的查找与插入 2.2二叉树的删除 2.3二叉搜索树的实现 2.3.1非递归实现 2.3.2递归实现 三、二叉搜索树的k模型和kv模型 一、二叉搜索树简介 二叉搜索树又称二叉排序树&#xff0c;它或者是一棵空树&#xff0…

vue——实现多行粘贴到table事件——技能提升

最近在写后台管理系统时&#xff0c;遇到一个需求&#xff0c;就是要从excel表格中复制多行内容&#xff0c;然后粘贴到后台系统中的table表格中。 如下图所示&#xff1a;一次性复制三行内容&#xff0c;光标放在红框中的第一个框中&#xff0c;然后按ctrlv粘贴事件&#xff0…

路由备份聚合排错

目录 实验拓扑图 实验要求 实验排错 故障一 故障现象 故障分析 故障解决 故障二 故障现象 故障分析 故障解决 故障三 故障现象 故障分析 故障解决 故障四 故障现象 故障分析 故障解决 故障五 故障现象 故障分析 故障解决 实验拓扑图 实验要求 按照图示配…

Typora导出html文件图片自动转换成base64

Typora导出html文件图片自动转换成base64 一、出现问题二、解决方案三、编码实现3.1.创建Java项目3.2.代码3.3.打包成Jar包 四、如何使用endl 一、出现问题 typora 导出 html 的时候必须带有原图片&#xff0c;不方便交流学习&#xff0c;文件太多显得冗余&#xff0c;只有将图…

Docker中安装MySql的遇到的问题

目录 一、mysql查询中文乱码问题 1. 进入mysql中进行查看数据库字符集 2. 修改 my.cnf 中的配置 3. 重启mysql容器&#xff0c;使得容器重新加载配置文件 4. 测试结果 二、主从同步中遇到的问题 2.1 Slave_IO_Running:Connecting 的解决方案 1. 确定宿主机防火墙开放my…

node.js与express.js创建项目以及连接数据库

搭建项目 一、技术准备 node版本&#xff1a;16.16.0 二、安装node成功后&#xff0c;安装express,命令如下&#xff1a; npm install -g express 或者&#xff1a; npm install --locationglobal express 再安装express的命令工具&#xff1a; npm install --location…

PVE安装后报错:NO IOMMU Detected解决办法

&#xff11;、首先在BIOS中确定图形界面卡&#xff0c;打开了VT-D功能。 &#xff12;、修改grub vim /etc/default/grub 找到&#xff1a;GRUB_CMDLINE_LINUX_DEFAULT"quiet" 然后修改为 GRUB_CMDLINE_LINUX_DEFAULT"quiet intel_iommuon" 3、使用命…

巨人踏步,港口自动驾驶提速向前打开行业新空间

按照吞吐量排名&#xff0c;全世界最大的50个港口&#xff0c;中国占了29个。在中国的港口和码头上&#xff0c;一场进化正在发生&#xff1a;人在这个生态中占的比重越来越少&#xff0c;技术接管的要素正在越来越多。像是最具代表性的全球综合自动化程度最高的码头——上海洋…

笔记本电脑Win11重装系统教程

在笔记本电脑Win11操作过程中&#xff0c;用户如果遇到很严重的系统问题&#xff0c;就可以重新正常的Win11系统&#xff0c;快速解决Win11系统问题。但是&#xff0c;部分新手用户不知道不知道如何操作才能给Win11笔记本电脑重装系统&#xff1f;以下小编分享笔记本电脑Win11重…

深入理解TCP网络协议(2)

目录 1.TCP的状态转换 1.1 LISTEN状态和ETABLISHED状态 ​编辑2.TIME_WAIT 和 CLOSE_WAIT 2.滑动窗口 1.TCP的状态转换 我们通过上图可以看到TCP状态转换的详细过程.在实际开发的过程中,我们不需要了解的这么细致.为了方便大家的理解,我挑几个主要的状态来给大家聊一下 1.…

易语言系列学习1

通过本文章你会学习到 如果 如果真 获取编辑框内容 关闭本程序 监听按键让它等价于点击某个按钮 运算&#xff1a;或 且 非&#xff08;注意中间要有一个空格&#xff0c;否则会报错&#xff09; 效果 .版本 2.程序集 窗口程序集_启动窗口.子程序 _按钮2_被单击. 如果真 (编…

docker-学习-4

docker学习第四天 docker学习第四天1. 回顾1.1. 容器的网络类型1.2. 容器的本质1.3. 数据的持久化1.4. 看有哪些卷1.5. 看卷的详细信息 2. 如何做多台宿主机里的多个容器之间的数据共享2.1. 概念2.2. 搭NFS服务器实现多个容器之间的数据共享的详细步骤2.3. 如果是多台机器&…

Vue学习笔记(一)JS导入导出

Vue学习笔记&#xff08;一&#xff09;JS导入导出 js文件-导出、批量导出、默认导出 showMessage.js export function simpleMessage(msg){console.log(msg); }export function complexMessage(msg){console.log(new Date()": "msg); }// 批量导出 // export {si…

[工具探索]Safari 和 Google Chrome 浏览器内核差异

最近有些Vue3的项目&#xff0c;使用了safari进行测试环境搞开发&#xff0c;发现页面存在不同程序的页面乱码情况&#xff0c;反而google浏览器没问题&#xff0c;下面我们就对比下他们之间的差异点&#xff1a; 日常开发google chrome占多数&#xff1b;现在主流浏览器 Goog…

stm32--simulink开发之--timer的学习,硬件输入中断,触发事件,STM32通用定时器之输出比较模式与PWM模式(重要理解)

下面三个模块&#xff0c;一个比一个高级&#xff0c;当然使用是越来越简单 STM32F4xx系列控制器有2个高级控制定时器、10个通用定时器和2个基本定时器(推荐学习) 1&#xff0c;第一个模块&#xff1a;Timer 浅层理解&#xff1a;计数&#xff0c;不停的触发 Starts timer co…

Nginx简单阐述及安装配置

目录 一.什么是Nginx 二.Nginx优缺点 1.优点 2.缺点 三.正向代理与反向代理 1.正向代理 2.反向代理 四.安装配置 1.添加Nginx官方yum源 2.使用yum安装Nginx 3.配置防火墙 4.启动后效果 一.什么是Nginx Nginx&#xff08;“engine x”&#xff09;是一个高性能的HTTP…