看到标题的小伙伴会问动态内存是可以变化吗?它有什么用呢?为什么存在动态内存呢?一次性开辟一个空间不可以吗?
目录
前言
在我们日常写代码的时候我们是否思考过,我们向内存申请的空间存的数组,数字,字符串存在了那里,当我们空间不够了又应该怎么办呢?空间大了又不是又浪费了吗?
一、为什么需要动态内存呢?
在我们没有学习本章内容的时候我们开辟一块空间是如下操作
int main()
{
int arr[10]={1,2,3,4,5,6,7,8,9,0};
int ch[]="abcdefg";
return 0;
}
但是上述的开辟空间的方式有两个特点: 1. 空间开辟大小是固定的。 2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。 但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道, 那数组的编译时开辟空间的方式就不能满足了。 这时候就只能试试动态存开辟了。
二、动态内存函数的介绍
1.动态内存相关的函数介绍
在学习之前,我们要先知道我们动态内存相关函数有一下几个
1.malloc分配内存块,向内存申请空间,它所需要的参数是size_t ,通俗来说也就是多少个字节。函数类型及参数如下:
void* malloc (size_t size);
思考
1.返回类型为一个指针当我们内存申请失败的时候,那么返回什么呢?
2.函数返回一个指针,那么我们是否需要创建一个指针用来接受它的返回值呢?
3.如果申请成功返回的不是空指针又是什么呢?
2.free解除分配内存块,当我们申请完空间,空间应用完需要解除和销毁当前空间,否则我们重复开辟空间,空间就回不够用,空间满了电脑就可能无法正常运行了
void free (void* ptr);
思考
1.它的作用就是销毁一块空间,他又是如何知道要销毁那一块空间呢?
3.calloc分配和零初始化数组,它和malloc其实是相似的,只不过传递的参数不同;
void* calloc (size_t num, size_t size);
思考
1.num我们知道是元素个数,size是类型大小,那么他们两的关系是什么呢?
2.calloc是两个参数,malloc是一个参数,为何说他们两是相似的呢?
4.realloc重新分配内存块,看了名字我们知道是重新分配内存,分配一块新的空间;
void* realloc (void* ptr, size_t size);
思考
1.参数的指针是干什么的,有什么用呢?
2.size是什么?是和malloc一样吗?
2、malloc和free
我们知道了malloc返回的是有一个指针,当我们开辟空间失败的时候,指针也就没有东西指了,那么只能返回一个空指针;
当申请成功的时候,我们就直接返回开辟空间的起始位置的地址了,我们要想运用这快空间,我们就需要创建一个指针,让指针接受开辟空间的位置,然后在向开辟空间储存内容的时候我们只需要调用指针就可以了。代码介绍
#include<stdio.h>
#include<stdlib>//我们动态空间是需要这个头文件的
int main()
{
int* p=(int*)malloc(40);//申请了40个字节,将其开辟空间的地址返回;
//malloc可能内存申请失败,而且返回地址为void*
//我们想要向开辟空间存放数字是,我们用int*类型的指针在我们指针运算时会非常的方便
memset(p, 0, 40);//初始化,在我们学习字符串和内存函数时我们已经学习过了,如果还是不太理解可以查看本博主相关博客
if (p == NULL)
{
printf("申请失败\n");//此写法也可以换成perror("malloc");这个写法和memset这个内存函数中都有介绍和说明
}
for (int i = 0; i < 10; i++)
{
printf("%p\n" ,*(p + i));
}//malloc申请到空间后,只会返回地址,并且+i打印为地址是同一个地址,不会初始化,若不初始化则打印的为随机值
//可以运用memset(p,0,40)进行修改
//malloc不会在程序退出后不会主动释放空间;
//用free释放
free(p);
void free (void* ptr);
p = NULL;//空间被释放,需要让p指向空指针
//free只能释放malloc;
//err示范
//int a = 0;
//int* ter = &a;
//free(ter);//不可取,错误代码
return 0;
}
在上述代码中都有相关说明,并且还有”字符串+内存函数“这篇博客相关知识,如果不太了解可以查看本人博客;
2.calloc
其实和malloc是一样的,只过接受参数有所不同,malloc接受的参数是字节,需要开辟的空间,通常还需自行计算,而calloc就不一样,它是接受所需要放入数据个数,并且还需要一个所需放入数据的类型,如果将两者相乘不就是malloc所需要字节这个参数了嘛!!!
typedef struct S {
int a;//4个字节
char c;//1个字节
int b;//4个字节
//在我们学习结构体时有所讲到这个自定义结构体所占的空间,大小为12个字节
};
int main()
{
struct S s = { 0 };
struct S* p = (struct S*)calloc(10, sizeof(struct S));//INT_MAX为整型最大空间
if (p == NULL)
{
printf("申请失败\n");
}
for ( s.a =0 ; s.a < 10; s.a++)
{
printf("%p\n ", *(p+s.a));
}
return 0;
}
3.realloc
void* realloc(void* ptr,size_t size),
ptr 是要调整的内存地址
size 调整之后新大小
返回值为调整之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。
realloc在调整内存空间的是存在两种情况:
情况1:原有空间之后有足够大的空间
正常调整
情况2:原有空间之后没有足够大的空间
创建以个新的空间,将旧的空间中的数据拷贝到新的空间中,将旧的空间释放掉,返回新的空间
int main()
{
int* ptr = (int*)malloc(100);
if (ptr != NULL)
{
printf("malloc开辟失败\n");
}
else
{
exit(EXIT_FAILURE);
}
//扩展容量
//代码1
ptr = (int*)realloc(ptr, 1000);//这样可以吗?(如果申请失败会如何?)
//代码2
int* p = NULL;
p = (int*)realloc(ptr, 1000);
if (p != NULL)
{
ptr = p;
}
free(ptr);
return 0;
}
我们先用malloc开辟一块空间,当空间大小不够的时候,再用realloc进行扩容,扩容后返回指针也就是扩容后的空间所在的地址;
二、常见的动态内存错误
1.对NULL指针的解引用操作
代码如下(示例):
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
当我们开辟空间时,如果申请空间失败,返回的地址是空指针,我们p=NULL;*p就会用问题
2.对动态开辟空间的越界访问
代码如下(示例):
void test()
{
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
exit(EXIT_FAILURE);
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
}
当i=10 的时候,p+i的解引用就会越界,在一个非开辟的任何地方可能放一个10进去。(因为我们开辟空间的地址不是每次都是固定的,所以p+i也就不是固定的值)
3.对非动态开辟内存使用free释放
代码如下(示例):
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
当我们将a的地址释放时,这里将不是自己的空间个释放了,我们开辟的空间在堆上,而这里的a在寄存器中;
4.使用free释放一块动态开辟内存的一部分
代码如下(示例):
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
这里空间释放不完全就可能导致内存泄漏;p不再指向动态内存的起始位置,只释放了一部分。
5.对同一块动态内存多次释放
代码如下(示例):
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
当我们释放第一次时,系统就将这一块空间给回收了,再次释放这里就可能放入了其他数据进来,再次释放,就可能导致程序崩溃; 养成习惯,释放完就将指针制空,再次释放不会出错。
6.动态开辟内存忘记释放(内存泄漏)
代码如下(示例):
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
在函数中申请了,但是这个地址找不到,无法再次寻找到这个空间,这个空间就会浪费,只有程序结束才能将内存释放。
动态内存申请的空间,就一定要将其释放。
内存只有在free或者退出程序才会将内存释放,否则就会内存泄露。
三、柔性数组
柔性数组就是数组可以随意大或者小,当想要放入的数组太多时,我们可以让空间变大;然而我们曾经学习的数组空间大小是固定的,无法改变大小,也称之为静态数组。
1.柔性数组的特点:
结构中的柔性数组成员前面必须至少一个其他成员。
sizeof 返回的这种结构大小不包括柔性数组的内存。
包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大 小,以适应柔性数组的预期大小。
代码如下(示例):
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a));//输出的是4//结构的大小是未知的,至少有一个成员,,在计算大小的时候,柔性数组不参与计算;柔性数组成员
2.柔性数组的使用
代码如下(示例):
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
//有些编译器会报错无法编译可以改成:
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
typedef struct st_type
{
int i;
int a[0];//结构的大小是未知的,至少有一个成员,,在计算大小的时候,柔性数组不参与计算;柔性数组成员
}type_a;
int main()
{
struct st_type* ps= (struct st_type*)malloc(sizeof(st_type) + 40);//在开辟空间的时候得另加上柔性数组成员大小
if (ps == NULL)
{
perror("malloc");
return 1;
}
free(ps);
ps = NULL;
//当空间不够得时候用realloc增加大小,数组大小就发生变化了;
return 0;
}
3.柔性数组的优势
第一个好处是:
方便内存释放 如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给 用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你 不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好 了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:
这样有利于访问速度. 连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正 你跑不了要用做偏移量的加法来寻址
总结
动态内存管理核心就是熟练掌握malloc、free、calloc、realloc 这四个函数,当我们熟练掌握了这几个函数,知道其实用方法,我们也就为以后链表和联合体大小坚实的基础了。