【C语言】动态分配内存

news2024/12/27 11:09:29

内存的五大分区

1、堆区(heap)——由程序员分配和释放, 若程序员不释放,程序结束时一般由操作系统回收。注意它与数据结构中的堆是两回事

2、栈区(stack)——由编译器自动分配释放 ,存放函数的参数值,局部变量等。其操作方式类似于数据结构中的栈

3、静态全局区

1)未初始化静态全局区 —— 静态变量,全局变量,没有初始化的存在此区
2)初始化的静态全局区 —— 静态变量、全局变量,赋过初值的存放在此区

4、文字常量区——常量、字符串就是放在这里的。 程序结束后由系统释放

5、(程序)代码区——用于存放函数体的(二进制)代码

内存五大区

静态分配与动态分配

在数组一章中,介绍过数组的长度是预先定义好的,在整个程序中固定不变。但是在实际的编程中,往往会发生所需的内存空间取决于实际输入的数据,而无法预先确定 。为了解决上述问题,C语言提供了一些内存管理函数,这些内存管理函数可以按需要动态的分配内存空间,也可把不再使用的空间回收再次利用。而动态分配内存就是在堆区分配空间。

静态分配

  1. 在程序编译或运行过程中,按事先规定大小分配内存空间的分配方式。如:int a[10]

  2. 必须事先知道所需空间的大小。

  3. 一般以数组的形式,分配在栈区或静态全局区。

  4. 按计划分配。

动态分配

  1. 在程序运行过程中,根据需要大小自由分配所需空间。

  2. 分配在堆区,一般使用特定的函数进行分配。

  3. 堆区开辟空间,手动申请手动释放,更加灵活。

  4. 按需分配。

动态分配内存函数

#include <stdlib.h>

malloc

void *malloc(unsigned int size);

malloc的全称是memory allocation,中文叫动态内存分配,用于申请一块连续的指定大小(size)的内存块区域以void*类型返回分配的内存区域地址,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态的分配内存,且分配的大小就是程序要求的大小。

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

  1. 如果开辟成功,则返回一个指向开辟好空间的指针;
  2. 如果开辟失败,则返回一个NULL指针,因此 malloc 的返回值一定要做检查;
  3. 返回值的类型是 void*,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定(强制类型转换);
  4. 如果参数 size 为 0 ,malloc 的行为是标准是未定义的,取决于编译器。
  5. 如果多次malloc申请的内存,第1次和第2次申请的内存不一定是连续的。
//我们直接给出分配的大小是可以的
char* rec2=(char*) malloc(20);
//当然,我们一般会以如下这种形式给出分配的大小。因为不同的操作系统可能数据类型的大小不同,这样写更符合规范
//指针 = (指针类型*)malloc(数据数量 *sizeof(指针类型))
char* rec1=(char*) malloc(20*sizeof(char));

free

void free(void *ptr)

我们不能为所欲为的开辟空间,因为空间是有限的,所以应当有借有还。C语言为我们提供了free函数,专门用来做动态内存的释放和回收。

  1. ptr:开辟后使用完毕的堆区的空间的首地址

  2. 如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的;

  3. 如果参数 ptr 是NULL指针,则函数什么操作都不进行。

  4. free函数只能释放堆区的空间,其他区域的空间无法使用free

  5. free释放空间必须释放malloc或者calloc或者realloc的返回值对应的空间,不能说只释放一部分。

  6. free§; 注意当free后,因为没有给p赋值,所以p还是指向原先动态申请的内存。但是内存已经不能再用了,p变成野指针了,所以一般为了防止野指针,会free完毕之后对p赋为NULL

  7. 一块动态申请的内存只能free一次,不能多次free

实例1:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
	// 向内存申请10个整型的空间
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		// 打印错误原因
		printf("%s\n", strerror(errno));
	}
	else
	{
		// 正常使用空间
		int i = 0;
		for (i = 0; i < 10; i++)
		{
			*(p + i) = i;
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));
		}
	}
	// 当动态申请的空间不再使用的时候应该还给操作系统
	free(p);
	// 将p置为NULL,防止野指针 
	p = NULL;
	return 0;
}

