动态内存管理

动态内存管理

为什么存在动态内存管理

学习过C语言基本语法的同学应该都熟练掌握了静态内存的开辟方式,例如:

int val1 = 20;			// 在静态区开辟一个4字节的空间
char arr1[10] = {0};	//在静态区开辟10个字节的连续空间
int main()
{
	int val2 = 20;			// 在栈空间开辟一个4字节的空间
	char arr2[10] = {0};	// 在栈空间上开辟10个字节的连续空间
}

上述案例中静态开辟内存的方式有2个特点:

  1. 开辟的空间大小是固定的
  2. 如果是为数组开辟空间,在声明时必须指定数组长度,它所需要的空间在编译时就会分配。

但是对于空间的需求,不仅仅是上述的情况,有时候我们需要开辟的空间大小只有在程序运行的时候才能知道,那么编译时开辟空间的方式就不能满足了,这时候就需要用到动态内存开辟。

备注:

C语言的C99标准支持了数组的动态创建,但是很多编译器并没有支持该标准,所以使用变量创建数组的代码在跨平台移植或者调用等场景下,可能会出现问题。

int main()
{
 int n = 0;
 scanf("%d\n", &n);
 int arr1[10];	// 静态数组创建
 int arr2[n];	// 动态数组创建(仅C99标准支持,很多编译器没有支持该标准,可移植性差)

 return 0;
}

动态内存函数介绍

malloc

头文件:#include <stdlib.h>

语法:void* malloc(sizeof_t sizeof);

说明:这个函数向内存申请一篇连续可用的内存空间,并返回指向这片内存空间的指针。

  • 参数size是需要开辟的空间大小,单位是字节。
  • 如果开辟成功,则返回执行开辟空间的首地址。
  • 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
  • 返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体类型由使用者自己决定。
  • 如果size为0,malloc的行为是未定义的,不同编译器可能会有不同的表现(要尽量避免这种行为)。

案例:

向内存申请10个int类型的空间

#include <stdio.h>
#inclued <stdlib.h>	// 引入malloc函数头文件
#include <string.h>
#inclued <erron.h>
int main()
{
    int* p = (int*)malloc(10 * sizeof(int));
    if(NULL = p)
    {
        printf("%d\n", strerror(erron));	// 打印C语言错误码
    }
    
    return 0;
}

在上述案例中,我们向内存申请了10个int类型的空间,但是使用结束后没有还给操作系统,这可能会导致内存泄漏。那么怎么将mallco申请的内存空间还给操作系统哪,就需要用到free函数

free

头文件:#include <stdlib.h>

语法:void free(void* ptr);

说明:free函数用来释放动态开辟的内存

  • 如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是未定义的 ,不同编译器可能有不同的表现(要尽量避免这种行为)。
  • 如果ptr是NULL指针,则函数什么事也不做。
  • 使用free函数释放了参数ptr指向的内存空间后,ptr还是指向该内存空间,需要让ptr指向NULL(易错)。

案例:

使用free释放malloc开辟的空间

#include <stdio.h>
#inclued <stdlib.h>	
#include <string.h>
#inclued <erron.h>
int main()
{
    int* p = (int*)malloc(10 * sizeof(int));
    if(NULL = p)
    {
        printf("%d\n", strerror(erron));	// 打印C语言错误码
    }
    
    free(p);	// 当申请的内存空间不再使用时,需要还给操作系统
    // *p = 10;	// 如果申释放空间后,P不指向NULL,就有可能继续访问这块已经被释放的内存空间
    p = NULL;
    
    return 0;
}

在上述案例中,我们向内存申请了10个int类型的空间,但是使用结束后没有还给操作系统,这可能会导致内存泄漏。那么怎么将mallco申请的内存空间还给操作系统哪,就需要用到free函数

calloc

头文件:#include <stdlib.h>

语法:void* calloc(size_t num, sizeof_t size);

参数:

  • size_t num元素的个数
  • sizeof_t size每个元素占用字节大小
  • 返回值void*,返回的是开辟空间的首地址

