1.为什么会存在动态内存分配?
2. 动态内存函数的介绍
-
2.1 malloc函数和free函数
-
2.2 calloc函数
-
2.3 realloc
3. 常见的动态内存错误
-
3.1 对NULL指针的解引用操作
-
3.2 对动态开辟空间的越界访问
-
3.3 对非动态开辟内存使用free释放
-
3.4 使用free释放一块动态开辟内存的一部分
-
3.5 对同一块动态内存多次释放
-
3.6 动态开辟内存忘记释放(内存泄漏)
1.为什么会存在动态内存分配?
我们现在知道的开辟内存方式是创建变量,创建数组。而这些东西是在栈区上开辟空间的,一旦创建完成,是无法更改的,是固定死的。就像数组,在创建数组时,已经指定了数组的大小,编译后无法再更改。
这时候就需要动态内存分配。
2. 动态内存函数的介绍
2.1 malloc函数和free函数
1.malloc函数
学习新函数时,就从库里面找函数的声明,解析来学习。
void* malloc (size_t size);
该函数的功能是,向堆区申请一块size个字节大小的空间。
如果申请成功,返回申请的空间的起始地址,如果申请失败,返回空指针NULL。 至于malloc函数在库里的为什么是void*,这是因为malloc函数也不知道使用者需要这块空间来存放什么,具体要什么类型的指针,使用者自己决定。
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
下面举个例子来深入了解:
在这段代码中,我们用了malloc函数来社区内40个字节的空间,(一个int是4个字节,*10是40字节)
但是这样写是不对的,前面说过,malloc是向内存中的堆区申请空间,如果申请失败,就返回NULL,如果现在不判断p的值,直接使用的话,就会出问题,所以我们应该判断p的有效性。
顺便提一下,perror是一个报错的函数,假如申请空间失败返回NULL,perror会报告相应的错误信息。来演示一下:
假如申请INT_MAX个字节的空间:
这是一个极大的数字,看看是否能够申请成功。
在这里,perror报告错误:没有足够的空间。具体的用法请学习一下。
free函数
free函数和malloc函数是成对出现的。
free函数是专门用来释放申请的空间的。
具体用法如下:
int main()
{
int* p = (int *)malloc(sizeof(int)*10);
if (p == NULL)
{
perror("malloc");
}
free(p);
p = NULL;
return 0;
}
在申请完空间之后,如果不用了,就需要释放掉,把申请的空间还给操作系统,即 有借有还,再借不难 的道理。
总结:malloc函数是向堆区申请一块空间,如果申请成功,则返回该空间的首地址,如果申请失败,则返回NULL。
free函数是将申请的空间释放掉
2.2 calloc函数
calloc函数也是向堆区中申请内存的函数。具体参数如下:
void* calloc (size_t num, size_t size);
第一个参数是申请的元素个数,第二个参数是申请的每个元素大小。
注意:calloc为元素数组分配一个内存块,并将其所有位初始化为零。
也就是说,calloc函数不仅申请空间,还将该空间初始化为0。
举个例子来证明:
int main()
{
int *p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
perror("calloc");
}
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
return 0;
}
结果如下:
结果就是:将申请的空间自动初始化为0。
calloc函数与malloc函数类似,只是malloc函数只负责申请空间,没有初始化,而calloc函数会申请空间并初始化。
不过也不是说calloc函数比malloc函数更高级,calloc函数 初始化也会花费时间,malloc函数不初始化,所以它也节省了一部分时间。
每个函数之间各有优缺点。
所以在使用函数的时候,结合实际情况来使用。
2.3 realloc函数
realloc函数,让我们动态申请的空间更加灵活。
当我们觉得动态申请的空间太大或者太小时,可以用realloc函数来调整动态申请的空间大小。
realloc函数的原型如下:
void* realloc (void* ptr, size_t size);
ptr是需要重新调整的空间的起始地址,
size 调整之后新大小
注意:是调整后的大小
假如刚开始动态申请的空间是40字节,发现不够用了,想加大10个字节的空间,那么使用realloc函数重新调整空间时,参数就是50。
realloc函数的返回值是调整后的空间的起始地址。
既然返回调整后的起始地址,那会不会出现申请失败的情况?会不会出现空间不足以申请的情况?
会的。
情况1: 重新申请空间时,假如后面的空间足够大,那么realloc函数就会返回空间的首地址。原来有的数据不会变化:
情况2: 如果重新申请空间失败,则返回NULL。
情况3:****如果在原来的空间后面没有足够大的空间来增容,realloc函数会自动在内存中的其他位置找一块满足我们要求的空间,并把原空间的数据拷贝到新空间中,然后返回新空间的起始地址。
基于情况2和情况3,我们是不是用原空间的指针来接收呢?
int main()
{
int* p = (int*)malloc(sizeof(int) * 5);
if (p == NULL)
{
perror("malloc");
return ;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*(p + i) = i;
}
不够了,增加空间
int* p = (int*)realloc(p, sizeof(int) * 10);
还是刚才的例子,上面这段代码对吗?
不对。
因为在重新realloc调整空间的时候,如果用原空间的地址来接收的话,万一申请失败呢?
如果申请失败,realloc函数会返回一个NULL,如果用p来接收的话,p原来指向的空间的数据就丢失了!就找不到了。
所以不能用原空间p来接收realloc返回的地址,我们需要用一个临时指针来接收返回的地址。
int* ptr = (int*)realloc(p, sizeof(int) * 10);
这样写才是正确的,然后判断ptr是否为空,如果不为空,再把这个ptr存的地址赋给p。
if (ptr != NULL)
{
p = ptr;
ptr = NULL;(防止ptr成为野指针)
}
以上就是三个申请空间的函数的基本情况,根据需求,选择不同的函数。
3. 常见的动态内存错误
3.1 对NULL指针的解引用操作
void test()
{
int* p = (int*)malloc(INT_MAX);
*p = 20;
free(p);
}
int main()
{
test();
return 0;
}
这段代码的错误是,没有判断p的值就对p进行解引用,如果申请的空间失败,返回NULL,对NULL进行*操作,是非法的。
改正很简单:只需判断p是否为NULL即可。
void test()
{
int* p = (int*)malloc(INT_MAX / 4);
if (p == NULL)
{
perror("malloc");
return;
}
*p = 20;
free(p);
p = NULL;
}
改正结果如上。
3.2 对动态开辟空间的越界访问
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
perror("malloc");
return;
}
for (i = 0; i < 20; i++)
{
*(p + i) = i;//当i是10的时候越界访问
}
free(p);
p = NULL;
}
int main()
{
test();
return 0;
}
上面这段代码的错误是,malloc函数只申请了10个整型大小的空间,(40)字节,但是在赋值的时候,i的范围取到了20,造成对后面的空间非法访问。
改正如下:只需把i的范围调整到i<10即可,或者最初申请的空间到20个整型大小(80字节)
void test()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
perror("malloc");
return;
}
for (i = 0; i < 10; i++)
{
*(p + i) = i;//当i是10的时候越界访问
}
free(p);
p = NULL;
}
3.3 对非动态开辟内存使用free释放
void test()
{
int a = 10;
int *p = &a;
free(p);
p = NULL;
}
int main()
{
test();
return 0;
}
上面代码的错误在于,a是一个变量,在内存中的栈区创建,不是用malloc等函数申请出来的空间,后面free§的时候,free释放的是堆区上申请的空间,明显两者有差异。释放是非法的。
运行时也产生错误,并且关掉这个程序会很卡顿。
改正方法有多种,可以把free§去掉。
3.4 使用free释放一块动态开辟内存的一部分
void test()
{
int* p = (int*)malloc(100);
if(p == NULL)
{
perror("malloc");
return;
}
p++;
free(p);
p = NULL;
}
int main()
{
test();
return 0;
}
上面代码的错误在于,申请的100字节的空间,用p来接收,但是p又++了,此时p不再指向申请的空间的起始地址,释放的时候,只释放一部分,剩下的空间没有释放完,造成内存泄漏。
运行时一样会报错。
所以申请的空间的起始地址不能丢失。
3.5 对同一块动态内存多次释放
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
int main()
{
test();
return 0;
}
重复释放也会出现错误。
运行时依然报错。
3.6 动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
在test函数内部申请的空间,除了test函数后,没有返回值,意味着申请的这块空间丢失了,没有人记得这块空间的存在,造成了内存泄露!
关于动态内存,你学会了吗?学会了不妨关注我吧!