【C语言进阶(九)】常见内存错误以及柔性数组

💓博主CSDN主页:杭电码农-NEO💓

⏩专栏分类:C语言学习分享

🚚代码仓库:NEO的学习日记🚚

🌹关注我🫵带你学习更多C语言知识

🔝🔝



常见内存错误

  • [1. 前言](#1. 前言 "#1__14")
  • [2. 对NULL指针解引用操作](#2. 对NULL指针解引用操作 "#2_NULL_32")
  • [3. 对动态开辟的空间越界访问](#3. 对动态开辟的空间越界访问 "#3___72")
  • [4. free没有将空间完全释放](#4. free没有将空间完全释放 "#4_free_99")
  • [5. 多次释放或忘记释放](#5. 多次释放或忘记释放 "#5__121")
  • [6. 经典笔试题目](#6. 经典笔试题目 "#6__139")
    • [6.1 题目一](#6.1 题目一 "#61__141")
    • [6.2 题目二](#6.2 题目二 "#62___173")
  • [7. 柔性数组](#7. 柔性数组 "#7__213")
    • [7.1 柔性数组的特点](#7.1 柔性数组的特点 "#71__230")
    • [7.2 柔性数组的使用](#7.2 柔性数组的使用 "#72__260")
    • [7.3 柔性数组的优势](#7.3 柔性数组的优势 "#73__286")
  • [8. 总结以及拓展](#8. 总结以及拓展 "#8__329")

1. 前言

本章重点:

本节着重讲解动态内存中的常见错误
并且分享几个经典的笔试题
并且介绍一个新概念: 柔性数组

动态开辟的内存在堆区,局部变量在栈区
它们的作用域什么时候销毁?
它们之间能不能相互关联起来使用?
包括一些错误的用法在面试中是常客!


2. 对NULL指针解引用操作

请看下面的代码:

c 复制代码
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. 对动态开辟的空间越界访问

请看以下代码:

c 复制代码
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没有将空间完全释放

请看以下代码:

c 复制代码
void test()
{
  int *p = (int *)malloc(100);
  p++;
  free(p);//p不再指向动态内存的起始位置
}

解释:

这段代码可以这样理解:


5. 多次释放或忘记释放

请看以下代码:

c 复制代码
int *p = (int *)malloc(100);
int *pp = (int *)malloc(100);//pp没有释放
 free(p);
 free(p);//重复释放

注意:
这里比较简单,但是值得注意的是
由于写代码时写着写着容易忘记释放空间
所以我建议在写完malloc的下面一条语句
直接写上free,再在它们中间写其他代码


6. 经典笔试题目

6.1 题目一

c 复制代码
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 题目二

c 复制代码
char *GetMemory(void)
{
  char p[] = "hello world";
  return p;
}
void Test(void)
{
  char *str = NULL;
  str = GetMemory();
  printf(str);
}

这段代码有什么问题?

解释:

首先,p指向的空间是栈区开辟的
出Get函数后,这份空间就返给了系统
使用已经还给系统的空间是不对的!

其次,这段代码不会打印hello world

函数的栈帧的建立和销毁的过程

大致可以这样理解:

拓展:

函数栈帧的创建与销毁过程
可以参考这篇博客:

函数栈帧的创建与销毁


7. 柔性数组

结构体中的最后一个元素
允许是未知大小的数组
这就叫做『柔性数组』成员。
柔性数组的元素个数是可变的

比如:

c 复制代码
struct NEO
{
  int i;
  int a[0];//柔性数组成员
  //int a[];或者使用这种写法
}

7.1 柔性数组的特点

基本特点:

  • 柔性数组前面至少有一个成员
  • 柔性数组必须是最后一个成员
  • 计算结构体大小时
    柔性数组不计算在内
  • 为拥有柔性数组的结构体开辟空间时
    除了结构体大小外还要加上数组大小

例如:

c 复制代码
struct NEO
{
 int i;
 int a[0];//柔性数组成员
}
printf("%d\n", sizeof(struct NEO));

打印4

这里只会计算成员i的大小

而柔性数组的大小不算在内


7.2 柔性数组的使用

先定义一个柔性数组:

c 复制代码
struct NEO
{
  int i;
  char ch;
  int a[0];//柔性数组成员
}

假设想让数组a有10个整型的空间

可以这样开辟空间:

c 复制代码
struct NEO* p = (struct NEO*)malloc(sizeof(struct NEO)+10*sizeof(int));

这段代码可以这样理解:


7.3 柔性数组的优势

在定义结构体时,假设使用正常的指针p

c 复制代码
struct NEO
{
  int i;
  int* p;
  char ch;
}

struct NEO* n;

然后为指针p动态开辟一份空间

首先要为结构体变量开辟空间

c 复制代码
n = (struct NEO*)malloc(sizeof(struct NEO))
n->p = (int*)malloc(sizeof(int)*10);

然而使用这个变量后,需要free掉空间
malloc了两次空间,所以需要释放两次

先释放谁? 当然是内部是指针p!
如果先把结构体变量n释放了
那么就找不到内部的指针p了

发现不使用柔性数组的话,很麻烦
并且很容易将释放顺序搞反

总结柔性数组的优势:

  1. 好处一:方便内存释放
  2. 好处二:有利于访问速度

使用柔性数组时,数组和其他成员的内存
是连续的而使用指针时,内存不连续


8. 总结以及拓展

使用动态开辟空间解决问题固然方便
但是稍不注意就会出现内存问题
这是一把双刃剑,使用应谨慎

拓展:柔性数组的用处

可以参考这篇文章:

开发使用方式之柔性数组

柔性数组拓展阅读:

结构体中的成员数组和指针


🔎 下期预告:文件操作 🔍

相关推荐
快乐小脾孩5 小时前
ubuntu20.04装nv驱动的一些坑
linux
java知路5 小时前
ubuntu修改时区
linux·运维·ubuntu
Long_poem7 小时前
【自学笔记】Mac OS语言基础知识点总览-持续更新
linux·服务器·笔记
ICT系统集成阿祥8 小时前
实验篇| CentOS 7 下 Keepalived + Nginx 实现双机高可用
linux·运维·服务器·nginx·centos
liudachu8 小时前
Centos固定IP配置
linux·tcp/ip·centos
li星野11 小时前
Linux应用:进程的回收
linux
20242817李臻11 小时前
课上测试:MIRACL共享库使用测试
linux·数据库
电鱼智能的电小鱼11 小时前
SAIL-RK3576核心板应用方案——无人机视觉定位与地面无人设备通信控制方案
linux·嵌入式硬件·无人机·边缘计算
.YYY12 小时前
Linux--普通文件的管理
linux
程序猿本员12 小时前
Linux多进程
linux·后端