内存管理

内存管理

变量和函数的生命周期

变量

  • 全局变量
    • 概念
      • 定义在函数外部的变量
    • 作用域
      • 从定义位置开始,默认到本文件内部
      • 其他文件如果想要使用,可以通过声明方式将作用域导出
  • 局部变量
    • 概念
      • 定义在函数内部的变量
    • 作用域
      • 从定义位置开始,到包裹该变量的第一个右大括号结束
  • static 全局变量
    • 定义语法
      • 在变量定义之前添加 static 关键字,例如:static int a = 10;
    • 作用域
      • 被限制在本文件内部,不允许通过声明导出到其他文件
  • static 局部变量
    • 定义语法
      • 在局部变量定义之前添加 static 关键字
    • 作用域
      • 从定义位置开始,到包裹该变量的第一个右大括号结束
    • 特性
      • 静态局部变量只定义一次,在全局位置

      • 通常用来做计数器

        #define _CRT_SECURE_NO_WARNINGS
        #include 
        #include 
        
        int b;
        
        void test(void)
        {
            static int b = 1;
            printf("%d\n", b++);
        }
        
        int main(void)
        {
            for (size_t i = 0; i < 10; i++)
            {
                test();  // 10
            }
            printf("%d\n", b); // 0
        
            system("pause");
            return 0;
        }
        

函数

  • 全局函数
    • 定义语法
      • 函数原型 + 函数体
  • static 函数
    • 定义语法
      • static + 函数原型 + 函数体
    • 作用域
      • static 函数只能在本文件内部使用,其他文件即使声明也无效 (类似于 static 变量)

变量和函数的生命周期

内存管理

内存四区模型

代码段

  • .text 段,程序源代码 (二进制形式)

数据段

  • 只读数据段 .rodata
    • 存放常量
  • 初始化数据段 .data
    • 存放初始化为非 0 的全局变量和静态变量
  • 未初始化数据段 .bss
    • 存放初始化为 0、未初始化的全局变量和静态变量
    • 程序加载执行前,会将该段整体赋值为 0

栈 (stack)

  • 在其上开辟栈帧
    • Windows:1-10M
    • Linux:8-16M
  • 特性
    • 存储特性:FILO (first in last out)
    • 系统自动管理:自动分配、自动释放
    • 比较小

堆 (heap)

  • 给用户自定义数据提供空间
  • 空间足够大,约 1.3 G+

示意图

内存管理

内存分区

编译四步骤

  • 预处理
    • 宏定义展开、头文件展开、条件编译,这里不检查语法
  • 编译
    • 将预处理后的文件编译成汇编文件,这里检查语法
  • 汇编
    • 将汇编文件生成目标文件 (二进制文件)
  • 链接
    • 将目标文件链接为可执行文件

