C语言

13. C语言进阶--动态内存管理

Posted on 2022-02-11,15 min read

5. 动态内存管理

5.1 为什么存在动态内存分配?

已经掌握的内存开辟方式有:

int val = 20; // 在栈空间上开辟四个字节
char arr[10] = { 0 }; //在栈空间上开辟10字节的连续空间

以上两种开辟空间方式特点如下:

  1. 空间开辟大小是固定的

  2. 数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配

  • 但有时并不能预先知道需要需要开辟的空间大小,此时就需要动态内存。

5.2 动态内存函数

动态内存函数用来管理(开辟和释放)动态内存的。存在于头文件stdlib.h可编程内存分为栈区、堆区、静态存储区三部分。

  • 栈区(stack):函数内部局部变量、函数参数、函数返回值在栈区创建,函数执行结束,存储单元自动释放。栈区内存分配运算内置于处理器指令集中,效率高,但分配的内存容量有限。
  • 堆区(heap):或称为动态内存分配区。程序运行时申请,使用结束后释放。如果不手动释放,在程序运行结束后将被操作系统回收。若不释放,会导致内存泄露。分配方式类似于链表。
  • 静态存储区(static):主要存放静态数据,全局数据和常量。该内存在程序编译时已经分配,在程序的整个运行期间都存在。
  • 更多—程序内存分配

5.2.1 malloc

// 原型
void* malloc (size_t size);
// 参数:size -- 需要开辟的动态内存大小 单位byte

// 功能:向内存申请一块连续可用的空间 并返回指向该空间的指针

  • 如果开辟成功,则返回一个指向开辟号空间的指针
  • 如果开辟失败,则返回一个NULL指针 因此malloc的返回值一定要做检查
  • 返回值的类型是void*,使用时需要强制类型转换为需要的类型
  • 如果参数size0malloc的行为是标准未定义的,取决于编译器
  • 使用
    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
      int* p = (int*)malloc(3 * sizeof(int)); // 申请3个int大小的堆区内存空间
      if (p == NULL)
      {
        perror("main");
      }
      for (int i = 0; i < 3; i++)
      {
        *(p + i) = i;
      }
      free(p);
      p = NULL;
      return 0;
    }
    

5.2.2 calloc

// 原型
void* calloc (size_t num, size_t size);
// 参数:num 申请元素个数 
//      size 申请元素大小

// 功能:申请动态内存

  • 函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0
  • 与函数malloc的区别只在于calloc会返回地址之前把申请的空间的每个字节初始化为0,如下图



  • 使用
    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
      int* p = (int*)calloc(3, sizeof(int)); // 申请3个int大小的堆区内存空间
      if (p == NULL)
      {
        perror("main");
      }
      for (int i = 0; i < 3; i++)
      {
        *(p + i) = i;
      }
      free(p);
      p = NULL;
      return 0;
    }
    
    

5.2.3 realloc

// 原型
void* realloc (void* ptr, size_t size);
// 参数:ptr -- 指向由malloc calloc realloc创建的动态内存块
//      size -- 内存块新的大小 单位byte
// 返回值:调整之后的内存块地址

// 功能:调正ptr指向的内存块大小
  • realloc函数调整内存空间时存在3种情况

    • 情况1:原有空间后面有够大的空间 — 直接在原有空间后面再开辟一块指定大小的空间
    • 情况2:原有空间后面的空间不够大 — 在动态内存中寻找足够大的空间,并开辟,把原有空间的数据拷贝到新空间中,并将原堆区空间释放
    • 情况3:在堆区空间并没有找到足够大的空间用来调整原动态空间的大小,此时函数返回NULL


      所以realloc函数返回的void*空指针值,可能还是被调整的动态空间的指针,也可能是新的动态空间的地址,也可能是NULL
  • 使用1

    int* p = (int*)malloc(3 * sizeof(int)); // 申请3个int大小的堆区内存空间
    // 当内存发现不够用时,使用realloc函数调整动态内粗空间的大小
    p = realloc(p, 5 * sizeof(int)); // ×
    // 能否直接使用 p 来接受该函数的返回值呢?
    
    // 答案:不能
    // 如果调整空间大小失败,即在堆区并没有找到足够大的空闲空间
    // 在该函数返回NULL,如果使用p接受返回值,此时 p=NULL
    // 导致 p 不能找到原来内存空间,本来 p 指向的内存空间的数据丢失
    // 所以应该创建一个临时指针变量用来接受返回值,再判断是否为NULL
    // 如果为NULL,则调正空间失败,另作打算;如果不为NULL,再将临时指针变量复制给p
    // 以保证动态内存空间操作的一致性
    // 代码应如下:
    int* ptr = realloc(p, 5 * sizeof(int));
    if (ptr != NULL)
    {
      p = ptr;
    }
    
    
  • 使用2

    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
      int* p = (int*)realloc(NULL, 3 * sizeof(int));
      // ptr=NULL 直接在堆区开辟12字节空间
      // 等价于
      // int* p = (int*)malloc(3 * sizeof(int));
      return 0;
    }
    
    