说明:calloc跟malloc函数的能力相似,都可以用来动态开辟内存

  • callo函数的功能是为num个大小为size的元素开辟一块连续的内存空间,并把空间的每个字节初始化为0。
  • 于函数malloc的区别只在于calloc会在返回地址前,把申请的内存空间的每个字节初始化为0。

案例:

向内存申请10个int类型的空间

#include <stdio.h>
#inclued <stdlib.h>
#include <string.h>
#inclued <erron.h>
int main()
{
    int* p = (int*)calloc(10, sizeof(int));
    if(NULL = p)
    {
        printf("%d\n", strerror(erron));	// 打印C语言错误码
    }
    
    free(p);	// 当申请的内存空间不再使用时,需要还给操作系统
    // *p = 10;	// 如果申释放空间后,P不指向NULL,就有可能继续访问这块已经被释放的内存空间
    p = NULL;    
    return 0;
}

使用calloc申请的内存空间,释放时也需要使用free函数

realloc

有时候我们会发现malloc或calloc函数申请的空间太大了,有时候我们又觉的申请的空间太小了。为了合理的使用内存,有时候会对内存大小进行灵活调整,那么reallo函数就可以做到对动态开辟的内存进行大小调整。

头文件:#include <stdlib.h>

语法:void* realloc(void* ptr, size_t size);

参数:

  • ptr是要调整的内存地址
  • size是调整后的内存地址大小,单位字节
  • 返回值void*,返回的是调整后的内存起始地址

说明:

  1. realloc追加空间时有两种情况:
    • 当已经申请的内存空间后面有足够的空闲空间可以追加,realloc会向操作系统申请追加这片空闲内存空间的使用权限,并返回已经申请的内存空间的首地址。
    • 当已经申请的内存空间后面的空闲空间不足以追加时,realloc会向操作系统重新申请一片足够大的内存空间,并将原内存空间中的数据拷贝到新的空间中,释放旧内存,最后返回新申请内存空间的首地址。

案例:

思考以下代码段2次输出结果输出的p1地址是否相同?

#include <stdio.h>
#inclued <stdlib.h>
#include <string.h>
#inclued <erron.h>
int main()
{
    int* p1 = (int*)malloc(10 * sizeof(int));
    if(NULL = p1)
    {
        printf("%d\n", strerror(erron));
    }else
    {
        printf("%p\n", p1);					// 第一次打印p1地址      
    }
  	
    p1 = (int*)realloc(p1, 1000 * sizeof(int));   
    if(NULL = p1)							// 注意:realloc函数也可能申请内存扩容失败,记得要对返回值判空
    {
        printf("%d\n", strerror(erron));	
    }else
    {
        printf("%p\n", p1);					// 第二次打印p1地址      
    }
    
    free(p1);
    p1 = NULL;
    return 0;
}

答案:不确定

2次输出结果输出的p1地址是否相同取决于使用realloc函数扩容时,申请的内存空间后面有足够的空闲空间可以追加。如果有足够空间,则输出结果相同。如果没有足够的内存空间,则输出结果不相同。

常见的动态内存错误

对NULL指针的解引用错误

使用malloc、calloc、realloc函数后,未对返回值判空就进行操作。

int main
{
    int p = (int*)malloc(sizeof(int));
    *p = 0;    // 万一malloc申请内存失败了,p会被赋值为NULL,直接使用p就会有空指针风险
    return 0;
}
对动态开辟空间的越界访问

即使是动态内存开辟,也不能访问未向操作系统申请的内存空间。

int main
{
    int p = (int*)malloc(sizeof(int));
    *(p+1) = 0; // 指针越界
    return 0;
}
对非动态开辟的空间使用free释放

对非使用malloc、calloc、realloc函数申请的内存空间,进行了free操作。

int main()
{
    int a = 10;
    int* p = &a;
    free(p);	// 非动态开辟的空间,是由操作系统进行管理的,不能使用free进行释放
    p = NULL;
    return 0;
}
使用free释放动态开辟内存的一部分

free函数只能一次释放malloc、calloc、realloc函数申请的整片内存空间,不能只释放其中的一部分

int main()
{
   	int p = (int*)malloc(10 * sizeof(int));
  	free(++p);	// 传递给free函数的不是动态开辟空间的首地址,不能使用free进行释放
    return 0;
}
对同一块动态内存进行多次释放

