C的栈、堆、自由存储区(C++)、全局/静态存储区、常量存储区

来源:互联网 发布:php ajax返回json数据 编辑:程序博客网 时间:2024/05/18 22:45

部分分析还没有得到有效验证,只做猜想


  • 内存分配方式有三种:
    • 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static变量。
    • 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
    • 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc或new申请任意多少的内存,程序员自己负责在何时用free或delete释放内存。动态内存的生存期由我们决定,使用非常灵活,但问题也最多。

对于全局变量,它的逻辑地址(段地址+偏移量)在编译后就确定了,在程序运行时分配内存。
对于局部变量,它的逻辑地址,是否在编译后就确定了呢?是

栈和堆都是内存空间,只是操作系统让它起到了不同的作用而已。
首先是栈:栈里存放的是局部变量,这些变量需要的空间大小在程序编译结束之后便已知晓,空间的使用与释放由操作系统负责。所以,一般来讲,栈所需要的大小不需要运行即可了解。
接下来是堆:堆里存放的是动态分配的内存。动态分配的内存,其分配和回收是在运行时通过调用函数(不是C的函数,而是系统调用。glibc中是brk系统调用)实现的,而不是由操作系统来处理的。如果进程需要使用,必须要向操作系统申请,操作系统检查进程的内存空间,如果满足条件再分配。


当问起C++的内存> 布局时,大概会回答:
在C++中,内存区分为5个区:栈、堆、自由存储区、全局/静态存储区、常量存储区。
如果我接着问你自由存储区有什么区别
你或许这样回答:malloc在堆上分配的内存块,使用free释放内存,而new所申请的内存则是在自由存储区上,使用delete来释放。
这样听起来似乎也没错,但如果我接着问:自由存储区与堆是两块不同的内存区域吗?它们有可能相同吗?你可能就懵了。
事实上,我在网上看的很多博客,划分自由存储区与堆的分界线就是new/delete与malloc/free。然而,尽管C++标准没有要求,但很多编译器的new/delete都是以malloc/free为基础来实现的。那么请问:借以malloc实现的new,所申请的内存是在堆上还是在自由存储区上?
从技术上来说,堆(heap)是C语言和操作系统的术语。堆是操作系统所维护的一块特殊内存,它提供了动态分配的功能,当运行程序调用malloc()时就会从中分配,稍后调用free可把内存交还。
而自由存储是C++中通过new和delete动态分配和释放对象的抽象概念,通过new来申请的内存区域可称为自由存储区。基本上,所有的C++编译器默认使用堆来实现自由存储,也即是缺省的全局运算符new和delete也许会按照malloc和free的方式来被实现,这时藉由new运算符分配的对象,说它在堆上也对,说它在自由存储区上也正确。但程序员也可以通过重载操作符,改用其他内存来实现自由存储,例如全局变量做的对象池,这时自由存储区就区别于堆了。我们所需要记住的就是:
堆是操作系统维护的一块内存,而自由存储是C++中通过new与delete动态分配和释放对象的抽象概念。堆与自由存储区并不等价。


参考:http://blog.csdn.net/lhzhang1985/article/details/6304404


一个由c/c++编译的程序占用的内存分为以下几个部分:

  • 栈区stack: 先进后出,就是那些由编译器编译过程中需要的时候自动分配,在不需要的时候自动清除的变量的存储区。存放函数的参数值、局部变量,函数结束后,出栈。在一个进程中,位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数的调用,和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。
  • 堆区heap:先进先出,一般由程序员在程序中new申请、delete释放,若程序员在程序中不释放,程序结束时(主进程推出后)由操作系统回收。堆可以动态地扩展可收缩。
  • 自由存储区(C++): 就是那些由new申请的内存块(堆中)。
  • 全局区(静态区)static: 全局变量和静态变量被分配到同一快内存中,在以前的c语言中,全局变量又分为初始化的和未初始化(初始化的全局变量和静态变量在一块区域,未初始化的全局变量于静态变量在相邻的另一块区域,同时未被初始化的对象存储区可以通过void *来访问和操纵,程序结束后由系统自行释放),在C++中没有这个区分了,它们共同占用同一块内存区。
  • 常量存储区:常量字符串放在这里,程序结束后由操作系统回收。这是一块比较特殊的存储区,它里面存放的是常量,不允许修改(当然,通过非正当手段也可以修改)。
  • 程序代码区:存放函数体的二进制代码。

