本章主要介绍C语言动态内存管理,主要内容为:malloc、calloc、realloc三个动态内存函数的使用。动态内存管理的核心也就是这三个函数的使用。以及柔性数组。
1、为什么存在动态内存分配?
我们已经掌握的内存开辟方式有:
int a = 20; //在栈空间上开辟四个字节。
char arr[20]; //在栈空间上开辟10个字节的连续空间。
但是上述的开辟空间的方式有两个特点:
- 开辟空间大小是固定的
- 数组在申请的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况,有时候我们需要的空间大小在程序运行的时候才能知道,这个时候就只能试试动态内存开辟了。
2、动态内存函数的介绍
2.1、malloc(申请内存空间)和free(释放/回收内存空间)
1、C语言提供了一个动态内存开辟的函数------malloc:
<stdlib.h>
void* malloc (size_t size);
这个函数向内存申请一块连续可用,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针(返回这块空间的起始地址)。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
- 如果参数size的单位字节为0,malloc的行为是标准是未定义的,取决于编译器。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main()
{
int arr[10] = { 0 };
//动态内存开辟
int* p = (int*)malloc(40);
int i = 0;
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ",* (p + i));
}
return 0;
//没有free,并不是说内存空间就不回收了,当程序退出的时候,系统会自动回收内存空间。
}
输出:
使用数组申请的内存空间和使用malloc申请的空间在不同的区域上:
2、free------释放/回收内存空间。
void free(void* ptr);
ptr为NULL,则什么事都不做。
ptr必须是动态分配的空间。
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main()
{
int arr[10] = { 0 };
//动态内存开辟
int* p = (int*)malloc(INT_MAX);
int i = 0;
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ",* (p + i));
}
//释放内存空间
free(p);
p = NULL;
return 0;
}
【注:】free释放的必须是动态内存的空间,也就是说释放的需要是在堆区中的空间,而不应该去释放栈区中的空间。
如下是错误的:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main()
{
int a = 0;
int* p = &a; //p是栈区里面的空间,不用free来释放
free(p);
p = NULL;
return 0;
}
2.2、calloc
C语言也提供了一个函数叫calloc
,calloc
函数也用来动态内存分配,原型如下:
void* calloc(size_t num,size_t size);
- num是代表要开辟多少个元素
- size代表开辟的每个元素是多少字节。
比如:想要开辟40字节的内存,num=10,size=4即可。
返回值是开辟的那块空间的起始地址。
这个函数还有一个特殊的地方:它在返回之前会把将要开辟的内存空间初始化一下,并初始化为全0。
代码验证:在使用calloc开辟好空间之后,我们来打印,看是不是全部初始化为0。
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main()
{
int* p = (int*)calloc(10, sizeof(int));
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
int i = 0;
for (i=0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
输出:
malloc和calloc如何选择呢?
如果想要初始化使用calloc,如果不初始化,两个都可以。
calloc相当于malloc+memset。
2.3、realloc
- realloc函数的出现让动态内存管理更加灵活
- 有时我们会发现去申请的空间太小,有时候我们又觉的申请的空间过大,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整,那
realloc
函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
void* realloc(void* ptr,size_t size);
- ptr是要调整的内存地址。
- size是调整后的内存大小。希望要调整为多大的空间。
- 返回值为调整之后的内存起始位置。
- 这个函数在调整原内存空间大小的基础上,还会将原内存中的数据移动到新的空间。
- realloc在调整内存空间的是存在两种情况:
- 情况1:原有空间之后有足够大的空间。
- 情况2:原有空间之后没有足够大的空间。
下面先来说下realloc的两种情况:
比如现在有个使用malloc分配的动态内存,大小为40字节。然后现在想要扩容到80字节。
已存在40个字节了,需要扩容为80字节,所以还需要在原有的内存上在使用realloc追加40个字节。
那主要问题就在于这新追加的40字节的内存位置在那。
1、原有空间之后有足够大的空间:
这种情况是直接追加在原有40字节的后面:这个实现很简单就是直接追加就行了。
2、原有空间之后没有足够大的空间:
这个就是如果在原有的40字节的后面直接在追加40个字节的内存后,由于原有空间之后没有足够大的空间,强行追加40个字节,会占用其它数据的内存地址。所以这样肯定是不行的。那如何解决呢?
答案:realloc会找到一个80字节大小的内存空间,然后先把原有的(使用malloc)动态分配的40字节移动到这个80个字节的前40个字节处,然后还剩40个字节,这个算是扩容后的内存地址。
并且,旧的原40个字节内存,会被realloc自动释放回收。
代码示例:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
int i = 0;
for (i=0; i < 10; i++)
{
*(p + i) = i+1;
}
//将p处的内存地址,扩容到80字节
int* ptr = realloc(p, 80);
if (ptr != NULL)
p = ptr;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
return 0;
}
输出:
2.4、realloc充当malloc
realloc(NULL,40); ========== malloc(40);
3、常见的动态内存错误
3.1、对NULL指针的解引用操作
//不进行NULL的判断,这样是存在安全隐患的。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(40);
*p = 20;
return 0;
}
//对指针进行NULL的判断
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
*p = 20;
free(p);
p = NULL;
return 0;
}
3.2、对动态开辟空间的越界访问
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
int i = 0;
//当i=10时,就越界访问了。
for (i = 0; i <= 10; i++)
{
p[i] = i;
}
free(p);
p = NULL;
return 0;
}
3.3、对非动态开辟内存使用free释放
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a = 10;
//p是非动态开辟内存,是不能用free释放的。
int* p = &a;
free(p);
p = NULL;
return 0;
}
3.4、使用free释放一块动态开辟内存的一部分
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*p = i;
p++; //p在++之后,p已经不在是起始位置了,所以下面free释放只是释放了一部分,所以不对。
}
free(p);
p = NULL;
return 0;
}
3.5、对同一块动态内存多次释放
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 1;
}
//多次释放,会报错
free(p);
free(p);
return 0;
}
//改进:要么free一次,要么添加p = NULL;
3.6、动态开辟内存忘记释放(内存泄漏)
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 1;
}
return 0;
}
忘记释放不再使用的动态开辟空间会造成内存泄漏。
切记:动态开辟的空间一定要释放,并且正确释放。
4、几个经典的笔试题
4.1、题目1:野指针---返回栈区空间地址问题
#include <stdio.h>
#include <string>
void GetMemory(char* p)
{
//p是形参,在栈区里面存放
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
问:运行结果?
传值调用,str是实参,p是形参,所以说GetMemory运行后,对str没啥影响,str还是空指针。并且p没有内存释放,导致内存泄漏。
所以说运行结果:
- 内存泄漏
- str是NULL,在strcpy时,需要传目标内存地址,而不是NULL,所以会导致内存崩溃。
正确修改:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void GetMemory(char** p)
{
*p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str);
strcpy(str, "hello world");
printf(str);
//这个打印相当于:因为即便printf("hello world");,那传给print函数的也是字符'h'的地址,起始是和直接传str地址是一样的道理。
printf("hello world");
free(str);
str = NULL;
}
int main()
{
Test();
return 0;
}
输出:
4.2、题目2:野指针---返回栈区空间地址问题
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
输出:
分析:数组p在GetMemory里面,且p存放的是字符'h'的地址,但是当函数GetMemory运行完毕,p数组就会销毁。然后str = GetMemory(),当GetMemory返回值为p,但是p已销毁。所以str是野指针。str指向的那块地址已经被销毁了,所以结果如上。
总结:以上两题都是返回栈区空间地址问题。让一个函数返回函数体里面的变量的地址时,用个变量接收,这个是非常危险的。
4.3、题目3:
int* f1(void)
{
int x = 10;
return (&x);
}
//判断下列代码的问题:野指针问题。
//x在函数f1内部,return &x,说明此函数返回个指针,但是这个函数在运行完毕后,x变量会销毁,所以&x就是野指针。
int* f2(void)
{
int* ptr;
*ptr = 10;
return ptr;
}
//也是野指针问题。
//ptr没有初始化,然后*ptr相当于随便找了地址来解引用,相当于随机访问,野指针问题。
4.4、野指针
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void Test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
Test();
return 0;
}
分析错误:使用malloc动态内存分配100字节大小的空间。然后拷贝,str里面存放了首字符'h'的地址。当free(str)后,动态分配的100字节大小的空间就交给操作系统回收了。但是因为没有进行str = NULL
这一步操作,所以str的值,也就是存放的首字符'h'的地址并没有变。然后str != NULL
为真,然后在进行拷贝,然后现在str已经时野指针了。虽然将"world"传给str,但是str指向的地址,已经不归我们使用了,所以在访问有可能时访问其它的数据的空间,所以此程序不对。
5、C/C++程序的内存开辟
内核空间是用来运行操作系统的。我们写的代码不可以运行在此处。
数据段又是静态区。
代码段:存放我们写的代码进行编译、链接后为可执行程序的二进制指令。
6、柔性数组
在C99中,结构体中的最后一个元素允许是未知大小的数组,这就叫做【柔性数组】成员。
1、必须在结构体中。
2、必须是最后一个成员。
3、必须是大小未知的数组。
eg:
typdef struct st_type
{
int i;
int a[0]; //柔性数组成员
int b[]; //这个写法也行
}type_a;
6.1、柔性数组的特点
- 结构中的柔性数组成员前面必须至少有一个其它成员。
- sizeof返回的这种结构体大小不包括柔性数组的内存。
- 包含柔性数组成员的结构用malloc()函数进行动态内存分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
例如:
typdef struct st_type
{
int i;
int a[0]; //柔性数组成员
}type_a;
prinf("%d\n,sizeof(type_a)"); //输出的是4。
6.2、柔性数组的使用
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
struct S
{
int i;
int arr[]; //打算给此柔性数组10个元素的大小。
};
int main()
{
//包含柔性数组成员的结构用malloc()函数进行动态内存分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
//sizeof(struct S)是结构体大小,40就是柔性数组的大小。
struct S* ps = (struct S*)malloc(sizeof(struct S) + 40);
if (ps == NULL)
{
printf("%s\n", strerror(errno));
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]);
}
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 80);
if (ptr != NULL)
{
ps = ptr;
}
free(ps);
ps = NULL;
return 0;
}
以后采用柔性数组的方法可以对结构体数组进行动态内存分配。
除此以上使用柔性数组的方法,其实我们也有第二种方法来对结构体中的数组进行动态内存分配:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
struct S
{
int i;
int* arr;
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S));
if (ps == NULL)
{
printf("%s", strerror(errno));
return 1;
}
//这里为什么需要对结构体进行malloc呢?将结构体malloc是为将结构体中的成员变量i也放在堆区。
//因为下面我们要将arr进行malloc,为了将结构体中的每个成员一致,所以先也将结构体malloc,这样以来i就放在了堆区里面了。
ps->i = 100;
//给指向arr的地址动态分配40个字节大小的空间。
ps->arr = (int*)malloc(40);
int i = 0;
for (i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
for (i = 0; i < 10; i++)
{
printf("%d ", ps->arr[i]);
}
int* ptr = (int*)realloc(ps->arr, 80);
if (ptr != NULL)
{
ps->arr = ptr;
}
free(ps->arr);
free(ps);
//这里直接一步到位把ps置为NULL,那ps->arr自然而然的就为NULL了。
ps = NULL;
return 0;
}
那以上两种方法如何选择呢?
- 采用柔性数组的方法,只需要一次malloc,后续不够在使用realloc。
- 而第二种方法,需要两次malloc,后续不够在使用realloc
注意:使用malloc越多,就越需要free,而且还会产生内存碎片。
总结:
- 采用柔性数组的好处是:方便内存释放。
- 第二个的好处是:有利用访问速度。