当前位置: 首页 > news >正文

从0开始学c语言-33-动态内存管理

上一篇:从0开始学c语言-32-自定义类型:结构体,枚举,联合_阿秋的阿秋不是阿秋的博客-CSDN博客

 

目录

动态内存管理

1. 为什么存在动态内存分配

2. 动态内存函数的介绍

2.1 malloc和free

malloc

free

应用

和数组的区别

2.2 calloc

2.3 realloc

变大

变小

3. 常见的动态内存错误

3.1 对NULL指针的解引用操作

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

3.3 对非动态开辟内存使用free释放

3.4 使用free释放一块动态开辟内存的一部分

3.5 对同一块动态内存多次释放

3.6 动态开辟内存忘记释放(内存泄漏)

4. 几个经典的笔试题

4.1 题目1:

查阅资料:

编译器角度入手

改善

4.2 题目2:

 4.3 题目3:

4.4 题目4:

4.5 题目5:

4.6 题目6:

5. C/C++程序的内存开辟


放个喜欢的图,哈哈哈,内容很长,主要是写给我自己复习看的。

动态内存管理

        其实内存里有三个区,每个区放不同的东西。我们的动态内存分配就是在堆区开辟空间的。

1. 为什么存在动态内存分配

我们已经学过的在栈区开辟内存空间的方式有

int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
但是上述的开辟空间的方式有两个特点:
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小需要动态变化,拿最熟悉的手机来说,你一定不希望被占用的内存越来越多,动态内存分配就可以实现你需要多少内存开辟多少内存的需求,在程序运行完后,内存释放。这就是为什么我们的手机和电脑卡了就重启,重启之后就不卡的原因了。

2. 动态内存函数的介绍

2.1 mallocfree

malloc

void* malloc (size_t size); //size单位是字节
这个函数向内存申请一块 连续可用 的空间,并返回指向这块空间的指针(起始地址)。
        如果开辟成功,则返回一个指向开辟好空间的指针。
        如果开辟失败,则返回一个NULL 指针, 因此malloc的返回值一定要做检查
        返回值的类型是 void* ,所以 malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定(加上强制类型转换)。
        如果参数 size 0 malloc 的行为是标准是未定义的(也就是说,没必要搞这种没意义的数字去试探函数的反应),这种结果取决于编译器。

free

函数 free 专门是用来做动态内存的释放和回收的 ,函数原型如下:
void free (void* ptr);
free 函数用来释放动态开辟的内存。
        如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的。(因为free就是专门用来释放动态开辟的空间)
        如果参数 ptr NULL 指针,则函数什么事都不做。

mallocfree都声明在 stdlib.h 头文件中。

应用

假设我们现在要开辟10个int类型的空间

如果是在栈区开辟,那么写一个数组就好了

int arr[10]; //栈区

主要演示动态内存怎么开辟,首先需要malloc来申请空间,

void* malloc (size_t size); //size单位是字节

因为要开辟10个int,那么void* malloc (size_t size); 当中的size便确定了,写成下面这样。

malloc(10*sizeof(int));

我们前面介绍过这个函数会返回一个指向开辟好空间的指针,那么现在我们需要用一个指针来接收它返回的地址。因为是int类型的空间,所以我们用int*指针来接收。又因为返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定(加上强制类型转换)。所以我们写成了下面这样。

int* p = (int*)malloc(10 * sizeof(int));

学到这里你是不是觉得,好像这个开辟空间的方式怎么和数组差不多呢?

我们知道数组名是首元素的地址,其实就是指向首元素的指针,在int数组中的数组名就是int*指针。而现在我们接收【开辟动态内存返回值】的 p指针变量 仿佛就是这10个int类型的数组名,p指向首元素,是int*类型的指针。

简单提一下,让你更理解数组。

若开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。

int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("main"); //用来报错
		return 0;
	}

接着,我们试着使用一下。

