动态内存函数
目录
前言:
? ? ? ?当我们每次使用数组时,有一弊端,就是固定大小的,不能改变其空间,于是乎我们会想,能不能有一种东西能方便我们随意得控制大小(链表可以,但这里先不做讨论)。于是就衍生出了内存函数。
? ? ? ?在了解内存函数前,我们先来看一段代码:
//C语言是可以创建变长数组 - C99中增加了
struct S
{
char name[20];
int age;
};
int main()
{
int n = 0;
scanf("%d", &n);
struct S arr[n];//能够存放50个struct S 类型的数据
//这样写vs不支持,数组中只能放常量
//在C99中可以使用,不是所有编译器都支持,C99标准用的不够普遍
//30 - 浪费
//60 - 不够
return 0;
}
? ? ? ?出于可移植性考虑的原因,有些编译器支持C99,有些不支持(但是刷题网站都支持),?变长数组我们一般不使用,所以我们无法创建完数组以后再指定其大小,因为大小已经固定了,此时就会用到动态内存函数。
动态内存函数:
? ? ? ?我们知道C语言中有很多函数,但是大家不要将内存函数和动态内存函数搞混了(内存函数可以看我这一篇文章内存函数(超详细)-CSDN博客)。我们使用内存会占用空间,内存大致分为三个区域,栈区、堆区和静态区。我们可以向空间申请动态内存,动态内存在栈区占据。
? ? ? ?因为我们平时使用函数创建的大部分变量都是放在栈区,我们也知道函数的创建会进行压栈(详细请看这篇文章函数的栈帧-CSDN博客) ,但是动态内存函数开辟的空间是放在堆区的,所以它是不是就不会像函数栈帧一样?出了函数就会自动销毁呢?
? ? ? ?答案是肯定的,堆区开辟的内存是不会随着函数一样,使用完就销毁。此时我们就要研究其性质了。
malloc函数:
? ? ? ?我们先来看malloc函数的原码定义:
? ? ? ?参数是大小,大小是指针指向空间的大小,单位是字节,返回的的却是void*,这是小伙伴们就有疑问?void*这不是空类型指针么?有毛用?这里我先埋个伏笔。它的具体作用就是开辟空间,是指针指向一片区域,并返回一个开辟内存的首地址(指针),类型为void*.
int main()
{
int* p = malloc(40);
//此时向堆区申请40个字节
return 0;
}
? ? ? ?我们知道有强制转换类型操作符,使用动态内存函数的人一定知道使用的是什么类型的指针,所以我们直接将其强制类型转换为想要的类型即可。
int main()
{
//int* p = (int*)malloc(0);//未定义行为,会报错
int* p = (int*)malloc(40);
return 0;
}
? ? ? ? 这些申请的内存不会被初始化,此时我们就来观察一下:
int main()
{
//int* p = (int*)malloc(0);//未定义行为,会报错
int* p = (int*)malloc(40);
//打印申请的每一个空间内容
for (int i = 0; i < 10; i++)
{
printf("%x ", p[i]);
}
return 0;
}
? ? ? ?奇了怪了,为什么 内容是cdcd……,此时就可以看我的这一篇文章(函数的栈帧-CSDN博客)。所以我们申请完以后空间,就需要将其初始化。
? ? ? ?上述代码中,我们不能传入大小为0的数,这样是没有意义的,所以C语言标准未定义。但是堆区是万能的吗?我们想开辟多少就开辟多少吗?
? ? ? ?因为内存大小是固定的,不可能像开辟多少空间就开辟多少空间,所以malloc函数有时开辟的空间过大就会返回空指针,比如我们向堆区开辟4000000000000个字节,此时就会失败,并将p赋为空指针。
int main()
{
//int* p = (int*)malloc(0);//未定义行为,会报错
int* p = (int*)malloc(4000000000000);
if (p == NULL)
{
printf("hahah");
}
return 0;
}
? ? ? ?此时我们就可以通过判断,来防止开辟空间失败的情况。其实这一步是很有必要的,因为开辟失败空间会赋值为空指针,不会报错,所以为了防止野指针的情况,我们必须判断。
? ? ? ?为了更好的看出具体报错内容,我们可以使用strerror报错函数(具体可以看这篇文章字符串函数(超详细)-CSDN博客),为了方便使用,我们直接使用perror函数(相当于printf+strerror函数)来观察哪里出先了错误。
int main()
{
int* p = (int*)malloc(404000000000000);
if (p == NULL)
{
//使用perror函数打印错误信息
perror("malloc");
return 1;
}
return 0;
}
? ? ? ? ?我们之前提到,函数一般使用是在栈区上是用内存的,而动态内存是在堆区上创建的,不会随着函数的使用完成就销毁,所以就有了free函数。
free函数:
? ? ? ?当我们向堆区动态内存申请空间指向完成以后,我们要释放使用堆区静态内存,就要使用free函数(只能释放动态内存),要传入指向动态内存的指针(指针指向动态内存的首个地址),之后释放这个指针指向的空间内存。
? ? ? ?但是free函数不会将该指针置为空指针,此指针指向的内存地址不变,我们要手动将此指针置为NULL(空指针)。
int main()
{
//int* p = (int*)malloc(0);//未定义行为,会报错
int* p = (int*)malloc(40);
if (p == NULL)
{
//使用perror函数打印错误信息
perror("malloc");
return 1;
}
//释放空间
free(p);
//必须手动置空
p = NULL;
return 0;
}
calloc函数:?
? ? ? ?这个函数和malloc函数几乎没有什么实质性区别,我们来看原码定义:
? ? ? ?上面的描述很清楚,和malloc函数一样,也是在堆取开辟的内存,并返回开辟内存的首地址(指针)。但是要传入两个参数,第一是分配类型的个数,第二是分配类型的大小。和malloc的的区别是它会将开辟的空间初始化为0.
int main()
{
//malloc(10*sizeof(int))
int *p=(int*)calloc(10, sizeof(int));//堆区开辟
//calloc传入创建的类型数量,之后传入每个类型的大小
//calloc会初始空间的变量,都是0
if (p == NULL)
{
printf("%s\n", strerror(errno));
}
else
{
int i = 0;
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}//打印出calloc函数初始化空间的值
}
//释放空间
//free函数是用来释放动态开辟的空间的
free(p);
p = NULL;
return 0;
}
问题来了,记得我们在开始提出的问题吗?我们使用动态内存函数就是为了合理使用空间,造成不必要的浪费,可此时申请的空间大小还固定的?而且申请了还要释放并且手动置空,还不如使用数组来的方便。哎,别着急,文章这才二分之一呢,接下来要出场的是——realloc 函数!
realloc函数:?
? ? ? ?这个函数就可以用来调节使用的空间了,但是也有弊端,我们先来看其具体定义:
? ? ? ?realloc函数可以调整动态内存的大小,当使用完malloc或calloc函数后,申请的内存都是固定的,我们可以通过realloc函数进行调整。
int main()
{
//realloc可以调整动态内存的大小
char* p1 = (char*)malloc(sizeof(char)* 2);
//赋值
int i = 0;
for (i = 0; i < 2; i++)
{
p1[i] = 'a';
}
//扩容(将原来的总大小2字节扩容为4字节
char* p2 = (char*)realloc(p1, sizeof(char) * 4);
//此时申请总大小为4字节
//判断扩容是否成功
if (p2 == NULL)
{
perror("realloc");
}
//开辟赋值
p1 = p2;
for (i = 2; i < 4; i++)
{
p1[i] = 'b';
}
//打印
for (i = 0; i < 4; i++)
{
printf("%c", p1[i]);
}
//释放
free(p1);
p1 = NULL;
return 0;
}
? ? ? ?上面的程序只是为了搞好的解释realloc的使用,因为字符串要有结束的标志‘\0’,所以要多申请一个字节放置‘\0’,所以我们给出一下改进代码:
int main()
{
//realloc可以调整动态内存的大小
char* p1 = (char*)malloc(sizeof(char)* 2);
//赋值
int i = 0;
for (i = 0; i < 2; i++)
{
p1[i] = 'a';
}
//扩容(将原来的总大小2字节扩容为4字节
//char* p2 = (char*)realloc(p1, sizeof(char) * 4);
char* p2 = (char*)realloc(p1, sizeof(char) * 5);
//多申请一个字节
//此时申请总大小为5字节
//判断扩容是否成功
if (p2 == NULL)
{
perror("realloc");
}
//开辟赋值
p1 = p2;
for (i = 2; i < 4; i++)
{
p1[i] = 'b';
}
//将最后一个字节赋值为'\0'
p1[i] = '\0';
printf("%s\n", p1);//打印
//释放
free(p1);
p1 = NULL;
return 0;
}
? ? ? ?通过以上内容,我们不难发现,realloc申请的空间是总大小,而不是添加的大小。
? ? ? ?realloc因为也会返回NULL(就是申请的空间过大),所以我们也要进行判断。所以是不是realloc也可以当做malloc和calloc函数一样使用呢?答案是肯定的,我们来看以下代码:
int main()
{
int* p = (int*)realloc(NULL, sizeof(int) * 5);
//直接传入NULL充当malloc的使用
if (p == NULL)
{
perror("realloc");
}
//赋值
int i = 0;
for (i = 0; i < 5; i++)
{
p[i] = i;
}
for (i = 0; i < 5; i++)
{
printf("%d ", p[i]);//打印
}
//释放
free(p);
p = NULL;//置空
return 0;
}
那么,realloc函数到底是如何开辟内存呢?为什么不直接就赋值给原来的指针呢?为什么还要创建一个临时指针变量来接受呢?接下来,为你解开谜团。?
realloc函数如何开辟内存??
1.开辟失败
? ? ? ?就是开辟空间过大或者不够用,就会返回NULL。
2.开辟成功
? ? ? ?因为内存分布有它自己的规则,比如原来申请20字节动态内存,要调整为40个字节,没有影响到下一块内存,则返回的地址没有改变;但要调整到40000字节,就会影响到下一块内存,这时realloc函数就会在堆区中开辟一块新的内存区域,返回的地址就是新开辟的内存区的首个字节内存的地址。
? ? ? ?我们来举一个改变原来指针指向的例子(就是申请字节过大):
int main()
{
int* p = malloc(NULL, sizeof(int) * 5);
//扩容为40000个字节
int* ptr = (int*)realloc(p, sizeof(int) * 40000);
if (ptr == NULL)
{
perror("realloc");
}
free(p);
p = NULL;//置空
return 0;
}
? ? ? ?此时就发现指向的位置发生了改变。
? ? ? ?那么之前的问题就迎刃而解了, 定义一个临时指针就是为了防止realloc开辟的内存失败,为了防止这种情况我们定义了一个临时指针。
这时,还是会有小伙伴有问题:为什么临时指针指向的空间没有释放?临时指针的值没有置空?好问题,接下来给你答案。
? ? ? ?所以,我们此时应该就能理解,只需要释放p即可。至于置空,free没有将传入指针指向改变的功能,所以要手动置空。但是却没有置空ptr,实际上,ptr的指向也确实没有改变,会形成野指针,但是毕竟是临时指针,我们其实也就是用这一次。
?free的使用:
? ? ? ?因为由malloc/calloc/realloc申请的空间,如果不主动释放,出了作用域是不会销毁的,和函数的栈帧不一样。有两种释放方式:
- free主动释放
- 直到程序结束,才由操作系统回收。
? ? ? ?所以一定要释放空间。
动态内存函数使用注意事项:
1.不能越界访问
? ? ? ?在使用动态内存时,首先注意不能越界访问。
int main()
{
//对动态内存的越界访问
int* p = (int*)malloc(5 * sizeof(int));
if (p == NULL)
{
return 1;
}
int i = 0;
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;
}
2.没有从最开始的空间释放内存?
? ? ? ?我们使指向堆区的指针改变了指向之后释放内存,会报错。
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 0;
}
int i = 0;
for (i = 0; i < 10; i++)
{
p++;
}//越界访问
free(p);
p = NULL;//错误示范,指针变量自增
return 0;
}
?3.多次释放同一块内存
? ? ? ?我们已经释放了动态开辟的空间,之后又释放了一次。
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 0;
}
free(p);
//p = NULL;
//除非手动把p指针赋值为空指针
free(p);//对同一块动态内存多次释放是不行的
return 0;
}
4.对非动态开辟的内存free?
? ? ? ?free只能由于动态函数,只能释放堆区空间,不能释放栈区空间。
int main()
{
//对非动态开辟内存使用free释放
int a = 10;
int* p = (int*)malloc(40);
if (p == NULL)
{
return 1;
}
p = &a;//p指向的空间不再是堆区的空间
free(p);
p = NULL;
return 0;
}
5.对NULL解引用?
? ? ? ?如果我们不做判断就赋值,此时也可能会出现问题。
int main()
{
//对NULL指针的解引用操作
int* p = (int*)malloc(100);
*p = 20;//p有可能是空指针
return 0;
}
? ? ? ?使用前一定要检测有效性。?
6.内存泄漏
? ? ? ?就是开辟内存以后没有释放内存,就叫做内存泄露,因为只有程序运行完以后,操作系统才会回收,所以一般就存在于运行中的程序。
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
{
return 0;
}
//没释放空间 free(p)
//内存没有释放
//手动置空没有用
p = NULL;
return 0;
}
? ? ? ? 此时观察不到结果,因为观察不到结果,就很难发现,此时就是一件很危险的事,所以我们使用动态内存一定在程序运行后记得使用free释放内存。
练习:
习题一:
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
int main()
{
Test();
return 0;
}
? ? ? ?来告诉我你们的答案,是不是hello world?答案是没有任何输出。此时我们画图来分析以上代码:
? ? ? ?开辟空间后,就会更新临时指针p的指向。但是执行完以后临时指针p销毁,而且没有释放空间,导致内存泄露。但是str的指向没有改变,所以还是空,此时还是拷贝不了。
? ? ? ?此时有两种解决方案:
方案1:
? ? ? ?我们传入str的地址,用二级指针的方式,这样就能修改str的指向。
void GetMemory(char** p) { *p = (char*)malloc(100); } void Test(void) { char* str = NULL; GetMemory(&str); strcpy(str, "hello world"); printf(str); free(str); } int main() { Test(); return 0; }
?方案2:
? ? ? ?我们使函数返回开辟空间的地址。
char* GetMemory(char* p) { p = (char*)malloc(100); return p; } void Test(void) { char* str = NULL; str=GetMemory(str); strcpy(str, "hello world"); printf(str); free(str); } int main() { Test(); return 0; }
? ? ? ?细心的同学会发现(图片中没有free,大家记得free一下),我并没有将str手动置空,这是因为函数是用完以后,函数里面的变量都已经销毁了,我们也并不会使用到了,所以没有必要多此一举。
习题二:?
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
? ? ? ??函数中创建的数据出函数后就销毁了,所以即使str指向p原来的内存,可是里面的数据已经销毁了,打印不出来。
习题三:
void test(void)
{
char* str = (char*)malloc(100);
strcpy(str, "hello");
free(str);//因为free不会把str置成空指针,所以str指向没有改变
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
int main()
{
test();
return 0;
}
? ? ? ?因为free不会将指针置空,所以str不为空,以至于打印world。? ? ? ? 打印world是非法访问,所以不能这样写,即使能运行想要的结果。
习题四:
int* test()
{
int a=10;//栈区
return &a;
}
int main()
{
int *p=test();
*p = 20;
printf("%d\n", *p);
return 0;
}
? ? ? ?即使返回了地址,但是返回的是地址,内存是会被销毁的,所以*p即使成功,也会非法访问。?
? ? ? ?此时解决办法就是将a的生命周期延长,用static修饰。
int* test()
{
static int a = 10;//静态区
//int a=10;//栈区
return &a;
}
int main()
{
int *p=test();
*p = 20;
printf("%d\n", *p);
return 0;
}
? ? ? ?此时a的内存就没有被销毁, 我们对其解引用就是正常访问。
结束语
? ? ? ?不要嫌文字多,文章长就逃避,我们应该迎难而上,细心看完这篇文章,相信你收获颇丰。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!