为什么存在动态内存管理?
在之前我们讲到的类型创建的变量再空间开辟好之后就不能再改变了,但是有时候我们希望能够自由地为某个变量分配空间,就比如上一篇文章中的通讯录,在没有动态内存管理情况下,我们就只能为通讯录开辟一点的空间,开辟之后就不能再更改了,如果一开始开辟的空间过大,可能会造成很大的浪费,如果一开始开辟的空间很小,那就可能不够用,当我们能够做到有几个联系人就开辟多大的空间时,空间利用率就会大大提升。这就是我们要讲的动态内存管理。
动态内存管理最重要的东西就是下面这四个函数:
malloc、calloc,realloc,free
malloc函数
malloc函数用于开辟内存快。他有一个参数,size,表示要开辟size个字节大小的空间,函数的返回值是这块空间的起始地址。
为什么返回的指针类型是void*呢? 因为malloc函数在设计时是不知道我们要开辟的这块空间的用途的,所以返回了一个void*的指针,然后我们要自己强制转换再使用。注意,malloc函数并不是每次申请空间都能成功,当申请空间失败时,函数会返回NULL,所以我们一定要对malloc函数的返回值进行有效性的判断,如果是空指针则说明开辟空间失败,这时候程序在执行下去已经没有意义了,我们可以打印错误信息在退出主程序, 对于参数size为0的情况,malloc的行为是标准未定义的,取决于编译器的实现。
int main()
{
int* p = (int* ) malloc(40);
if (p == NULL)
{
perror("malloc fail");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
上面的代码中我们用malloc开辟了一块四十个字节的空间,并将其返回值强制转换为int*类型的指针。用一个指针变量接收返回值之后,对这块空间的使用就很简单了,我们在指针章节已经把指针的用法讲的很详细了。
在上面我们相当于用malloc开辟的空间实现了一个整型数组的功能,那么malloc申请的空间和正常创建的数组的空间有什么区别呢?
动态开辟的空间都是在堆区开辟的,想malloc、calloc和realloc申请的空间都是堆区的空间。像局部变量,形式参数这些临时性的变量都是在栈区开辟的。
free函数
我们用malloc等动态内存开辟函数申请的空间一般都会在程序退出时系统自动回收,我们也可以用free主动释放。如果我们的程序是那种一直在运行不退出的,如果动态申请的空间用完之后不及时用free函数释放的话,这一块空间就一直是有归属的,其他的程序无法使用它,这就会导致内存泄漏或者空间浪费。
free是用来释放动态开辟的空间的。
我们需要传给free函数一个动态开辟的空间的起始位置,这时free就会释放这块空间。
如果传给free的地址不是动态开辟的,那么free的行为是未定义的,取决于编译器的实现。
如果传给free一个空指针,那么free什么也不会做。
free(p);
p = NULL;
像上面的代码,我们用free释放的p所指向的动态开辟的空间,但是他并不会改变p的值,也就是说p还是指向那块空间的,这时的p可以说是一个野指针了,所以为了防止我们后面对p误操作导致非法访问,我们要及时对p置空。
错误的使用方法
free(p = NULL);
像这样的代码,p先被赋值成了空指针,这时free的参数是一个空指针,free什么也不会做,但是我们之前开辟的内存空间由于p的值被修改所以已经找不到了,这也导致了内存泄漏。
calloc
calloc的作用和malloc都是开辟一块内存空间,同时返回这块空间的首地址,只是传的参数有所不同,calloc两个参数。num是要开辟的元素个数,size是每个元素的大小,虽然传的参数有每个元素的大小,但是这块空间与malloc开辟的空间是一样的,返回的是一个无类型的void* 指针,所以还是需要我们对其强制转换后再使用,我们同样要对其有效性进行检查,因为他开辟空间失败的时候也会返回空指针。
cclloc与malloc开辟的空间有一个不同点,就是malloc开辟完空间之后就返回首地址了,而calloc再返回之前会对这块空间初始化
所以在使用这两个函数时,如果你想对这块空间初始化,就用calloc函数来开辟,如果你不想要初始化,就用malloc函数开辟空间。
int main()
{
int* p1 = (int*)malloc(40);
if (NULL == p1)
{
perror("mallocc fail");
return 1;
}
int* p2 = (int*)calloc(10, 4);
if (NULL == p2)
{
perror("calloc fail");
}
printf("%d\n", *p1);
printf("%d\n", *p2);
return 0;
}
realloc
前面的malloc和calloc函数虽然能够开辟出我们需要的空间大小,但是还没有达到调整大小的效果,而这里的realloc函数就是用来实现调整动态申请的空间的大小的,realloc函数的出现让动态内存管理更加灵活。
从这里我们可以看到realloc函数的参数有两个,一个是要调整的空间的而起始位置,另一个是调整后的空间大小。要注意的是,传过去的空间的起始地址必须是一个动态开辟的内存的指针,必须是malloc、calloc或者realloc函数的返回值的指针。
realloc函数有一个特点,就是当源地址空间后面的内存如果够要增加的大小,他就会在原地扩容,这时候返回的就是原来的地址。而如果后面的空间不够扩容,那他就会在堆区重新找一块足够大的空间来开辟,同时会把原空间的内容拷贝过来,然后自动释放掉原来的空间,并且返回新的起始地址。这一点不难理解,因为内存中空间是成块的,系统在分配内存时不可能让每一块空间之间都没有间隙,空间之间的间隙就是内存碎片,我们很难保证动态开辟的空间后面的空间都没有进程占用,所以realloc函数是很灵活的。
当然,realloc函数也可能扩容失败,这时候会返回空指针,为了保护原来的那一块空间,我们不要用原空间的指针去接受realloc的返回值,而要用一个新的指针去接收,再判断完指针的有效性之后再赋值给原指针。
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc fail");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
int* ptr = (int*) realloc(p, 60);
if (ptr == NULL)
{
perror("realloc fail");
return 1;
}
p = ptr;
ptr = NULL;
for (i = 10; i < 15; i++)
{
*(p + i) = i;
}
for (i = 0; i < 15; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
在这一次实现时,realloc是在原地扩容的。
realloc函数实现malloc函数的功能:
我们只需要给realloc函数的第一个参数传空指针,那么这个函数就与malloc函数的功能一样了。
int main()
{
int* p = (int*)realloc(NULL, 40);
if (p == NULL)
{
perror("realloc fail");
return 1;
}
free(p);
p = NULL;
return 0;
}
讲到这里我们就能知道动态内存分配的基本使用了,主要就是要记住这三板斧:
函数返回值有效性检查,free释放空间,指针置为NULL
常见的动态内存错误:
1.对NULL指针的解引用,因为动态开辟内存可能会失败返回空指针,所以要检查指针是否为NULL
2.对动态开辟内存的越界访问,在使用开辟的动态内存时,我们一定要注意边界问题,不要越界。
3.对非动态开辟的内存使用free释放,如果这样做,程序会崩溃。
4.使用free释放一块动态开辟内存的一部分。再使用动态开辟内存时,我们要注意与原来的普通指针的使用相区别,在使用这些指针时,我们一定要保留动态内存的起始位置,传给free函数的也要是一块动态内存的起始位置,如果传的不是起始位置,程序会崩溃。
5.对同一块空间的多次释放程序也会崩溃。因为当我们第一次释放完p所指向的空间之后,这块空间就已经被操作系统回收了,不再属于我们的程序,但是p的值还没有变,还是指向那块空间,这时候p已经是个野指针了,我们不能对不属于自己的空间去进行释放。所以再写代码是,free完之后一点要及时置空。这样的话即使再次free,这时的p已经是空指针了,free什么也不会做i,但是我们还是要注意不要这样做。
6..动态开辟内存忘记释放,这就是我们前面说的内存泄漏问题了。
柔性数组
在C99标准中,结构体的最后一个元素允许是未知大小的的而数组,这就叫做柔性数组。
struct S
{
int a;
int arr[0];//元素个数为0表示还不确定数组的大小
//如果写成0在有些编译器编不过去,那就把0删了写成下面这种形式
//int arr[];
};
柔性数组的特点
1.结构体中的柔性数组成员前面必须有至少一个其他成员。
2.sizeof返回的结构体大小不包含柔性数组的内存。
3.包含柔性数组成员的结构用malloc等函数进行动态的内存分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
我们可以这样分配内存
struct S* ps=(struct S*) malloc ( sizeof (struct S) + 要给柔性数组开辟的内存);
柔性数组使用时也要数组使用是一样的,ps->arr[ i ] 或者 *(ps->arr+i)
struct S
{
int a;
int arr[0];//元素个数为0表示还不确定数组的大小
//如果写成0在有些编译器编不过去,那就把0删了写成下面这种形式
//int arr[];
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
if ( ps==NULL)
{
perror("malloc fail");
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]);
}
free(ps);
ps = NULL;
return 0;
}
柔性数组的柔性体现在哪里?
当我们发现一开始为数组预留的空间不够我们使用时,这时候我们就可以用realloc函数对这块进行扩容。
可能有人会说,柔性数组好像并不是很有必要存在,因为我们似乎可以单纯用一个指针来指向一块动态开辟的内存来实现这种功能,而指针指向的这块空间也是可以调整的。
struct S
{
int a;
int* p;
};
int main()
{
struct S * ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL)
{
perror("malloc fail");
return 1;
}
ps->p = (int*)malloc(40);
if (ps->p == NULL)
{
perror("malloc fail");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
ps->p[i] = i;
}
int* ptr=(int*)realloc(ps->p,60);
if (ptr == NULL)
{
perror("realloc fail");
return 1;
}
ps->p = ptr;
ptr = NULL;
for (i = 10; i < 15; i++)
{
ps->p[i] = i;
}
for (i = 0; i < 15; i++)
{
printf("%d ", ps->p[i]);
}
free(ps->p);
free(ps);
ps = NULL;
return 0;
}
在上面这段代码中,我们为了和柔性数组的实现对标,结构体也是用nalloc动态分配内存,从这里我们就能发现一些柔性数组的优点,或者说一些指针实现的缺点:当我们用这种指针实现数组时,给结构体要malloc,创建数组也要malloc,这种方案开辟的空间是分开的,而且当malloc使用的次数多了之后,可能导致内存之间留下碎片,空间利用率不高,而结构体柔性数组的空间和数组是连续的,不会导致内存碎片。同时,在使用这种指针的时候,我们要进行两次free,要注意free的顺序问题。
柔性数组的优势:
1.方便内存释放
2.提高访问速度(提高得也有限),有益于减少内存能碎片
在学习完动态内存开辟之后,我们就能够实现动态版本的通讯录了,下一篇文章会对之前的通讯录进行一些修改,节省内存的同时不用担心空间不够。