5.2.4 free

// 原型
void free (void* ptr);
// 参数:需要释放的空间的地址

// 功能:动态内存的释放和回收

  • 如果参数ptr指向的空间不是动态开辟的,free函数的行为是未定义的
  • 如果参数ptrNULL指针,则函数什么也不做

在使用free释放动态内存空间之后,需要再将指向动态内存的指针置为NULL。原因如下:第一点,如果指针变量仍然指向那块内存空间,但是那块内存空间已经释放,对该指针变量操作必然会引发异常;第二点,置为NULL之后可以避免多次释放动态内存空间,因为当free函数的参数为NULL时,什么也不做;第三点,free函数释放内存空间,并不会将指针变量置为NULL,况且传递的是指针变量,也不是指针变量的地址。

5.3 常见的动态内存错误

5.3.1 对NULL指针的解引用操作

int main()
{
  int* p = (int*)malloc(4); // 开辟4byte动态空间 如果开辟动态内存失败
  *p = 20;                  // 则对 p 解引用操作 会引发异常
  free(p);
  return 0;
}

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

int main()
{
  int* p = (int*)malloc(4); // 开辟4byte动态空间
  if (p == NULL)
  {
    perror("test");
  }
  *(p + 1) = 20; // p+1 已经在访问开辟的动态空间之后的内存 造成越界访问
  free(p);
  return 0;
}

5.3.3 对非动态开辟内存使用free释放

int main()
{
  int a = 7; // a在栈区开辟空间 并不是动态内存
  int* p = &a;
  free(p);
  return 0;
}

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

int main()
{
  int* p = (int*)malloc(8);
  if (p == NULL)
  {
    perror("test");
  }
  p++;
  free(p); // 此时p已经不再指向开辟内存空间的首地址
  return 0;
}

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

int main()
{
  int* p = (int*)malloc(100);
  if (p == NULL)
  {
    perror("test");
  }
  *p = 100;

  free(p); // 第一次释放
  free(p); // 第二次释放
  return 0;
}

// free(p) 释放之后 将p置为NULL 可以避免多次释放

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

#include <stdio.h>
#include <stdlib.h>

void test()
{
  int* p = (int*)malloc(sizeof(int));
  if (p==NULL)
  {
    perror("test");
  }
  *p = 100;
}


int main()
{
  test();
  //while (1); // 模拟服务器程序(程序上线持续运行)
  return 0;
}

5.4 经典笔试题

指出代码段中存在的问题并修正。

5.4.1 练习1

#include <stdio.h>
#include <stdlib.h>

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;
}

存在问题:① 第12行 GetMemory(str); 传参是值传递,GetMemory函数内的 p 只是str的一份临时拷贝,并不会影响Test函数内str变量的值,所以 GetMemory(str); 执行之后,str值依然是NULLstrcpy(str, "hello world!"); 向空指针拷贝字符串则会引发异常。② 第六行p = (char*)malloc(100); 向堆区申请一块内存空间,但是并没有释放该空间,造成内存泄漏。

  • 修改如下:
    • 方法一

      void GetMemory(char** p)
      {
        *p = (char*)malloc(100); // 修改str变量的值
      }
      
      void Test(void)
      {
        char* str = NULL;
        GetMemory(&str); // 将str的地址传递给GetMemory函数
        strcpy(str, "hello world!");
        printf(str);
        free(str); // 释放向堆区申请的内存空间
        str = NULL;
      }
      
      int main()
      {
        Test();
        return 0;
      }
      
      
    • 方法二

      char* GetMemory(char* p)
      {
        p = (char*)malloc(100);
        return p; // 将向堆区申请的空间地址返回
      }
      
      void Test(void)
      {
        char* str = NULL;
        str = GetMemory(str); // str为GetMemory函数内向堆区申请的地址空间
        strcpy(str, "hello world!");
        printf(str);
        free(str); // 释放向堆区申请的内存空间
        str = NULL;
      }
      
      int main()
      {
        Test();
        return 0;
      }
      

5.4.2 练习2

#include <stdio.h>
#include <stdlib.h>

char* GetMemory()
{
  char p[] = "hello world";
  return p;
}

void Test(void)
{
  char* str = NULL;
  str = GetMemory();
  printf(str);
}

int main()
{
  Test();
  return 0;
}

存在问题:GetMemory 函数内在栈区开辟一块内存空间,用来存放字符串"hello world",并将字符串地址赋值给变量p ,但是函数返回之后,在堆区开辟的内存空间释放,尽管通过p将那块空间地址返回给str变量,但那块空间已被操作系统接管,不属于该程序,则造成非法访问。返回栈空间地址的问题。

