💓博主CSDN主页:杭电码农-NEO💓
⏩专栏分类:C语言学习分享⏪
🚚代码仓库:NEO的学习日记🚚
🌹关注我🫵带你学习更多C语言知识
🔝🔝
常见内存错误
- 1. 前言
- 2. 对NULL指针解引用操作
- 3. 对动态开辟的空间越界访问
- 4. free没有将空间完全释放
- 5. 多次释放或忘记释放
- 6. 经典笔试题目
- 6.1 题目一
- 6.2 题目二
- 7. 柔性数组
- 7.1 柔性数组的特点
- 7.2 柔性数组的使用
- 7.3 柔性数组的优势
- 8. 总结以及拓展
1. 前言
本章重点:
本节着重讲解动态内存中的常见错误
并且分享几个经典的笔试题
并且介绍一个新概念: 柔性数组
动态开辟的内存在堆区,局部变量在栈区
它们的作用域什么时候销毁?
它们之间能不能相互关联起来使用?
包括一些错误的用法在面试中是常客!
2. 对NULL指针解引用操作
请看下面的代码:
void test()
{
int *p = (int *)malloc(40);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
解释:
malloc函数开辟空间失败会返回NULL
而每次使用完malloc后就应该判空
否则使用这个指针时不知道它是否为空
拓展:
现在的编译器很智能,电脑本身性能也强
malloc一般都不会失败
即使开辟五万个字节的空间
空间肯定也是会开辟成功的!
但是为了养成良好的习惯,应该记得判空
早在12年的时候,就有大佬回答:
堆区空间大约2GB
2GB=2×1024×1024×1024 个字节
暂且别说五万个字节的空间
就算是5亿个字节也完全能开辟出来
3. 对动态开辟的空间越界访问
请看以下代码:
int i = 0;
int *p = (int *)malloc(10*sizeof(int));
if(NULL == p)
{
exit(-1);
}
for(i=0; i<=10; i++)
{
*(p+i) = i;//当i是10的时候越界访问
}
free(p);
解释:
此处的越界访问和数组的越界访问类似
在开辟的空间外面的空间是未知的值
这里不做过多讲解
4. free没有将空间完全释放
请看以下代码:
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
解释:
这段代码可以这样理解:
5. 多次释放或忘记释放
请看以下代码:
int *p = (int *)malloc(100);
int *pp = (int *)malloc(100);//pp没有释放
free(p);
free(p);//重复释放
注意:
这里比较简单,但是值得注意的是
由于写代码时写着写着容易忘记释放空间
所以我建议在写完malloc的下面一条语句
直接写上free,再在它们中间写其他代码
6. 经典笔试题目
6.1 题目一
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
这段代码的问题是什么?
解释:
首先,Get函数的参数是char
str的类型也是char,所以是传值传参
而形参p和实参str没有必然联系
改变形参p不会对str造成影响
所以应该传str的地址,用二级指针接受**
其次,这份动态开辟的空间没有释放
并且此时str还是NULL
使用strcpy相当于对NULL解引用会报错
6.2 题目二
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
这段代码有什么问题?
解释:
首先,p指向的空间是栈区开辟的
出Get函数后,这份空间就返给了系统
使用已经还给系统的空间是不对的!
其次,这段代码不会打印hello world
函数的栈帧的建立和销毁的过程
大致可以这样理解:
拓展:
函数栈帧的创建与销毁过程
可以参考这篇博客:
函数栈帧的创建与销毁
7. 柔性数组
结构体中的最后一个元素
允许是未知大小的数组
这就叫做『柔性数组』成员。
柔性数组的元素个数是可变的
比如:
struct NEO
{
int i;
int a[0];//柔性数组成员
//int a[];或者使用这种写法
}
7.1 柔性数组的特点
基本特点:
-
柔性数组前面至少有一个成员
-
柔性数组必须是最后一个成员
-
计算结构体大小时
柔性数组不计算在内 -
为拥有柔性数组的结构体开辟空间时
除了结构体大小外还要加上数组大小
例如:
struct NEO
{
int i;
int a[0];//柔性数组成员
}
printf("%d\n", sizeof(struct NEO));
打印4
这里只会计算成员i的大小
而柔性数组的大小不算在内
7.2 柔性数组的使用
先定义一个柔性数组:
struct NEO
{
int i;
char ch;
int a[0];//柔性数组成员
}
假设想让数组a有10个整型的空间
可以这样开辟空间:
struct NEO* p = (struct NEO*)malloc(sizeof(struct NEO)+10*sizeof(int));
这段代码可以这样理解:
7.3 柔性数组的优势
在定义结构体时,假设使用正常的指针p
struct NEO
{
int i;
int* p;
char ch;
}
struct NEO* n;
然后为指针p动态开辟一份空间
首先要为结构体变量开辟空间
n = (struct NEO*)malloc(sizeof(struct NEO))
n->p = (int*)malloc(sizeof(int)*10);
然而使用这个变量后,需要free掉空间
malloc了两次空间,所以需要释放两次
先释放谁? 当然是内部是指针p!
如果先把结构体变量n释放了
那么就找不到内部的指针p了
发现不使用柔性数组的话,很麻烦
并且很容易将释放顺序搞反
总结柔性数组的优势:
-
好处一:方便内存释放
-
好处二:有利于访问速度
使用柔性数组时,数组和其他成员的内存
是连续的而使用指针时,内存不连续
8. 总结以及拓展
使用动态开辟空间解决问题固然方便
但是稍不注意就会出现内存问题
这是一把双刃剑,使用应谨慎
拓展:柔性数组的用处
可以参考这篇文章:
开发使用方式之柔性数组
柔性数组拓展阅读:
结构体中的成员数组和指针