实例2:倒序输出一个字符串

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{
	char* src="hello,world"; 
	char* dest=NULL;
	int len=strlen(src);
	dest=(char*)malloc(len+1);// 要为\0分配空间
	char* d=dest;
	char* s=src+len-1;// 指向最后一个字符
	while(len--!=0){ 
		*(d++)=*(s--);// 注意不要丢掉*号
		*d ='\0';// 字符串的结尾不要忘记'\0'
	} 
	printf("%s",dest);
	free(dest);// 使用完要释放空间,避免内存泄露
	dest = NULL; // 释放不等于安全,将其置为空指针的操作不可省略
	return 0;
}

calloc

void * calloc(size_t nmemb,size_t size);
  • size_t :无符号整型,它是在头文件中,是用typedef定义出来的
  • nmemb:要申请的空间的块数
  • size:每块的字节数

(1)函数的功能是为 nmemb 个大小为 size 的元素开辟一块空间,并且把这块空间的每个字节初始化为0;
(2)与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0.

realloc

void* realloc(void *s,unsigned int newsize);

如果我们在使用内存的过程中需要对内存的大小进行调整怎么办呢?C语言同样为我们提供了一个函数叫 realloc

  • s:原本开辟好的空间的首地址
  • newsize:重新开辟的空间的大小
  • 返回值:新的空间的首地址

在原本申请好的堆区空间的基础上重新申请内存,新的空间大小为函数的第二个参数
如果原本申请好的空间的后面不足以增加指定的大小,系统会重新找一个足够大的位置开辟指定的空间,然后将原本空间中的数据拷贝过来,然后释放原本的空间
如果newsize比原先的内存小,则会释放原先内存的后面的存储空间,只留前面的newsize个字节

char* p1=(char*)malloc(80*sizeof(char));//申请80个字节的内存
p1=(char*)realloc(p1,100);//将内存重新开辟为100个字节,可以认为是增加了20个字节 
p1=(char*)realloc(p1,50);//将内存重新开辟为50个字节,可以认为是减少了30个字节 

常见的动态内存易错警示

1、不能对NULL指针的解引用操作

因为NULL是一个特殊的指针值,表示指针没有指向任何有效的对象或地址。对NULL指针解引用会导致程序崩溃或未定义的行为,因为程序在试图访问一个不存在的内存地址。

因此,在使用指针之前,应检查其是否为NULL,并确保指向有效的内存地址。

2、不能对动态开辟空间的越界访问

对动态内存的越界访问可能会导致程序崩溃或产生未定义的行为。

这是因为动态内存分配需要在运行时进行,并且程序员需要手动管理内存的分配和释放。如果程序员在访问动态内存时越界,就会导致访问到未分配的内存或者已经释放的内存,从而可能导致程序崩溃或出现未定义的行为。

此外,动态内存的越界访问还可能会导致数据损坏、安全漏洞等问题。因此,程序员需要注意动态内存的边界,并且避免越界访问。

3、不能对非动态内存使用free

因为非动态开辟的内存是在程序运行时从栈上分配的,而不是从堆上分配的。栈上分配的内存是由系统自动管理的,程序员无法控制其释放。因此,如果试图使用free函数来释放栈上的内存,会导致程序崩溃或不可预测的行为。所以只有动态开辟的内存才能使用free函数进行释放。

4、不能对同一块动态内存free多次

对同一块动态内存多次释放会导致程序崩溃或出现未定义的行为。因为在第一次释放后,操作系统会将该内存块标记为可用,此时这块内存空间就可以被其他变量所占用。所以再次释放时该内存块由于已经被标记为可用,所以释放操作将无法成功,从而导致程序出现异常。

此外,多次释放同一块内存还会导致内存泄漏和程序性能下降的风险。因此,程序员需要确保只释放已经分配的内存,且只释放一次。

