在前面我们已经学习了变量在内存中的存储,但它们大多都是在栈区开辟内存的,这一章将带大家介绍在堆区开辟内存的动态内存。
文章目录
一、动态内存分配的原因
内存一般分为栈区、堆区、静态区:
一般情况下我们开辟的局部变量或者全局变量它们开辟的空间是固定的,而且数组在开辟内存时也必须要指定长度,这显然很难满足我们日常的需求。因此这个时候就需要能自由开辟内存空间的动态内存开辟了。
二、动态内存函数
2.1malloc开辟空间和free释放空间
malloc
可以向内存在堆区申请开辟size个空间,单位是字节,由于返回值是void*
,因此需要指针接收这个返回值,而且返回值也需要被强制类型转换为我们需要的指针类型,申请的空间在用完以后必须用free
释放掉。
在开辟内存时可能存在开辟失败的情况,这时候的返回值是NULL
并返回错误信息,这时候我们用前面学的strerror
函数就可以打印错误信息了。
因此一个比较完整的程序如下所示:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int num = 0;
int i = 0;
scanf("%d", &num);
int* ptr = NULL;
ptr = (int*)malloc(num * sizeof(int));
if (ptr == NULL)//判断ptr指针是否为空,为空则开辟失败,此时打印错误信息
{
printf("%s\n", strerror(errno));
}
else
{
for (i = 0; i < num; i++)
{
*(ptr + i) = i;
}
}
for (i = 0; i < num; i++)
{
printf("%d ", *(ptr + i));
}
free(ptr);//释放ptr所指向的动态内存,将内存还给编译器
ptr = NULL;//ptr置为空指针,如果不置为空指针ptr就为野指针了
return 0;
}
开辟失败打印错误,INT_MAX是一个很大的数字(2147483647),编译器无法开辟:
2.2calloc开辟空间和free释放空间
calloc
和malloc
的差别不是很大,它们用完都要被free
释放掉,calloc的传参不同,calloc会开辟num
个大小为size
的空间,并且会把这些空间的内容初始化为0
,比如上面的程序改成calloc:
2.3realloc开辟空间和free释放空间
realloc
可以在已开辟的内存基础上进行增加,对动态开辟的内存大小进行灵活的调整,在传参时:
- ptr 是要调整的内存地址
- size 调整之后新大小
- 返回值为调整之后的内存起始位置。
- 在开辟调整完内存以后,原内存中的数据都会得到保留,并不会丢失
由于数据在内存中并不是连续存储的,因此realloc
在调整空间时会有以下两种情况:
-
如果原来的空间p后面的空间有足够的内存可以追加,则直接追加,并返回原来的指针ptr
-
如果ptr指向的空间之后没有足够的内存空间可以追加,则realloc函数会重新找一个新的内存区域开辟一块满足需求的空间,并且把原来内存中的数据拷贝回来,释放旧的内存空间,最后返回新开辟的内存空间地址。
由于realloc
在调整内存时会出现开辟内存失败的情况,这个时候会返回空指针,因此如果用原来的指针ptr接收则会造成错误,因此可以用一个新的指针接收这个新的返回值,在判断不为空指针后再赋值给ptr。
因此上面的程序可以作出修改:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
int main()
{
int num = 0;
int i = 0;
scanf("%d", &num);
int* ptr = NULL;
ptr = (int*)malloc(num*sizeof(int));
if (ptr == NULL)//判断ptr指针是否为空,为空则开辟失败,此时打印错误信息
{
printf("%s\n", strerror(errno));
}
else
{
for (i = 0; i < num; i++)
{
*(ptr + i) = i;
}
}
for (i = 0; i < num; i++)
{
printf("%d ", *(ptr + i));
}
printf("\n");
int* p = NULL;
p = realloc(ptr, 1000);
if (p != NULL)
{
ptr = p;
for (i = num; i < 10; i++)
{
*(ptr + i) = i;
}
}
for (i = 0; i < 10; i++)
{
printf("%d ", *(ptr + i));
}
free(ptr);
return 0;
}
三、常见内存动态分配错误
- 对空指针(NULL)进行解引用
在动态开辟内存时会存在开辟失败的情况,此时返回空指针,空指针是无法解引用的:
因此在解引用时必须要对返回值进行判断。
- 对动态开辟的内存越界访问
与数组类似,这种情况会直接报错
#include<stdio.h>
#include<stdlib.h>
int main()
{
int i = 0;
int* p = (int*)malloc(10 * sizeof(int));
if (NULL == p)
{
return 0;
}
else
{
for (i = 0; i < 10; i++)
{
*(p + i) = i;//当i是10的时候越界访问
}
}
free(p);
}
- 对非动态开辟内存使用free释放,非动态开辟的内存无法用free释放
#include<stdio.h>
#include<stdlib.h>
int main()
{
int a = 10;
int* p = &a;
free(p);
return 0;
}
- free只释放动态开辟内存的一部分
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(10);
p++;
free(p);//p不再指向动态内存的起始位置
return 0;
}
- 对同一块开辟的空间进行多次释放
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(100);
free(p);
free(p);//重复释放
return 0;
}
为了出现这样的错误,可以在释放以后置为空指针:
- 对动态开辟的空间忘记释放
如果总是开辟内存而不释放,会造成内存泄漏的情况,内存泄漏就是已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
因此动态开辟的空间一定要正确释放。
四、动态内存开辟常见的笔试题
- 程序有什么样的结果?
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char* p)
{
p = (char*)malloc(1000);
}
void Test(void)
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf("%s",str);
}
int main()
{
test();
return 0;
}
GetMemory函数的参数是一个形参,它并不能给str开辟空间,因为str是值传递。
开辟的内存也没有释放。
修改后的代码:
#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("%s",str);
}
int main()
{
Test();
return 0;
}
GetMemory函数调用完以后在栈区开辟的p数组的内存就已经被释放了,因此str会非法访问内存。
修改后:
也可以使用动态内存开辟,但要记得释放。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf("%s",str);
}
int main()
{
Test();
return 0;
}
str没有被释放,会存在内存泄漏的问题。
修改:
#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("%s",str);
}
}
int main()
{
Test();
return 0;
}
free(str)
释放以后并没有将str置为空指针,strcpy(str, "world");
会非法访问内存。
修改:
五、柔性数组
C99 中,结构体中的最后一个元素允许是未知大小的数组,这个成员就是柔性数组成员。
struct S
{
int n;
int arr[];//柔性数组成员,其大小可以调整
};
柔性数组的特点:
- 结构中的柔性数组成员前面必须至少一个其他成员。
- sizeof 返回的这种结构大小不包括柔性数组的内存。
- 包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
#include<stdio.h>
#include<stdlib.h>
struct S
{
int n;
int arr[];
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 5 * sizeof(int));
ps->n = 10;
int i = 0;
for (i = 0; i < 5; i++)
{
ps->arr[i] = i;
}
//释放空间
free(ps);
return 0;
}
柔性数组和下面的程序十分类似:
#include<stdio.h>
#include<stdlib.h>
struct S
{
int n;
int* arr;//用指针不用数组
};
int main()
{
struct S* p = malloc(sizeof(struct S));//先开辟S的空间
p->n = 100;
p->arr = (int*)malloc(p->n * sizeof(int));//再开辟arr的空间
int i;
for (i = 0; i < 5; i++)
{
p->arr[i] = i;
}
//释放空间
free(p->arr);
p->arr = NULL;
free(p);
p = NULL;
return 0;
}
柔性数组相比于用指针的形式,其优势:
- 用指针的形式会做二次内存开辟,在释放时结构体的成员也必须free掉并且置为空指针。而柔性数组只需要释放一次
- 柔性数组由于只内存开辟一次,因此内存是连续的,访问速度也比较快。
总结
动态开辟内存用完以后一定要记得释放并把指针置为空指针!!!
今天的文章13C语言中的动态内存管理分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/60958.html