程序的内存分区模型

  • 运行之前
    • 代码区 (.text 段)
      • 是共享的 (其它程序可调用它)、只读的 (防止程序以外修改它的指令)
    • .data 和 .bss 合起来称为全局区/静态区
      • 全局初始化数据区/静态数据区 (.data 段)
        • 包括已初始化的全局变量、已初始化的静态变量和常量
        • 全局静态存储区内的常量分为常变量和字符串常量。这里的常变量和局部变量不同。
          • 局部变量存放于栈,实际可通过指针或引用进行修改
          • 全局常变量存放于静态常量区,不可以间接修改
      • 未初始化数据区 (.bss 区)
        • 保存全局未初始化的变量和未初始化的静态变量
        • .bss 区的数据在程序开始执行前被内核初始化为 0 或 NULL
  • 运行之后

    程序在加载到内存之前,代码区和全局区的大小是固定的,程序运行期间不能改变。
    程序运行后,操作系统把程序从硬盘加载到内存,除了根据可执行程序的信息分出代码区 (.text)、数据区 (.data) 和未初始化数据区 (.bss) 之外,还额外增加了栈区、堆区。

    • 代码区 (.text 段)

    • 全局区/静态区

      • 全局初始化数据区/静态数据区 (.data 段)
      • 未初始化数据区 (.bss 区)
    • 栈区 stack
      • 特性
        • 符合先进后出的数据结构
        • 编译器自动管理 (分配和释放)
        • 容量有限,会溢出
      • 示例
        #define _CRT_SECURE_NO_WARNINGS
        #include 
        #include 
        #include 
        
        int* myFunc()
        {
            int a = 10;
            return &a;
        }
        
        char* getString()
        {
            char str[] = "hello world";
            return str;
        }
        
        int main(void)
        {
            int *p = myFunc();
            printf("%d\n", *p);  // 返回 10
            printf("%d\n", *p);  // 返回 8460943
        
            char* q = NULL;
            q = getString();
            printf("%s\n", q);  // 返回 烫烫烫烫烫烫橚?
        
            system("pause");
            return 0;
        }
        ``
        
        - 局部变量 a 早已被释放,因此没有权限操作这块内存空间
        - 第一次 printf,返回 10,是由于编译器自动优化导致的
        
    • 堆区 heap
      • 特性
        • 容量远远大于栈,不是无限
        • 手动开辟 (malloc),手动释放 (free);若不手动释放,在程序结束时会隐式回收
      • 注意事项
        • 示例:可以正确显示,因为没有手动释放
          #define _CRT_SECURE_NO_WARNINGS
          #include 
          #include 
          #include 
          
          int* myFunc()
          {
              int a = 10;
              return &a;
          }
          
          char* getString()
          {
              char str[] = "hello world";
              return str;
          }
          
          int* getSpace()
          {
              int* p = malloc(sizeof(int) * 5);
          
              if (p == NULL)
              {
                  return;
              }
          
              for (int i = 0; i< 5; i++)
              {
                  p[i] = i + 10;
                  printf("%d\n", p[i]);
              }
          
              return p;
          }
          
          int main(void)
          {
              int *p = myFunc();
              printf("%d\n", *p);  // 返回 10
              printf("%d\n", *p);  // 返回 8460943
          
              char* q = NULL;
              q = getString();
              printf("%s\n", q);  // 返回 烫烫烫烫烫烫橚?
          
              int *k = getSpace();
          
              for (int i = 0; i < 5; i++)
              {
                  p[i] = i + 10;
                  printf("%d\n", k[i]);
              }
          
              free(k);
              k = NULL;
          
              system("pause");
              return 0;
          }
          
        • 给指针开辟内存时,传入的函数中不要用相同等级指针,需要传入指针的地址,函数中接收这个地址后再进行开辟内存
          #define _CRT_SECURE_NO_WARNINGS
          #include 
          #include 
          #include 
          
          // 错误示例
          void allocateSpace1(char *pp)
          {
              char* temp = malloc(100);
              memset(temp, 0, 100);
              strcpy(temp, "hello world");
              pp = temp;
          }
          
          void test0501()
          {
              char* p = NULL;
              allocateSpace1(p);
              printf("%s\n", p);
          }
          
          // 正确示例
          void allocateSpace2(char **pp)
          {
              char* temp = malloc(100);
              memset(temp, 0, 100);
              strcpy(temp, "hello world");
              *pp = temp;
          }
          
          void test0502()
          {
              char* p = NULL;
              allocateSpace2(&p);
              printf("%s\n", p);
          }
          
          int main(void)
          {
              //test0501();  // 返回 (null)
              test0502();  // 返回 hello world
          
              system("pause");
              return 0;
          }
          
          • 示意图
            内存管理