#include <stdio.h>#include <string.h>int a = 0;//全局初始化区char *p1;//全局未初始化区main(void){    int b;//栈    char s[] = "abc";//栈    char *p2;//栈    char *p3 = "123456";//12345在文字常量区,p3在栈    static int c = 0;//全局(静态)初始化区    p1 = (char *)malloc(10);//10个在堆区    p2 = (char *)malloc(20);//20个在堆区    strcpy(p1,"123456");    //123456在文字常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方    return 0;}

栈stack、堆heap


  • 申请方式
    • 栈:不需要程序员向系统申请,由系统自动分配,引用性声明、定义性声明的局部变量存放在栈中。
    • 堆:需要程序员向系统申请,并指明大小。
      • C:char * p1 = (char *)malloc(10);
      • C++:char *p2 = new char[10];
      • C++:char *p3 = new char[10] ();
      • p1、p2自己在栈中,如果p1、p2是局部变量。
  • 系统内存分配机制:

    • 栈:只要栈的剩余空间大于所需空间,系统就为程序提供内存,否则报栈溢出。
    • 堆:
      • 操作系统有一个记录空闲内存地址的链表,
      • 当系统收到申请时,会遍历该链表,寻找第一个空间大于所需空间的堆节点,然后将该节点从空闲链表中删除,并将该节点返回。
      • 另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的空间大小,这样代码中的delete语句才能正确释放本次的空间
      • 另外,由于找到的堆节点的大小大于等于申请空间大小,系统会自动将多余的那些空间重新放回空闲链表中。
  • 申请空间大小限制

    • 栈:在windows系统中,
      • 栈是从高地址向低地址扩展的数据结构,
      • 是一块连续的内存区域。
      • 栈顶的地址和栈的最大容量是系统事先规定好的,在windows系统中,栈的大小是2M(也有说是1M,总之是一个编译时就确定的常数),如果所需的空间超过栈的剩余空间,会报栈溢出(overflow),栈的大小有限,需要合理使用。
    • 堆:
      • 堆是从低地址向高地址扩展的数据结构。
      • 不连续的内存区域。
      • 因为操作系统是使用链表来存储空闲内存地址的,链表的遍历方向是由低地址到高地址。堆的大小受限于计算机系统中有效的虚拟内存,堆获得的空间比较灵活,空间也大。
  • 申请效率

    • 栈:由系统自动分配,速度较快,但是程序员无法控制。
    • 堆:由程序员手动new分配的内存,速度较慢,容易产生碎片。
    • VirtualAlloc:在windows系统中,最好的方式是使用VirtualAlloc分配内存,它不在堆,也不栈,它是直接在进程的地址空间中保留一块内存,用起来不方便,但是速度快,灵活。
  • 栈和堆中保存的内容

    • 栈:在函数调用时,第一个进栈的是主函数中的下一条指令(函数调用语句的下一条可执行语句)的地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。静态变量不入栈,入全局/静态存储区。
    • 堆:一般是在堆的头部用一个字节存放堆的大小,堆中的具体内容由程序员安排。
  • 存取效率

char s1[] = "aaaaaa";char *s2 = "bbbbbb";aaaaaa是在运行时刻赋值的bbbbbb是在编译时就确定的但是,在以后的存取中,在栈上的数组比指针指向的字符串(例如堆)快。
#include <stdio.h>int main(void){    char a = '\0';    char s1[] = "aaaaaa";//运行时    char *s2 = "bbbbbb";//编译时    a = s1[1];//运行时    a = s2[1];//运行时    return 0;}
  • 对应的汇编代码
    • 10: a = c[1];
      00401067 8A 4D F1 mov cl,byte ptr [ebp-0Fh]
      0040106A 88 4D FC mov byte ptr [ebp-4],cl
    • 11: a = p[1];
      0040106D 8B 55 EC mov edx,dword ptr [ebp-14h]
      00401070 8A 42 01 mov al,byte ptr [edx+1]
      00401073 88 45 FC mov byte ptr [ebp-4],al>
  • 第一种:在读取时直接就把字符串中的元素读到寄存器cl中,数组在栈上可直接读取,速度较快。
  • 第二种:指针在底层汇编中需要用edx寄存器中转,要先把指针值读到edx中,在根据edx读取字符,显然慢了。