5.4.3 练习3

#include <stdio.h>
#include <stdlib.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 main()
{
  Test();
  return 0;
}

存在问题:在GetMemory函数中*p = (char*)malloc(num);向堆区申请的内存空间使用之后并没有释放,造成内存泄漏。

  • 修改如下
    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;
    }
    

5.4.4 练习4

#include <stdio.h>
#include <stdlib.h>

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

int main()
{
  Test();
  return 0;
}

存在问题:第8行free(str);char* str = (char*)malloc(100);向堆区申请的内存空间释放,归还给操作系统,所以之后不能再访问str指向的内存空间。但是free(str);并不会将str置为NULL,所以if语句中的条件判断为真,对str进行操作,造成异常。

  • 修改如下
    #include <stdio.h>
    #include <stdlib.h>
    
    
    void Test(void)
    {
      char* str = (char*)malloc(100);
      strcpy(str, "hello");
      free(str);
      str = NULL; // 将str置为NULL if语句判断为假 则不会对str再进行操作
      if (str != NULL)
      {
        strcpy(str, "world");
        printf(str);
      }
    }
    
    int main()
    {
      Test();
      return 0;
    }
    

5.5 柔性数组(或伸缩性数组成员)

C99中,结构体中的最后一个元素允许是未知大小的数组,称为柔性数组成员

例如:(有些编译器可能只支持写法1和写法2中的一种写法)

  • 写法1

    struct fa
    {
      int i;
      int a[0]; // 柔性数组成员
    };
    
  • 写法2

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

5.5.1 柔性数组的特点

  • 结构体中柔性数组成员前面必须有至少一个其他成员

  • sizeof返回的这种结构大小不包括柔性数组的内存

    #include <stdio.h>
    
    struct S
    {
      int n;
      int a[]; // 柔性数组
    };
    
    int main()
    {
      printf("%d\n", sizeof(struct S)); // 4
      return 0;
    }
    
    // 输出结果 
    // 4
    // 结构体S的大小仅仅是结构体成员n的大小 而不包括a[]
    
  • 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构体的大小,以适应柔性数组的预期大小

5.5.2 使用实例

#include <stdlib.h>
#include <stdio.h>

struct S
{
  int n;
  int arr[0]; // 柔性数组成员
};

int main()
{
  // 期望arr是4个整形的数组
  struct S* ps = (struct S*)malloc(sizeof(struct S) + sizeof(int) * 4);
  if (ps==NULL)
  {
    perror("空间开辟失败");
  }
  ps->n = 4; // 存放数组arr的容量大小
  for (int i = 0; i < 4; i++)
  {
    ps->arr[i] = i;
  }
  // 增加数组容量 8个整形的数组
  struct S* ptr = (struct S*)realloc(ps, sizeof(struct S) + sizeof(int) * 8);
  if (ptr == NULL)
  {
    perror("增容失败");
  }
  ps = ptr;
  // 使用
  ps->n = 8;
  for (int i = 4; i < 8; i++)
  {
    ps->arr[i] = i;
  }
  // 打印数组数据
  for (int i = 0; i < 8; i++)
  {
    printf("%d ", ps->arr[i]);
  }

  // 释放内存空间
  free(ps);
  ps = NULL;
  return 0;
}

5.5.3 使用指针模拟柔性数组

#include <stdlib.h>
#include <stdio.h>

struct S
{
  int n;
  int* pa; // 数组指针
};

int main()
{
  // 结构体开辟空间
  struct S* ps = (struct S*)malloc(sizeof(struct S));
  if (ps == NULL)
  {
    return 1;
  }
  // 为结构体中数组开辟空间,pa指向数组地址(堆区开辟的内存空间地址)
  ps->pa = (int*)malloc(sizeof(int) * 3);
  if (ps->pa == NULL)
  {
    return 1;
  }
  // 使用
  // coding...
  // 增加数组容量 存放6个整形
  int* ptr = (int*)malloc(sizeof(int) * 6);
  if (ptr == NULL)
  {
    return 1;
  }
  ps->pa = ptr;
  // 使用
  // coding...
  // 释放内存
  free(ps->pa); // 释放数组内存
  ps->pa = NULL;
  free(ps); // 释放结构体内存
  ps = NULL;
  return 0;
}

5.5.4 柔性数组的优势

使用指针模拟同样可以实现柔性数组相同的功能,那柔性数组有什么优势呢?

  1. 方便内存释放 — 使用柔性数组,给结构体分配内存,一次申请,一次释放。
  2. 有利于访问速度 — 柔性数组,堆区申请的内存空间是连续的,有益于提高访问速度,且减少内存碎片。(影响不大)

其他:

C语言结构体里的成员数组和指针 | 酷 壳 - CoolShell


下一篇: 12. C语言进阶--自定义类型→