避免对同一块动态开辟的内存空间进行多次free,一般遵循以下原则:

  1. 谁申请谁回收
int main()
{
   	int p = (int*)malloc(10 * sizeof(int));
  	free(p);
    free(p);
    p = NULL;
    return 0;
}
动态开辟的内存空间忘记释放(内存泄漏)

动态申请内存空间后,不进行回收。导致内存泄漏,轻则程序性能下降,重则内存溢出程序崩溃。

int main()
{
    while(1)
    {
        malloc(1);	// 只开辟不回收,内存泄漏。 
    }
    return 0;
}

动态内存经典面试题

  1. 判断以下代码段是否能正常输出,如果可以,输出结果是什么?

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

    答案:该段代码运行结果如下:

    1. 运行时程序会出现崩溃
    2. 程序存在内存泄漏问题

    分析:代码崩溃原因如下:

    1. 指针str是以值传递的形式给p,而p是GetMemory函数的形参,只在函数的内部有效。等GetMemory函数执行结束后,p就会被销毁,p指向的动态开辟的内存空间并未被释放,且此时并没有指针指向该段动态开辟的内存空间,所以会造成内存泄漏。

    代码改造:对上述代码进行改造,解决程序崩溃及内存泄漏问题

    1. 改造方式1:将str的值传递,改为地址传递

      void GetMemory(char** p)	// 2. 双重指针接收指向地址的指针
      {
          *p = (char*)malloc(100);	// 3. 解引用,改变指针指向
      }
      void Test(void)
      {
          char* str = NULL;
          GetMemory(&str);	// 1. 值传递改为地址传递
          strcpy(str, "hello world");
          printf(str);
      
          free(str);	// 4. 动态开辟的空间,使用结束后记得释放,并将指针指向NULL	
          str = NULL;
      }
      int main()
      {
          Test();
          return 0;
      }
      
    2. 改造方式2:将GetMemory函数中申请的动态内存地址作为返回值,赋值给str

      void* GetMemory(char* p)	// 1. GetMemory函数返回申请的动态内存地址
      {
          p = (char*)malloc(100);
          return p;
      }
      void Test(void)
      {
          char* str = NULL;
          str = GetMemory(str);	// 2. str接收动态开辟内存空间的地址 
          strcpy(str, "hello world");
          printf(str);
      
          free(str);
          str = NULL
      }
      int main()
      {
          Test();
          return 0;
      }
      
  2. 请问运行以下代码段中Test函数会有什么样的结果?

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

    答案:程序崩溃或者打印随机值

    原因:经典的返回栈空间的地址问题

    解析:GetMemory方法中定义的数组p是函数内部变量,是在栈空间中定义的,GetMemory函数执行结束后,对应的栈空间就会被销毁。在被销毁前,GetMemory函数返回了p的值,并且被Test函数捕获到并且进行了调用,结果是未可知的。不同的编译器会有不同的表现,有些编译器会直接崩溃,有些编译器可能会越界访问已经被回收的内存地址。

    代码改造:对上述代码进行改造,解决程序崩溃或打印随机值问题

    1. 改造方式1:将变量p声明为静态变量,静态区中的变量函数执行结束不会被销毁,那么它的值就是可以被返回的。

      char* GetMemory(void)
      {
          static char p[] = "hello world";	// static修饰的变量是存在静态区的,函数结束不会被销毁
          return p;
      }
      void Test(void)
      {
          char* str = NULL;
          str = GetMemory();
          printf(str);
      }
      
    2. 改造方式2:将变量p指向的空间使用动态内存开辟方式,动态开辟的内存在堆区,函数结束不会被销毁。

      char* GetMemory(void)
      {
          char* p = (char*)malloc(100 * sizeof(char));
          strcpy(p, "hello worl");
          return p;
      }
      void Test(void)
      {
          char* str = NULL;
          str = GetMemory();
          printf(str);
      }
      
  3. 请问运行以下代码段中Test函数会有什么样的结果?

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

    答案:正常输出hello

    解析:在调用GetMemory函数时,使用了地址传递的方式,在GetMemory函数中,将str指针指向了一片malloc动态开辟的内存空间,所以在Test函数中可以正常的对这片申请的内存空时使用。

    缺陷:malloc函数申请的内存空间使用结束后没有进行释放,造成了内存泄漏。

  4. 请问运行以下代码段中Test函数会有什么样的结果?

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

    答案:篡改了已经还给操作系统的栈区空间,结果难以预料,非常危险。因为free之后,str已经变成了野指针。

    改进:使用free释放动态开辟的内存空间后,应该即可将str指向NULL。