其中需要注意的是,free释放的是free释放的是内存空间,而不是指针。free之后,指针仍然存在,指针指向也不变,而指针指向的内容要视情况而定,可能存在也可能不存在,具体还要看环境和编译器(VS2022是将其置为随机值的)。所以释放后的输出可能和原来的内容一样,也可能是乱码。但是综合考虑,为了安全起见还是不要有对同一块动态内存多次释放这种操作。

5、不能使用free释放动态开辟内存的一部分

错误示例:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	// 使用free释放动态开辟内存的一部分
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		return 0;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*p++ = i;
	}
	// 回收空间
	free(p);
	p = NULL;
	return 0;
}

我们有这样一个操作 “*p++ = i;” ,当这个操作结束的时候,我们的指针p指向的空间已经不是我们动态开辟的完整空间了,不仅仅局限指向末尾,只要这里的p不再指向空间的初始位置,都会导致程序的崩溃。

6、不能忘记释放动态开辟的内存(内存泄漏)

动态分配的内存是由程序员手动分配的,而不是由系统自动管理的。如果程序员忘记释放动态分配的内存,那么这些内存将一直占据系统资源,导致内存泄漏和程序性能下降。此外,如果程序员在使用未初始化的动态分配内存时发生访问错误,会导致程序崩溃或出现不可预测的行为。因此,释放动态分配的内存是程序员的责任,必须确保释放内存以避免这些问题。

错误案例一:

char* p=(char*)malloc(100);
p="hellow world!";

案例分析:开始定义了一个指针型变量p在堆区开辟了100个字节的空间,而 p=“hellow world!” 之后,p指向了 hellow world! 的文字常量区,p指向的地址内存分区发生变化,那么p在堆区申请的100个字节的内存(的首地址)就丢了,即发生了内存泄漏。

错误案例二:

void fun()
{
    char* p=(char*)malloc(80);
}
int main()
{
    fun(); //第一次调用
    fun(); //第二次调用
//每调用一次则内存泄漏一次(80字节)
    return 0;
}

案例分析:fun函数每调用一次内存就会泄漏一次。因为fun函数中定义了一个指针型变量p在堆区开辟了80个字节的空间,而主函数调用完fun函数之后,既没释放也没返回,所以调用完之后开辟的空间就丢了,就会发生内存泄漏。

解决方案:可以设置一个函数的返回值,主调函数接收这个返回值并对其使用、处理或者释放。

两个问题

free(NULL)的问题

在C语言中free(NULL)的操作是合法的,C语言标准规定:如果free的参数是NULL,那么这个函数就什么也不做。

malloc(0)的问题

在C语言中malloc(0)的语法也是对的,而且确实也分配了内存,但是内存空间是0,这个看起来说法很奇怪,但是从操作系统的原理来解释就不奇怪了。

在内存管理中,内存中有栈和堆两个部分,栈有自己的机器指令,是一种先进后出的数据结构。而malloc分配的内存是堆内存,由于堆没有自己的机器指令,所以要由自己编写算法来管理这片内存,通常的做法是用链表在每片被分配的内存前加个表头,里面存储了被分配内存的起始地址和大小。malloc等函数返回的就是表头里的起始指针(这个地址是由一系列的算法得来的,而这些操作又是由编译器的底层为我们做的,我们并不需要关心如何操作)

动态分配内存成功之后,就会返回一个有效的指针。而对于分配0空间来说,算法会得出一个可用内存的起始地址,但可用的空间为0,而操作系统一般不知道其终止地址,一般是根据占用大小来推出终止地址的。所以对malloc(0)返回的指针进行操作就是错误的。

但需要注意,即使malloc(0)也要记得free掉,因为malloc还会额外分配内存来维护申请的空间,malloc(0)时并不是什么也不做。

四道试题

题目1

void GetMemory(char* p)
{
	p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}

请问:运行 Test 函数会有什么样的结果?
答案为:程序崩溃

