目录
malloc
注意点1
注意点2
注意点3
calloc
realloc
realloc分配空间的规则
使用内存函数时的常见错误
对空指针的解引用
对动态内存的越界访问
对非动态内存的释放
释放开辟的动态内存的一部分
返回栈空间地址的问题
样例1
样例2
样例3
区分返回栈空间的值
malloc、calloc、realloc都是动态内存函数,它们都是在堆区上面开辟内存,其头文件都是
malloc函数的声明: void* malloc(size_t size);
从中可以知道的信息是:
1、malloc函数的参数只有一个,代表了要开辟的内存大小。(单位是字节)
2、malloc函数返回一个指针 ,指向已分配大小的内存。(如果请求失败,则返回 NULL。)
比如下方代码,malloc开辟了20个字节的空间,然后对这20个字节的空间进行操作。
在malloc函数前面加一个强制类型转换的原因是:malloc开辟的空间并没有指定用来存储什么类型的数据,其返回的指针是void*类型的,必须要强制类型转换成某个确定类型的指针。在这里,接收malloc函数返回值的是p,p是int*类型的指针,所以把malloc函数返回的指针转化成int*类型。calloc和realloc也类似,下面不多赘述。
在结尾处有free(p); 的操作,free也是内存函数,只不过它的作用是把动态开辟的内存释放,在这段代码就是把p指向的空间(20个字节)还给操作系统,这样就成为了随机值。与此同时,这块空间还给了操作系统,那么我们就失去了对这块空间的使用权限,再访问这块空间就是非法访问,但是这时候p指针并没有什么改变,所以p指针依然可以指向这块空间,为了避免非法访问,所以把p置为NULL。下面每一段代码都是如此,也不多赘述。
在开辟内存之后,第一件事是判断malloc返回的指针是否为空,因为可能存在开辟失败的情况。
#include
#include
int main()
{
int* p = (int*)malloc(20);
if (p==NULL)
{
printf("malloc fail!");
exit(-1);
}
for (int i = 0;i < 5;i++)
p[i] = i;
for (int i = 0;i < 5;i++)
printf("%d ", p[i]);
free(p);
p=NULL;
return 0;
}
内存开辟失败的情况:如下,要开辟INT_MAX大小的空间,INT_MAX指的是int类型数据能存储的最大值,即2^32-1,这个空间太大了,堆区没有那么大的空间,所以开辟失败。但是这里开辟失败如果没有对应的提示,则比较难找到错误。相反,有了提示则一目了然:
当然,calloc和realloc同样会存在这种问题,所以下面不再过多赘述。
#include
#include
#include
int main()
{
int* p = (int*)malloc(INT_MAX);
if (p == NULL)
{
printf("malloc fail!");
exit(-1);
}
for (int i = 0;i < 5;i++)
p[i] = i;
for (int i = 0;i < 5;i++)
printf("%d ", p[i]);
free(p);
p=NULL;
return 0;
}
calloc函数的声明: void* calloc(size_t nitems,size_t size);
从声明也可以看出一些信息:
1、calloc函数有两个参数,nitems表示要开辟的元素个数,size表示每个元素的大小(单位是字节)。2、malloc函数返回一个指针 ,指向已分配大小的内存。(如果请求失败,则返回 NULL。)
calloc和malloc的区别就是:calloc开辟空间的同时,会设置分配的内存为0,而malloc不会,malloc会设置分配的内存为随机值。
比如下方代码,calloc分配了5个元素的空间,每个元素占据四个字节的内存,所以一共是20个字节的内存,然后直接打印,全是0。
#include
#include
int main()
{
int* p = (int*)calloc(5,4);
if (p==NULL)
{
printf("calloc fail!");
exit(-1);
}
for (int i = 0;i < 5;i++)
printf("%d ", p[i]);
free(p);
p=NULL;
return 0;
}
realloc函数的声明:void* realloc(void* ptr , int size);
其中的信息有:
1、ptr -- 指针指向一个要重新分配内存的内存块,该内存块之前是通过调用 malloc、calloc 或 realloc 进行分配内存的。如果为空指针,则会分配一个新的内存块,且函数返回一个指向它的指针。
2、size -- 内存块的新的大小,以字节为单位。如果大小为 0,且 ptr 指向一个已存在的内存块,则 ptr 所指向的内存块会被释放,并返回一个空指针。
比如下方代码,首先malloc除了4个int类型数据的空间(16个字节),但是想要存放更多数据,所以realloc空间,此时想要存储10个int类型的数据,然后对没有填入数据的空间进行操作,最后输出结果如下。
在realloc的时候,用一个tmp指针接收的原因是:如上的注意点3,可能存在开辟失败的情况。如果realloc开辟失败,那就返回NULL。此时如果用p指针接收,那么p指针也就成了NULL,再想要访问原本malloc出来的空间,就访问不到了,那么原有数据就会丢失。所以先用一个临时的指针接收,再判断,最后赋给p。
#include
#include
int main()
{
int* p = (int*)malloc(4 * sizeof(int));
if (p==NULL)
{
exit(-1);
return;
}
for (int i = 0;i < 4;i++)
p[i] = i;
int* tmp = (int*)realloc(p, 10 * sizeof(int));
if (tmp==NULL)
{
exit(-1);
return;
}
p = tmp;
for (int i = 4;i < 10;i++)
p[i] = i;
for (int i = 0;i < 10;i++)
printf("%d ", p[i]);
free(p);
p=NULL;
return 0;
}
不过,在使用malloc函数的时候,值得注意的是,其分配空间的规则。
比如下方图片,现在已经有一块空间是malloc或者calloc出来的,但是这块空间不够,我想要再多一些空间,于是realloc。
情况1:如果这块空间后面的内存足够大,那么realloc函数就在这块空间的基础上,在它后面扩容,增加一些存储空间,原有的数据不变,如情况1的图片。
情况2:如果这块空间后面的空间大小,不够realloc之后新增的空间,那么就再堆区新找一片空间,如情况2的红色框,然后把之前的空间里的内容拷贝到新空间(如情况2里面,红色框中的黑色框),然后把原有空间free掉。
比如下方代码,由最开始malloc处的注意点可知,malloc不出INT_MAX大小的空间,所以返回NULL,故p是NULL,但此时又对p解引用,完全是错误的。程序运行会崩溃。
#include
#include
#include
//对NULL的指针的解引用操作
int main()
{
int* p = (int*)malloc(INT_MAX);
*p = 5;
free(p);
p = NULL;
return 0;
}
正确写法如下,要先判断开辟的空间是否为空,不为空才对这片空间进行操作:
#include
#include
#include
//对NULL的指针的解引用操作
int main()
{
int* p = (int*)malloc(INT_MAX);
if (p == NULL)
{
exit(-1);
return;
}
*p = 5;
//应该这样子写
free(p);
p = NULL;
return 0;
}
如下,malloc开辟了20个字节,可以存放5个int类型的数据,但是在下面却想要存放20个int类型的数据,造成了动态内存的越界访问,程序会崩溃。所以在使用动态内存的时候,一定要注意大小,不可以越界访问。
#include
#include
#include
int main()
{
int* p = (int*)malloc(20);//开辟了20个字节,可以存放5个int类型的数据
if (p == NULL)
{
exit(-1);
return;
}
for (int i = 0;i < 20;i++)
p[i] = i;
//越界访问了,程序会崩溃
free(p);
p = NULL;
return 0;
}
有的时候,看到指针就想释放,但是却会造成错误,如下代码。a是局部变量,在栈区,不在堆区,而定义一个指针变量p,指向a,后面却想释放p。p是a的指针,释放p就是释放a所在的空间,但是a的空间是在栈区,free函数只能释放堆区的空间,所以这就造成了对非动态内存的释放。下面的图片是报错信息。
要记住,我们要释放的是堆区的空间,而不是释放指针。指针只是指向这某块空间,通过指向堆区的指针,我们才能够释放堆区的内存,并不是所有指针都可以释放。
#include
#include
#include
int main()
{
int a = 20;
int* p = &a;
if (p == NULL)
{
exit(-1);
return;
}
free(p);//对非动态内存的释放
p = NULL;
return 0;
}
如下代码,开辟了40个字节的空间,p最开始指向这块空间的起始位置。p是int*类型的指针,所以每次++,都要跳过4个字节的空间,在赋值的时候,p++一共指行了5次,所以赋值完毕后,p指向这块空间中的第21个字节的空间。此时free(p),也会造成报错,如下图。
解决方法一般分为两种。第一种是在移动p指针之前,定义一个同类型的指针,把开辟的空间起始位置存起来,最后释放直接释放该指针指向的空间即可。第二种是,p--,++了多少次就--多少次,这样就回到了最开始的位置,然后释放。 一般情况下建议第一种方法。
#include
#include
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
exit(-1);
return;
}
for (int i = 0;i < 5;i++)
{
*p = i;
p++;
}
free(p);//这时候是错误的,因为p以经不指向最初开辟的起始位置了
p = NULL;
}
栈空间地址,顾名思义,是某个函数返回栈区的某一块空间的地址。
如下的代码,str指针接收的是GetMemory函数的返回内容,再看GetMemory函数内部,定义了一个字符数组,然后返回该数组的起始位置。乍一看这个代码貌似没有什么问题。
但是,仔细思考就会发现大问题!!!栈区,栈区,就如同客栈一样,当一个函数的生命周期结束,其内部在栈区开辟的空间就会被还给操作系统。对应本段代码,就是GetMemory函数内部,其 char p[] = "hello world"; 这是在栈区开辟空间存储的,所以在这个函数调用完之后,这块空间就会还给操作系统了,我们就没有使用权限,那么我们也不知道这块空间被操作系统更改没有。但是依然返回了p,p依然是指向这块空间,所以str也指向了这块空间,那么打印出来的结果未知,如下图,可以看出,在本段代码,这块空间被操作系统更改了,所以打印一段乱码。
#include
#include
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
但是有人通过别的例子发现,返回栈空间地址依然可以得到想要的结果,如下代码和运行结果,运行后依然打印出来10。根据上面标红的文字所说,被还给操作系统的这块空间,我们并不知道操作系统有没有更改它,在这里仅仅是操作系统恰好没有修改这块空间。但是并不是每一次操作系统都不会改这块空间,比如上面的情况。所以保险起见,还是不能返回栈空间地址。
#include
#include
int* test()
{
int a = 10;
return &a;//这里恰好没被改
}
int main()
{
int *ret = test();
printf("%d ", *ret);
return 0;
}
在打印返回的栈空间地址内容之前,创建新的函数战争,覆盖先前的地址,就会导致函数返回的栈空间地址被修改,打印出来的值不同。由函数栈帧的创建和销毁知识,test函数的栈帧销毁了,然后printf的栈帧又创建,所以a的地址的内容被改了,打印出来的内容不是10。如下代码和运行结果。通过流程图可以简易理解。:
#include
#include
int* test()
{
int a = 10;
return &a;//这里恰好没被改
}
int main()
{
int* ret = test();
printf("hehe\n");
printf("%d ", *ret);
return 0;
}
如下代码,test函数不是返回栈空间的地址,返回的是一个值,这样子是可以的,就是正常的写法,不要和返回栈空间地址搞混了。在这里不免会提出疑问,a变量所在空间不是已经还给操作系统了吗,为什么可以这样呢?这是由于,这种情况下,a的值先被存在寄存器中,然后由ret变量接收,并不是把a所在空间的内容返回给ret,所以可行。
#include
#include
int test()
{
int a = 10;
return a;
}
int main()
{
int ret = test();
printf("%d ", ret);
return 0;
}
关于动态内存管理的内容就到这里了,如果有错误欢迎多多指正!!!