全局/静态存储区


  • 全局变量或者静态变量,它们都放在里的。
    • 为什么说在堆上分配内存比在栈上分配内存慢?堆空间的开辟需要用系统函数,栈上直接修改指针。堆空间的管理需要系统记帐,栈上的空间可以由编译器管理或是保存在某个处理器寄存器中。堆空间的释放需要系统管理,栈上的释放可以直接丢弃。堆空间需要通过栈上的指针间接引用,所以访问会慢。记得在apue2上面看到关于线程中有这样一段话,大致意思是,一个线程有自己的堆栈,可以在堆栈上分配内存,比如说一个结构体,如果这个线程调用了pthread_exit()返回这个结构体指针的时候之后要特别的小心,因为很有可能这个结构体里面的成员值发生改变,这个可以理解,因为同一个进程所有线程的资源是共享的,当这个线程退出之后那部分以前用过的堆栈很可能被其它线程占用,但同时又说如果malloc就不会出现这样的问题,比如,在栈上分一个int,只要esp-4就可以了,在堆上系统要记录被分配内存的信息,以便释放。BTW:栈有序,堆无序。

字符数组,字符指针,Sizeof总结

  • 字符串形式出现的,编译器都会为该字符串自动添加一个0作为结束符,如在代码中写 “abc”,那么编译器帮你存储的是”abc\0”
  • “abc”是常量吗?答案是有时是,有时不是。
    • 不是常量的情况:”abc”作为字符数组初始值的时候就不是,如:char str[] = “abc”;
      因为定义的是一个字符数组所以就相当于定义了一些空间来存放”abc”,而又因为字符数组就是把字符一个一个地存放的,所以编译器把这个语句解析为char str[3] = {‘a’,’b’,’c’};又根据上面的总结1,所以 char str[] = “abc”;的最终结果是 char str[4] = {‘a’,’b’,’c’,’\0’};做一下扩展,如果char str[] = “abc”;是在函数内部写的话,那么这里 的”abc\0”因为不是常量,所以应该被放在栈上。
    • 是常量的情况: 把”abc”赋给一个字符指针变量时,如char* ptr = “abc”; 因为定义的是一个普通指针,并没有定义空间来存放”abc”,所以编译器得帮我们找地方来放”abc”,显然,把这里的”abc”当成常量并把它放到程序的常量区是编译器最合适的选择。所以尽管ptr的类型不是const char*,并且ptr[0] = ‘x’;也能编译 通过,但是执行ptr[0] = ‘x’;就会发生运行时异常,因为这个语句试图去修改程序 常量区中的东西。记得哪本书中曾经说过char* ptr = “abc”;这种写法原来在c++标准中是不允许的, 但是因为这种写法在c中实在是太多了,为了兼容c,不允许也得允许。虽然允许, 但是建议的写法应该是const char* ptr = “abc”;这样如果后面写ptr[0] = ‘x’的话编译器就不会让它编译通过,也就避免了上面说的运行时异常。又扩展一下,如果char* ptr = “abc”;写在函数体内,那么虽然这里的”abc\0”被放在常量区中,但是ptr本身只是一个普通的指针变量,所以ptr是被放在栈上的, 只不过是它所指向的东西被放在常量区罢了。
  • 数组的类型是由该数组所存放的东西的类型以及数组本身的大小决定的。 如char s1[3]和char s2[4],s1的类型就是char[3],s2的类型就是char[4], 也就是说尽管s1和s2都是字符数组,但两者的类型却是不同的。
  • 字符串常量的类型可以理解为相应字符常量数组的类型, 如”abcdef”的类型就可以看成是const char[7]
  • sizeof是用来求类型的字节数的。如int a;那么无论sizeof(int)或者是sizeof(a)都 是等于4,因为sizeof(a)其实就是sizeof(type of a)。
  • 对于函数参数列表中的以数组类型书写的形式参数,编译器把其解释为普通的指针类型,如对于void func(char sa[100],int ia[20],char * p) 则sa的类型为char*,ia的类型为int* ,p的类型为char*。
  • 根据上面的总结,来实战一下:
    • 对于char str[] = “abcdef”;就有sizeof(str) == 7,因为str的类型是char[7], 也有sizeof(“abcdef”) == 7,因为”abcdef”的类型是const char[7]。
    • 对于char * ptr = “abcdef”;就有sizeof(ptr) == 4,因为ptr的类型是char* 。
    • 对于char str2[10] = “abcdef”;就有sizeof(str2) == 10,因为str2的类型是char[10]。
    • 对于void func(char sa[100],int ia[20],char * p); 就有sizeof(sa) == sizeof(ia) == sizeof(p) == 4, 因为sa的类型是char* ,ia的类型是int* ,p的类型是char*。

目前内存管理就写这么多,以后在工作中继续学习,继续探索,大部分都是猜想,没有验证,如有错误,欢迎留言讨论。不论到了哪一年那一月。

阅读全文
0 0