动态内存管理与柔性数组

简介:

@[TOC]

前言

  • 博主实力有限,博文有什么错误,请你斧正。
  • 本文讨论动态内存开辟的事情与注意点
  • 本文需要函数栈帧的知识,见我另外一篇博客

思维导图

动态内存管理

C/C++程序内存区域分类

  • 内存中有这几个区:栈区,堆区,代码段(也称谓常量区),数据段(也称谓静态区)
  • 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  • 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似于链表。
  • 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
  • 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

image-20211006142908040

  • 动态内存开辟申请的空间是 在==堆区==上,堆区上,堆区上。!!!!
  • 一旦申请空间 ,无论何时都要立刻检测是否申请成功。

动态申请 :malloc ,calloc,realloc

malloc

函数体形式

void * malloc(size_t size);

size :申请的空间大小,单位字节

注意点

  • 返回值的类型是 void* ,malloc函数并不知道开辟空间的类型,因此需要强制转换
  • 如果申请的空间满足要求,返回申请空间的起始地址,反之 返回NULL
  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
  • size_t 说明 一次性开辟内存是有上限的。0~4,294,967,295(42亿)

EXP

int p=(int )malloc( 100*sizeof(int ));

struct st t =(struct st )malloc(100* sizeof(struct st));

calloc

函数形式

​ void*calloc(size_t num,size_t size);

num:申请数组元素个数,

size : 数组元素的大小,单位字节

注意点

  • 返回值的类型是 void* ,calloc函数并不知道开辟空间的类型,因此需要强制转换
  • 如果申请的空间满足要求,返回申请空间的起始地址,反之 返回NULL
  • calloc申请的空间会初始化0;
  • 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
  • size_t 说明 一次性开辟内存是有上限的。0~4,294,967,295(42亿)

EXP

int p =(int )calloc(100,sizeof(int ));

struct st T =(struct st ) calloc(100,sizeof(struct st ));

realloc(重新分配已申请的空间)

函数形式

void realloc(voidmemblock,size_t size);

  • Memblock参数指向动态内存块的开头。

    • 如果memblock为NULL,realloc的行为与malloc相同,并分配一个新的大小为字节的块,返回起始地址。
    • 如果memblock不为空,则它应该是上一次调用calloc、malloc或realloc返回的指针。
  • Size参数提供块的新大小(以字节为单位)。

    • 如果==size 大小为零且Buffer参数不为空==,则返回值为NULL 且原始块释放
    • 如果size 不为0且memblock参数不为NULL,

      • 若在memblock后面不存在 连续的 size 个空间,编译器会在堆中找寻新的合适大小的空间,将原始块内容拷贝到新的位置,并free原始块,如果后面存在就返回原始块地址。
      • 若没有足够的空间区分配,那么返回NULL,且原始块保持不变

        image-20211006161754006

注意点

  • 如果membloc 为NULL,realloc行为与 malloc相同
  • realloc再找寻新的地址时,成功就会free原始块

三者联系

  • malloc 与calloc 行为基本一样,只是 calloc会初始化 开辟的内存为0 .另外calloc开辟的形式是数组,数组中的元素类型要一致。
  • realloc 传入的指针 是动态内存的地址。
  • realloc 内部有free功能

动态释放关键字:free

free

函数形式

void free(void memblock);

注意点

  • free释放的必须是动态内存的地址
  • free只是释放了堆区空间,不会改变membloc,

    因此memblock记住了堆区空间的地址,但是地址的内容是随机的。这就意味着free后 memblock 成为了危险的 ,危险的,危险的野指针!!!!!!。

    对于这种情况一般我们free后将memblock置为NULL

  • 向堆区动态申请的空间,要时刻及时free,不然程序过大,会出现严重的问题:内存泄漏

内存泄露

指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

常见动态内存错误

忘记free(内存泄漏)!!!!!!!!

int main()
{
   int* p = (int*)malloc(100);
   printf("hello\n");
   //忘记free,导致堆区一直被占用,如果遇到到处malloc,那么堆区的内存会被耗干,导致内存泄漏
}

不及时检测是否申请成功(对NULL的解引用)

error

int main()
{

   int* p = (int*)malloc(100 * sizeof(int));
   *p = 100;
//如果malloc返回NULL呢?程序会crash



}

cor

int main()
{

   int* p = (int*)malloc(100 * sizeof(int));
   if (NULL == p)
   {
       printf("%s\n", strerror(errno));//打印错误信息
       assert(NULL);//通过assert,终止程序。
   }
   else
   {
       *p = 100;
   }

}

