C语言进阶---动态内存管理

news/2024/11/24 2:22:29/

1、为什么存在动态内存分配?

我们已经掌握的内存开辟方式有:

int a = 20;        //在栈空间上开辟四个字节。
char arr[20];      //在栈空间上开辟10个字节的连续空间。

但是上述的开辟空间的方式有两个特点:

  • 开辟空间大小是固定的
  • 数组在申请的时候,必须指定数组的长度,它所需要的内存在编译时分配。

但是对于空间的需求,不仅仅是上述的情况,有时候我们需要的空间大小在程序运行的时候才能知道,这个时候就只能试试动态内存开辟了。

2、动态内存函数的介绍

2.1、malloc(申请内存空间)和free(释放/回收内存空间)

1、C语言提供了一个动态内存开辟的函数------malloc:

<stdlib.h>      
void* malloc (size_t size);

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

  • 如果开辟成功,则返回一个指向开辟好空间的指针(返回这块空间的起始地址)。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
  • 如果参数size的单位字节为0,malloc的行为是标准是未定义的,取决于编译器。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>int main()
{int arr[10] = { 0 };//动态内存开辟int* p = (int*)malloc(40);int i = 0;if (p == NULL){printf("%s\n", strerror(errno));return 1;}for (i = 0; i < 10; i++){*(p + i) = i;}for (i = 0; i < 10; i++){printf("%d ",* (p + i));}return 0;//没有free,并不是说内存空间就不回收了,当程序退出的时候,系统会自动回收内存空间。
}

输出:

在这里插入图片描述

使用数组申请的内存空间和使用malloc申请的空间在不同的区域上:

在这里插入图片描述

2、free------释放/回收内存空间。

void free(void* ptr);
  • ptr为NULL,则什么事都不做。

  • ptr必须是动态分配的空间。

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>int main()
{int arr[10] = { 0 };//动态内存开辟int* p = (int*)malloc(INT_MAX);int i = 0;if (p == NULL){printf("%s\n", strerror(errno));return 1;}for (i = 0; i < 10; i++){*(p + i) = i;}for (i = 0; i < 10; i++){printf("%d ",* (p + i));}//释放内存空间free(p);p = NULL;return 0;
}

【注:】free释放的必须是动态内存的空间,也就是说释放的需要是在堆区中的空间,而不应该去释放栈区中的空间。

如下是错误的:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>int main()
{int a = 0; int* p = &a;         //p是栈区里面的空间,不用free来释放free(p);p = NULL;return 0;
}

2.2、calloc

C语言也提供了一个函数叫calloccalloc函数也用来动态内存分配,原型如下:

void* calloc(size_t num,size_t size);
  • num是代表要开辟多少个元素
  • size代表开辟的每个元素是多少字节。

比如:想要开辟40字节的内存,num=10,size=4即可。

  • 返回值是开辟的那块空间的起始地址。

  • 这个函数还有一个特殊的地方:它在返回之前会把将要开辟的内存空间初始化一下,并初始化为全0。

代码验证:在使用calloc开辟好空间之后,我们来打印,看是不是全部初始化为0。

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>int main()
{int* p = (int*)calloc(10, sizeof(int));if (p == NULL){printf("%s\n", strerror(errno));return 1;}int i = 0;for (i=0; i < 10; i++){printf("%d ", *(p + i));}free(p);p = NULL;return 0;
}

输出:

在这里插入图片描述

malloc和calloc如何选择呢?

如果想要初始化使用calloc,如果不初始化,两个都可以。

calloc相当于malloc+memset。

2.3、realloc

  • realloc函数的出现让动态内存管理更加灵活
  • 有时我们会发现去申请的空间太小,有时候我们又觉的申请的空间过大,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整,那realloc函数就可以做到对动态开辟内存大小的调整。

函数原型如下:

void* realloc(void* ptr,size_t size);
  • ptr是要调整的内存地址。
  • size是调整后的内存大小。希望要调整为多大的空间。
  • 返回值为调整之后的内存起始位置。
  • 这个函数在调整原内存空间大小的基础上,还会将原内存中的数据移动到新的空间。
  • realloc在调整内存空间的是存在两种情况:
    • 情况1:原有空间之后有足够大的空间。
    • 情况2:原有空间之后没有足够大的空间。

