上篇文章用C语言实现通讯录,在实现通讯录的过程中,对于通讯录中可以存储保存联系人信息的结构体的数量是固定的,这会导致出现需要存储的数量大于设置的数量的问题。为了结果这个问题,让变量在内存中的大小的调节更为灵活,本文将引入动态内存管理。
正如上面所说,目前对于内存空间的开辟方式一共两种:一种是创建一个变量:
int a = 0;
第二种是创建一个数组:
int arr[10] = {0};
不过对于这两种内存开辟的方式存在一定的局限性:
为了解决上面的问题,所以引入动态内存管理:
对于malloc函数的特点,由下面的一张图给出:
mallco函数的参数只有一个,这个参数代表想要开辟空间的大小,单位是字节。对于malloc函数的使用,下面给出一个例子:用malloc函数向内存申请40个字节大小的空间:
int main()
{
int* p = (int*)malloc(40);
return 0;
}
上面说到,malloc函数会存在开辟失败的情况,所以,为了后续正常使用malloc函数,最好在开辟完内存空间后,检查一下是否开辟成功:
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
return 0;
}
开辟空间后,下面对开辟的空间的地址进行打印:
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%p\n", *(p + i));
}
return 0;
}
结果如下:
malloc函数申请的空间的地址,是随机值 ,并且malloc在申请到空间后,会直接返回这块空间的初始地址,不会对空间初始化。并且malloc函数申请的空间在程序退出时会还给操作系统,当程序不退出,动态申请的内存不会主动释放。为了能够合理的释放malloc函数申请的内存空间,下面引入free函数:
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%p\n", *(p + i));
}
free(p);
p = NULL;
return 0;
}
之所以在free释放内存空间后将p赋值NULL,是为了避免指针p变成野指针。 并且,对于free释放的空间,只能是动态开辟的空间,例如下面举一个错误的例子:用free释放非动态开辟空间:
int main()
{
int i = 0;
int* p = &i;
free(p);
p = NULL;
return 0;
}
另外,如果在free中传入空指针NULL,则函数什么也不做。可以视为空指令。
对于calloc函数,他的功能和malloc函数相比则更为丰富:
calloc函数的参数有两个,第一个参数用来填写想要开辟空间的数量,第二个参数是用来填写开辟的空间的每个元素的大小,例如,用malloc函数开辟十个整型空间:
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", p[i]);
}
return 0;
}
打印结果如下:
realloc函数的参数同样也是两种:
其中,第一个参数表示被调整空间的起始地址,需要注意的时,被调整的空间是已经用malloc、realloc、calloc创建好的。第二个参数表示新的空间的大小。对于第一个参数,如果传递一个空指针,则realloc函数的作用 = malloc函数。
例如下面的例子,用realloc函数对malloc函数申请的空间进行扩大到80字节:
int main()
{
int* p = (int*)malloc(10, sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
realloc(p, 80);
return 0;
}
不过,对于realloc函数的返回值有两种不同的情况:
1. 当后面的空间足够时,realloc函数返回被调整空间的起始地址
2.当后面的空间不足够时,会开辟一个新的空间,并返回这个新的空间的地址。对于这种情况,realloc函数会分四步完成: 1. 开辟新的空间 2. 将旧的空间中的数据拷贝到新的空间中 3.释放旧的空间 4. 返回新的空间的地址
对于realloc返回值的接收并不能由指针p直接接受,因为,如果realloc函数创建空间失败,则会返回一个空值,此时如果用p进行接收,会造成不但不能正常开辟空间,还会造成原来被malloc开辟的空间因为p变成了野指针而无效化。
所以,对于realloc函数返回值的接受,应该创建另一个指针来检验返回值是否为空,不为空再赋给p:
int main()
{
int* p = (int*)malloc(10, sizeof(int));
if (p == NULL)
{
perror("malloc");
return 1;
}
int* ptr = realloc(p, 40);
if (ptr != NULL)
{
p = ptr;
}
return 0;
}
为了更好的展示realloc函数的作用,给出下面一个情景:先利用malloc开辟40字节大小的内存空间,再将这些空间赋值1到10,再用realloc函数增加80字节的空间,即:
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
p[i] = 1+i;
}
int* ptr = realloc(p, 40);
if (ptr != NULL)
{
p = ptr;
ptr = NULL;
}
for (i = 0; i < 20; i++)
{
printf("%d ", p[i]);
}
free(p);
free(ptr);
return 0;
}
打印结果为:
上面对于动态内存函数开辟空间时提到过,动态内存函数又可能出现开辟空间失败的情况,一旦开辟失败并且不对返回值进行检测,极有可能造成对NULL指针进行解引用操作的错误。
例如下面的情况:
int main()
{
int* p = malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 20; i++)
{
printf("%d ", p[i]);
}
free(p);
return 0;
}
在代码的开头利用malloc函数向内存申请了40个字节,也就是10个整形大小的空间,但是在下面对这块空间进行访问访问时,访问的大小超过了申请的大小,造成了越界访问。
对非动态开辟内存使用free函数的情况在上面进行了说明,这里不再解释。
例如下面的例子:
int main()
{
int* p = malloc(40);
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*p = i;
p++;
}
free(p);
return 0;
}
还是利用malloc函数开辟10个整型大小的动态空间,但是进行释放时,指释放了部分的空间,此时程序会运行错误。
所以,如果需要访问动态内存的内容且需要地址变化时,最好再创建一个指针,利用这个指针代替创建动态内存使用的指针,避免更改原指针,导致发生上述错误。
int main()
{
int* p =(int*)malloc(40);
if (p == NULL)
{
perror("malloc");
}
free(p);
free(p);
return 0;
}
例如上面给出的代码中,连续两次释放指针p。此时会造成程序异常:
对于上述情况,如果需要对同一个指针多次释放,则在释放一次后,将指针初始化为NULL即可。
例如下面给出的代码:
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
return 0;
}
用于存放动态空间地址的指针p在函数结束后自动销毁,但是下面while(1)却是一个死循环,此时程序不结束,动态内存空间不能及时释放。