动态内存管理那些事:malloc、calloc、realloc、free 下

简介: 动态内存管理那些事:malloc、calloc、realloc、free

三、常见的动态内存错误

💦 对NULL指针的解引用操作

#include<stdio.h>
#include<stdlib.h>
int main01()
{
  int* p = (int*)malloc(10000000000);
  int i = 0;
  for(i = 0; i < 10; i++)
  {
    *(p + i) = i;//int* p = NULL; 如果开辟失败,就会非法访问内存
  }
  return 0;
}
/*--------------------改正--------------------*/
int main()
{
  int* p = (int*)malloc(10000000000);
  if(p == NULL)
  {
    perror("main");
    return 0; 
  }
  int i = 0;
  for(i = 0; i < 10; i++)
  {
    *(p + i) = i;//int* p = NULL; 如果开辟失败,就会非法访问内存
  }
  return 0;
}

小结

1️⃣ 对于malloc、calloc、realloc的返回值要作判空理

💦 对动态开辟空间的越界访问

#include<stdio.h>
#include<stdlib.h>
int main()
{
  int* p = (int*)malloc(10 * sizeof(int));
  if(p == NULL)
  {
    return 0; 
  }
  int i = 0;
  for(i = 0; i < 40; i++)
  {
    *(p + i) = i;//越界访问
  }
  free(p);
  p = NULL;
  return 0;
}

💦 使用free释放非动态开辟的空间

#include<stdio.h>
#include<stdlib.h>
int main()
{
  int arr[10] = { 0 };
  int* p = arr;
  int i = 0;
  for(i = 0; i < 10; i++)
  {
    *(p + i) = i;
  }
  free(p);//这是err的
  p = NULL;
  return 0;
}

💦 使用free释放一块动态开辟内存的一部分

#include<stdio.h>
#include<stdlib.h>
int main01()
{
  int* p = (int*)malloc(10 * sizeof(int));
  if(p == NULL)
  {
    return 0; 
  }
  int i = 0;
  for(i = 0; i < 5; i++)
  {
    *p++ = i;
  }
  free(p);//没有完全回收
  p = NULL;
  return 0;
}
/*--------------------改正--------------------*/
int main()
{
  int* p = (int*)malloc(10 * sizeof(int));
  if(p == NULL)
  {
    return 0; 
  }
  //无非就是想让数组的的前5个元素初始化为0 1 2 3 4,只要不让p真实的往后走即可
  int i = 0;
  for(i = 0; i < 5; i++)
  {
    //1.
    p[i] = i;
    //2.
    //*(p + i) = i;
  }
  free(p);
  p = NULL;
  return 0;
}

小结

1️⃣ 未完全释放动态开辟的空间是err的

2️⃣ 可能会造成内存泄漏,因为没有人再能记住开辟的起始空间了

💦 对同一块动态内存多次释放

#include<stdio.h>
#include<stdlib.h>
int main01()
{
  int* p = (int*)malloc(100);
  //使用
  //...
  //释放
  free(p);
  //...
  //...
  free(p);//err
  return 0;
}
/*--------------------改正--------------------*/
#include<stdio.h>
#include<stdlib.h>
int main()
{
  int* p = (int*)malloc(100);
  //使用
  //...
  //释放
  free(p);
  p = NULL;
  //...
  //...
  free(p);//无意义
  return 0;
}

小结

1️⃣ 在free完malloc、calloc、realloc开辟空间后,要及时置为NULL

💦 动态开辟内存忘记释放 (内存泄漏)

#include<stdio.h>
#include<stdlib.h>
void test()
{
  int* p = (int*)malloc(100);//p是局部变量
  if(p == NULL)
    return;
  //使用
  //...
}
int main01()
{
  test();
  //... 
  //这里就内存泄漏了:
  //在test内动态开辟了空间,忘了释放。且局部变量p也没留下任何遗言,所以在函数外部也释放不了。只要程序没有死,这块空间就没人能找到 
  return 0;
}

💦 C/C++中程序内存区域划分示意图

1️⃣ 栈区( stack ):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。


2️⃣ 堆区( heap ) :一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配方式类似于链表。


3️⃣ 数据段(静态区 ) ( static )存放全局变量、静态数据。程序结束后由系统释放。


4️⃣ 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

四、几个经典的笔试题

💦 1.

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char* p)//p是str的一份临时拷贝
{
  p = (char*)malloc(100);//2.动态开辟空间后没有释放;p为局部变量,出了范围就找不到开辟的空间了,所以内存泄漏     
}
void Test(void)
{
  char* str = NULL;
  GetMemory(str);//值传递
  strcpy(str, "hello world");//1.同strcpy(NULL, "hello world")
  printf(str);
}
int main01()
{
  Test();
  return 0;
}
/*--------------------改正1--------------------*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
char* GetMemory(char* p)
{
  p = (char*)malloc(100);   
  return p;//在局部变量p销毁前,把指向动态开辟好的空间的地址返回回来
}
void Test(void)
{
  char* str = NULL;
  str = GetMemory(str);
  strcpy(str, "hello world");
  printf(str);
  free(str);
  str = NULL;
}
int main02()
{
  Test();
  return 0;
}
/*--------------------改正2--------------------*/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char** p)
{
  *p = (char*)malloc(100); //*p找到str  
}
void Test(void)
{
  char* str = NULL;
  GetMemory(&str);//传址
  strcpy(str, "hello world");
  printf(str);
  free(str);
  str = NULL;
}
int main()
{
  Test();
  return 0;
}

💦 2.