int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
		printf("%d ", p[i]); //p[i] <=> *(p+i)
	}

你会发现,10个int的空间就相当于一个int[10]类型的数组。

现在进行空间释放。使用free函数把p指针指向的动态内存空间回收掉,但是p指针还存放着这块空间的地址,所以释放后我们还需要把这个指针置位空指针,否则它就是个野指针。

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int arr[10]; //栈区
	//动态内存开辟
	int* p = (int*)malloc(10 * sizeof(int));
	if (p == NULL)
	{
		perror("main");
		return 0;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		*(p + i) = i;
		printf("%d ", p[i]);
	}
	//回收空间
	free(p);
	p = NULL; //不置为空指针,p就会成为野指针
	return 0;
}

这就是比较完整的使用过程了,

1·开头【开辟空间后】 的 指针接收 和 强制类型转换,以及检查是否开辟失败进行报错。

2·结尾的 回收空间,置为空指针

这两个步骤很关键。

和数组的区别

我上面说这个和数组很像,但是并不完全一样。

用这段代码和你说,先和你说calloc开辟的空间会初始化为0,因为我之前说p指针变量好像就和数组名一样,都指向首元素的地址,后面都是9个连续的int空间,所以我进行了如下探索。

int main()
{
	int arr[10] = { 0 };
	int* p = (int*)calloc(10 ,sizeof(int));
	if (p == NULL)
	{
		perror("main");
		return 0;
	}
	//使用
	while (*p++ == *arr++)
		printf("qiu");
	//释放空间
	free(p);
	p = NULL; //不置为空指针,p就会成为野指针
	return 0;
}

结果发现数组名是不能改变的指针,而p指针是可以改变的。

虽然p指针和arr数组名都可以做到*(p+i)和*(arr+i)来访问并改变元素内容,但是数组名arr这个指针本身并不能被改变,而指针p却是可以改变的。

这大概就是为什么数组大多都是函数传参过去实现更多功能的原因了,因为虽然数组名可以当做指针用,但是我们并不希望在使用的时候改变数组名指向的地址位置,同时却又希望利用数组首元素地址来实现更多功能,如下。

2.2 calloc

calloc 函数也用来动态内存分配。原型如下:
void* calloc (size_t num, size_t size);
1·函数的功能是为 num 个、每个元素大小为 size 的元素们开辟一块空间, 并且把空间的每个字节初始化为0
2·与函数 malloc 的区别只在于 calloc 会在返回地址之前 把申请的空间的 每个字节初始化为全0 。(malloc只是开辟,并不会初始化开辟的动态空间,未初始化之前都是随机值)
在使用方面,除了会初始化开辟的空间外,就是参数不太一样。

可以看到,如果都开辟10个int大小的空间,那么malloc的参数是40,而calloc的参数是(num)10个(size)大小为4byte的空间。本质上都是开辟了40个byte的空间,所达到的结果也一样,区别只有calloc会初始化开辟的空间。

 上面监视的窗口输入的是p,可别习惯性加个&写成&p,那就是 保存p指针 的地址了。

int main()
{
	//动态内存开辟
	/*int* p = (int*)malloc(10 * sizeof(int));*/
	int* p = (int*) calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("main");
		return 0;
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}
	//释放空间
	free(p);
	p = NULL; //不置为空指针,p就会成为野指针
	return 0;
}

2.3 realloc

        realloc函数的出现让动态内存管理更加灵活。
        有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
void* realloc (void* ptr, size_t size);
ptr 是要调整的内存地址
size 调整后的空间大小
返回值为调整之后的内存起始位置。
这个函数调整在原内存空间大小的基础上, 还会将原来内存中的数据移动到 新 的空间。
realloc 在调整内存空间的是存在两种情况:
        情况1 :原有空间之后有足够大的空间

        情况2:原有空间之后没有足够大的空间

情况 1
        当是情况1 的时候,要扩展内存的时候就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况 2
        当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
