在发布通讯录的第二个版本,也就是动态版本之前,对于不了解动态内存的小伙伴来说,你们可以先看一看这篇博客,然后才能一步一步的了解动态版本的通讯录是如何搞定的
对于内存开辟来说,我们可以通过创建数组来开辟一片连续的内存,就像我的上一篇关于通讯录静态版本的博客里一样,但是我们发现这并不能很好的满足要求,假如你有100个联系人需要存储,但你固定的开辟1000大小的数组,那就会造成内存浪费,因此,为了能更好的利用内存,我们也应该学会动态内存的管理。
在了解动态内存函数之前,我们要了解一点,就是关于动态内存开辟与之前的数组开辟有什么区别?
这里我们要引入一个堆栈的概念。之前我们在函数内部创建的局部变量,当然也包括创建一个数组,以及函数的形参是存储在栈区,而动态内存开辟的是存储在堆区,全局变量和静态变量(static修饰的变量)是存储在静态区。下面附上一张图以便了解
malloc,free,calloc,realloc对应的头文件都是
malloc的函数原型:void* malloc (size_t size),size就是你想要开辟的内存大小的字节数,最后返回一个void* 的指针
要注意,1 如果开辟成功,则会返回一个指向开辟空间的指针
2 如果开辟失败,则会返回NULL空指针,所以开辟之后要记得检查是否为空指针。
3 因为void*并无法解引用,所以在使用malloc之后要记得将它强转为你想要的指针类型。
4 size可以等于0,不过这是未定义的行为,结果取决于编译器。
free的函数原型:void free(void* memblock),memblock是之前开辟在堆区的地址,也就是用动态内存函数开辟的地址,free之后,不返回。要注意,free这个函数并不会将里面的指针置空,它仅仅只是将指针所指向的空间释放而已
要注意:1 如果free一个不是内存开辟的地址,那么行为将是未定义的
2 如果free的指针是NULL空指针,那么将会什么事情也不做。
下面附上简单使用malloc 和free的代码图
1.先附上一个错误代码图
2.
#define _CRT_SECURE_NO_WARNINGS
#define count 5
#include
#include
struct S
{
char a;
int b;
double c;
};
int main()
{
int *arr2 = (int*)malloc(count*sizeof(int));//开辟一个数组指针
if(arr2== NULL)
{
printf("开辟失败\n");
}
else
{
free(arr2);
arr2 = NULL;
}
struct S *p1 = (struct S*)malloc(count*sizeof(struct S));//开辟一个结构体指针
if(p1==NULL)
{
printf("开辟失败\n");
}
else
{
free(p1);
p1 = NULL;
}
}
calloc的功能同malloc差不多,与之相异的就只是calloc具有初始化的功能
calloc的函数原型:void * calloc(size_t num,size_t size),第一个参数是你想要开辟的元素的个数,第一个参数是每个元素对应的字节大小
同时开辟成功后会将开辟的空间里的每个字节都设为0。
举个例子,在调试程序的时候打开内存可以看到开辟的内存全部被设置成0
realloc 是动态内存管理的灵魂之处,因为它可以真正实现动态内存的实现。
realloc的函数原型:void* realloc(void* memblock,size_t size)。第一个参数是之前开辟的地址,第二个参数是要调整之后的新大小。
对于返回值我们要注意到有以下两种情况
1.原有空间之后有足够大的空间来调整
2.原有空间之后没有足够大的空间来调整
因此,realloc的返回值就有两种情况
对于第一种情况,就直接在原空间末尾处继续追加空间,同时返回原空间起始地址,并且不改变原空间里的内容。
对于第二种情况,重新在堆区找到一片合适大小的内存,复制原空间里的内容到新空间,最后返回新空间的起始地址。
因此realloc返回值并不一定是之前开辟的地址。
下面附上简单使用realloc的代码
顾名思义,就是对一个NULL空指针进行解引用访问,而联系到上文,我们说过如果malloc,calloc如果开辟失败,那么将会返回一个空指针NULL,看了下面的代码你就明白为什们我们需要对malloc,calloc返回值进行判断了。
越界访问虽然不会报错,但是我们在写代码的过程中还是要避免越界访问
由实验我们可以看到,释放一个非动态内存开辟的地址是会直接报错的
这个我们就不做实验了,但是要知道,如果开辟内存忘记释放,会造成内存泄漏,在一些小型程序中还不太明显,在一些大型程序中就会遗留很多的内存碎片,影响程序的运行和效率。
什么是柔性数组呢
啥是柔性数组呢?也许是你第一次听到这个概念,但是这确实是存在的。在C99规定的中,结构体中的最后一个成员变量允许是一个未知大小的数组,这也就是它叫柔性数组的来源。
柔性数组的特点:
1 柔性数组前面至少要保证有一个成员变量
2 sizeof这个操作符返回的结构体的大小不包括柔性数组的大小
3 柔性数组使用上文提到的malloc函数来赋大小。
下面附上关于sizeof操作符计算带有柔性数组结构体的大小的代码图
结合上图,我们知道了sizeof返回的是不包括柔性数组的大小的,所以在对柔性数组赋值的时候我们就可以像下面这样写
当然,在一个结构体里面包含一个数组啥的,我们之前的知识也能完成,但是柔性数组有什么优势呢?下面我们做个实验
#define count 5
#include
#include
#include
struct S1
{
char a;
int b;
double c;
int arr1[];
};
struct S2
{
char a;
int b;
double c;
int *arr2;
};
int main()
{
struct S1*p1 = (struct S1*)malloc(sizeof(struct S1)+count*sizeof(int));//这里后半部分为柔性数组开辟空间
free(p1);;
p1 = NULL;
struct S2 *p2 = (struct S2*)malloc(sizeof(struct S2));
p2->arr2 = (int *)malloc(sizeof(count*sizeof(int)));
free(p2);
p2 = NULL;
free(p2->arr2);
p2->arr2 = NULL;
}
我们发现如果使用柔性数组,那么我们只开辟一次空间,释放一次空间。否则我们要开辟两次,释放两次。更具体一点,如果这个结构体是在在一个给其他人使用的函数中,用户可能知道要释放结构体的内存,但并不一定知道也要释放结构体里面的成员变量。而不释放内存的危害我们在上文已经说过了,所以,当你在结构体中需要加上一个数组成员变量时,尽量使用柔性数组。
到这,我们总结一下,对于动态内存的操作有malloc,calloc,realloc,free这样的内存开辟释放函数,也有柔性数组这样在结构体中实现动态大小的数组。
最后,感谢观看,希望这篇文章对你有所帮助!