#include<stdio.h>
char* GetMemory(void)
{
  char p[] = "hello world";//p是局部变量,但是接收的内容是在栈区创建的
  return p;//这里虽然返回p的地址,但是这块空间的内容已经销毁了
}
void Test(void)
{
  char* str = NULL;
  str = GetMemory(); 
  printf(str);//非法访问内存:现在str的空间就不是自己的了,所以打印的时候,就烫烫烫了
}
int main()
{
  Test();
  return 0;
}

💦 3.

#include<stdio.h>
int* f2(void)
{
  int* ptr;
  *ptr = 10;//野指针问题:ptr没有初始化,这时候去解引用就出问题了
  return ptr;
}
int main()
{
  f2();
  return 0;
}

💦 5.

#include<stdio.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(str);
}
int main01()
{
  Test();//没有free
  return 0;
}
/*--------------------改正--------------------*/
#include<stdio.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(str);
  free(str);//改正处
  str = NULL;
}
int main()
{
  Test();
  return 0;
}

💦 6.

#include<stdio.h>
#include<stdlib.h>
void Test(void)
{
  char* str = (char*)malloc(100);
  strcpy(str, "hello")
  free(str);
  if(str != NULL)//free不会主动置为NULL
  {
    strcpy(str, "world");//str已经释放了,非法访问内存
    printf(str);
  }
}
int main01()
{ 
  Test();
  return 0;
}
/*--------------------改正--------------------*/
#include<stdio.h>
#include<stdlib.h>
void Test(void)
{
  char* str = (char*)malloc(100);
  strcpy(str, "hello")
  free(str);
  str = NULL;//主动置NULL
  if(str != NULL)
  {
    strcpy(str, "world");
    printf(str);
  }
}
int main()
{ 
  Test();
  return 0;
}

五、柔性数组

💦 什么是柔性数组

🎗 想必很多人都未听说过柔性数组 (flexible array) 这个概念,但是它确实存在。C99中,结构体中的最后一个元素允许是未知大小的数组,这就叫做 “柔性数组” 成员。注:不代表所有编译器都能支持

struct S1
{
  int n;
  int arr[];//这就叫做 "柔性数组" 成员
};
struct S2
{
  int n;
  int arr[0];//也可以这样写
};

💦 柔性数组的特点

1️⃣ 结构体中的柔性数组成员前面必须至少一个成员

2️⃣ sizeof返回的这种结构体大小不包括柔性数组的内存

3️⃣ 包含柔性数组成员的结构体使用malloc函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小

4️⃣ 柔性数组的成员都在堆上开辟

#include<stdio.h>
#include<stdlib.h>
struct S
{
  int n;
  int arr[];//1.在之前必须有1个成员以上
};
int main()
{
  struct S s = { 0 };
  printf("%d\n", sizeof(s));//2.4Byte
  //3.期望arr的大小是10个整型
  struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));
  if(ps == NULL)
  {
    return 0; 
  }
  //赋值
  ps->n = 10;
  int i = 0;
  for(i = 0; i < 10; i++)
  {
     ps->arr[i] = 0;
  }
  //调整
  struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + 20 * sizeof(int));
  if(ptr != NULL)
  {
    ps = ptr; 
  }
  //释放
  free(ps);
  ps = NULL;
  return 0;
}


🎗 看到这里,是不是觉得柔性数组没必要存在,因为只要让指针指向动态开辟的空间即可 (模拟柔性数组)

#include<stdio.h>
#inlude<stdlib.h>
struct S
{
  int n;
  int* arr;
};
int main()
{
  struct S* ps = (struct S*)malloc(sizeof(struct S));
  if(ps == NULL)
    return 0;
  ps-> n = 10;
  ps->arr = (int*)malloc(10 * sizeof(int))
  if(ps->arr == NULL)
    return 0;
  //赋值
  int i = 0;
  for(i = 0; i < 10; i++)
  {
    ps->arr[i] = i;
  }
  //增加
  int* ptr = (int*)realloc(ps->arr, 20 * sizeof(int));
  if(ptr != NULL)
  {
    ps->arr = ptr;
  }
  //释放
  //这里需要回收2个空间,且回收必须有先后
  free(ps->arr);
  ps->arr = NULL;
  free(ps);
  ps = NULL;
  return 0;
}

对比:柔性数组➰指针模拟柔性数组

1️⃣ 指针模拟的方式需要2次malloc、2次free,也就意味着容易出错;而柔性数组只要1次malloc、1次free即可

2️⃣ 其次malloc多了,内存碎片相对的也变多了,内存的利用率就降低了

💨 小结柔性数组的好处:

1️⃣ 方便内存释放

如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。

2️⃣ 有利于访问速度

连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,也没多高,反正你跑不了要用做偏移量的加法来寻址)


推荐一篇关于柔性数组的文章

C语言结构体里的成员数组和指针

相关文章
|
21天前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
1月前
|
编译器 C语言 C++
详解C/C++动态内存函数(malloc、free、calloc、realloc)
详解C/C++动态内存函数(malloc、free、calloc、realloc)
135 1
|
1月前
一刻也没有为它哀悼~接下来登场的是动态内存分配的malloc与realloc以及free函数
一刻也没有为它哀悼~接下来登场的是动态内存分配的malloc与realloc以及free函数
60 0
|
4月前
|
存储 分布式计算 Hadoop
HadoopCPU、内存、存储限制
【7月更文挑战第13天】
276 14
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
339 0
|
15天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
31 1
|
20天前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
24天前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
29天前
|
存储 编译器
数据在内存中的存储
数据在内存中的存储
37 4
|
27天前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
50 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配