内存分配_内存分配方式 - CSDN
精华内容
参与话题
  • C语言中内存分配

    万次阅读 多人点赞 2013-09-03 19:17:42
    在任何程序设计环境及语言中,内存管理都十分重要。在目前的计算机系统或嵌入式系统中,内存资源仍然是有限的。...第2节主要介绍C语言中内存分配及释放函数、函数的功能,以及如何调用这些函数申请

    在任何程序设计环境及语言中,内存管理都十分重要。在目前的计算机系统或嵌入式系统中,内存资源仍然是有限的。因此在程序设计中,有效地管理内存资源是程序员首先考虑的问题。

    第1节主要介绍内存管理基本概念,重点介绍C程序中内存的分配,以及C语言编译后的可执行程序的存储结构和运行结构,同时还介绍了堆空间和栈空间的用途及区别。

    第2节主要介绍C语言中内存分配及释放函数、函数的功能,以及如何调用这些函数申请/释放内存空间及其注意事项。

    3.1 内存管理基本概念

    3.1.1 C程序内存分配

    1.C程序结构

    下面列出C语言可执行程序的基本情况(Linux 2.6环境/GCC4.0)。

    [root@localhost Ctest]# ls test -l     //test为一个可执行程序
    -rwxr-xr-x  1 root root 4868 Mar 26 08:10 test
    [root@localhost Ctest]# file test //此文件基本情况
    test: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV),
    for GNU/Linux 2.2.5, dynamically linked (uses shared libs), not stripped
    [root@localhost Ctest]# size test  //此二进制可执行文件结构情况
    //代码区静态数据/全局初始化数据区 未初始化数据区 十进制总和 十六进制总和 文件名
    text   data         bss    dec    hex filename
    906    284          4    1194    4aa test

    可以看出,此可执行程序在存储时(没有调入到内存)分为代码区(text)、数据区(data)和未初始化数据区(bss)3个部分。

    (1)代码区(text segment)。存放CPU执行的机器指令(machine instructions)。通常,代码区是可共享的(即另外的执行程序可以调用它),因为对于频繁被执行的程序,只需要在内存中有一份代码即可。代码区通常是只读的,使其只读的原因是防止程序意外地修改了它的指令。另外,代码区还规划了局部变量的相关信息。

    (2)全局初始化数据区/静态数据区(initialized data segment/data segment)。该区包含了在程序中明确被初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据(如字符串常量)。例如,一个不在任何函数内的声明(全局数据):

    int   maxcount = 99;

    使得变量maxcount根据其初始值被存储到初始化数据区中。

    static mincount=100;

    这声明了一个静态数据,如果是在任何函数体外声明,则表示其为一个全局静态变量,如果在函数体内(局部),则表示其为一个局部静态变量。另外,如果在函数名前加上static,则表示此函数只能在当前文件中被调用。

    (3)未初始化数据区。亦称BSS区(uninitialized data segment),存入的是全局未初始化变量。BSS这个叫法是根据一个早期的汇编运算符而来,这个汇编运算符标志着一个块的开始。BSS区的数据在程序开始执行之前被内核初始化为0或者空指针(NULL)。例如一个不在任何函数内的声明:

    long  sum[1000];
    

    将变量sum存储到未初始化数据区。

    图3-1所示为可执行代码存储时结构和运行时结构的对照图。一个正在运行着的C编译程序占用的内存分为代码区、初始化数据区、未初始化数据区、堆区和栈区5个部分。

    (点击查看大图)图3-1 C程序的内存布局

    (1)代码区(text segment)。代码区指令根据程序设计流程依次执行,对于顺序指令,则只会执行一次(每个进程),如果反复,则需要使用跳转指令,如果进行递归,则需要借助栈来实现。

    代码区的指令中包括操作码和要操作的对象(或对象地址引用)。如果是立即数(即具体的数值,如5),将直接包含在代码中;如果是局部数据,将在栈区分配空间,然后引用该数据地址;如果是BSS区和数据区,在代码中同样将引用该数据地址。

    (2)全局初始化数据区/静态数据区(Data Segment)。只初始化一次。

    (3)未初始化数据区(BSS)。在运行时改变其值。

    (4)栈区(stack)。由编译器自动分配释放,存放函数的参数值、局部变量的值等。其操作方式类似于数据结构中的栈。每当一个函数被调用,该函数返回地址和一些关于调用的信息,比如某些寄存器的内容,被存储到栈区。然后这个被调用的函数再为它的自动变量和临时变量在栈区上分配空间,这就是C实现函数递归调用的方法。每执行一次递归函数调用,一个新的栈框架就会被使用,这样这个新实例栈里的变量就不会和该函数的另一个实例栈里面的变量混淆。

    (5)堆区(heap)。用于动态内存分配。堆在内存中位于bss区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时有可能由OS回收。

    之所以分成这么多个区域,主要基于以下考虑:

    一个进程在运行过程中,代码是根据流程依次执行的,只需要访问一次,当然跳转和递归有可能使代码执行多次,而数据一般都需要访问多次,因此单独开辟空间以方便访问和节约空间。

    临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。

    全局数据和静态数据有可能在整个程序执行过程中都需要访问,因此单独存储管理。

    堆区由用户自由分配,以便管理。

    下面通过一段简单的代码来查看C程序执行时的内存分配情况。相关数据在运行时的位置如注释所述。

    //main.cpp 
    int a = 0;    //a在全局已初始化数据区
    char *p1;    //p1在BSS区(未初始化全局变量)
    main()
    {
    int b;    //b在栈区
    char s[] = "abc"; //s为数组变量,存储在栈区,
    //"abc"为字符串常量,存储在已初始化数据区
    char *p1,p2;  //p1、p2在栈区
    char *p3 = "123456"; //123456\0在已初始化数据区,p3在栈区
    static int c =0;  //C为全局(静态)数据,存在于已初始化数据区
    //另外,静态数据会自动初始化
    p1 = (char *)malloc(10);//分配得来的10个字节的区域在堆区
    p2 = (char *)malloc(20);//分配得来的20个字节的区域在堆区
    free(p1);
    free(p2);
    }

    2.内存分配方式

    在C语言中,对象可以使用静态或动态的方式分配内存空间。

    静态分配:编译器在处理程序源代码时分配。

    动态分配:程序在执行时调用malloc库函数申请分配。

    静态内存分配是在程序执行之前进行的因而效率比较高,而动态内存分配则可以灵活的处理未知数目的。

    静态与动态内存分配的主要区别如下:

    静态对象是有名字的变量,可以直接对其进行操作;动态对象是没有名字的变量,需要通过指针间接地对它进行操作。

    静态对象的分配与释放由编译器自动处理;动态对象的分配与释放必须由程序员显式地管理,它通过malloc()和free两个函数(C++中为new和delete运算符)来完成。

    以下是采用静态分配方式的例子。

    int a=100;

    此行代码指示编译器分配足够的存储区以存放一个整型值,该存储区与名字a相关联,并用数值100初始化该存储区。

    以下是采用动态分配方式的例子。

    p1 = (char *)malloc(10*sizeof(int));//分配得来得10*4字节的区域在堆区

    此行代码分配了10个int类型的对象,然后返回对象在内存中的地址,接着这个地址被用来初始化指针对象p1,对于动态分配的内存唯一的访问方式是通过指针间接地访问,其释放方法为:

    free(p1);
    

    3.1.2 栈和堆的区别

    前面已经介绍过,栈是由编译器在需要时分配的,不需要时自动清除的变量存储区。里面的变量通常是局部变量、函数参数等。堆是由malloc()函数(C++语言为new运算符)分配的内存块,内存释放由程序员手动控制,在C语言为free函数完成(C++中为delete)。栈和堆的主要区别有以下几点:

    (1)管理方式不同。

    栈编译器自动管理,无需程序员手工控制;而堆空间的申请释放工作由程序员控制,容易产生内存泄漏。

    (2)空间大小不同。

    栈是向低地址扩展的数据结构,是一块连续的内存区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,当申请的空间超过栈的剩余空间时,将提示溢出。因此,用户能从栈获得的空间较小。

    堆是向高地址扩展的数据结构,是不连续的内存区域。因为系统是用链表来存储空闲内存地址的,且链表的遍历方向是由低地址向高地址。由此可见,堆获得的空间较灵活,也较大。栈中元素都是一一对应的,不会存在一个内存块从栈中间弹出的情况。

    (3)是否产生碎片。

    对于堆来讲,频繁的malloc/free(new/delete)势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低(虽然程序在退出后操作系统会对内存进行回收管理)。对于栈来讲,则不会存在这个问题。

    (4)增长方向不同。

    堆的增长方向是向上的,即向着内存地址增加的方向;栈的增长方向是向下的,即向着内存地址减小的方向。

    (5)分配方式不同。

    堆都是程序中由malloc()函数动态申请分配并由free()函数释放的;栈的分配和释放是由编译器完成的,栈的动态分配由alloca()函数完成,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行申请和释放的,无需手工实现。

    (6)分配效率不同。

    栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行。堆则是C函数库提供的,它的机制很复杂,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大的空间,如果没有足够大的空间(可能是由于内存碎片太多),就有需要操作系统来重新整理内存空间,这样就有机会分到足够大小的内存,然后返回。显然,堆的效率比栈要低得多。

    3.1.3 Linux数据类型大小

    在Linux操作系统下使用GCC进行编程,目前一般的处理器为32位字宽,下面是/usr/include/limit.h文件对Linux下数据类型的限制及存储字节大小的说明。

    /* We don't have #include_next.   Define ANSI  for standard 32-bit words.  */
    /* These assume 8-bit 'char's, 16-bit 'short int's,   and 32-bit 'int's and 'long int's.  */

    1.char数据类型

    char类型数据所占内存空间为8位。其中有符号字符型变量取值范围为?128~127,无符号型字符变量取值范围为0~255。其限制如下:

    /* Number of bits in a 'char'. */
    #  define CHAR_BIT 8          //所占字节数
    /* Minimum and maximum values a 'signed char' can hold.  */  //有符号字符型范围
    #  define SCHAR_MIN (-128)
    #  define SCHAR_MAX 127
    /* Maximum value an 'unsigned char' can hold.  (Minimum is 0.)  */ //无符号字符型范围
    #  define UCHAR_MAX 255
    /* Minimum and maximum values a 'char' can hold.  */
    #  ifdef __CHAR_UNSIGNED__
    #   define CHAR_MIN 0
    #   define CHAR_MAX UCHAR_MAX
    #  else
    #   define CHAR_MIN SCHAR_MIN
    #   define CHAR_MAX SCHAR_MAX
    #  endif

    2.short int数据类型

    short int类型数据所占内存空间为16位。其中有符号短整型变量取值范围为?32768~32767,无符号短整型变量取值范围为0~65535。其限制如下:

    /* Minimum and maximum values a 'signed short int' can hold.  */ // 有符号短整型范围
    #  define SHRT_MIN (-32768)
    #  define SHRT_MAX 32767
    /* Maximum value an 'unsigned short int' can hold.  (Minimum is 0.)  */// 无符号短整型范围
    #  define USHRT_MAX 65535

    3.int数据类型

    int类型数据所占内存空间为32位。其中有符号整型变量取值范围为?2147483648~2147483647,无符号型整型变量取值范围为0~4294967295U。其限制如下:

    /* Minimum and maximum values a 'signed int' can hold.  */  //整形范围
    #  define INT_MIN (-INT_MAX - 1)     
    #  define INT_MAX 2147483647
    /* Maximum value an 'unsigned int' can hold.  (Minimum is 0.)  */ //无符号整形范围
    #  define UINT_MAX 4294967295U

    4.long int数据类型

    随着宏__WORDSIZE值的改变,long int数据类型的大小也会发生改变。如果__WORDSIZE的值为32,则long int和int类型一样,占有32位。在Linux GCC4.0-i386版本中,默认情况下__WORDSIZE的值为32。其定义如下:

    //come from /usr/include/bits/wordsize.h
    #define __WORDSIZE 32

    在64位机器上,如果__WORDSIZE的值为64, long int类型数据所占内存空间为64位。其中有长整型变量取值范围为-9223372036854775808L~3372036854775807L,无符号长整型变量取值范围为0~18446744073709551615UL。其限制如下:

    /* Minimum and maximum values a 'signed long int' can hold.  */ //有符号长整形范围
    #  if __WORDSIZE == 64
    #   define LONG_MAX 9223372036854775807L
    #  else
    #   define LONG_MAX 2147483647L
    #  endif
    #  define LONG_MIN (-LONG_MAX - 1L)
    /* Maximum value an 'unsigned long int' can hold.  (Minimum is 0.)  *///无符号长整形范围
    #  if __WORDSIZE == 64
    #   define ULONG_MAX 18446744073709551615UL
    #  else
    #   define ULONG_MAX 4294967295UL
    #  endif

    5.long long int数据类型

    在C99中,还定义了long long int数据类型。其数据类型限制如下:

    #  ifdef __USE_ISOC99
    /* Minimum and maximum values a 'signed long long int' can hold.  *///无符号长长整形范围
    #   define LLONG_MAX 9223372036854775807LL
    #   define LLONG_MIN (-LLONG_MAX - 1LL)
    /* Maximum value an 'unsigned long long int' can hold.  (Minimum is 0.)  *///有符号长长整形范围
    #   define ULLONG_MAX 18446744073709551615ULL
    #  endif /* ISO C99 */

    3.1.4 数据存储区域实例

    此程序显示了数据存储区域实例,在此程序中,使用了etext、edata和end3个外部全局变量,这是与用户进程相关的虚拟地址。

    在程序源代码中列出了各数据的存储位置,同时在程序运行时显示了各数据的运行位置,图3-2所示为程序运行过程中各变量的存储位置。

    图3-2 函数运行时各数据位置

    主函数源代码如下:

    [root@localhost linux_app]# cat mem_add.c
    #include
    #include
    #include
    #include
    extern void afunc(void);
    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 Locations:\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 Locations:\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);         //释放申请的空间,以避免内存泄漏
    }

    子函数源代码如下:

    void afunc(void)
    {
    static int long level=0;          //静态数据存储在数据段中
    int      stack_var;                 //局部变量,存储在栈区
    if(++level==5)
    {
    return;
    }
    printf("stack_var is at:%p\n",&stack_var);
    //      SHW_ADR("stack_var in stack section",stack_var);
    //      SHW_ADR("Level in data section",level);
    afunc();
    }

    函数运行结果如下:

    [root@localhost linux_app]# gcc -o mem_add mem_add.c //编译
    [root@localhost linux_app]# ./mem_add     //运行结果
    Adr etext: 8048702       Adr edata  8049950      Adr end  804995c
    text Location:
    the     main     is at adr: 8048418
    the    afunc     is at adr: 8048611
    bss Location:
    the  bss_var     is at adr: 8049958
    data location:
    the data_var     is at adr: 804994c
    Stack Locations:
    the stack_var in stack section    is at adr:bfbf6c44
    the Level in data section         is at adr: 8049954
    the stack_var in stack section    is at adr:bfbf6c24
    the Level in data section         is at adr: 8049954
    the stack_var in stack section    is at adr:bfbf6c04
    the Level in data section         is at adr: 8049954
    the stack_var in stack section    is at adr:bfbf6be4
    the Level in data section         is at adr: 8049954
    the    start     is at adr:bfbf6c74
    the      end     is at adr:bfbf6cf0
    Heap Locations:
    the Heap start: 0x8453008
    the Heap end:0x8453040
    b and nb in Stack
    the        b     is at adr:bfbf6c70
    the       nb     is at adr:bfbf6c6c

    如果运行环境不一样,运行程序的地址与此将有差异,但是,各区域之间的相对关系不会发生变化。可以通过readelf命令来查看可执行文件的详细内容。

    [root@localhost yangzongde]# readelf -a memadd

    3.2 内存管理函数

    3.2.1 malloc/free函数

    Malloc()函数用来在堆中申请内存空间,free()函数释放原先申请的内存空间。Malloc()函数是在内存的动态存储区中分配一个长度为size字节的连续空间。其参数是一个无符号整型数,返回一个指向所分配的连续存储域的起始地址的指针。当函数未能成功分配存储空间时(如内存不足)则返回一个NULL指针。

    由于内存区域总是有限的,不能无限制地分配下去,而且程序应尽量节省资源,所以当分配的内存区域不用时,则要释放它,以便其他的变量或程序使用。

    这两个函数的库头文件为:

    #include

    函数定义如下:

    void *malloc(size_t size)   //返回类型为空指针类型
    void free(void *ptr)

    例如:

    int *p1,*p2;
    p1=(int *)malloc(10*sizeof(int));
    p2=p1;
    ……
    free(p2) ;      /*或者free(p1)*/
    p1=NULL;       /*或者p2=NULL */

    malloc()函数返回值赋给p1,又把p1的值赋给p2,所以此时p1,p2都可作为free函数的参数。使用free()函数时,需要特别注意下面几点:

    (1)调用free()释放内存后,不能再去访问被释放的内存空间。内存被释放后,很有可能该指针仍然指向该内存单元,但这块内存已经不再属于原来的应用程序,此时的指针为悬挂指针(可以赋值为NULL)。

    (2)不能两次释放相同的指针。因为释放内存空间后,该空间就交给了内存分配子程序,再次释放内存空间会导致错误。也不能用free来释放非malloc()、calloc()和realloc()函数创建的指针空间,在编程时,也不要将指针进行自加操作,使其指向动态分配的内存空间中间的某个位置,然后直接释放,这样也有可能引起错误。

    (3)在进行C语言程序开发中,malloc/free是配套使用的,即不需要的内存空间都需要释放回收。

    下面是使用这两个函数的一个例子。

    [root@localhost yangzongde]# cat malloc_example.c 
    #include               //printf()    //(1)头文件信息
    #include              //malloc()    //(2)
    int main(int argc,char* argv[],char* envp[])   //(3)
    {
    int count;
    int* array;
    if((array=(int *)malloc(10*sizeof(int)))==NULL)  //(4)分配空间
    {
    printf("malloc memory unsuccessful");
    exit(1);
    }
    for (count=0;count<10;count++)      //(5) 赋值
    {
    *array=count;
    array++;
    }
    for(count=9;count>=0;count--)                  //(6)赋值
    {
    array--;
    printf("%4d",*array);
    }
    printf("\n");
    free(array);        //(7)释放空间
    array=NULL;       //(8)将指针置为空,避免不安全访问
    exit (0);
    }
    [root@localhost yangzongde]# gcc -o malloc_example malloc_example.c  //编译
    [root@localhost yangzongde]# ./malloc_example       //运行
    9   8   7   6   5   4   3   2   1   0

    在以上程序中,(1)句中包含stdio.h头文件,从而在后面可以调用printf()函数。(2)句中包含stdlib.h头文件,其是malloc()函数的头文件。(3)句为函数的入口位置,此处采用Linux下编程标准,返回值为int型,argc为参数个数, argv[]为参数,envp[]存放的是所有环境变量。(4)句动态分配了10个整型存储区域,此语句可以分为以下几步。

    ① 分配10个整型的连续存储空间,并返回一个指向其起始地址的整型指针。

    ② 把此整型指针地址赋给array。

    ③ 检测返回值是否为NULL。

    (5)、(6)句为数组赋值并打印输出,以免内存泄漏。(7)句调用free()函数释放内存空间。(8)句将一个NULL指针传递给array,虽然在很多情况下可以不用此句,但这样处理可以避免此指针成为野指针。

    在C++中,使用new和delete运算符来实现内存的分配和释放,使用new/delete运算符实现内存管理比使用malloc/free函数更有优越性。new/delete运算符定义如下:

    static void* operator new(size_t sz);     //new运算符
    static void  operator delete(void* p);      //delete运算符

    下面是一段C++程序代码:

    void UseNewDelete(void)
    {
    Obj  *a = new Obj;           //申请动态内存并且初始化
    //…
    delete a;                   //清除并且释放内存
    }

    下面详细介绍C++中new/delete运算符的使用方法。

    class A
    {
    public:
    A()  {   cout<<"A is here!"<<ENDL;&NBSP;&NBSP; 构造函数 ~A() {   cout<<"A is dead!"<<ENDL;&NBSP;&NBSP; }  ="" 析构函数 private:
    int i;
    };
    A* pA=new A;     //调用new运算符申请空间
    delete pA;      //删除pA

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

    (1)调用运算符new,在自由存储区分配一个sizeof(A)大小的内存空间。

    (2)调用构造函数A(),在这块内存空间上初始化对象。

    当然,delete pA完成相反的两件事:

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

    (2)调用运算符delete,释放内存。

    由此可以看出,运算符new和delete提供了动态分配和释放存储区的功能。它们的作用相当于C语言的malloc()和free()函数,但是性能更为优越。使用new比使用malloc()有以下几个优点:

    (1)new自动计算要分配给对象的内存空间大小,不使用sizeof运算符,简单,而且可以避免错误。

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

    (3)用构造函数给分配的对象进行初始化。

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

    3.2.2 realloc--更改已经配置的内存空间

    realloc()函数用来从堆上分配内存,当需要扩大一块内存空间时,realloc()试图直接从堆上当前内存段后面的字节中获得更多的内存空间,如果能够满足,则返回原指针;如果当前内存段后面的空闲字节不够,那么就使用堆上第一个能够满足这一要求的内存块,将目前的数据复制到新的位置,而将原来的数据块释放掉。如果内存不足,重新申请空间失败,则返回NULL。此函数定义如下:

    void *realloc(void *ptr,size_t size)

    参数ptr为先前由malloc、calloc和realloc所返回的内存指针,而参数size为新配置的内存大小。其库头文件为:

    #include<stdlib.h>

    当调用realloc()函数重新分配内存时,如果申请失败,将返回NULL,此时原来指针仍然有效,因此在程序编写时需要进行判断,如果调用成功,realloc()函数会重新分配一块新内存,并将原来的数据拷贝到新位置,返回新内存的指针,而释放掉原来指针(realloc()函数的参数指针)指向的空间,原来的指针变为不可用(即不需要再释放,也不能再释放),因此,一般不使用以下语句:

    ptr=realloc(ptr,new_amount)

    如果内存减少,malloc仅仅改变索引信息,但并不代表被减少的部分还可以访问,这一部分内存将交给系统内存分配子程序。

    下面是一个使用relloc函数的实例。

    [root@localhost yangzongde]# cat realloc_example.c 
    #include <stdio.h>
    #include <stdlib.h>
    int main (int argc,char* argv[],char* envp[])   //(1)主函数
    {
    int input;
    int n;
    int *numbers1;
    int *numbers2;
    numbers1=NULL;
         if((numbers2=(int *)malloc(5*sizeof(int)))==NULL) //(2)numbers2指针申请空间
    {
    printf("malloc memory unsuccessful");
    //free(numbers2);
    numbers2=NULL;
    exit(1);
    }
    for (n=0;n<5;n++)       //(3)初始化
    {
    *(numbers2+n)=n;
    printf("numbers2's data: %d\n",*(numbers2+n));
    }
         printf("Enter an integer value you want to remalloc ( enter 0 to stop)\n");//(4)新申请空间大小 
    scanf ("%d",&input);

    numbers1=(int *)realloc(numbers2,(input+5)*sizeof(int));  //(5)重新申请空间
    if (numbers1==NULL)
    {
    printf("Error (re)allocating memory");
    exit (1);
    }
         for(n=0;n<5;n++)       //(6)这5个数是从numbers2拷贝而来
    {
    printf("the numbers1s's data copy from numbers2: %d\n",*(numbers1+n));
    }
         for(n=0;n<input;n++)       //(7)新数据初始化
    {
    *(numbers1+5+n)=n*2;
    printf ("nummber1's new data: %d\n",*(numbers1+5+n)); // numbers1++;
    }
    printf("\n");
    free(numbers1);       //(8)释放numbers1
    numbers1=NULL;
    // free(numbers2);     //(9)不能再释放numbers2
    return 0;
    }
    [root@localhost yangzongde]# gcc -o realloc_example realloc_example.c
    [root@localhost yangzongde]# ./realloc_example
    numbers2's data: 0
    numbers2's data: 1
    numbers2's data: 2
    numbers2's data: 3
    numbers2's data: 4
    Enter an integer value you want to remalloc ( enter 0 to stop) //重新申请空间

    the numbers1s's data copy from numbers2: 0
    the numbers1s's data copy from numbers2: 1
    the numbers1s's data copy from numbers2: 2
    the numbers1s's data copy from numbers2: 3
    the numbers1s's data copy from numbers2: 4
    nummber1's new data: 0
    nummber1's new data: 2
    nummber1's new data: 4
    nummber1's new data: 6
    nummber1's new data: 8

    此程序是一个简单的重新申请内存空间的实例,(1)为函数入口,前面已经介绍过。(2)从堆空间中申请5个int空间,将返回地址赋给numbers2,如果返回值为NULL,将返回错误信息,释放numbers2并退出。(3)为新申请的空间初始化。(4)输入需要增加的内存数量。(5)调用realloc()函数重新申请内存空间,重新申请内存空间大小为原有空间大小加上用户输入的内存空间数。如果申请失败,将返回NULL,此时numbers2仍然有效。如果申请成功,将重新分配一块大小合适的空间,并将新空间首地址赋给numbers1,同时将numbers2所指向的5个空间的数据复制到新的内存空间中,释放掉原来numbers2所指向的内存空间。(6)打印从numbers2所指向的原空间拷贝的数据,(7)句对新增加的空间进行初始化。(8)句释放number1所指向的新申请空间。(9)为注释掉的代码,提示读者此时对原空间再次释放,因为第(5)已经完成了这一操作。

    3.2.3 其他内存管理函数calloc和alloca

    1.calloc函数

    calloc是malloc函数的简单包装,它的主要优点是把动态分配的内存进行初始化,全部清零。其操作及语法类似malloc()函数。

    ptr=(struct data *)calloc (count,sizeof(strunt data)) //申请并初始化空间

    下面是这个函数的实现描述:

    void *calloc(size_t nmemb,size_t size)
    {
    void *p;
    size_t total;
    total=nmemb *size;
    p=malloc(total);         //申请空间
    if(p!=NULL)
    memset(p,'\0',total);      //将其实始化为\0
    return p;
    }

    2.alloca函数

    alloca()函数用来在栈中分配size个字节的内存空间,因此函数返回时会自动释放掉空间。alloca函数定义及库头文件如下:

    /* Allocate a block that will be freed when the calling function exits.  */
    extern void *alloca (size_t __size) __THROW;   //从栈中申请空间

    返回值:若分配成功返回指针,失败则返回NULL。

    它与malloc()函数的区别主要在于:

    alloca是向栈申请内存,无需释放,malloc申请的内存位于堆中,最终需要函数free来释放。

    malloc函数并没有初始化申请的内存空间,因此调用malloc()函数之后,还需调用函数memset初始化这部分内存空间;alloca则将初始化这部分内存空间为0。



     


    展开全文
  • 详解操作系统分配内存

    万次阅读 2017-07-08 16:37:29
    计算机体系结构和内存层次 操作系统中内存的最小访问单位是 字节 ,也就是8bit。 通常我们所说的计算机系统是32位的总线,所谓的32位总线就是说一次读写可以从内存当中读或者写32位(也就是4字节)。 ...

    计算机体系结构和内存层次

    操作系统中内存的最小访问单位是 字节 ,也就是8bit。

    通常我们所说的计算机系统是32位的总线,所谓的32位总线就是说一次读写可以从内存当中读或者写32位(也就是4字节)。

    因为一次读写是32位,所以需要地址对齐,访问的时候不能从任意地方开始。

    在CPU中可以看到高速缓存,由于指令执行和访问数据都需要从内存里读数据,如果此时有大量数据要读写而且会重复利用的话,那么在CPU中加高速缓存会使读写进行得更快。

    有了这样一个大致了解,可以来看内存层次结构。

    首先CPU在读写指令和数据的时候,如果缓存里已经有相应的内容,那么CPU直接从缓存中拿到数据,这时候速度是最快的。(写程序的时候完全感受不到L1缓存和L2缓存的存在,因为这部分是硬件控制的,不能显示使用它们。)

    如果缓存不命中,那么就必须去内存中读。如果内存还是没找到,那就去外存中读。(访问速度又有很大差别)

    虽然计算机硬件一直在飞速发展,内存容量也在不断增长,但是仍然不可能将所有用户进程和系统所需要的全部程序和数据放入主存中,所以操作系统就要对内存进行合理地划分和有效地动态分配。操作系统需要做到四个方面:抽象、保护、共享和虚拟化。

    如图,四个进程在操作系统的划分和管理下,彼此之间在逻辑上既需要相互独立,又可以相互通信。

    操作系统中采用的内存管理方式有:

    • 重定位(relocation)
    • 分段(segmentation)
    • 分页(paging)
    • 虚拟存储(virtual memory)

    目前多数系统(如 Linux)采用按需页式虚拟存储。

    地址空间和地址生成

    物理地址空间——硬件支持的地址空间,起始地址从0直到MAXsys。这个编号在存储单元角度来讲是唯一的,但是这种唯一对于我们写程序来说是不大容易的,因为我们在写程序的时候对于到底要使用哪个地址可能是不知道的。

    逻辑地址空间——在CPU运行的进程看到的地址,起始地址从0知道MAXprog。对应的是可执行文件的那段空间。

    对于我们用高级语言编写的程序,经过如下过程生成它的逻辑地址:

    此时的逻辑地址生成时机的不同会有不同的限制。

    • 编译时:已经假设起始地址已知,如果起始地址改变,必须重新编译。例如以前的手机,程序是写死的,不能自己安装应用,只能用手机里编译好的。
    • 加载时:如编译时起始位置未知,编译器需生成可重定位的代码。例如现在的智能手机,可以下载很多APP。
    • 执行时:执行时代码可移动,但需地址转换(映射)硬件支持。这种情况出现在使用虚拟存储的系统,

    逻辑地址生成后,接着就是生成物理地址,过程如下:

    因为内存有着诸多限制,所以也有着对应的检查机制。检查过程如图:

    连续地址分配

    生成地址后接着分配地址,连续内存分配是给进程分配一块不小于指定大小的连续的物理内存区域。地址分配从两个角度考虑:如何去找你要用的空间分区;如何处理不能利用的小的空闲分区。

    这里不能利用的小的空闲分区我们统称为 内存碎片 ,内存碎片分为三类:

    • 内存碎片:有的还可以用,有的无论如何都用不起来了。
    • 外部碎片:分配单元之间的未被使用内存
    • 内部碎片:分配单元内部的未被使用内存(你只占500字节,但是不得不分配512字节)

    这里我们就不得不有所取舍地选择不同的分配策略,常用的有三种:最先匹配(First-fit)、最佳匹配(Best-fit)、最差匹配(Worst-fit)。

    最先匹配(First Fit Allocation)策略

    原理 & 实现:

    • 空闲分区列表按地址顺序排序
    • 分配过程时,搜索一个合适的分区
    • 释放分区时,检查是否可与临近的空闲分区合并

    优点:

    • 简单(这也算优点的话)
    • 在高地址空间有大块的空闲分区

    缺点:

    • 外部碎片
    • 分配大块时较慢

    最佳匹配(Best Fit Allocation)策略

    原理 & 实现:

    • 空闲分区列表按照大小排序
    • 分配时,查找一个合适的分区
    • 释放时,查找并且合并临近的空闲分区(如果找到)

    优点:

    • 大部分分配的尺寸较小时,效果很好(可避免大的空闲分区被拆分)
    • 可减小外部碎片的大小

    缺点:

    • 外部碎片
    • 释放分区缓慢
    • 容易产生很多无用的小碎片

    最差匹配(Worst Fit Allocation)策略

    原理 & 实现:

    • 空闲分区列表按由大到小排序
    • 分配时,选最大的分区
    • 释放时,检查是否可与临近的空闲分区合并,进行可能的合并,并调整空闲分区列表顺序

    优点:

    • 中等大小的分配较多时,效果最好
    • 避免出现太多的小碎片

    缺点:

    • 释放分区较慢
    • 外部碎片
    • 容易破坏大的空闲分区,因此后续难以分配大的分区

    碎片整理

    通过调整进程占用的分区位置来减少或避免分区碎片

    1. 紧凑(compaction) 通过移动分配给进程的内存分区,以合并外部碎片。紧凑的条件是:所有的应用程序可动态重定位。

    2. 分区对换(Swapping in/out) 通过抢占并回收处于等待状态进程的分区,以增大可用内存空间。

    伙伴系统(Buddy System)

    伙伴系统即,整个可分配的分区大小2^U,需要的分区大小为2^(U-1) < s ≤ 2^U 时,把整个块分配给该进程。

    伙伴系统的实现参见: Buddy memory allocation



      内存是计算机中最重要的资源之一,通常情况下,物理内存无法容纳下所有的进程。虽然物理内存的增长现在达到了N个GB,但比物理内存增长还快的是程序,所以无论物理内存如何增长,都赶不上程序增长的速度,所以操作系统如何有效的管理内存便显得尤为重要。本文讲述操作系统对于内存的管理的过去和现在,以及一些页替换的算法的介绍。

     

    对于进程的简单介绍

        在开始之前,首先从操作系统的角度简单介绍一下进程。进程是占有资源的最小单位,这个资源当然包括内存。在现代操作系统中,每个进程所能访问的内存是互相独立的(一些交换区除外)。而进程中的线程所以共享进程所分配的内存空间。

        在操作系统的角度来看,进程=程序+数据+PCB(进程控制块)。这个概念略微有点抽象,我通过一个类比来说吧:比如,你正在厨房做饭,你一边看着菜谱一边按照菜谱将原料做成菜,就在这时,你儿子进来告诉你他擦破了腿,此时你停下手中的工作,将菜谱反扣过来,然后找来急救书按照书中的内容给你儿子贴上创口贴,贴完后你继续回去打开菜谱,然后继续做饭。在这个过程中,你就好比CPU,菜谱就好比程序,而做菜的原料就好比数据。你按照程序指令加工数据,而急救工作好比一个更高优先级的进程,中断了你当前做饭的工作,然后你将菜谱反扣过来(保护现场),转而去处理高优先级的进程,处理完毕后你继续从刚才的页读菜谱(恢复现场),然后继续执行做菜这个进程。

        在简单介绍完进程的概念后,我们来转入内存。

     

    没有内存抽象的年代

        在早些的操作系统中,并没有引入内存抽象的概念。程序直接访问和操作的都是物理内存。比如当执行如下指令时:

    mov reg1,1000

        这条指令会毫无想象力的将物理地址1000中的内容赋值给寄存器。不难想象,这种内存操作方式使得操作系统中存在多进程变得完全不可能,比如MS-DOS,你必须执行完一条指令后才能接着执行下一条。如果是多进程的话,由于直接操作物理内存地址,当一个进程给内存地址1000赋值后,另一个进程也同样给内存地址赋值,那么第二个进程对内存的赋值会覆盖第一个进程所赋的值,这回造成两条进程同时崩溃。

        没有内存抽象对于内存的管理通常非常简单,除去操作系统所用的内存之外,全部给用户程序使用。或是在内存中多留一片区域给驱动程序使用,如图1所示。

        1

        图1.没有内存抽象时,对内存的使用

      

        第一种情况操作系统存于RAM中,放在内存的低地址,第二种情况操作系统存在于ROM中,存在内存的高地址,一般老式的手机操作系统是这么设计的。

        如果这种情况下,想要操作系统可以执行多进程的话,唯一的解决方案就是和硬盘搞交换,当一个进程执行到一定程度时,整个存入硬盘,转而执行其它进程,到需要执行这个进程时,再从硬盘中取回内存,只要同一时间内存中只有一个进程就行,这也就是所谓的交换(Swapping)技术。但这种技术由于还是直接操作物理内存,依然有可能引起进程的崩溃。

        所以,通常来说,这种内存操作往往只存在于一些洗衣机,微波炉的芯片中,因为不可能有第二个进程去征用内存。

     

    内存抽象

        在现代的操作系统中,同一时间运行多个进程是再正常不过的了。为了解决直接操作内存带来的各种问题,引入的地址空间(Address Space),这允许每个进程拥有自己的地址。这还需要硬件上存在两个寄存器,基址寄存器(base register)和界址寄存器(limit register),第一个寄存器保存进程的开始地址,第二个寄存器保存上界,防止内存溢出。在内存抽象的情况下,当执行

    mov reg1,20

        这时,实际操作的物理地址并不是20,而是根据基址和偏移量算出实际的物理地址进程操作,此时操作的实际地址可能是:

    mov reg1,16245

     

        在这种情况下,任何操作虚拟地址的操作都会被转换为操作物理地址。而每一个进程所拥有的内存地址是完全不同的,因此也使得多进程成为可能。

        但此时还有一个问题,通常来说,内存大小不可能容纳下所有并发执行的进程。因此,交换(Swapping)技术应运而生。这个交换和前面所讲的交换大同小异,只是现在讲的交换在多进程条件下。交换的基本思想是,将闲置的进程交换出内存,暂存在硬盘中,待执行时再交换回内存,比如下面一个例子,当程序一开始时,只有进程A,逐渐有了进程B和C,此时来了进程D,但内存中没有足够的空间给进程D,因此将进程B交换出内存,分给进程D。如图2所示。

        2

        图2.交换技术

     

        通过图2,我们还发现一个问题,进程D和C之间的空间由于太小无法另任何进程使用,这也就是所谓的外部碎片。一种方法是通过紧凑技术(Memory Compaction)解决,通过移动进程在内存中的地址,使得这些外部碎片空间被填满。还有一些讨巧的方法,比如内存整理软件,原理是申请一块超大的内存,将所有进程置换出内存,然后再释放这块内存,从而使得从新加载进程,使得外部碎片被消除。这也是为什么运行完内存整理会狂读硬盘的原因。另外,使用紧凑技术会非常消耗CPU资源,一个2G的CPU没10ns可以处理4byte,因此多一个2G的内存进行一次紧凑可能需要好几秒的CPU时间。

        上面的理论都是基于进程所占的内存空间是固定的这个假设,但实际情况下,进程往往会动态增长,因此创建进程时分配的内存就是个问题了,如果分配多了,会产生内部碎片,浪费了内存,而分配少了会造成内存溢出。一个解决方法是在进程创建的时候,比进程实际需要的多分配一点内存空间用于进程的增长。一种是直接多分配一点内存空间用于进程在内存中的增长,另一种是将增长区分为数据段和栈(用于存放返回地址和局部变量),如图3所示。

        3

        图3.创建进程时预留空间用于增长

     

        当预留的空间不够满足增长时,操作系统首先会看相邻的内存是否空闲,如果空闲则自动分配,如果不空闲,就将整个进程移到足够容纳增长的空间内存中,如果不存在这样的内存空间,则会将闲置的进程置换出去。

         当允许进程动态增长时,操作系统必须对内存进行更有效的管理,操作系统使用如下两种方法之一来得知内存的使用情况,分别为1)位图(bitmap) 2)链表

         使用位图,将内存划为多个大小相等的块,比如一个32K的内存1K一块可以划为32块,则需要32位(4字节)来表示其使用情况,使用位图将已经使用的块标为1,位使用的标为0.而使用链表,则将内存按使用或未使用分为多个段进行链接,这个概念如图4所示。

        4

         图4.位图和链表表示内存的使用情况

     

         使用链表中的P表示进程,从0-2是进程,H表示空闲,从3-4表示是空闲。

         使用位图表示内存简单明了,但一个问题是当分配内存时必须在内存中搜索大量的连续0的空间,这是十分消耗资源的操作。相比之下,使用链表进行此操作将会更胜一筹。还有一些操作系统会使用双向链表,因为当进程销毁时,邻接的往往是空内存或是另外的进程。使用双向链表使得链表之间的融合变得更加容易。

        还有,当利用链表管理内存的情况下,创建进程时分配什么样的空闲空间也是个问题。通常情况下有如下几种算法来对进程创建时的空间进行分配。

    •      临近适应算法(Next fit)---从当前位置开始,搜索第一个能满足进程要求的内存空间
    •      最佳适应算法(Best fit)---搜索整个链表,找到能满足进程要求最小内存的内存空间
    •      最大适应算法(Wrost fit)---找到当前内存中最大的空闲空间
    •      首次适应算法(First fit) ---从链表的第一个开始,找到第一个能满足进程要求的内存空间

     

    虚拟内存(Virtual Memory)

        虚拟内存是现代操作系统普遍使用的一种技术。前面所讲的抽象满足了多进程的要求,但很多情况下,现有内存无法满足仅仅一个大进程的内存要求(比如很多游戏,都是10G+的级别)。在早期的操作系统曾使用覆盖(overlays)来解决这个问题,将一个程序分为多个块,基本思想是先将块0加入内存,块0执行完后,将块1加入内存。依次往复,这个解决方案最大的问题是需要程序员去程序进行分块,这是一个费时费力让人痛苦不堪的过程。后来这个解决方案的修正版就是虚拟内存。

        虚拟内存的基本思想是,每个进程有用独立的逻辑地址空间,内存被分为大小相等的多个块,称为(Page).每个页都是一段连续的地址。对于进程来看,逻辑上貌似有很多内存空间,其中一部分对应物理内存上的一块(称为页框,通常页和页框大小相等),还有一些没加载在内存中的对应在硬盘上,如图5所示。

        5

        图5.虚拟内存和物理内存以及磁盘的映射关系

     

        由图5可以看出,虚拟内存实际上可以比物理内存大。当访问虚拟内存时,会访问MMU(内存管理单元)去匹配对应的物理地址(比如图5的0,1,2),而如果虚拟内存的页并不存在于物理内存中(如图5的3,4),会产生缺页中断,从磁盘中取得缺的页放入内存,如果内存已满,还会根据某种算法将磁盘中的页换出。

        而虚拟内存和物理内存的匹配是通过页表实现,页表存在MMU中,页表中每个项通常为32位,既4byte,除了存储虚拟地址和页框地址之外,还会存储一些标志位,比如是否缺页,是否修改过,写保护等。可以把MMU想象成一个接收虚拟地址项返回物理地址的方法。

        因为页表中每个条目是4字节,现在的32位操作系统虚拟地址空间会是2的32次方,即使每页分为4K,也需要2的20次方*4字节=4M的空间,为每个进程建立一个4M的页表并不明智。因此在页表的概念上进行推广,产生二级页表,二级页表每个对应4M的虚拟地址,而一级页表去索引这些二级页表,因此32位的系统需要1024个二级页表,虽然页表条目没有减少,但内存中可以仅仅存放需要使用的二级页表和一级页表,大大减少了内存的使用。

     

    页面替换算法

        因为在计算机系统中,读取少量数据硬盘通常需要几毫秒,而内存中仅仅需要几纳秒。一条CPU指令也通常是几纳秒,如果在执行CPU指令时,产生几次缺页中断,那性能可想而知,因此尽量减少从硬盘的读取无疑是大大的提升了性能。而前面知道,物理内存是极其有限的,当虚拟内存所求的页不在物理内存中时,将需要将物理内存中的页替换出去,选择哪些页替换出去就显得尤为重要,如果算法不好将未来需要使用的页替换出去,则以后使用时还需要替换进来,这无疑是降低效率的,让我们来看几种页面替换算法。

       

    最佳置换算法(Optimal Page Replacement Algorithm)

         最佳置换算法是将未来最久不使用的页替换出去,这听起来很简单,但是无法实现。但是这种算法可以作为衡量其它算法的基准。

     

    最近不常使用算法(Not Recently Used Replacement Algorithm)

         这种算法给每个页一个标志位,R表示最近被访问过,M表示被修改过。定期对R进行清零。这个算法的思路是首先淘汰那些未被访问过R=0的页,其次是被访问过R=1,未被修改过M=0的页,最后是R=1,M=1的页。

     

    先进先出页面置换算法(First-In,First-Out Page Replacement Algorithm)

        这种算法的思想是淘汰在内存中最久的页,这种算法的性能接近于随机淘汰。并不好。

     

    改进型FIFO算法(Second Chance Page Replacement Algorithm)

        这种算法是在FIFO的基础上,为了避免置换出经常使用的页,增加一个标志位R,如果最近使用过将R置1,当页将会淘汰时,如果R为1,则不淘汰页,将R置0.而那些R=0的页将被淘汰时,直接淘汰。这种算法避免了经常被使用的页被淘汰。

     

    时钟替换算法(Clock Page Replacement Algorithm)

        虽然改进型FIFO算法避免置换出常用的页,但由于需要经常移动页,效率并不高。因此在改进型FIFO算法的基础上,将队列首位相连形成一个环路,当缺页中断产生时,从当前位置开始找R=0的页,而所经过的R=1的页被置0,并不需要移动页。如图6所示。

        6

        图6.时钟置换算法

     

    最久未使用算法(LRU Page Replacement Algorithm)

        LRU算法的思路是淘汰最近最长未使用的页。这种算法性能比较好,但实现起来比较困难。

     

    下面表是上面几种算法的简单比较:

    算法 描述
    最佳置换算法 无法实现,最为测试基准使用
    最近不常使用算法 和LRU性能差不多
    先进先出算法 有可能会置换出经常使用的页
    改进型先进先出算法 和先进先出相比有很大提升
    最久未使用算法 性能非常好,但实现起来比较困难
    时钟置换算法 非常实用的算法

     

        上面几种算法或多或少有一些局部性原理的思想。局部性原理分为时间和空间上的局部性

        1.时间上,最近被访问的页在不久的将来还会被访问。

        2.空间上,内存中被访问的页周围的页也很可能被访问。



    展开全文
  • C/C++程序内存分配

    万次阅读 多人点赞 2018-08-23 09:11:06
    一、一个C/C++编译的程序占用内存分为以下几个部分: 栈区(stack):由编译器自动分配与释放,存放为运行时函数分配的局部变量、函数参数、返回数据、返回地址等。其操作类似于数据结构中的栈。 堆区(heap):...
    一、一个C/C++编译的程序占用内存分为以下几个部分:
    • 栈区(stack):由编译器自动分配与释放,存放为运行时函数分配的局部变量、函数参数、返回数据、返回地址等。其操作类似于数据结构中的栈。
    • 堆区(heap):一般由程序员自动分配,如果程序员没有释放,程序结束时可能有OS回收。其分配类似于链表。
    • 全局区(静态区static):存放全局变量、静态数据、常量。程序结束后由系统释放。全局区分为已初始化全局区(data)和未初始化全局区(bss)。
    • 常量区(文字常量区):存放常量字符串,程序结束后有系统释放。
    • 代码区:存放函数体(类成员函数和全局区)的二进制代码。
    二、三种内存分配方式
    • 从静态存储区分配

      内存在程序编译的时候已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。

    • 在栈上创建

      在执行函数时,函数内局部变量的存储单元可以在栈上创建,函数执行结束时,这些内存单元会自动被释放。
      栈内存分配运算内置于处理器的指令集,效率高,但是分配的内存容量有限。

    • 从堆上分配

      亦称为动态内存分配。
      程序在运行的时候使用malloc或者new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。
      动态内存的生命周期有程序员决定,使用非常灵活,但如果在堆上分配了空间,既有责任回收它,否则运行的程序会出现内存泄漏,频繁的分配和释放不同大小的堆空间将会产生内存碎片。

    三、内存分配简易图

    这里写图片描述

    • 补充:
      在 C 语言中,全局变量又分为初始化的和未初始化的(未被初始化的对象存储区可以通过 void* 来访问和操纵,程序结束后由系统自行释放),在 C++ 里面没有这个区分了,他们共同占用同一块内存区。
    四、堆和栈的区别
    • 管理方式不同:栈是由编译器自动申请和释放空间,堆是需要程序员手动申请和释放;
    • 空间大小不同:栈的空间是有限的,在32位平台下,VC6下默认为1M,堆最大可以到4G;
    • 能否产生碎片:栈和数据结构中的栈原理相同,在弹出一个元素之前,上一个已经弹出了,不会产生碎片,如果不停地调用malloc、free对造成内存碎片很多;
    • 生长方向不同:堆生长方向是向上的,也就是向着内存地址增加的方向,栈刚好相反,向着内存减小的方向生长。
    • 分配方式不同:堆都是动态分配的,没有静态分配的堆。栈有静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 malloc 函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释放,无需我们手工实现。
    • 分配效率不同:栈的效率比堆高很多。栈是机器系统提供的数据结构,计算机在底层提供栈的支持,分配专门的寄存器来存放栈的地址,压栈出栈都有相应的指令,因此比较快。堆是由库函数提供的,机制很复杂,库函数会按照一定的算法进行搜索内存,因此比较慢。
    五、关于内存分配这块,我们常常会提到一个比较重要的知识点就是动态内存管理,我在之前的博客中总结过,现将连接贴在下面,便于查看。
    六、静态全局变量、全局变量、静态局部变量、局部变量的区别
    • 静态全局变量、全局变量区别

    (1)静态全局变量和全局变量都属于常量区
    (2)静态全局区只在本文件中有效,别的文件想调用该变量,是调不了的,而全局变量在别的文件中可以调用
    (3)如果别的文件中定义了一个该全局变量相同的变量名,是会出错的。

    • 静态局部变量、局部变量的区别

    (1)静态局部变量是属于常量区的,而函数内部的局部变量属于栈区;
    (2)静态局部变量在该函数调用结束时,不会销毁,而是随整个程序结束而结束,但是别的函数调用不了该变量,局部变量随该函数的结束而结束;
    (3)如果定义这两个变量的时候没有初始值时,静态局部变量会自动定义为0,而局部变量就是一个随机值;
    (4)静态局部变量在编译期间只赋值一次,以后每次函数调用时,不在赋值,调用上次的函数调用结束时的值。局部变量在调用期间,每调用一次,赋一次值。

    展开全文
  • 原文地址:...一、数据结构中的栈和堆 虽说我们经常把堆栈放在一起称呼,但是不可否认的是,堆栈实际上是两种数据结构:堆和栈。 堆和栈都是一种数据项按序...

    原文地址:https://blog.csdn.net/u013007900/article/details/79338653
    参考文章:http://www.cnblogs.com/hanyonglu/archive/2011/04/12/2014212.html
    一、数据结构中的栈和堆
    虽说我们经常把堆栈放在一起称呼,但是不可否认的是,堆栈实际上是两种数据结构:堆和栈。

    堆和栈都是一种数据项按序排列的数据结构。

    栈:就像装数据的桶或箱子,它是一种具有后进先出性质的数据结构。

    堆:一种经过排序的树形数据结构,每个结点都有一个值。通常我们所说的堆的数据结构,是指二叉堆。堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。由于堆的这个特性,常用来实现优先队列,堆的存取是随意,这就如同我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,书架这种机制不同于箱子,我们可以直接取出我们想要的书。

    二、内存分配中的栈和堆
    注意:一般情况下,当我们说“堆栈”的时候,其实说的是“栈”!

    一般情况下程序存放在Rom或Flash中,运行时需要拷到内存中执行,内存会分别存储不同的信息。内存中的栈区处于相对较高的地址以地址的增长方向为上的话,栈地址是向下增长的。

    栈中分配局部变量空间,堆区是向上增长的用于分配程序员申请的内存空间。另外还有静态区是分配静态变量,全局变量空间的;只读区是分配常量和程序代码空间的;以及其他一些分区。

    程序的内存分配:
    一个由C/C++编译的程序占用的内存分为以下几个部分:
    1、栈区(stack)— 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
    2、堆区(heap) — 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS(操作系统)回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。
    3、全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
    4、文字常量区 —常量字符串就是放在这里的。程序结束后由系统释放。
    5、程序代码区—存放函数体的二进制代码。

    实例讲解

    int  a=0;   全局初始化区    
    
    char *p1;   全局未初始化区    
    int  main()    
    {    
      int  b; //栈    
      char  s[]="abc"; //栈    
      char  *p2; //栈    
      char  *p3="123456"; //123456/0在常量区,p3在栈上。    
    
      static int c =0;//全局(静态)初始化区    
      p1 =  (char  *)malloc(10);  //分配得来得10和20字节的区域就在堆区
      p2  = (char  *)malloc(20);       
      strcpy(p3,"123456"); //123456/0放在常量区,编译器可能会将它与p3所指向的"123456"  优化成一个地方。    
    } 
    

    下面详细讲解一下内存分配的几个区:

    栈:就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。

    堆:就是那些由 new 分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个 new 就要对应一个 delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。堆可以动态地扩展和收缩。

    自由存储区,就是那些由 malloc 等分配的内存块,他和堆是十分相似的,不过它是用 free 来结束自己的生命的。

    全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的 C 语言中,全局变量又分为初始化的和未初始化的(初始化的全局变量和静态变量在一块区域,未初始化的全局变量与静态变量在相邻的另一块区域,同时未被初始化的对象存储区可以通过 void* 来访问和操纵,程序结束后由系统自行释放),在 C++ 里面没有这个区分了,他们共同占用同一块内存区。

    常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(当然,你要通过非正当手段也可以修改,而且方法很多)

    void f() { int* p=new int[5]; }
    1
      这条短短的一句话就包含了堆与栈,看到 new,我们首先就应该想到,我们分配了一块堆内存,那么指针 p 呢?他分配的是一块栈内存,所以这句话的意思就是:在栈内存中存放了一个指向一块堆内存的指针 p。在程序会先确定在堆中分配内存的大小,然后调用 operator new 分配内存,然后返回这块内存的首地址,放入栈中。

    那么该怎么去释放呢?

    使用 delete []p,这是为了告诉编译器:我删除的是一个数组,VC6 就会根据相应的 Cookie 信息去进行释放内存的工作。

    堆和栈究竟有什么区别?
    主要的区别由以下几点:

    1、管理方式不同;

    2、空间大小不同;

    3、能否产生碎片不同;

    4、生长方向不同;

    5、分配方式不同;

    6、分配效率不同;

    管理方式:对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak。

    空间大小:一般来讲在 32 位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在VC6下面,默认的栈空间大小是1M(好像是,记不清楚了)。当然,我们可以修改:打开工程,依次操作菜单如下:Project->Setting->Link,在 Category 中选中 Output,然后在 Reserve 中设定堆栈的最大值和 commit。注意:reserve 最小值为 4Byte;commit 是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。

    碎片问题:对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出,详细的可以参考数据结构,这里我们就不再一一讨论了。

    生长方向:对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。

    分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 malloc 函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

    分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是 C/C++ 函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

    从这里我们可以看到,堆和栈相比,由于大量 new/delete 的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP 和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。

    虽然栈有如此众多的好处,但是由于和堆相比不是那么灵活,有时候分配大量的内存空间,还是用堆好一些。

    无论是堆还是栈,都要防止越界现象的发生(除非你是故意使其越界),因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果,就算是在你的程序运行过程中,没有发生上面的问题,你还是要小心,说不定什么时候就崩掉,那时候 debug 可是相当困难的 :)

    堆和栈还有几点不同:

    1、申请后系统的响应
    栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
    堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大小,这样,代码中的delete语句才能正确的释放本内存空间。
    另外,由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

    2、申请大小的限制
    栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。
    堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

    3、申请效率的比较:
    栈由系统自动分配,速度较快。但程序员是无法控制的。
    堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。
    另外,在WINDOWS下,最好的方式是用VirtualAlloc分配内存,他不是在堆,也不是在栈是直接在进程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。

    4、堆和栈中的存储内容
    栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的。
    当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。
    堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

    5、堆和栈上的内存操作越界

    1>堆内存越界主要是操作的内存超过了calloc/malloc/new等在堆上分配内存函数所分配的大小,后果导致下次calloc/malloc/new的失败,malloc失败发生_int_malloc错误(引起abort)大多是这种情况引起的;

    2>栈内存越界的情况大多出现在对数组的操作上,数组下标超过了数组定义的长度,后果导致覆盖其他变量。

    小结一下
    堆和栈的区别可以用如下的比喻来看出:
    使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。
    使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

    展开全文
  • 内存分配的三种方式

    千次阅读 2018-08-14 15:04:23
    要回答这个问题,我们必须先要理解C++的内存管理方式,需要站在内存四区的角度去审视一下代码,这样整个理解了内存分配的方式,回答的时候就只需要注意一些细节了。写一个例子然后画一下内存示意图。 int getmem...
  • 内存分配

    2020-08-04 11:45:00
    1.内存分配方式  内存分配方式有三种:  [1]从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。  [2]在栈上创建。在执行函数时,函数内...
  • c 程序内存分配管理

    千次阅读 2018-09-23 15:44:33
    给变量分配内存空间可分为静态内存分配和动态内存分配。 静态内存分配属于编译时给变量分配的空间,动态分配属于在程序运行时给变量分配的空间 静态分配属于栈分配,动态分配属于堆分配 运行效率上,静态内存比...
  • Java 内存分配全面浅析

    万次阅读 多人点赞 2015-10-23 16:31:12
    本文将由浅入深详细介绍Java内存分配的原理,以帮助新手更轻松的学习Java。这类文章网上有很多,但大多比较零碎。本文从认知过程角度出发,将带给读者一个系统的介绍。 进入正题前首先要知道的是Java程序运行在JVM...
  • 连续内存分配与非连续内存分配

    千次阅读 2018-09-22 08:45:03
    连续内存分配 首次适配:空闲分区以地址递增的次序链接。分配内存时顺序查找,找到大小能满足要求的第一个空闲分区。 最优适配:空闲分区按容量递增形成分区链,找到第一个能满足要求的空闲分区。 最坏适配:空闲...
  • 内存的四种分配方式

    千次阅读 2018-03-23 22:53:26
    由于堆是程序员管理的,如果管理不当会导致内存泄露的问题,内存泄露指已经分配内存空间无法被系统回收也无法被继续使用。解决这个问题,C++可以使用智能指针对象去指向分配内存,在对象析构时释放内存防止内存...
  • 内存分配方式有几种?

    万次阅读 2013-10-12 09:11:55
    内存分配方式有几种? 静态存储区 栈 堆 的内存分配 1,从静态存储区域分配内存。程序编译的时候内存已经分配好了,并且在程序的整个运行期间都存在,例如全局变量。 2,在栈上创建。在执行函数时,函数内局部...
  • Android studio 分配内存设置方法

    万次阅读 2016-11-18 15:26:52
    如果Android studio运行起来非常卡顿,有时可能是因为初始分配内存不够用了。 查看当前分配的Heap 总大小以及使用状况可以在studio中设置展示。 具体位置Settings -> Appearance 页里,打开Show memory ...
  • Matlab预分配内存优化for循环

    万次阅读 2016-06-04 23:30:22
    在Matlab中for循环在进行前没有预分配内存。重复扩展数组的尺寸,会花费更多的时间分配内存,导致程序性能降低。并且这些内存不一定是连续的,这更会减慢程序的操作。因此,我们可以采用预分配数组空间来解决这一...
  • Matlab预分配内存

    万次阅读 多人点赞 2016-10-15 10:31:31
    分配内存简介:对于for,while循环,在循环的过程中每次不断的增加数据结构的大小,影响了性能和内存的使用。重复的调整数据的大小需要Matlab花费额外的时间寻找更大的连续内存块,并且将现在的数组移动到连续的...
  • 内存的静态分配和动态分配的区别

    万次阅读 2011-08-27 12:04:13
    内存的静态分配和动态分配的区别主要是两个:  一是时间不同。静态分配发生在程序编译和连接的时候。动态分配则发生在程序调入和执行的时候。  二是空间不同。堆都是动态分配的,没有静态分配的堆。栈有2种分配...
  • Linux查看内存分配情况的几种方法

    千次阅读 2012-11-22 11:27:30
    top free vmstat /proc/meminfo /proc//statm /proc//status /proc//maps
  • 问题:为了减少运行时间,在对特定大小的矩阵进行预分配内存可以实现。现在测试几种预分配内存的方法,比较运行时间。 clear tic; a(10000,20000) = 0; %方法一,直接赋值为零 time=toc disp(['用直接...
  • eclipse 性能调优之内存分配

    万次阅读 2011-03-30 16:34:00
    建议读者打开 eclipse 的 Window -> Preferences -> General,将 Show heap status 选中,可以随时在右下角监视内存的使用情况。
  • 多半是废了 ,使劲加大内存条,哥有的是钱 ,干脆换电脑吧~ 项目跑不动怎么办 ? 内存溢出怎么办 ? 运行大型项目卡顿怎么办 ? 不用怕 ,建仔教你一招,只需要修改JVM内存大小,让idea运行健步如飞 ! 修改IDEA内存...
  • C++动态分配内存空间 : new 和 delete

    万次阅读 多人点赞 2017-09-05 19:13:42
    1.什么是new和delete在C语言中我们可以利用标准库函数中的 malloc 和 free 来动态分配内存空间,而在C++中提供了运算符 new 和 delete 来取代 malloc 和 free 进行动态分配内存空间。2.new的用法表达式- new 数据...
1 2 3 4 5 ... 20
收藏数 1,200,416
精华内容 480,166
关键字:

内存分配