内存管理

    技术2022-05-20  29

    内存管理

    1 C 程序结构

             C 程序在没有调入内存之前(也就是在存储时),分为代码区( text )、数据区( data )和未初始化数据区( bss 3 个部分。

    ¨          代码区 存放 CPU 执行的机器指令,即 函数体的二进制代码 。由于对于频繁被执行的程序,只需要在内存中有一份代码即可,所以代码区是可共享的 (可以被别的程序调用)。为了防止程序意外地修改代码区的机器指令,通常代码区是只读的

    ¨          全局初始化数据区和静态数据区 包含已经被初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。

    ¨          未初始化数据区 存储的是全局未初始化变量。

             以上谈的是存储时 C 语言的程序结构,下面看看运行时 C 语言的程序结构。

    ¨          代码区 该区的机器指令根据程序设计流程依次执行。代码区的指令包括操作码和要操作的对象(或对象地址引用,即寄存器间接寻址等)。如果操作对象是立即数,则将直接包含在代码中;如果操作对象是局部数据,则将在栈区分配空间,然后引用该数据地址;如果操作对象存在于 BSS 区和数据区,则在代码区中也将引用该数据地址。

    ¨          全局初始化数据区和静态数据区 只做一次初始化

    ¨          未初始化数据区 在运行时改变其值

    ¨          栈区 由编译器自动分配释放,存放函数的参数值、局部变量的值等等, 其操作方式类似于数据结构中的栈。 这里简要谈一下 C 程序函数调用过程中栈框架的建立过程:

    1)  第一个进栈的是主函数中函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址(目的是为了恢复现场

    2)  然后是函数的各个参数,在大多数的 C 编译器中,参数是由右往左入栈的

    3)  然后是函数中的局部变量(注意静态变量是不入栈的)

    4)  当本次函数调用结束后,局部变量先出栈,然后是参数 ,最后栈顶 指针 指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

    ¨          堆区 用于动态分配内存 ,位于未初始化数据区和栈区之间, 一般由程序员分配和释放,若程序员不释放,程序结束时可能由 OS 回收。

              问题:为什么要专门开辟代码区

    一个进程在运行的过程中,代码是根据流程一次执行的,只需要执行一次 (当然跳转和递归也可使代码执行多次),然而程序可能会对数据进行多次访问(但没有必要为了对数据进行多次访问而多次访问代码 ),此时我们就有必要把代码区和数据区区分开来管理。

              一例程序

    //main.cpp

    int a = 0;   //a 在全局初始化数据区

    char *p1;    //p1 在全局未初始化数据区

    int main(int argc, char *argv[ ])

    {

    int b;      // b 为局部变量,所以存储于栈区

    char s[] = "abc";   // s 为数组局部变量,存在于栈区

    char *p2;   // p2 为局部变量,所以存储于栈区

    char *p3 = "123456";   // 123456/0 在常量区(已初始化数据区) p3 在栈上。

    static int c =0 // c 存储于静态数据区(静态数据区和全局初始化区同在一个区域)

    p1 = (char *)malloc(10);   // 系统动态分配得来的 10 20 字节的区域就在堆区

    p2 = (char *)malloc(20);  

    free(p1);

    free(p2);

    return 0;

    }

     

     

     

    2 、内存的分配(内存的申请)

    1)  申请方式

    ¨      Stack (静态分配):静态对象是有名字的变量,可以直接对其进行操作。由系统自动分配内存。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为 b 开辟空间。

    ¨      Heap (动态分配):动态对象是没有名字的变量,需要通过指针间接地对它进行操作。需要程序员自己申请内存,并指明大小。

    C malloc 函数 ,如 p1 = (char *)malloc(10);

    C++ 中用 new 运算符,如 p2 = new char[20];    //(char *)malloc(20);

    分配堆空间之后, p1 p2 得到所分配堆空间首地址,将会指向堆空间。

    2)  申请后系统的响应

    ¨      栈:只要栈的剩余空间大于所申请空间, 系统将为程序提供内存,否则将报异常提示栈溢出

    ¨      堆:首先 应该知道操作系统有一个记录空闲内存地址的链表当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点, 然后将该结点从空闲结点链表中删除, 并将该结点的空间分配给程序。

    其次 ,大多数系统,会在这块内存空间中的首地址处记录本次分配的大小这样,代码中的 delete 语句才能正确的释放本内存空间。

    此外 ,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中 。

     

    3 申请大小的限制

    栈: Windows 下,栈是高地址向低地址扩展的数据结构 , 是一块连续 的内存的区域。 这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的

    WINDOWS 下,栈的大小是 2M (也有的说是 1M ,总之它是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示 overflow 。因此,能从栈获得的空间较小。

    堆:堆是低地址向高地址扩展的数据结构是不连续的内存区域 。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。

    堆的大小受限于计算机系统中有效的虚拟内存 ,由此可见,堆获得的空间很灵活。

    但由程序员操作的过程中容易发生内存泄露,也极易产生内存空间的不连续,即内存碎片(频繁使用 malloc free new delete )的结果)。

    4)     申请效率的比较

    ¨      栈由系统自动分配,速度较快 ,但程序员是无法控制的。

    ¨      堆是由 malloc 或者 new 分配的内存,一般速度比较慢 ,而且容易产生内存碎片 , 不过用起来最方便 .

    5 分配效率的比较

           栈是机器提供的数据结构,机器会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令。

    堆则是 C 函数库提供的,它的机制很复杂。 首先 应该知道操作系统有一个记录空闲内存地址的链表当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点, 然后将该结点从空闲结点链表中删除, 并将该结点的空间分配给程序。

    此外 ,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

    可见,堆得存取效率和栈比较起来要低得多。

    3 、下面再给出一个数据存储区域的实例

    #include <stdio.h>

    #include <malloc.h>

    #include <unistd.h>

    #include <alloca.h>

     

    extern void afunc(void);  // 声明 afunc() 函数

    extern etext, edata, end;   // 声明三个外部变量

     

    int bss_var; // 未初始化全局数据存储在 BSS

    int data_var = 42; // 初始化全局数据存储在数据区

     

    // 定义了一个宏,用于打印地址

    #define SHW_ADR(ID, I) printf("the %8s/t is at adr:%8x/n", ID, &I);

     

    int main(int argc, char *argv[])

    {

           char *p, *b, *nb;

           printf("Adr etext:%8x/t Adr edata%8x/t Adr end %8x/t/n", &etext, &edata,&end);

           printf("/ntext Location:/n");

           SHW_ADR("main", main);  // 查看代码段 main 函数位置

           SHW_ADR("afunc", afunc);   // 查看代码段 afunc 函数位置

     

           printf("/NBSS Location:/n");

           SHW_ADR("bss_var", bss_var);  // 查看 BSS 段变量位置

     

           printf("/NDATA Location:/n");

           SHW_ADR("data_var", data_var);  // 查看数据段变量位置

     

           printf("/nSTACK Location:/n");

     

           afunc();

     

           p = (char *)alloca(32);     // 从栈中分配空间

           if(p != NULL){

                  SHW_ADR("start", p);    // 打印栈空间的起始位置

                  SHW_ADR("end", p+31); // 打印栈空间的结束位置

           }

     

           b = (char *)malloc(32*sizeof(char)); // 从堆中分配空间

           nb = (char *)malloc(16*sizeof(char)); // 从堆中分配空间

     

           printf("/NHEAP Location:/n");

           printf("the heap start: %p/n", b); // 打印堆起始位置

           printf("the heap end: %p/n", (nb+16*sizeof(char)));   // 打印堆结束位置

     

           printf("/nb and nb in Stack/n");

           SHW_ADR("b", b);          // 打印栈中字符指针变量 b 的存储位置

           SHW_ADR("nb", nb);  // 打印栈中字符指针变量 nb 的存储位置

     

           free(b);  // 释放申请的堆空间

           free(nb);   // 释放申请的堆空间

          

    return 0;

    }

     

    void afunc(void)

    {

           static int long level = 0; // 静态数据存储在数据段中

           int stack_var;  // 局部变量,存储在栈区

           if(++level == 5 ){

                  return;

           }

           printf("stack_var is at:%p/n", &stack_var); // 打印局部变量的地址

           afunc(); // 递归执行 afunc 函数

    }

    4 、介绍几个内存管理函数

    1)     Malloc/free 函数

    原型: extern void *malloc(size_t   num_bytes);  

    头文件: include <stdlib.h>  

    功能:分配长度为 num_bytes 字节的内存块   

    返回值:如果分配成功则返回 指向被分配内存首地址的指针 ,否则返回空指针 NULL

    说明:

    ¨          该函数返回为 void 型指针,因此必要时要进行类型转换。   

    ¨          当内存不再使用时,应使用 free() 函数将内存块释放。   

    问题:为什么不再使用时,要用 free() 释放掉所申请的内存?

    ¨          由于内存区域总是有限的,不能无限制地分配下去。

    ¨          程序应该尽可能地去节省资源,当申请的堆空间不再使用时,应该释放掉,交由其它进程来使用。

    注意:不能用 free() 来释放非 malloc(), calloc(), realloc() 函数所创建的堆空间 , 否则会发生错误。

    2 new/delete (在 C++ 中)

           使用 new/delete 运算符实现内存管理比 malloc/free 函数更有优越性。它们的定义如下:

    Static void* operator new(size_t sz);

    Static void* operator delete(void* p);

    先看一段 C++ 代码:

    void test(void)

    {

    // 申请一个 sizeof obj )大小的一块动态内存,并把头指针赋值给 obj 类型的指针变量 a

           obj *a = new obj;

           delete a; // 清除并且释放所申请的内存

    }

    下面通过一段代码具体介绍一下 new/delete 的用法:

    Class A

    {

           Public:

                  A() { count << “A is here!” << endl; }   // 构造函数

                  ~A() { count << “A is here!” << endl; }  // 析构函数

           Private:

                  Int I;

    };

    A* pA = new A;   // 调用 new 运算符申请空间

    delete pA;     // 删除 pA

           其中,语句 new A 完成了一下两个功能:

    A.      调用 new 运算符,在堆上分配一个 sizeof(A) 大小的内存空间。

    B.     调用构造函数 A() ,在所分配的内存空间上初始化对象。

    语句 delete pA 完成的是相反的两件事:

    A.      调用析构函数 ~A() ,销毁对象。

    B.     调用运算符 delete ,释放内存。

    注意:

    ¨      使用 new 比使用 malloc() 有以下优点

    A.      New 自动计算要分配给对象的内存空间大小,不使用 sizeof 运算符,这样一来简单,而来可以避免错误。

    B.     自动地返回正确的指针类型,不用进行强制类型转换。

    C.     用构造函数给分配的对象进行初始化。

    ¨      使用 malloc 函数和 new 分配内存的时候,本身并没有对这块内存空间做清零等任何工作。因此,申请内存空间后,其返回的新分配的空间是没有用零填充的,程序员须使用 memset() 函数来初始化内存。

    3)  realloc 函数(更改已经配置的内存空间)

    头文件: include <stdlib.h>  

    函数定义:

           void *realloc(void *ptr, size_t size)

    参数 ptr 为先前由 malloc calloc realloc 所返回的内存指针 ,而参数 size 为新配置的内存大小。

           realloc 函数用来从堆上分配内存,当需要扩大一块内存空间时, realloc() 试图直接从堆上当前内存段后面的字节中 获得更多的内存空间:

    ¨      如果能够分配成功,则返回指向这块新内存空间的首地址,而将原来的指针( realloc 函数的参数指针)指向的空间释放掉;

    ¨      如果当前内存段后面的空闲字节不够,那么就使用堆上第一个能够满足这一要求的内存块,将目前的数据复制到新的位置,而将原来的数据块释放掉;

    ¨      如果内存不足,重新申请空间失败,则返回 NULL ,此时原来的指针( realloc 函数的参数指针)仍有效。

    #include <stdio.h>

    #include <stdlib.h>

     

    int main (int argc, char* argv[], char* envp[])                                          // 主函数

    {

      int input;

      int n;

      int *numbers1;

      int *numbers2;

      numbers1 = NULL;

     

      if((numbers2 = (int *)malloc(5*sizeof(int))) == NULL)          //numbers2 指针申请空间

    {

    printf("malloc memory unsuccessful");

    //free(numbers2);

    //numbers2=NULL;

    exit(1);

      }

      for (n=0; n<5; n++)                                                                      // 初始化 (0-4)

      {

          *(numbers2+n)=n;

          printf("numbers2's data: %d/n", *(numbers2 +n));     // 0-4 打印出来

      }

     

      printf("Enter an integer value you want to remalloc ( enter 0 to stop)/n"); // 新申请空间大小 

      scanf ("%d", &input );

        

      numbers1 = (int *)realloc(numbers2, (input +5)*sizeof(int));                     // 重新申请空间

      if (numbers1 == NULL)

    {

             printf("Error (re)allocating memory");

             exit (1);

      }

     

    for(n=0;n<5;n++)                          // 5 个数是从 numbers2 (原指针空间)拷贝而来

    {

    printf("the numbers1s's data copy from numbers2: %d/n", *(numbers1 +n));

    }

     

      for(n=0; n<input; n++)                                             // 新数据初始化( 0-input

      {

          *(numbers1+5+n ) = n*2;

          printf ("nummber1's new data: %d/n", *(numbers1+5+n ));  // numbers1++;

      }

     

      printf("/n");

        free(numbers1);                                                                           // 释放 numbers1

      numbers1 = NULL;

    //  free(numbers2);                 // 不能再释放 numbers2 ,因为 number2 早已被系统自动释放

      return 0;

    }


    最新回复(0)