越界访问开辟的空间

error

int main()
{
   int i = 0;
   int* p = (int*)malloc(10 * sizeof(int));
   if (NULL == p)
   {
       exit(EXIT_FAILURE);//EXIT_FAILURE ==1,exit(1).结束程序
   }
   for (i = 0; i <= 10; i++)
   {
       *(p + i) = i;//当i是10的时候越界访问
   }
   free(p);
}

cor

int main()
{
   int i = 0;
   int* p = (int*)malloc(10 * sizeof(int));
   if (NULL == p)
   {
       exit(EXIT_FAILURE);//EXIT_FAILURE ==1,exit(1).结束程序
   }
   for (i = 0; i < 10; i++)
   {
       *(p + i) = i;
   }
   free(p);
}

对非动态开辟空间的指针free

int main()
{
   char* str = "hello";

   free(str);//str是栈区局部变量
   
}

未完全释放动态申请的空间

int main()
{
   int* p = (int*)malloc(100);
   p++;
   free(p);//p不再指向动态内存的起始位置
}

free后忘记将野指针置为NULL

int main()
{
   int* p = (int*)malloc(100);
   free(p);//忘记置NULL
   //p =NULL;

}

重复free同一块动态内存

int main()
{
   int* p = (int*)malloc(100);
   free(p);
   //第一free后,p成为了野指针,对野指针的任何操作都会导致程序 crash
   free(p);
}

经典面试题

面试题一

void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}

问题:请问运行Test 函数会有什么样的结果

分析:

  • str只是将值NULL,传给形参变量p。

    因此在GetMemory 函数栈帧后,p虽然被回收了,

    但是申请的空间忘记free了,会导致内存泄漏!!!!!!!!!

  • GetMemory后 str 仍为NULL,因此strcpy会导致程序crash
  • 另外 printf传入NULL也会导致crash

面试题二

char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
   printf("%c\n",*str);
printf(str);
}

问题:请问运行Test 函数会有什么样的结果

分析:

  • GetMemory函数虽然返回了 字符数组的首导致,

    但是GetMemory函数栈帧结束后,为栈帧开辟的空间收回,那部分地址的内容的不变的。因此可以* str,这是虽然 printf栈帧,但是我已经把一个字符传进去了,因此可以打印。

    但是当 传入一个 str时,printf栈帧后,访问地址内容是随机的。

    因此我们说 str这个是非常危险,非常危险,非常危险的野指针。

面试题三

void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}

问题:请问运行Test 函数会有什么样的结果

分析:

  • 输出 :hello,但是没有free(str)
  • GetMemory 的参数是二级指针。因此需要传入 一级指针的地址,对二进指针解引用一次可以找寻到实参一级指针。因此通过这种方法可以改变实参str的指向。这是通过函数改变实参的一种方法。
  • 因此 str就会指向堆中开辟的空间,str成为了有效指针。因此strcpy,printf 都没有问题。但是忘记free(str)了

面试题四

void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}

问题:请问运行Test 函数会有什么样的结果

分析:

  • 未检测是否成功分配成功,free后 str成为了野指针,对它的任何操作都是非法的,不合规则的。虽然后面的strcpy和printf看似没有问题。

柔性数组

柔性数组是 C99标准增加的 一类 只能结构体中 定义的特殊数组。

形式:

struct st_type
{
   int i;
   int a[];//柔性数组成员
}type_a;

//有些编译器 会报错,可以改成
struct st_type
{
   int i;
   int a[0];//柔性数组成员
}type_a;

柔性数组的特点

  • 结构中的柔性数组成员前面必须有至少一个其他成员。
  • sizeof 计算结构体大小时不包含柔性数组

柔性数组的使用