realloc函数如果后面空间不够,就开辟新空间,拷贝原来数据,返回的地址是新开辟空间的地址。 如果找不到合适的空间,就会返回空指针。

绿色代表reallloc函数开辟的空间,情况1有足够的空间来开辟,所以指针和空间都保存原有位置,返回的地址是原有空间的起始地址。

变大

情况二后面的空间不够了,我们会在堆区上找新的空间来开辟,并把原有空间的数据拷贝到这个新空间,返回一个指向新空间的地址。

 

 这是情况二的示范,记住这时候的空间是20000个int大小的空间,包含原有p指针指向的元空间大小。

int main()
{
	//动态内存开辟
	/*int* p = (int*)malloc(10 * sizeof(int));*/
	int* p = (int*) calloc(100, sizeof(int));
	if (p == NULL)
	{
		perror("main");
		return 0;
	}
	printf("%p\n", p);
	//使用时候想要在p指针后开辟更大的空间
//别直接用p接收realloc的返回值
//要用另一个指针接收
//为了防止找不到会返回NULL的情况
	int* ptr = realloc(p, 20 * sizeof(int));
	if (ptr != NULL)
	{
		p = ptr;
	}
	printf("%p\n", p);
	printf("%p\n", ptr);
	//释放空间
	free(p);
	p = NULL; //不置为空指针,p就会成为野指针
	ptr = NULL;
	return 0;
}

需要知道的是,如果我们给realloc函数传一个NULL指针过去,那么所实现的效果和malloc函数类似,就是直接在堆区开辟空间。

int* ptr = realloc(NULL, 20 * sizeof(int));

变小

然后示范一下变小的,可以看到就算空间变小,数据也不会变。

3. 常见的动态内存错误

可别觉得你不会犯这些错误,找不到问题的时候就可以照着这几条找。

3.1 NULL指针的解引用操作

void test()
{
 int *p = (int *)malloc(INT_MAX/4);
 *p = 20;//如果p的值是NULL,就会有问题
 free(p);
}

上面的代码就是忘记了对malloc函数的返回值进行NULL的检查,很容易会出问题。

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

void test()
{
 int i = 0;
 int *p = (int *)malloc(10*sizeof(int));
 if(NULL == p)
 {
     return 0;
 }
 for(i=0; i<=10; i++)
 {
     *(p+i) = i;//当i是10的时候越界访问
 }
     free(p);
}

3.3 对非动态开辟内存使用free释放

void test()
{
     int a = 10;
     int *p = &a;
     free(p);//ok?
}

这样可不行哦,这个p指针是在栈区上申请空间的,而且free函数是专门释放在堆区动态开辟的内存空间的。可别乱用。

3.4 使用free释放一块动态开辟内存的一部分

void test()
{
 int *p = (int *)malloc(100);
 p++;
 free(p);//p不再指向动态内存的起始位置
}

要注意前置后置++、- - 对于指针的作用,会改变指针的位置。

3.5 对同一块动态内存多次释放

void test()
{
 int *p = (int *)malloc(100);
 free(p);
 free(p);//重复释放
}

3.6 动态开辟内存忘记释放(内存泄漏)

void test()
{
 int *p = (int *)malloc(100);
 if(NULL != p)
 {
 *p = 20;
 }
}
int main()
{
 test();
 while(1);
}
忘记释放不再使用的动态开辟的空间会造成内存泄漏。
切记:
动态开辟的空间一定要释放,并且正确释放
动态开辟内存的两种回收方式:
1·主动free
2·程序结束

4. 几个经典的笔试题

4.1 题目1

void GetMemory(char *p) {
 p = (char *)malloc(100);
}
void Test(void) {
 char *str = NULL;
 GetMemory(str);
 strcpy(str, "hello world");
 printf(str);
}
请问运行 Test 函数会有什么样的结果?

查阅资料:

首先需要理解第一句的空指针意味着什么
详情放上链接 【C语言】空指针_SouLinya的博客-CSDN博客_空指针
我们会用到的:
1·空指针不等同于未初始化的指针,未初始化的指针通常指野指针、
2·空指针不会指向任何地方,它不是任何对象或函数的地址
总的来说,NULL的作用就是当一个指针变量没有被显性初始化时,将该指针变量的值赋为NULL。解引用前判断指针变量是否为NULL,如果为NULL说明该指针没有初始化过,不可解引用。
 char *str = NULL;

或者更直白一些,这个指针变量str会占用4个字节的内存,保存了NULL这个空指针,NULL只是表示这个地址为空。

所以如果我们直接传递str指针变量,是传递过去了指针变量str中保存的地址,而这个地址是NULL。

编译器角度入手

可以看到在vs编译器中str的地址(也就是NULL的地址)会默认为0x00000000,而NULL 指针什么也不指向,自然也就不会再内存中申请空间,所以char*str现在所指向的空间是不存在的。

不懂的话你就拿数组来类比,

我们知道指针是保存地址的,那么现在p这个指针变量所申请的内存空间中就住着arr这个数组首元素的地址。

 上面这个图的意思就是p这个指针变量中保存了arr[0]这个元素的地址,这个地址中存的值是{0}。

 那么同样的空指针,就可以这样理解。

 char *str = NULL;

 这是比较具体化的理解了。我的能力到此为止了。

void GetMemory(char *p) {
 p = (char *)malloc(100);
}

也就是说现在我们传过来了str指针中保存的地址,而这个地址是NULL。

也就是说,p指针变量中存放了NULL,在经过动态开辟后,让p指针变量指向这个动态开辟空间的地址,并没有改变str指针变量中存储的内容。

那么怎么才可以改变str指针变量中存放的NULL呢?

改善

第一种:给函数返回值,让 str指针 接收动态开辟内存空间的地址。

(用完后,别忘了释放空间并置为空指针)

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

第二种:二级指针接收,传过去str的地址

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

4.2 题目2

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

上面这段也是有错的,要特别清楚,

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

这段代码是在栈区开辟空间的,那便是临时变量,退出函数后就会归还空间。

void Test(void) {
     char *str = NULL;
     str = GetMemory();
     printf(str);
}

所以str接收到的地址已经被归还了,是没有访问权限的。

虽然在监视窗口中,你会看到这样的结果。

但实际上这块空间已经被收回了,实际上我们打印的结果会是这样。

 4.3 题目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);
}

似乎看着很完美了,

但是要知道现在str指针存放的是动态内存分配的区域,所以使用后必须释放空间,并且置为空指针

4.4 题目4

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

这个free掉了这块空间的访问权,我们不能再访问,str虽然还是指向这个空间的地址,但是没有访问权,所以再次访问属于非法访问。

4.5 题目5

int* f2(void)
{
    int* ptr;
    *ptr = 10;
    return ptr;
}

        指针要么指向一个空间,要么是空指针。如果不初始化指针,就随机指向一个空间,对这个不初始化的指针进行解引用就会成为野指针。

4.6 题目6

我自己出的题,哈哈哈。

void test(int*arr)
{
	int*p= (int*)malloc(3 * sizeof(int));
	if (p != NULL)
	{
		arr = p;
	}
}
int main()
{
	int arr[] = { 1,2 };
	test(arr);
	*(arr + 2) = 3;
	printf("%d\n", *(arr + 2));
    free(arr);
    arr=NULL;
}

看似很完美?或者问题多多?

其实也就问题很大,你在栈区上开辟了arr空间,又在堆区上让arr指向动态开辟的内存空间。

哈哈哈,不能这样瞎搞。

5. C/C++程序的内存开辟

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

        1. 栈区( stack ):在执行函数时, 函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放
        栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
        2. 堆区( heap ):一般由程序员分配释放, 若程序员不释放,程序结束时可能由 OS 回收 。分配方式类似于链表。
        3. 数据段(静态区)( static )存放全局变量、静态数据。程序结束后由系统释放。
        4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。