下面先来说下realloc的两种情况:

比如现在有个使用malloc分配的动态内存,大小为40字节。然后现在想要扩容到80字节。

已存在40个字节了,需要扩容为80字节,所以还需要在原有的内存上在使用realloc追加40个字节。

那主要问题就在于这新追加的40字节的内存位置在那。

1、原有空间之后有足够大的空间:

这种情况是直接追加在原有40字节的后面:这个实现很简单就是直接追加就行了。

在这里插入图片描述

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

这个就是如果在原有的40字节的后面直接在追加40个字节的内存后,由于原有空间之后没有足够大的空间,强行追加40个字节,会占用其它数据的内存地址。所以这样肯定是不行的。那如何解决呢?
答案:realloc会找到一个80字节大小的内存空间,然后先把原有的(使用malloc)动态分配的40字节移动到这个80个字节的前40个字节处,然后还剩40个字节,这个算是扩容后的内存地址。

并且,旧的原40个字节内存,会被realloc自动释放回收。

在这里插入图片描述

代码示例:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>int main()
{int* p = (int*)malloc(40);if (p == NULL){printf("%s\n", strerror(errno));return 1;}int i = 0;for (i=0; i < 10; i++){*(p + i) = i+1;}//将p处的内存地址,扩容到80字节int* ptr = realloc(p, 80);if (ptr != NULL)p = ptr;for (i = 0; i < 10; i++){printf("%d ", *(p + i));}free(p);p = NULL;return 0;
}

输出:

在这里插入图片描述

2.4、realloc充当malloc

realloc(NULL,40);    ==========       malloc(40);

3、常见的动态内存错误

3.1、对NULL指针的解引用操作

//不进行NULL的判断,这样是存在安全隐患的。
#include <stdio.h>
#include <stdlib.h>int main()
{int* p = (int*)malloc(40);*p = 20;return 0;
}//对指针进行NULL的判断
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>int main()
{int* p = (int*)malloc(40);if (p == NULL){printf("%s\n", strerror(errno));return 1;}*p = 20;free(p);p = NULL;return 0;
}

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

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>int main()
{int* p = (int*)malloc(40);if (p == NULL){printf("%s\n", strerror(errno));return 1;}int i = 0;//当i=10时,就越界访问了。for (i = 0; i <= 10; i++){p[i] = i;}free(p);p = NULL;return 0;
}

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

#include <stdio.h>
#include <stdlib.h>int main()
{int a = 10;//p是非动态开辟内存,是不能用free释放的。int* p = &a;free(p);p = NULL;return 0;
}

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

#include <stdio.h>
#include <stdlib.h>int main()
{int* p = (int*)malloc(40);if (p == NULL){return 1;}int i = 0;for (i = 0; i < 10; i++){*p = i;p++;             //p在++之后,p已经不在是起始位置了,所以下面free释放只是释放了一部分,所以不对。}free(p);p = NULL;return 0;
}

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

#include <stdio.h>
#include <stdlib.h>int main()
{int* p = (int*)malloc(40);if (p == NULL){return 1;}//多次释放,会报错free(p);free(p);return 0;
}//改进:要么free一次,要么添加p = NULL;

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

#include <stdio.h>
#include <stdlib.h>int main()
{int* p = (int*)malloc(40);if (p == NULL){return 1;}return 0;
}

忘记释放不再使用的动态开辟空间会造成内存泄漏。

切记:动态开辟的空间一定要释放,并且正确释放。

4、几个经典的笔试题

4.1、题目1:野指针—返回栈区空间地址问题

#include <stdio.h>
#include <string>void GetMemory(char* p)
{//p是形参,在栈区里面存放p = (char*)malloc(100);
}void Test(void)
{char* str = NULL;GetMemory(str);strcpy(str, "hello world");printf(str);
}int main()
{Test();return 0;
}

问:运行结果?

传值调用,str是实参,p是形参,所以说GetMemory运行后,对str没啥影响,str还是空指针。并且p没有内存释放,导致内存泄漏。

所以说运行结果:

  • 内存泄漏
  • str是NULL,在strcpy时,需要传目标内存地址,而不是NULL,所以会导致内存崩溃。

正确修改:

#define _CRT_SECURE_NO_WARNINGS
#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);//这个打印相当于:因为即便printf("hello world");,那传给print函数的也是字符'h'的地址,起始是和直接传str地址是一样的道理。printf("hello world");free(str);str = NULL;
}int main()
{Test();return 0;
}