对于本题,很多人的注意力会集中于 “printf(str);” ,实际上这里并没有问题,它等价于 “printf(“%s\n”,str);” 。

解析代码
看到 “GetMemory(str);” ,我们在这里传递的是 str 本身的值,而不是 str 的地址,进入 GetMemory 函数以后,我们在堆上开辟了100个空间,我们将这些空间放置在 p 中,这里的 p 作为一个形参变量,在 GetMemory 函数结束以后,这个 p 就销毁了, 实际上 str 仍然是NULL,而接下来我们想要将 “hello world” copy 到 str 中去,但是 str 作为NULL,它并没有指向一个有效的空间,进行操作的时候,无法避免的进行了非法访问,虽然后边的 printf 操作没有问题,但是程序在 strcpy 操作时就已经崩溃了。

总结
(1)运行代码程序会出现崩溃现象;
(2)程序存在内存泄漏问题:

str 以值传递的形式给 p
p 是 GetMemory 函数的形参,只在函数内有效
等 GetMemory 函数返回之后,动态开辟内存尚未释放
并且无法找到,所以会造成内存泄漏

题目2

“返回栈空间地址问题”

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

请问:运行 Test 函数会有什么样的结果?
答案为:随机值(或者崩溃)

解析代码
看到 “str = GetMemory();” ,进入 GetMemory 函数的时候,p[] 这个数组是GetMemory 函数内的形参,它申请了一个空间,这个空间只在 GetMemory 函数内存在,在 GetMemory 函数结束的时候,的确将 p 的地址返回了,放置在 str 中,但是当 GetMemory 调用完成之后,p 这个数组开辟的空间返还给操作系统了,这个空间里存放的值,我们是不清楚的,接下来 “printf(str);”
打印出来的值我们不清楚,所以结果为随机值。

题目3

void* GetMemory(char** p, int num)
{
	*p = (char*)malloc(num);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
}

请问:运行 Test 函数会有什么样的结果?
答案为:
(1)输出hello
(2)但是有内存泄漏

解析代码
看到 “GetMemory(&str, 100);” ,将 str的地址传入 GetMemory 函数,用二级指针p 来接收,那么 *p 指向的地址即为 str ,然后将 “hello” copy 到 str 当中,再打印出来,这些操作都没有问题,但是当我们使用完 str 以后,忘记释放动态开辟的内存,导致了内存泄漏。

题目4

void Test(void)
{
	char* str = (char*)malloc(100);
	strcpy(str, "hello");
	free(str);
	if (str != NULL)
	{
		strcpy(str, "world");
		printf(str);
	}
}

请问:运行 Test 函数会有什么样的结果?
答案为:
(1)world
(2)非法访问内存(篡改动态内存区的内容,后果难以预测,非常危险)

解析代码
首先,我们向系统申请了100个字节,地址存放在 str 中;然后,我们把 “hello” copy 到 str 当中去;接下来,我们释放了这块空间,之后, str 指向的这块空间已经还给操作系统了;然后,进行判断: str 是否为空指针,虽然之前我们对申请的动态内存进行了释放,但是 str 的值并没有改变,仍然是 “hello”,所以它不为空指针;进入if语句后,将 “world” copy 到 str 当中,world 就把 hello 给覆盖了;所以打印 str 以后结果为 world。

虽然打印了world,但是这个程序依然出了问题,对于 “free(str);” 操作:已经把空间释放掉了,这表明这块空间已经不属于我们了,我们已经不能再使用这块空间了,但是接下来我们还将 world 放进去,并且打印,这就属于非法访问内存了。

参考博文:

https://blog.csdn.net/m0_73759312/article/details/128763422

https://blog.csdn.net/qq_61672347/article/details/125904571

https://blog.csdn.net/WZRbeliever/article/details/121461425

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

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

相关文章

CST电磁仿真软件远场源的导出调用和提取结果【小白必看】