有了这幅图,我们就可以更好的理解之前 讲的 static 关键字修饰局部变量的例子了。
        实际上普通的局部变量是在栈区 分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。但是被static 修饰的变量存放在 数据段(静态区) ,数据段的特点是在上面创建的变量,直到程序结束才销毁,所以生命周期变长。

相关文章:

  • java详解队列
  • springboot - 2.7.3版本 - (八)ELK整合Kafka
  • uniCloud开发公众号:一、接收、解析、组装xml消息
  • YOLO系列目标检测算法-YOLOv1
  • JavaScript高级,ES6 笔记 第三天
  • 【雷达图】R语言绘制雷达图(ggradar),NBA季后赛数据为例
  • 机器学习笔记 - 在QT/PyTorch/C++ 中加载 TORCHSCRIPT 模型
  • redis 技术分享
  • 怎么让面试官喜欢你?
  • 深度学习模型理解-CNN-手写数据字代码
  • C# ZBar解码测试(QRCode、一维码条码)并记录里面隐藏的坑
  • 【技术美术图形部分】图形渲染管线3.0-光栅化和像素处理阶段
  • css:一个容器(页面),里面有两个div左右摆放并且高度和容器高度一致,左div不会随着页面左右伸缩而变化,右div随页面左右伸缩宽度自适应(手写)
  • Kubernetes 1.25 集群搭建
  • 【每周CV论文推荐】GAN在医学图像生成与增强中的典型应用
  • [ 一起学React系列 -- 8 ] React中的文件上传
  • 2017-08-04 前端日报
  • javascript数组去重/查找/插入/删除
  • JavaWeb(学习笔记二)
  • magento 货币换算
  • Sass 快速入门教程
  • - 概述 - 《设计模式(极简c++版)》
  • 给Prometheus造假数据的方法
  • 关于 Cirru Editor 存储格式
  • 技术发展面试
  • 排序算法学习笔记
  • 前嗅ForeSpider教程:创建模板
  • 网络应用优化——时延与带宽
  • 温故知新之javascript面向对象
  • 写代码的正确姿势
  • 学习HTTP相关知识笔记
  • 移动互联网+智能运营体系搭建=你家有金矿啊!
  • 在electron中实现跨域请求,无需更改服务器端设置
  • 新年再起“裁员潮”,“钢铁侠”马斯克要一举裁掉SpaceX 600余名员工 ...
  • ​力扣解法汇总1802. 有界数组中指定下标处的最大值
  • ## 临床数据 两两比较 加显著性boxplot加显著性
  • #include到底该写在哪
  • (13)Latex:基于ΤΕΧ的自动排版系统——写论文必备
  • (2009.11版)《网络管理员考试 考前冲刺预测卷及考点解析》复习重点
  • (HAL)STM32F103C6T8——软件模拟I2C驱动0.96寸OLED屏幕
  • (附源码)ssm码农论坛 毕业设计 231126
  • (附源码)基于ssm的模具配件账单管理系统 毕业设计 081848
  • (九)c52学习之旅-定时器
  • (最简单,详细,直接上手)uniapp/vue中英文多语言切换
  • .net 4.0发布后不能正常显示图片问题
  • .net 8 发布了,试下微软最近强推的MAUI
  • .NET Core 和 .NET Framework 中的 MEF2
  • .NET Entity FrameWork 总结 ,在项目中用处个人感觉不大。适合初级用用,不涉及到与数据库通信。
  • .net 受管制代码
  • .NET框架类在ASP.NET中的使用(2) ——QA
  • .php结尾的域名,【php】php正则截取url中域名后的内容
  • .so文件(linux系统)
  • @AliasFor注解
  • []sim300 GPRS数据收发程序
  • [C# 网络编程系列]专题六:UDP编程