全局区/静态区

  • 全局变量区
    • extern 变量
      • 告诉编译器,下面代码中出现某变量不要报错,是外部链接属性,在其他文件中
        #define _CRT_SECURE_NO_WARNINGS
        #include 
        #include 
        #include 
        
        int main(void)
        {
            extern int g_a;  // 在另一个文件中初始化 int g_a = 10;在本文件中使用 extern 链接过去
            printf("%d\n", g_a);
        
            system("pause");
            return 0;
        }
        
        • 在其他文件中定义 g_a
        • 链接阶段的错误:一个无法解析的外部命令
      • extern 可以提高变量作用域
        • C 语言下,全局变量前都隐式加了关键字 extern
  • 静态变量区
    • static 变量
      • 在运行前分配,程序运行结束
      • 在本文件内可以使用静态变量
  • 常量区
    • const 修饰的变量
      • const 修饰全局变量
        • 直接修改,失败
        • 间接修改,语法通过,运行失败,受到常量区保护
      • const 修饰局部变量
        • 直接修改,失败;间接修改,成功,存放在栈上
        • 伪常量
          • 在 C 语言中,const 修饰的局部变量,称为伪常量
          • 不可以初始化数组
      • 示例
        #define _CRT_SECURE_NO_WARNINGS
        #include 
        #include 
        #include 
        
        const int a = 0;  // 全局变量
        
        // 修改全局变量
        void test_0701()
        {
            //a = 100;  // 直接修改,失败
            printf("%d\n", a);
        
            int* p = &a;
            *p = 20;  // 间接修改,失败
            printf("%d\n", a);
        }
        
        // 修改局部变量
        void test_0702()
        {
            const int b = 100;
            //b = 10;  // 直接修改,失败
            printf("%d\n", b);
        
            int* p = &b;
            *p = 20;  // 间接修改,成功
            printf("%d\n", b);
        }
        
        int main(void)
        {
            //test_0701();
            test_0702();
        
            system("pause");
            return 0;
        }
        
    • 字符串常量
      • 字符串地址是否相同
        • tc2.0,同文件字符串常量不同
        • VS,同文件和不同文件的多个相同字符串常量看成一个
        • Dev c++、Qt,同文件相同,不同文件不同
      • 不可以修改字符串常量

        ANSI 并没有指定出字符串是否可以修改的标准,根据编译器的不同,可能最终结果也不相同。

      • 示例

        #define _CRT_SECURE_NO_WARNINGS
        #include 
        #include 
        #include 
        
        void test0801()
        {
            char* p1 = "hello world";
            char* p2 = "hello world";
            char* p3 = "hello world";
            char* p4 = "hello world";
        
            printf("%p\n", p1);
            printf("%p\n", p2);
            printf("%p\n", p3);
            printf("%p\n", p4);
        
            printf("%p\n", &"hello world");
        
            p1[0] = 'c';
            printf("%p\n", p1);  // 也不报错,但是无法修改
        }
        
        int main(void)
        {
            test0801();  // 返回值都是 00BB8B44
        
            system("pause");
            return 0;
        }
        

函数调用模型

  • 宏函数

    通常将一些频繁、短小的函数,写成宏函数。宏函数需要加小括号修饰,保证运算的完整性。

    • 优点
      • 以空间换时间。比普通函数在一定程度上,效率高,省去普通函数入栈、出栈时间上的开销
    • 示例
      #define _CRT_SECURE_NO_WARNINGS
      #include 
      #include 
      #include 
      
      #define MYADD1(x, y) x + y  // 计算会有歧义
      #define MYADD2(x, y) ((x) + (y))  // 正确方法
      
      int main(void)
      {
          printf("x + y = %d\n", MYADD1(10, 20));  // x + y = 30
          printf("x + y = %d\n", MYADD1(10, 20) * 2);  // x + y = 50
      
          printf("x + y = %d\n", MYADD2(10, 20) * 2);  // x + y = 60
      
          system("pause");
          return 0;
      }
      
  • 栈帧

    函数调用所需的信息保存在栈上,通常叫栈帧 (stack frame) 或活动记录 (activate record)。

    • 示意图:入栈 (push)、出栈 (pop)
      内存管理
    • 经典操作系统中,栈总是向下增长。压栈操作使栈顶地址减小;出栈操作使栈顶地址增大。
  • 函数调用所需的信息包括
    • 函数返回地址
    • 函数形参
    • 临时变量
    • 保存的上下文 (包括函数调用前后需要保持不变的寄存器)
  • 调用惯例

    主调函数和被调函数必须有一致性约定,才能正确的调用函数,这个约定叫做调用惯例。

    • 包含内容
      • 出栈方、参数传递顺序和方式

      • 栈的维护方式 (名字修饰)

        为了在链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰。

    • 常用的调用惯例
      内存管理

      • cdecl
        • _func

        • 示例:函数 func 的完整写法

          // _cdecl 不是标准的关键字,在不同的编译器中有不同的写法
          int _cdecl fnc(int a, int b); 
          
      • stdcall
        • _func@8
  • 示意图:函数变量传递分析
    内存管理