输出:

在这里插入图片描述

4.2、题目2:野指针—返回栈区空间地址问题

#define _CRT_SECURE_NO_WARNINGS
#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;
}

输出:

在这里插入图片描述

分析:数组p在GetMemory里面,且p存放的是字符’h’的地址,但是当函数GetMemory运行完毕,p数组就会销毁。然后str = GetMemory(),当GetMemory返回值为p,但是p已销毁。所以str是野指针。str指向的那块地址已经被销毁了,所以结果如上。

总结:以上两题都是返回栈区空间地址问题。让一个函数返回函数体里面的变量的地址时,用个变量接收,这个是非常危险的。

4.3、题目3:

int* f1(void)
{int x = 10;return (&x);
}//判断下列代码的问题:野指针问题。
//x在函数f1内部,return &x,说明此函数返回个指针,但是这个函数在运行完毕后,x变量会销毁,所以&x就是野指针。
int* f2(void)
{int* ptr;*ptr = 10;return ptr;
}//也是野指针问题。
//ptr没有初始化,然后*ptr相当于随便找了地址来解引用,相当于随机访问,野指针问题。

4.4、野指针

#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;
}

分析错误:使用malloc动态内存分配100字节大小的空间。然后拷贝,str里面存放了首字符’h’的地址。当free(str)后,动态分配的100字节大小的空间就交给操作系统回收了。但是因为没有进行str = NULL这一步操作,所以str的值,也就是存放的首字符’h’的地址并没有变。然后str != NULL为真,然后在进行拷贝,然后现在str已经时野指针了。虽然将"world"传给str,但是str指向的地址,已经不归我们使用了,所以在访问有可能时访问其它的数据的空间,所以此程序不对。

在这里插入图片描述

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

在这里插入图片描述

内核空间是用来运行操作系统的。我们写的代码不可以运行在此处。

数据段又是静态区。

代码段:存放我们写的代码进行编译、链接后为可执行程序的二进制指令。

6、柔性数组

在C99中,结构体中的最后一个元素允许是未知大小的数组,这就叫做【柔性数组】成员。

1、必须在结构体中。

2、必须是最后一个成员。

3、必须是大小未知的数组。

eg:

typdef struct st_type
{int i;int a[0];    //柔性数组成员int b[];     //这个写法也行
}type_a;

6.1、柔性数组的特点

  • 结构中的柔性数组成员前面必须至少有一个其它成员。
  • sizeof返回的这种结构体大小不包括柔性数组的内存。
  • 包含柔性数组成员的结构用malloc()函数进行动态内存分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。

例如:

typdef struct st_type
{int i;int a[0];    //柔性数组成员
}type_a;
prinf("%d\n,sizeof(type_a)");        //输出的是4。

6.2、柔性数组的使用

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>struct S
{int i;int arr[];   //打算给此柔性数组10个元素的大小。
};int main()
{//包含柔性数组成员的结构用malloc()函数进行动态内存分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。//sizeof(struct S)是结构体大小,40就是柔性数组的大小。struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);if (ps == NULL){printf("%s\n", strerror(errno));return 1;}int i = 0;for (i = 0; i < 10; i++){ps->arr[i] = i;}for (i = 0; i < 10; i++){printf("%d ", ps->arr[i]);}struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 80);if (ptr != NULL){ps = ptr;}free(ps);ps = NULL;return 0;
}

以后采用柔性数组的方法可以对结构体数组进行动态内存分配。