柔性数组

什么是柔性数组

在C99标准中,结构体中的最后一个元素允许是一个未知大小的数组,这就叫做“柔性数组”成员。例如:

typedef struct st_type
{
    int i;
    int arr[0];	// 未知大小的柔性数组成员
}type_a;

补充:arr后面的[ ]中不填写值也可以,填0也可以。

柔性数组的特点
  • 结构体中的柔性数组成员前面必须至少包含一个其他成员。
  • sizeof返回的这种结构大小不包含柔性数组的内存。
  • 包含柔性数组成员的结构体使用malloc函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小。
带柔性数组的结构体大小

定义:

  1. 操作系统不会在编译阶段为柔性数组开辟内存空间,所以带柔性数组的结构体大小,就是除柔性数组外,结构体大小的总和。

案例:

  1. 思考以下代码段的输出结果:

    struct S1
    {
        int i;
        int arr[0];	// 未知大小的柔性数组成员
    };
    int main()
    {
        struct S1 s;
        printf("%d\n", sizeof(S1));
        return 0;
    }
    

    答案:输出结果为4,因为擦操作系统不会为柔性数组开辟内存空间。

柔性数组的使用

既然操作系统不会为柔性数组开辟内存空间,那么就需要程序员来动态申请。

案例

struct S1
{
    int i;
    int arr[0];	// 未知大小的柔性数组成员
};
int main()
{
	// 为柔性数组arr申请长度为5个元素的空间
    struct S1* s = (struct S1*)malloc(sizeof(S1) + 5 * sizeof(int));
    for(int i = 0;i < 5; i++)
    {
    	s->arr[i] = i;
    }
    
    // 为柔性数组arr申请追加长度为6个元素的空间
    struct S1* tempS = (struct S1*)realloc(s, sizeof(S1) + 6 * sizeof(int));
    if(NULL != tempS)
    {
        s = tempS;
    }
    s->arr[5] = 5;
    
    free(s);
    s = NULL;
    return 0;
}
使用非柔性数组方式

思考一个问题,在没有柔性数组的时代,是怎么让一个结构体成员包含未知大小的数组的?

案例:

struct S1
{
    int i;
    int* arr;	// 未知大小的柔性数组成员
};
int main()
{
	// 为结构体申请内存空间
    struct S1* s = (struct S1*)malloc(sizeof(S1));
    // 为结构体中的指针申请一片连续内存空间,用于存储5个整型元素。
    s->arr = (int*)malloc(5 * sizeof(int));
    
    // 调整结构体中的指针指向数组的大小
    int* ptr = (int*)realloc( s->arr, 6 * sizeof(int));
    if(NULL != ptr)
    {
        s->arr = ptr;
    }
    
    // 使用结束,释放内存
    free(s->arr);
    s->arr = NULL;
    free(s);
    s = NULL;
    
    return 0;
}

思考:想一下,先free(s),再free(s->arr)可以吗,会有什么后果?

柔性数组的优点
  1. 方便内存释放

    在编码过程中,malloc、calloc、realloc等动态开辟内存的函数调用次数越多,就需要对应调用更多次的free。如果我们的结构体所占空间是动态申请的,结构体中的元素所占空间也是动态申请的,那么使用结束就需要逐级向操作系统归还内存,这种情况下,既需要多次free,又对free的先后顺序有严格限制,很容易代码编写失误造成内存泄漏。

    而使用柔性数组,能够降低动态开辟内存的次数。

  2. 有利于访存

    根据段/页式存储和局部性原理,连续的内存空间更容易被加载到cache中,有益于提高CPU访存速度,也有溢于减少内存碎片。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值