栈的生长方向和内存存放方向

  • 栈的生长方向 (向下生长)

    #define _CRT_SECURE_NO_WARNINGS
    #include 
    #include 
    #include 
    
    void test0201()
    {
        int a = 10;  // 栈底  -> 高地址
        int b = 10;
        int c = 10;
        int d = 10;  // 栈顶  -> 低地址
    
        printf("%d\n", &a);
        printf("%d\n", &b);
        printf("%d\n", &c);
        printf("%d\n", &d);
    
        /*
        13629540  // 高地址
        13629528
        13629516
        13629504  // 低地址
        */
    }
    
    int main(void)
    {
        test0201();
    
        system("pause");
        return 0;
    }
    
    • 栈底 -> 高地址
    • 栈顶 -> 低地址
  • 内存存放方向
    • 高位字节数据 -> 放在高地址,低位字节数据 -> 放低地址
    • 例外 (小端对齐)
      内存管理

开辟和释放 heap 空间

heap 空间是连续的,可以当作数组使用。

malloc 函数

  • 函数描述
    • 申请 size 大小的空间
  • 函数原型
    void *malloc(size_t size);
    
    • 参数
      • size
        • 申请内存的大小 (字节)
    • 返回值
      • 成功,返回分配空间的起始地址;失败,返回 NULL
      • 因为返回值是 void,所以需要强转一下

free 函数

  • 函数描述
    • 释放申请的空间
  • 函数原型
    void free(void *ptr)
    
    • 参数
      • malloc 返回的地址值
  • 注意事项
    • free 后的空间不会立即失效,通常 free 后的地址值为 NULL
    • free 的地址必须是 malloc 申请的地址,否则会出错
    • 如果 malloc 之后的地址一定会变化,使用临时变量 tmp 保存一份
  • 示例
    #define _CRT_SECURE_NO_WARNINGS
    #include 
    #include 
    #include 
    
    int main(void)
    {
        //int str[10] = { 10, 20, 30 };
        int* p = (int *)malloc(sizeof(int) * 10);  // 申请的长度要与数组长度一致
        char* temp = p;  // 临时用来存储申请地址
        if (p == NULL)  // 固定用法
        {
            printf("malloc error:\n");
            return -1;
        }
    
        for (size_t i = 0; i<10; i++)
        {
            //p[i] = i + 10;  // 写数据到 malloc 空间
            *p = i + 10;
    
            //printf("%d\n", *(p+i));  // 读数据
            printf("%d\n", *p);
            p++;  // 如果这里改变指针地址,上面需要使用变量 temp 存储最初的申请地址
        }
    
        //free(p);  // 释放申请的内存
        //p = NULL;  // 手动置 null
    
        free(temp);
        temp = NULL;
    
        system("pause");
        return 0;
    }
    

calloc 函数

  • 函数描述
    • 分配内存

      在内存动态存储区中分配 nmemb 块长度为 size 字节的连续区域。calloc 自动将分配的内存置0。

    • calloc 会自动初始化数据为 0

  • 函数原型

    void *calloc(size_t nmemb, size_t size);
    
    • 参数
      • nmemb
        • 所需内存单元数量
      • size
        • 每个内存单元的大小 (单位:字节)
    • 返回值
      • 成功,分配空间的起始地址;失败,返回 NULL
  • 与 malloc 对比
    • malloc(sizeof(int) * 10)
      • 不会自动初始化数据
    • calloc(10, sizeof(int))
      • calloc 会自动初始化数据为 0