除此以上使用柔性数组的方法,其实我们也有第二种方法来对结构体中的数组进行动态内存分配:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>struct S
{int i;int* arr;
};int main()
{struct S* ps = (struct S*)malloc(sizeof(struct S));if (ps == NULL){printf("%s", strerror(errno));return 1;}//这里为什么需要对结构体进行malloc呢?将结构体malloc是为将结构体中的成员变量i也放在堆区。//因为下面我们要将arr进行malloc,为了将结构体中的每个成员一致,所以先也将结构体malloc,这样以来i就放在了堆区里面了。ps->i = 100;//给指向arr的地址动态分配40个字节大小的空间。ps->arr = (int*)malloc(40);int  i = 0;for (i = 0; i < 10; i++){ps->arr[i] = i;}for (i = 0; i < 10; i++){printf("%d ", ps->arr[i]);}int* ptr = (int*)realloc(ps->arr, 80);if (ptr != NULL){ps->arr = ptr;}free(ps->arr);free(ps);//这里直接一步到位把ps置为NULL,那ps->arr自然而然的就为NULL了。ps = NULL;return 0;
}

那以上两种方法如何选择呢?

  • 采用柔性数组的方法,只需要一次malloc,后续不够在使用realloc。
  • 而第二种方法,需要两次malloc,后续不够在使用realloc

注意:使用malloc越多,就越需要free,而且还会产生内存碎片。

总结:

  • 采用柔性数组的好处是:方便内存释放。
  • 第二个的好处是:有利用访问速度。

http://www.ppmy.cn/news/701421.html

相关文章

ubuntu下安卓刷机教程和scrcpy无线控制手机

由于手头有个闲置的安卓手机&#xff0c;平时一般固定在手机支架上&#xff08;如下图&#xff09;&#xff0c;当做时钟、闹钟还有树莓派远程桌面&#xff0c;偶尔也拿来看看视频&#xff0c;但是每次拿上拿下太麻烦了。突然想到能不能用电脑来控制手机&#xff0c;这样就方便…

android手机无分区无法刷机,adb sideload 刷机教程:当你手机无法开机,内存里没有ROM时......

本帖最后由 木力曾 于 2015-11-24 11:26 编辑 adb sideload刷机教程 当你手机无法开机,内存里没有ROM时的解决方法(根据网上教程自己整理了下) 这个教程很简单,大神可以路过。遇到手机突然无法开机,但是内存里又没可以卡刷的ROM时。 如果这样你就可以通过 adb sideload 的刷…

三星a7108android 7.0,三星A7108刷机教程_三星A7108线刷官方系统包_可救砖用

来说说咱们的三星A7108手机的线刷教程&#xff0c;这个线刷教程在三星手机手机中没什么稀奇的&#xff0c;因为这个线刷的教程主咱们的官方的rom包标配的教程了&#xff0c;之前有很多机友还不知道如何进行线刷操作&#xff0c;所以下面整理了一下详细的线刷教程供大家参考了&a…

Map和Set详解

Map和Set详解 一.引言二.搜索树2.1 概念2.2二叉搜索树基本操作2.3 操作-查找2.4 操作-插入2.5 操作-删除2.6 性能分析 三. Map和set简介四.Map接口和实现类4.1 HashMap的api4.2 TreeMap的api 五.Set接口和实现类5.1HashSet常用的Api5.2 TreeSet的常用api 六.哈希表的概念6.1 概…

前端去除浏览器账号密码提示

在输入框前加上代码&#xff1a; <input type"password" name"oldPwdInput" placeholder"" maxlength"20" autocomplete"off" style"position: fixed;top:-1000px"> <input style"display:none&qu…

彻底解决不要脸的360更改浏览器主页【转载】

1.IE浏览器/工具/internet选项 把主页更改了&#xff08;基本没用&#xff09; 2.右键浏览器图标/属性/快捷方式/目标 看看是不是在.exe后面有没有指向360的目标&#xff0c;有的话删掉。如果不能更改的话&#xff0c;将文件的只读属性勾选掉 3.如果还是不行&#xff0…

如何重新设置苹果id密码_苹果怎么重新设置id账号和密码

点击进入手机的【设置】&#xff0c;然后点击【Apple ID选项】&#xff0c;选择iForgot选项&#xff1b;您可以通过输入安全问题&#xff0c;或者获取验证邮件的方式&#xff0c;获得密码重设的链接&#xff0c;点击进入重设即可。以下是详细介绍&#xff1a; 1、打开苹果手机的…

360浏览器保存的密码-亲测

F12 把密码对应输入框的input的type改成txt&#xff0c;密码就出来了&#xff0c;设置啥的看不到密码&#xff0c;亲测