远场源的导出&调用(1) 提取Hybrid仿真所需的远场源&#xff01; Post-Processing > Tools > Result Templates Tools >Farfield and Antenna Properties > Export Farfields As Source 混合求解(Hybrid Simulation)是对安装在舰船等大型平台上的天线进行仿真…

【docker】常用的Docker编排和调度平台

常用的Docker编排和调度平台 Kubernetes (K8s): Kubernetes是目前市场上最流行和功能最全面的容器编排和调度平台。它由Google开发并开源&#xff0c;现由CNCF&#xff08;云原生计算基金会&#xff09;维护。Kubernetes设计用于自动化容器部署、扩展和管理&#xff0c;支持跨…

EPSON的温补晶振TG7050SKN

EPSON公司推出的温补品振(TCXO)TG7050SKN&#xff0c;尺寸大小为7.0x5.0x1.5 mm&#xff0c;具有高稳定性(105℃℃高温)等特点&#xff0c;可输出10MHz~54MHz的频率可应用在网络同步&#xff0c;BTS&#xff0c;微波&#xff0c;以及需要符合Stratum3、SyncE和IEEE1588等规范的…

张大哥笔记:商标也可以赚大钱

一大早看群聊&#xff0c;发现群里几个小伙伴在讨论商标转让的事情&#xff0c;我一时兴起&#xff0c;就想知道怎么通过商标来赚钱呢&#xff1f;不查不知道&#xff0c;原来还可以这样用商标赚大钱&#xff01; 下面我给大家掰扯一下&#xff0c;首先我们来了解一下什么是商标…

LeetCode面试298,二叉树最长连续序列(Python)

开始想着dfs&#xff0c;两种情况 1.以root为根 2.不以root为根 但是这样需要两个dfs分别进行&#xff0c;那么时间复杂度就上去了。 class Solution:def longestConsecutive(self, root: Optional[TreeNode]) -> int:def dfs(root):# 以root为根节点&#xff0c;可以延…

递归与递推---题目练习

目录 1、递归实现指数型枚举 2、递归实现排列型枚举 3、递归实现组合型枚举 4、带分数 方法一 方法二 5、翻硬币 6、飞行员兄弟 7、费解的开关 递归是指在函数的定义中使用函数自身的方法。它通过不断地将问题分解为更小的子问题&#xff0c;直到达到基本情况&#xf…

【Gaea+UE5】创建基本的大型世界场景

目录 效果 步骤 一、在Gaea中生成地形 二、确定导出的地形规模 三、在UE中创建地形 四、验证UE创建的地形规模是否正确 五、使用M4自动地形材质 效果 步骤 一、在Gaea中生成地形 1. 打开Gaea官网下载软件 2. 打开Gaea软件&#xff0c;我们可以选择一个预设的山体 创…

机器学习——4.案例: 简单线性回归求解

案例目的 寻找一个良好的函数表达式,该函数表达式能够很好的描述上面数据点的分布&#xff0c;即对上面数据点进行拟合。 求解逻辑步骤 使用Sklearn生成数据集定义线性模型定义损失函数定义优化器定义模型训练方法&#xff08;正向传播、计算损失、反向传播、梯度清空&#…

Implicit Diffusion Models for Continuous Super-Resolution

CVPR2023https://github.com/Ree1s/IDM问题引入&#xff1a; – LIIF方法可以实现任意分辨率的输出&#xff0c;但是因为是regression-based方法&#xff0c;所以得到的结果缺少细节&#xff0c;而生成的方法(gan-based,flow-based&#xff0c;diffusion-based等)可以生成细节&…

JavaScript中的RegExp和Cookie

个人主页&#xff1a;学习前端的小z 个人专栏&#xff1a;JavaScript 精粹 本专栏旨在分享记录每日学习的前端知识和学习笔记的归纳总结&#xff0c;欢迎大家在评论区交流讨论&#xff01; 文章目录 &#x1f506;RegExp &#x1f3b2; 1 什么是正则表达式 &#x1f3b2;2 创建…

从招标到合作:如何筛选与企业需求匹配的6sigma咨询公司

