c语言动态内存分配
叶超凡 人气:0为什么有动态内存分布
大家发现一个问题没有,就是我们之前写代码创建数组的时候,似乎都存在着这么一个问题,就是我们开辟一个数组的时候,这个数组的大小好像都是固定的,不能改变的,比如说我这里创建了一个能装100个字符的数组,那么这个数组在这整个程序中的大小都是不会改变的,那么我们这里的程序在以后的使用中导致这个数组装不下了,那么是不是就会出现问题啊,那么我们又不知道这个数组到底得多大,到底多少才能合适,小了装不下,多了又浪费空间,所以为了解决这个情况,我们这里就给出了动态内存分布这个概念,那么这篇文章就带着大家学习一下如何来实现我们的动态内存分布。
malloc函数的使用
那么我们这里的动态内存分布是通过我们的函数来实现的,那么我们这里首先来看看malloc函数的介绍
那么我们看到这个函数的作用就是开辟一个空间,使用这个函数需要引用的头文件stdlib.h,然后这个函数需要的一个参数就是一个整型,那么这个函数所开辟的空间的大小就是根据这个整型来确定的,然后这个函数的返回值是void*类型的一个指针,那么这里就有小伙伴们会感到疑惑了,为什么这里返回的类型是一个void*类型的指针呢?那么这里我们就要想象一个场景了,在编写这个函数的时候,我们的作者知不知道你拿这个开辟的空间去干嘛,他不知道,他不知道你是拿去存储一个char类型的变量还是一个int类型的变量还是一个结构体类型的变量,所以我们这里就只能返回一个void*类型的指针来作为返回值,所以我们在使用这个函数的时候就得强制类型转化一下,转换成你想要使用的类型,那么看到这里想必大家硬干能够了解这个函数使用的方法,那么我们这里就来看看这个函数使用例子,那么我们这里假设要开辟40个字节的空间,然后用这个开辟好的空间来存储10个整型的变量,然后将这个10个整型变量初始化为1,2,3,4,5,6,7,8,9,10,那么我们这里的代码如下:
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)malloc(40); int i = 0; for (i = 0; i < 10; i++) { *(p + i) = i; } for (i = 0; i < 10; i++) { printf("%d ", *(p+i)); } return 0; }
那么这里我们需要40个字节的大小的内存,所以我们这里就在括号里面写进一个40,然后我们这里需要的整型的指针,所以我们这里就在接收时将他强制类型转换一下,那么我们这里的指针p指向的就是这个开辟的内存的其实位置,那么我们这里的p他是int*类型,那么我们这里每加一个1就会跳过4个字节,解引用的时候访问4个字节,那么我们这里在使用的时候就可以挨个跳过挨个解引用挨个赋值,用for循环来实现上诉的功能,我们在打印的时候便也是跟上面的思路差不多,那么我们这里就可以来看看这个代码运行的结果为:
那么我们这里使用这个函数的时候还需要知道的一点就是我们这里的开辟空间的时候,如果开辟成功则这个函数返回的是一个指向开辟好空间的指针。如果开辟失败,则返回一个NULL指针,那么我们上面的代码应为他需要的空间较小,所以他开辟成功了,但是如果我们这里开辟的空间较大的话,我们这里是会返回一个NULL指针的,那么我们这里如果不做检查的话,我们就会对空指针做一些操作解引用啥的等等,那么这样的话就会报出错误,因为我们这里规定的就是不准对NULL解引用使用,所以我们每次在使用这个函数的时候最好加上一个判断条件,判 断它是否开辟成功了,那么我们这里就可以将我们的代码进行一下改进:
#include<stdio.h> #include<stdlib.h> #include<errno.h> #include<string.h> int main() { int* p = (int*)malloc(100000000000000000); int i = 0; if (p == NULL) { printf("%s", 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; }
那么我们在这里就进行了一下简单的修改,我们这里加了一个if语句用来判断我们这里的内存是否开辟成功,那么我们这里就使用的是if语句,那么我们这里进入这个if语句的内部的条件就是当我们这个的p等于空指针的时候,那么我们这里if语句的内部就可以用到我们之前讲的strerror错误信息报告函数,和errno来显示我们这里的错误码,来显示我们这里的错误,然后再添加一个return 1 ,来结束我们这个程序,那么这样的话我们既避免了解引用空指针,又可以显示我们的错误所在,那么我们讲上面的代码运行一下就可以看到我们这里出错的原因:
那么我们这里就打印出来了我们这里出错误的原因,那么我们这里就相当于给我们这里的代码上了一层保险避免出错,那么我们这里开辟的空间和我们之前之间创建一个变量和一个数组开辟的空间有什么区别?那么这里的区别肯定是有的,我们这里的动态开辟的空间他开辟的位置是在我们的内存中的堆区,而我们的创建的变量的所申请的空间是在我们内存中的栈区,而我们栈区的内存使用习惯是从高地址往低地址进行使用,而我们的堆区中开辟的内存就没有这个习惯,而且我们在栈区中创建的变量的声明周期往往都是所在的代码块,比如说我们在函数中创建的一个局部变量,那么这个变量的生命周期往往就是这个函数的内部,出了这个函数它就没了,但是我们在这个堆区申请的空间那就不大一样,只要你不主动的释放那么他除了你程序的结束,他能一直都在所以这里就是我们另外的一个小小的区别,那么这里说到释放又是一个怎么回事呢?那么这个问题我们待会再聊,那么这里有这么一个问题,就是大家可能听过变长数组这个东西,那么有人就要问了,这个变长数组是不是也是动态内存中的一部分啊,因为他是变长啊他的大小听上去好像可以变啊,那么这里大家可能理解上就有那么一点点的偏差了,我们这里的变长数组并不是说他的长度可以变,而是说我们在创建这个数组的时候可以先输入一个变量上去,这个变量可以在我们程序运行起来的时候再·输入这个变量的大小,那么这里就是我们的变长数组,他在内存中申请的空间位置是在栈区,而不是堆区,所以我们这里的变长数组并不是我们的动态内存开辟的内容,那么我们这里的变长数组的使用方式就是如下的代码,那么这里还有一定那就是支持c99编译器的才能使用我们这里的变长数组,那么我们这里的vs编译器不支持c99标准所以用不了,那么我们这里的代码形式如下:
#include<stdio.h> int main() { int n = 0; scanf("%d", &n); int arr[n] = { 0 }; return 0; }
free函数的用法
那么我们这一章的内容叫做动态的分配,那么这个动态目前为止好像体现的不是很明显啊,我们上面的代码只体现出来了我们开辟了一个空间,但是这个功能我们普通的创建变量也有这个功能啊,那么我们就可以来讲讲我们这里的动态内存可以体现出来在哪里,我们上面的代码
#include<stdio.h> #include<stdlib.h> #include<errno.h> #include<string.h> int main() { int* p = (int*)malloc(40); int i = 0; if (p == NULL) { printf("%s", 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; }
这里是申请了一个40个字节大小的空间,那么我们这里申请这个空间的目的就是打印1到10,那么我们这里打印完了之后是不是就用不到这个空间了,既然我们不用了那么我们这里是不是就可以讲这个空间进行释放了呢?把这个空间还给我们的操作系统他好再分给其他的地方进行使用,那么这样的话我们的内存利用效率是不是就会大大的提高了呢?对吧这就是我们动态内存的一个很好的特点,相比于我们之前用的那个创建变量的方法这个是不是就灵活了很多啊,因为我们那个方法你申请之后他就一直都在你无法进行释放吗,那么这样的话我们的利用率就会很低,所以这里释放空间的方法就是我们这里的free函数,那么我们这里就可以先看看我们free函数的基本的介绍:
那么我们这个free函数需要的参数就只有一个就是一个指针,而且没有返回值,那么我们这里就可以讲上面的代码进行修改加一个free函数上去来释放我们这里开辟的一个空间
#include<stdio.h> #include<stdlib.h> #include<errno.h> #include<string.h> int main() { int* p = (int*)malloc(40); int i = 0; if (p == NULL) { printf("%s", 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; }
那么我们这里就可以使用调试来进行一下观察我们这里变化的过程
在释放前我们这里内存的数据就是0到9,那么我们来看看执行一下我们这里的free函数看看会变成什么:
我们发现在执行完我们这里free函数之后我们这里的内存中的值都变成了随机值,也就是说我们这里的内存是还给了我们的操作系统,但是我们这里发现了一个问题就是我们这里p的值是没有发生变化的也就是说我们这里的p还是保留的原来的值,那么这里既然保留原来的值的话,我们是不是就可以通过这个只值来找到我们原来的那块内存啊,那么既然这里能够找到这个块空间的话我们是不是就可以访问这里的内存了啊,但是这块内存他已经不属于你的这个程序啊,但是你却能够找到这块空间的话是不是就会报错误了啊,因为这个时候我们的p已经成为了一个野指针,所以我们这里在释放空间之后还得干的一个步骤就是讲我们这里的指针变量p的值赋值为一个空指针,这就是为了违法访问而带来的问题。那么看到的这里想必大家知道了我们的free函数使用的规则已经作用但是这里还有几个小点需要大家注意一下:
第一点:
我们的free函数不能释放不是动态内存开辟的空间,比如说我们下面的代码:
#include<stdio.h> #include<stdlib.h> int main() { int a = 10; int* p = &a; free(p); return 0; }
那么我们这里就在栈区上面创建了一个变量,然后我们取出他的地址,赋值给了我们的指针变量p,然后我们在使用我们的free函数,释放我们这里的空间,那么这里我们在运行之后就可以看到我们这里报出了错误:
那么我们这里创建的变量是在内存中的栈区开辟的空间,并不是在动态内存中开辟的空间,所以你是不能对其进行释放的,那么我们这里如果要进行释放的话就会报出错误。
第二点:
我们这里的free函数只能整体整体的进行释放,不能一部分一部分的来释放,比如说我们一开始申请的是40个字节,那么你释放的时候就得将一次性将这40个字节的内容全部都释放掉,不能说这前面的20个字节还有用,后面的20个字节没有用了,所以我们就找到后20个字节的起始位置然后把他释放掉,那么这里是不可以的哈我们可以看看下面的代码:
#include<stdio.h> #include<stdlib.h> int main() { int i = 0; int* p = (int*)malloc(40); for (i = 0; i < 10; i++) { *p = i; p++; if (i == 5) { free(p); } } return 0; }
那么我们这里一下子开辟了40个字节大小的空间,然后我们就将这个这个的起始地址转换成int*类型的指针赋值给我们的p,然后我们就进入了一个循环,我们这个循环就对这个开辟的空间里面进行赋值,每次循环我们就使这个p++,然后我们这里再在里面加上一个判断的条件如果我们这里的i等于5的话我们就释放这里p所指向的一个空间,然后return 1使得这个程序结束,那么我们这里将这个程序运行起来之后就可以看到:
我们这里报错了,那么我们这里报错的原因就是因为我们这里的p在循环的过程中就已经发生了改变,那么我们这里再进行释放的话指向的就不是一开始的地址,而是我们申请的内存中的地址,那么这样释放的话就会出现问题因为这样操作的话释放的就是一部分的空间并不是整个的空间所以我们的编译器就会报错 。
第三点:
我们的free函数里面的参数要是是空指针的话,那么我们这里的free函数不会有任何的作用,就是啥也不干。
第四点:
那么根据上面的第二点我们可以发现一件事情就是我们这里将p的值改变了,那么这就导致了我们无法找到一开始开辟空间的那个地址,那么这样的话我们就无法释放这个开辟好的内存,我们就把这种情况称为内存泄漏,那么这就是一个非常严重的问题因为这个空间你不用你也不还给操作系统,那么我们想用也用不着,那么久而久之我们的内存就会越来越少直到无法运行,那么这里我们就要提醒大家的就是使用动态内存的时候一定得记得如果不用的话得将内存进行释放,不然就容易导致内存泄漏的问题。当然我们将程序结束了也可以释放内存。
calloc的用法
那么除了我们的malloc函数可以申请空间之外,我们函数库里面还有一个函数也可以申请空间就是我们这里的calloc,那么我们来看看这个函数的基本参数和返回值,和介绍:
那么我们这个函数的作用跟我们的malloc函数的作用几乎是一模一样的,但是我们这里的参数是有区别的,我们的malloc是直接填入你想申请的字节数,而我们这里是将参数分为了两个,第一个是你想开辟的空间里面有几个元素,第二个是每个元素的大小是多少,那么我们这里可以通过下面的代码来学习一下这个该如何来使用:
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<errno.h> int main() { int* p = (int*)calloc(8, sizeof(int)); int i = 0; if (p == NULL) { printf("%s", strerror(errno)); return 1; } for (i = 0; i < 8; i++) { *(p + i) = i; } for (i = 0; i < 8; i++) { printf("%d ", *(p + i)); } free(p); p = NULL; return 0; }
那么我们这里在开辟内存的时候就用了另外的一个函数calloc我们要是想开辟的空间能够容纳10个整型的话,那么我们这里就只用在第一个参数的位置填入8,表示开辟8个元素,然后第二个元素填入每个元素的大小即可,那么我们这里将其运行一下:
就可以发现确实可以开辟成功,但是我们这里calloc函数跟我们的malloc函数还是有一个区别就是我们这里的calloc函数可以自动的进行初始化,将所用的元素都初始化为0,而我们的malloc却不会,那么我们这里可以通过调试来看看
我们可以看到我们这里确实是将所有的元素都初始化为了0,而我们的malloc却是这样的:
那么这里也是一个区别。
realloc的使用方法
尽管有了我们上面的三个函数,但是我感觉这里依然有点不是那么的动态,感觉还差点什么?对要是我们能够临时的更改我们的内存的大小的话,是不是就显得更加的灵活了啊,比如说一开始我们申请了20个字节的空间,但是这20个空间要是不够用了,我能够再将他的空间扩大一点变成40个字节,那么这样的话我们这里是不是就显得更加的灵活了啊,那么我们如何实现扩容的功能呢?那么这里我们的c语言就给了这么一个函数realloc他的功能就是扩容用的,那么我们来看看这个函数的基本的介绍:
我们可以看到我们这个函数的需要两个参数,一个是指针,另外一个就是整型,那么这里的指针就是你想要扩容的地址,那么这个地址跟我们free函数的要求是一样的,得是你申请这个内存的起始地址,不能是中间的某个地址,那么如果我们扩容成功的话我们这里返回的就是新开辟内存的起始地址,如果没有开辟成功的话就返回的是空指针,那么我们这里就可以看看下面的代码:
#include<stdio.h> #include<stdlib.h> #include<errno.h> #include<string.h> int main() { int* p = (int *)malloc(40); if (p == NULL) { printf("%s", strerror(errno)); return 1; } int i = 0; for (i = 0; i < 10; i++) { p[i] = i; } for(i = 0; i < 10; i++) { printf("%d ", p[i]); } int* p1 = (int *)realloc(p, 80); if (p == NULL) { printf("%s", strerror(errno)); return 1; } p = p1; p1 = NULL; for (i = 0; i < 10; i++) { p[i + 10] = i + 11; } printf("\n"); for (i = 0; i < 20; i++) { printf("%d ", p[i]); } free(p); p = NULL; return 0; } }
那么我们这里来看看这段代码的运行结果为:
那么我们这里就可以看到我们这里却是将我们的内存进行了扩展,我们这里一下子就能够容纳下20个整型,那么这里大家仔细观察我们的代码就可以看到的就是我们这里扩容的时候是先创建一个变量来接收我们这里扩容之后的返回值,那么我们这里因为返回的值可能空指针,所以我们这里为了防止这里因为扩容失败而导致我们原来的开辟的内存的地址丢失,就先来创建一个变量来试试水,下面的if语句就是用来判断的过程判断他是否开辟成功如果开辟成功的话我们才对其进行赋值,然后再将这个变量初始化为0,那么这里我们还有一个小点就是我们这里这里的realloc可以传空指针,如果我们这里传空指针的话那么我们这里的realloc的作用就是跟我们这里的malloc的作用一模一样了,那么看到这里想必大家应该直到如何来进行扩容了,那么我们这里就再来了解一个小点就是我们这里堆区的使用规则是和我们的栈区是不相同的,我们的栈区它是从地址高的往地址低的方向进行使用,但是我们的堆区就没有这个规则他是随便的使用,那么这样的话我们这里就出现了一个问题就是我们这里要是频繁的使用我们的malloc等函数来开辟空间的话,那么我们这里的空间是不是就变得碎片化了起来,每隔几个空的内存就会有一个被占用的内存,那么如果是这样的化我们要是开辟一个大型的空间,那么所能够容纳下这个空间地方是不是就越来越少了啊,那么这样就会导致我们空间利用率较低,而且我们这里开辟的空间并不是这个函数来开辟的,而是这个函数来调用我们的操作系统的接口来申请的空间,那么如果是这样的话那么我们每调用一次这个函数,都会像我们的操作系统来进行申请的话,这样我们的效率是不是也就降低了很多啊,也就浪费了很多的时间,所以这里也是我们动态开辟内存的一个缺点,那么这里我们就可以来讲讲我们这里我们的realloc扩容空间的两种情况,第一种就是我原本申请的那个空间后面的空间充足没有被其他的东西所占用,那么这种情况我们要进行扩容的话,我们会直接将后面的空间划分给你,你就可以直接使用了,但是如果你一次性扩容的非常大的话,后面的空间中有被其他人占用的话,那么这个时候你是不能直接把别人赶出去把这一大块空间划分给你的,你只能自己找一个更大的空间来容纳下你自己,并且还将自己原本的数据拷贝过去并且释放掉你原来的空间,那么这里我们就可以通过代码来验证一下:
#include<stdio.h> #include<stdlib.h> int main() { int* p = (int*)malloc(40); p = realloc(p, 8000); return 0; }
在扩容前我们的p的内容为:
但是我们扩容之后我们的地址却变为:
那么这里我们p的内容发生了改变,那么我们将这里的扩容改小一点我们就可以发现我们这里的地址在扩容前后没有发生改变,扩容前:
扩容后:
那么这里就证明我们扩容时的两种不同的情况。
柔性数组
也许你从来没有听说过柔性数组这个概念,但是它确实时存在的,在c99中,结构体中的最后一个元素允许时位置大小的数组,那么这就叫做柔性数组的成员比如说下面的代码:
typedef struct st_type1 { int i; int a[0]; }type_a; typedef struct st_type2 { int i; int a[]; }type_a;
我们这里创建的就是两个柔性数组,但是我们这里的第一个柔性数组在有些编译器上跑不过去,所以我们一般都采用第二种柔性数组的创建方法,那么我们的柔性数组就有这么几个特征:
- 结构中的柔性数组成员前面必须至少有一个其他的成员。
- sizeof返回的这种结构大小不包括柔性数组的内存
- 包含柔性数组成员的结构得用malloc函数来进行内存的动态分配,并且分配的内存应该大于结构的大小。
比如说下面的代码
#include<stdio.h> typedef struct st_type { int i; int a[0]; }type_a; int main() { printf("%d", sizeof(type_a)); return 0; }
我们这里像打印这个结构体的大小,但是我们将这个代码运行的时候就会发现我们这里打印的结果是:
是单独的一个4就是我们这里结构体中整型的大小,那么这是因为我们这里的第二点:sizeof返回的这种结构大小不包括柔性数组的内存,那么我们的柔性数组又该如何来使用呢?那么我们这里就说啊要想使用柔性数组就得用malloc来对其进行,并且我们对其分配的大小应该大于我们的sizeof求得的大小,那么这里多出来的空间就是给我们这里的柔性数组的,比如说下面的代码:
#include<stdio.h> #include<stdlib.h> struct s { int i; int a[0]; }; int main() { struct s* ps = (struct s *)malloc(sizeof(struct s) + 40); ps->i = 100; int i = 0; for (i = 0; i < 10; i++) { ps->a[i] = i; } for (i = 0; i < 10; i++) { printf("%d ", ps->a[i]); } return 0; }
那么这里就是我们柔性数组的使用,我们先用该结构创建出来一个指针,然后再用malloc来开辟一个空间,那么这里开辟的空间的大小就是
sizeof(struct s) + 40,那么这里多出来的40就是给我们这里的数组的大小,然后我们的数组的每个元素的类型就是int类型,那么我们这里就相当于这个数组有10个元素,那么我们这里就可以对这个数组了来进行一个一个的赋值,并且打印那么我们这里的代码如下:
但是我们这里叫的是柔性数组,说明我们这里是可以来进行修改的,所以当我们感觉这个数组的大小要是不够用的话我们这里就可以使用realloc来对其进行扩容,那么我们这里的代码就如下:
#include<stdio.h> #include<stdlib.h> struct s { int i; int a[0]; }; int main() { struct s* ps = (struct s *)malloc(sizeof(struct s) + 40); if (ps == NULL) { return 1; } ps->i = 100; int i = 0; for (i = 0; i < 10; i++) { ps->a[i] = i; } for (i = 0; i < 10; i++) { printf("%d ", ps->a[i]); } struct s *ps1 =(struct s*)realloc(ps, sizeof(struct s) + 80); if (ps1 != NULL) { ps = ps1; ps1 = NULL; } free(ps); ps = NULL; return 0; }
那么我们这里的代码就是将这个这里的内容进行扩容,然后在对其进行释放。但是看到这里有些小伙伴们就有了这么一个疑问说为什么非要用柔性数组呢?如果说为了实现大小变大的话,我们完全可以这么做啊,就是创建一个结构体里面装一个整型和一个指针,这个整型我们就跟上面的一样用来存储数据,而我们的指针就是指向一个数组的首元素的地址,然后为了统一我们这里就可以使用malloc来讲这个结构体创建在堆区里面,然后我们再用里面在堆区里面创建一块地址,讲这个地址中的首元素的地址赋值给我们的这里结构体中的那个指针不也可以实现上面柔性数组的功能吗?对吧也可以增大扩容啊,那么我们讲上面的思路转换成代码就是这样:
#include<stdio.h> struct s { int a; int* arr; }; int main() { struct s* ps = (struct s*)malloc(sizeof(struct s)); if (ps == NULL) { return 1; } ps->a = 100; ps->arr = (int*)malloc; if (ps->arr = NULL) { //.... 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]); } //扩容 int* ptr = (int*)realloc(ps->arr, 80); if (ptr == NULL) { return 1; } //使用 // //释放 free(ps->arr); free(ps); ps = NULL; return 0; }
那么我们的柔性数组相对于这个方式就有两大优点:
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:
这样有利于访问速度.连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)
加载全部内容