image-20211006185717441
  • 包含柔性数组成员的结构用动态开辟内存函数(malloc,calloc,realloc)进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小 .

    EXA

    struct st_type
    {
       int t;
       int a[];//柔性数组成员
    }type_a;
    
    int main()
    {
       struct st_type* T1 = (struct st_type*)malloc(sizeof(struct st_type) + 10 * sizeof(int));
       if (NULL == T1)
       {
           exit(1);
       }
    
       struct st_type* T2 = (struct st_type*)calloc(1, sizeof(struct st_type) + 10 * sizeof(int));
       if (NULL == T2)
       {
           exit(1);
       }
    
       struct st_type* T3 = (struct st_type*)realloc(T1, sizeof(struct st_type) + 20 * sizeof(int));
       if (NULL == T3)
       {
       exit(1);
       }
       //输出T1
       T1->t = 100;
       printf("%d\n", T1->t);
       for (size_t i = 0; i < 10; i++)
       {
           *(T1->a + i) = i;
           printf("%d  ", *(T1->a + i));
       }
       printf("\n");
       //输出T2
       T2->t = 1000;
       printf("%d\n", T2->t);
       for (size_t i = 0; i < 10; i++)
       {
           *(T2->a + i) = i;
           printf("%d  ", *(T2->a + i));
       }
       printf("\n");
       //输出T3
       T3->t = 1000;
       printf("%d\n", T3->t);
       for (size_t i = 0; i < 20; i++)
       {
           *(T3->a + i) = i;
           printf("%d  ", *(T3->a + i));
       }
       printf("\n");
    
    //在本例子,T3重新分配空间时,已free过 T1.因此不需要再出free野指针。
       free(T2);
       T2 = NULL;
       free(T3);
       T3 = NULL;
       
    }
    

    image-20211006201447634

柔性数组的优势

  • 柔性数组这种只能结构体动态开辟空间时,才用到的特点,类似下面这种方式

    typedef struct st_type
    {
        int i;
        int* p_a;
    }type_a;
    int main() {
    
    
        type_a* p = (type_a*)malloc(sizeof(type_a));
        if (NULL == p)
        {
        
            exit(1);
        }
        p->i = 100;
        printf("%d\n", p->i);
    
        p->p_a = (int*)malloc(p->i * sizeof(int));
        if (NULL == (p->p_a))
        {
            exit(1);
        }
        for (size_t i = 0; i < 100; i++)
        {
            p->p_a[i] = i;
            printf("%d  ", p->p_a[i]);
        }
    
        free(p->p_a);
        p->p_a = NULL;
        free(p);
        p = NULL;
    }

    image-20211006202620637

  • 虽然2种方式都可以完成相同功能。但是柔性数组的方式有二个优势:

    • 方便内存释放

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

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

总结

  • 无论何时借别人的东西(动态开辟内存),都要记得还(free)
  • 任何对野指针的运算都是非法的,都会导致程序crash
相关文章
|
7月前
|
编译器
动态内存管理与柔性数组 1
动态内存管理与柔性数组
24 0
|
7月前
|
程序员 C语言 C++
动态内存管理函数的使用与优化技巧(内存函数、柔性数组)(下)
动态内存管理函数的使用与优化技巧(内存函数、柔性数组)(下)
25 0
|
7月前
|
程序员 编译器 C语言
动态内存管理函数的使用与优化技巧(内存函数、柔性数组)(上)
动态内存管理函数的使用与优化技巧(内存函数、柔性数组)(上)
33 0
|
7月前
|
程序员 编译器 C语言
动态内存函数,内存开辟,柔性数组(超详细)
动态内存函数,内存开辟,柔性数组(超详细)
40 0
|
7月前
|
编译器 程序员 测试技术
详解动态内存管理【malloc/calloc/realloc/free函数/柔性数组】【C语言/进阶/数据结构基础】
详解动态内存管理【malloc/calloc/realloc/free函数/柔性数组】【C语言/进阶/数据结构基础】
174 0
|
7月前
|
编译器 程序员 C语言
【C语言】动态内存管理(malloc,free,calloc,realloc,柔性数组)
【C语言】动态内存管理(malloc,free,calloc,realloc,柔性数组)
|
4月前
|
C语言
C语言——动态内存管理(malloc, calloc, realloc, free, 柔性数组详解)
C语言——动态内存管理(malloc, calloc, realloc, free, 柔性数组详解)
|
4月前
|
存储 编译器 C语言
learn_C_deep_10 extern在多文件下的理解、struct 关键字的理解与柔性数组、union 的内存级布局理解、enum 关键字的基本理解、typedef 的理解与分类、关键字总结
learn_C_deep_10 extern在多文件下的理解、struct 关键字的理解与柔性数组、union 的内存级布局理解、enum 关键字的基本理解、typedef 的理解与分类、关键字总结
|
9月前
|
程序员 C语言 C++
c语言学习第三十二课---内存开辟位置与柔性数组
c语言学习第三十二课---内存开辟位置与柔性数组
59 0
|
5月前
|
存储 程序员 编译器
动态内存管理+柔性数组+经典笔试题
动态内存管理+柔性数组+经典笔试题
45 0