在市场竞争激烈的环境中&#xff0c;领军企业需要不断改进和创新才能在行业中保持竞争优势。为了解决产品质量、生产流程和客户满意度等方面的挑战&#xff0c;许多企业选择与6sigma咨询公司合作&#xff0c;推动企业的全面变革和持续发展。下面是企业在选择合作伙伴时通常会经…

一、Redis五种常用数据类型

Redis优势&#xff1a; 1、性能高—基于内存实现数据的存储 2、丰富的数据类型 5种常用&#xff0c;3种高级 3、原子—redis的所有单个操作都是原子性&#xff0c;即要么成功&#xff0c;要么失败。其多个操作也支持采用事务的方式实现原子性。 Redis特点&#xff1a; 1、支持…

Golang日志实战教程:掌握log与syslog库的高效使用

Golang日志实战教程&#xff1a;掌握log与syslog库的高效使用 简介理解 Golang 的 log 库基本概念创建日志记录器自定义日志记录器日志级别 深入 syslogsyslog 的基础配置和使用 syslog高级应用 日志格式化与管理日志格式化日志文件管理 日志的高级应用集成第三方日志框架使用 …

Python程序中温度更新出现振荡问题的分析和解决方案

在处理温度更新出现振荡问题时&#xff0c;可以考虑以下分析和解决方案&#xff1a;检查温度更新算法是否正确&#xff0c;可能存在错误导致振荡。检查温度更新的步长&#xff08;时间步长&#xff09;是否合适&#xff0c;步长过大可能导致振荡。检查系统动力学模型是否准确&a…

场外个股期权和场内个股期权的优缺点是什么?

场外个股期权和场内个股期权的优缺点 场外个股期权是指在沪深交易所之外交易的个股期权&#xff0c;其本质是一种金融衍生品&#xff0c;允许投资者在股票交易场所外以特定价格买进或卖出证券。场内个股期权是以单只股票作为标的资产的期权合约&#xff0c;其内在价值是基于标…

如何用Kimi,5秒1步生成流程图

引言 在当前快节奏的工作环境中&#xff0c;拥有快速、专业且高效的工具不可或缺。 Kimi不仅能在5秒内生成专业的流程图&#xff08;kimi&#xff09;&#xff0c;还允许实时编辑和预览&#xff0c;大幅简化了传统流程图的制作过程。 这种迅速的生成能力和高度的可定制性使得…

员工账号生命周期如何“全场景”自动化管理?

当企业在信息化建设中引入越来越多的业务系统时&#xff0c;必然存在系统内账号互相独立、无法打通的情况。一有人事变动&#xff0c;HR、IT 管理员、应用管理员、业务部门主管等人就需要在系统里手动更新账号状态。重复、低效&#xff0c;且不可避免出现安全隐患。困扰着 IT 管…

冯喜运:5.7全球紧张局势中,黄金原油投资者转向需谨慎

【黄金消息面分析】&#xff1a;周一&#xff08;5月6日&#xff09;&#xff0c;现货黄金触底回升&#xff0c;盘中交投于2320美元附近。自美国4月非农就业数据出炉和美联储主席鲍威尔货币政策新闻发布会以后&#xff0c;现货黄金从4月12日的历史高点2431美元下跌了大约6.3%&a…

AI口语对话训练有哪些软件?推荐这5款,简单易用

AI口语对话训练有哪些软件&#xff1f;AI口语对话训练软件在近年来得到了飞速的发展&#xff0c;为语言学习者提供了更为便捷、高效的学习方式。它们借助先进的自然语言处理技术和机器学习算法&#xff0c;不仅模拟了真实对话场景&#xff0c;还提供了个性化的学习建议和即时反…

笔试强训Day15 二分 图论

平方数 题目链接&#xff1a;平方数 (nowcoder.com) 思路&#xff1a;水题直接过。 AC code&#xff1a; #include<iostream> #include<cmath> using namespace std; int main() {long long int n; cin >> n;long long int a sqrtl(n);long long int b …