realloc 函数

  • 函数描述
    • 在堆中重新分配内存

      重新分配用 malloc 或 calloc 函数在堆中分配内存的大小。
      realloc 不会自动清理增加的内存,需要手动清理。
      如果指定的地址后面有连续的空间,那么就会在原有地址上增加内存;如果原地址后面没有空间,realloc 会重新分配新的连续内存,把旧的内存的值拷贝到新内存,同时释放旧内存。

  • 函数原型

    void *realloc(void *ptr, size_t size);
    
    • 参数
      • ptr
        • 之前用 malloc 或 calloc 分配的内存地址。如果置为 NULL,那么和 malloc 或 calloc 功能一致。
      • size
        • 重新分配内存的大小 (单位:字节)
    • 返回值
      • 成功,返回新分配的内存地址;失败,返回 NULL
  • 重新分配的机制
    • 示意图
      内存管理

    • 如果重新分配的内存比原来大,不会初始化新空间为 0

      • 先在原有空间后去查找后续的空闲空间是否足够大,如果够大,那么直接扩展
      • 如果后续空间不足,会重新找一块足够大的内存,将原有空间内的数据拷贝到新空间下,释放掉原有空间,将新空间的首地址返回
    • 如果重新分配的内存比原来小,那么会释放后续空间,只有权限操作申请空间

    • 示例

      #define _CRT_SECURE_NO_WARNINGS
      #include 
      #include 
      
      // calloc 自动初始化数据为 0
      void calloc_mem()
      {
          printf("第一次申请内存  -> 5个\n");
          int* p = malloc(sizeof(int) * 5);
          for (size_t i = 0; i  6个,但是只手动初始化5个值\n");
          p = calloc(6, sizeof(int));
          for (size_t i = 0; i  5个\n");
          int* p = calloc(5, sizeof(int));
          for (int i = 0; i  7个,后续空间足够,直接在后面扩展\n");
          p = realloc(p, 7 * sizeof(int));
          for (int i = 0; i  600个,后续空间不足,重新找一块足够大的空间,拷贝原有数据到新空间\n");
          p = realloc(p, 600 * sizeof(int));
          for (int i = 0; i  5个,重新分配的空间比原有空间小\n");
          p = realloc(p, 5 * sizeof(int));
          for (int i = 0; i < 6; i++)
          {
              printf("p[%d]: %d, %p\n", i, p[i], &p[i]);  // 
          }
      
          printf("无权操作未申请的内存 p[5]\n");
          //p[5] = 0;  // Debug Error!
      
          // 释放内存
          if (p != NULL)
          {
              free(p);
              p = NULL;
              printf("释放内存\n");
          }
      }
      
      int main(void)
      {
          printf("----------------calloc_mem() begin\n");
          calloc_mem();
      
          printf("----------------realloc_mem() begin\n");
          realloc_mem();
      
          system("pause");
          return EXIT_SUCCESS;
      }
      

示例

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

void test01()
{
    int* p = calloc(10, sizeof(int));
    if (p == NULL)
    {
        return;
    }

    for (int i = 0; i < 10; i++)
    {
        p[i] = 10 * i;
        printf("%d ", p[i]);  // 0 10 20 30 40 50 60 70 80 90
    }
    putchar('\n');

    free(p);
}

void test02()
{
    int* p1 = malloc(10 * sizeof(int));
    if (p1 == NULL)
    {
        return;
    }

    for (int i = 0; i < 10; i++)
    {
        p1[i] = i * 10;
        printf("%d ", p1[i]);  // 0 10 20 30 40 50 60 70 80 90
    }
    putchar('\n');

    int *p2 = realloc(p1, 15 * sizeof(int));
    if (p2 == NULL)
    {
        return;
    }

    printf("p1: %d, p2: %d\n", p1, p2);  // p1: 12699888, p2: 12675608

    for (int i = 0; i < 15; i++)
    {
        printf("%d ", p2[i]);  // 0 10 20 30 40 50 60 70 80 90 -842150451 -842150451 -842150451 -842150451 -842150451
    }
    putchar('\n');

    //free(p1);  // 旧的内存已经被释放,重复释放报错
    free(p2);
}

int main()
{
    test01();
    test02();

    return 0;
}

二级指针对应的 heap 空间

  • 释放空间时,先释放内层空间,再释放外层空间

  • 示意图
    内存管理

  • 示例

    #define _CRT_SECURE_NO_WARNINGS
    #include 
    #include 
    #include 
    
    int main(void)
    {
        // 申请外层指针
        int** p = (int **)malloc(sizeof(int *) * 3);
        if (p == NULL)
        {
            printf("malloc error:");
            return -1;
        }
    
        // 申请内层指针
        for (size_t i = 0; i < 3; i++)
        {
            p[i] = (int *)malloc(sizeof(int) * 5);
            if (p[i] == NULL)
            {
                printf("malloc error p+%d", i);
                return -2;
            }
        }
    
        // 写和读
        for (size_t i = 0; i<3; i++)
        {
            for (size_t j = 0; j < 5; j++)
            {
                p[i][j] = i * 10 + j;
    
                //printf("%d\n", *(*(p + i) + j));
                printf("%d\n", p[i][j]);
            }
        }
    
        // 释放内层指针
        for (size_t i = 0; i < 3; i++)
        {
            free(p[i]);
        }
    
        // 释放外层指针
        free(p);
        p = NULL;
    
        system("pause");
        return 0;
    }
    
英仔
版权声明:本站原创文章,由 英仔 2022-08-11发表,共计8770字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)