C/C++ 内存分配情况

一、C语言中的内存地址分配模型如

 

  C/C++ <wbr>内存分配情况

1、程序代码区:存放函数体的二进制代码。  

2、全局区数据区:全局数据区划分为三个区域。全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。常量数据存放在另一个区域里。这些数据在程序结束后由系统释放。我们所说的BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。

3、栈区:由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

4、堆区:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。

5、命令行参数区:存放命令行参数和环境变量的值。   
   关于局部的字符串常量是存放在全局的常量区还是栈区,不同的编译器有不同的实现。可以通过汇编语言察看一下。不过vc环境下,局部常量就像局部变量一样存储于栈中,全局常量、字符常量存储于文字常量区。TC在常量区。

    在linux下:可以通过参数-c来编译生成汇编文件。如:  
    gcc -c *.c
    gcc *.o -Map test.txt -o test.elf
    用文本编辑器查看test.txt文件,你就看到那些bss段,data段,text段等信息了,但是没有堆栈段相关信息,用objdump命令查看.o文件的反汇编后的信息,或者用gcc -S *.c,查看各个.S文件就明白了。

 

C语言程序编译的内存分配:
1.
栈区(stack) --编译器自动分配释放,主要存放函数的参数值,局部变量值等;
2.
堆区(heap) --由程序员分配释放;
3.
全局区或静态区 --存放全局变量和静态变量;程序结束时由系统释放,分为全局初始化区和全局未初始化区;
4.
字符常量区 --常量字符串放与此,程序结束时由系统释放;
5.
程序代码区
例: //main.c
int a=0; //
全局初始化区
char *p1; //
全局未初始化区
void main()
{
int b; //

char s[]="bb"; //
char *p2; //

char *p3="123"; //
其中,“123\ 0”常量区,p3在栈区
static int c=0; //
全局区
p1=(char*)malloc(10); //10
个字节区域在堆区
strcpy(p1,"123"); //"123\0"
在常量区,编译器可能会将它与p3所指向的"123"优化成一个地方。
}

 

堆与栈的区别  

一、预备知识―程序的内存分配

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

 

1、栈区(stack)―   由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。  

2、堆区(heap) ―   一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。

3、全局区(静态区)(static)―,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。

4、文字常量区  ―常量字符串就是放在这里的。程序结束后由系统释放

5、程序代码区―存放函数体的二进制代码。

二、例子程序

这是一个前辈写的,非常详细

//main.cpp

int a = 0; 全局初始化区  

char *p1; 全局未初始化区  

main()  

{

int b; 栈  

char s[] = "abc"; 栈

char *p2; 栈

char *p3 = "123456"; 123456\0在常量区,p3在栈上。  

static int c =0;全局(静态)初始化区  

p1 = (char *)malloc(10);  

p2 = (char *)malloc(20);

分配得来得10和20字节的区域就在堆区。  

strcpy(p1, "123456"); 123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。  

 

二、堆和栈的理论知识  

2.1申请方式  

stack: 由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开空  间。  

heap: 需要程序员自己申请,并指明大小,在c中malloc函数  

如p1 = (char *)malloc(10);

在C++中用new运算符

如p2 = (char *)malloc(10);

但是注意p1、p2本身是在栈中的。

 

2.2 申请后系统的响应  

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

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

 

2.3申请大小的限制  

栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。  

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

2.4申请效率的比较:

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

2.5堆和栈中的存储内容

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

堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容有程序员安排。

2.6存取效率的比较  

char s1[] = "aaaaaaaaaaaaaaa";

char *s2 = "bbbbbbbbbbbbbbbbb";

aaaaaaaaaaa是在运行时刻赋值的;而bbbbbbbbbbb是在编译时就确定的; 但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。  比如:  

#include  

void main()

{

char a = 1;

char c[] = "1234567890";

char *p ="1234567890";

a = c[1];

a = p[1];

return;

}

对应的汇编代码

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读取字符,显然慢了。

2.7小结:

堆和栈的区别可以用如下的比喻来看出: 使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。


二、一个典型的C程序存储空间布局

一个典型的C程序存储空间布局由以下几个部分组成:

(正文段) CPU执行的指令部分,也就是主要的程序代码编译出来的结果,只读,通常可以共享。

(初始化数据段)通常称之为数据段,包含了程序中需要明确赋值的变量,譬如一些初始化的全局变量等,如int a = 10,变量名和值都存放在这个段中。

(未初始化数据段)通常称之为BSSBlock Started by Symbol)段,包含了程序中没有进行赋值的变量,譬如一些未初始化的全局变量,如 int a,在程序执行之前,内核会把这部分全部置为0NULL

()自动变量以及每次函数调用时所需保存的信息放在此段中。如函数调用时要保存返回地址等。栈是从上向下分配的。

()通常在堆中进行动态存储分配,如malloc, calloc, realloc等都从这里面分配。堆是从下向上分配的。

通常堆顶和栈底之间的虚拟地址空间是很大的。

X86处理器上的Linux,正文段从0x08048000开始,栈底则从0xC0000000之下开始。

下图是一个典型的C程序存储空间的逻辑布局:

C/C++ <wbr>内存分配情况

//main.c

int a = 0; 全局初始化区

char *p1; 全局未初始化区

main( )

{

int b;                  //

char s[] = "abc"; //

char *p2;           //

char *p3 = "123456";    //123456\0在常量区,p3在栈上。

static int c =0;        //全局(静态)初始化区

p1 = (char *)malloc(10);

p2 = (char *)malloc(20); //分配得来得1020字节的区域就在堆区。

strcpy(p1, "123456");    //123456\0放在常量区,编译器可能会将它与p3所指向的"123456"优化成一个地方。

}

 

三、c/c++语言中的内存分配──堆和栈的区别

基本知识

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

如下面的一个例子:

//main.cpp

#include <string>
#include <iostream>
using namespace std;

int a = 0; //全局初始化区
char *p1; //全局未初始化区

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

 二、变量的存储方式可分为“静态存储”和“动态存储”两种。

     静态存储变量通常是在变量定义时就分定存储单元并一直保持不变,直至整个程序结束。动态存储变量是在程序执行过程中,使用它时才分配存储单元, 使用完毕立即释放。典型的例子是函数的形式参数,在函数定义时并不给形参分配存储单元,只是在函数被调用时,才予以分配,调用函数完毕立即释放。如果一个函数被多次调用,则反复地分配、 释放形参变量的存储单元。

三、而变量存储类型是怎样与内存分配相对应的呢?

     存储类型有以下四种:自动(auto)、静态(static)、外部(extern)、寄存器(register)。变量说明的完整形式应为:存储类型说明符 数据类型说明符 变量名,变量名…; 例如:static int a,b; 说明a,b为静态类型变量
auto char c1,c2; 说明c1,c2为自动字符变量
static int a[5]={1,2,3,4,5}; 说明a为静整型数组
extern int x,y; 说明x,y为外部整型变量

auto和register存储类型的变量属于动态存储方式,而static和extern的变量属于静态存储方式。

 static变量放在内存中的全局区(静态区),在程序一开始就为这个变量分配内存空间。全局静态变量在全局可见,局部的静态变量在局部可见,但在程序执行过程中一直驻留内存。

   static int a=0; //全局静态变量

    void foo()
    {
       static int v=0;//局部静态变量

      …
     } 

     auto变量是自动存储类型,每次执行到定义该变量的语句块时,都将会为该变量在内存中产生一个新的拷贝,并对其进行初始化。实际上,如果不特别指明,局部变量的存储类型就默认为自动的,因此,加不加auto都可以。只有局部变量才能定义成寄存器变量 

深入分析

下面通过分析一个例子来深入分析一下:

#include <string>
#include <iostream>
using namespace std;
int a = 12;
extern double b ; //全局初始化区
char *d; //全局未初始化区
static int c = 12;

void foo(int foo1)
{
const char *foo2 = "abcsf";
static int foo3 = 12;
cout << " foo2:" << foo2 << " foo3:" << foo3 << endl;
}
main()
{
int e;                                       // 栈

char f[] = "abc";                        // "abc"在常量区,f[]在栈上
char *g;                                   //栈
char *h = "123456";                    //123456\12在常量区,h在栈上。
static int i = 12;                         //全局(静态)初始化区
extern int j;
register int k = 12;
int *l = new int(12);//放在堆上
d = new char[112];
strcpy(d, h);                             //123456\12放在常量区,编译器可能会将它与p3

                                               //所指向的"123456"优化成一个地方。
g=h;
e=1;
foo(12);
cout <<  " a:"<< a << "  b:"<< b
<< "  c:"<< c << "  d:"<< d
<< "  e:"<< e << "  f:"<< f
<< "  g:"<< g << "  h:"<< h
<< "  i:"<< i << "  j:"<< j
<< "  k:"<< k << "  l:"<< *l
<< endl;                                     //防止编译器优化
delete l;
delete [] d;

 

//foo1.cpp

static int j = 12;
static float b = 1.0F;

    用"gcc -S foo.cpp foo2.cpp"命令生成汇编代码foo.s, foo1.s,观察每一段代码的对应汇编程序。首先看foo1.cpp编译后结果:

 

.file "foo1.cpp"

.globl _j                 全局变量
.data                   说明以下的定义是放在数据段上的,也即放在全局区(静态区) 

                          (static)上
.align 4                 确保指令会在字(word)的边缘开始,如果j是double型的将

                            为.align 8
_j:                        j是foo.cpp文件中声明的外部变量,在foo1.cpp中是静态变量
.long 12                j的值为12

.globl _j
.align 4
_b:
.long 1065353216   b的值为1,为什么是这个数需要看浮点数的表示方法,如果b设为

                            double型,则这里需要分两步设置.long 0 .long 1072693248 

     然后再看foo.cpp里面的内容吧: 非静态变量a的定义与静态变量c的定义基本是一样的有一点不同就是a的定义加入了一句.globl _a,表明这个变量是全局变量,而在文件中的static变量则没有声明为全局变量。则变量的作用域只限于本文件
 

.globl _a
.data
.align 4
_a:
.long 12

.data
.align 4
_c:
.long 12

   而对于未初始化的变量d,则声明如下

.globl _d
.globl _d
.bss                   说明以下的定义是放在未初始化的全局区(静态区)(static)上
.align 4
_d:
.space 4             只分配大小为4的空间(指针有4个字)节,但并没有给这个变量赋值

     与我们预想的情况一样,这里并没有对extern变量的声明,因为我们只要拿来用就可以了,当将两个程序链接的时候会将几个文件的data段,bss段,text段等等连接到一起,这时候就可以找到没有声明的变量了。

     下面还有一段关于foo3的定义。这说明编译器将所有static变量都放在data段,而局部static变量则给他起一些奇怪的名字(带有这个变量的作用域等信息)_ZZ3fooiE4foo3,来限定他们的作用域,这样局部的static变量就只能在局部作用域中被访问了。 

.align 4
_ZZ3fooiE4foo3:
.long 12

     接下来是text段,记录了一组常数变量。这是在foo函数中用到的常量,编译器实际上会找到本段代码中的常量然后将其放在一起。

.text
LC0:
.ascii "abcsf\0"
LC1:
.ascii " foo2:\0"
LC2:
.ascii " foo3:\0"

      下面进入foo函数的代码段。下面代码的意思为将当前基栈地址入栈,然后将栈顶地址(esp)存入栈底寄存器(ebp)(注意,因为栈的结构是往小地址扩展的,大地址为栈底,小地址为栈顶),将当前栈顶自减24,所以当前栈内的空间就为24,在24个字节的空间中有4个字节的 const char* 型的foo2。为什么要分配24个字节?这与编译器的设置有关,gcc编译器默认的最小栈空间大小是24字节。

.globl __Z3fooi
.def __Z3fooi; .scl 2; .type 32; .endef
__Z3fooi:
LFB1:
pushl �p
LCFI0:
movl %esp, �p
LCFI1:
subl $24, %esp

     下面一句话将LC0放入ebp-4的地址中(即栈底基地址-4),这里就可以发现常量数据不可更改的性质实际上是由编译器来决定的,当形成汇编语言之后常量型数据和局部变量就没有什么区别了,即函数中代码 int a =10; 与 const int b = 10; 的编译结果都是一样的,不同的是当在本代码块中改变b时编译通不过而已。

movl $LC0, -4(�p)

      下面的几行代码比较难理解,将一个东西放到栈顶,然后将LC1(要输出的第一个字符串)放到栈顶朝栈底方向+4个地址的位置,然后调用一个函数。可以想象这个函数要用到esp地址与esp+4地址中的变量,这可能是cout的一个机制。

movl $__ZSt4cout, (%esp)
movl $LC1, 4(%esp)
call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc

       接着将eax值放入esp,这是cout函数的返回值,这句话与movl $__ZSt4cout, (%esp)
首次用cout的作用是一样的,返回一个流可以继续 <<( C++ primer 里面有详细的说明)不用理他。再将ebp-4地址中的变量(foo2)放入esp+4,由于栈寄存器不能直接赋值所以用了一下eax,然后调用同样的函数。
movl �x, (%esp)
movl -4(�p), �x
movl �x, 4(%esp)
call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc

     下面的代码同理,不详述。

movl �x, (%esp)
movl $LC2, 4(%esp)
call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
movl �x, (%esp)
movl _ZZ3fooiE4foo3, �x
movl �x, 4(%esp)
call __ZNSolsEi
movl �x, (%esp)
movl $__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, 4(%esp)
call __ZNSolsEPFRSoS_E
leave
ret

      我们的话题有点偏离主题了,现在在重新回到内存分配的主题。下面又是一个很大的text段,编译器将本段内的字符串常量都放在这里了

.text
LC3:
.ascii "abc\0"
LC4:
.ascii "123456\0"
LC5:
.ascii " a:\0"
LC6:
.ascii "  b:\0"
LC7:
.ascii "  c:\0"
LC8:
.ascii "  d:\0"
LC9:
.ascii "  e:\0"
LC10:
.ascii "  f:\0"
LC11:
.ascii "  g:\0"
LC12:
.ascii "  h:\0"
LC13:
.ascii "  i:\0"
LC14:
.ascii "  j:\0"
LC15:
.ascii "  k:\0"
LC16:
.ascii "  l:\0"

    同样,下面的代码将函数块中的static变量放入data块中。

.data
.align 4
_ZZ4mainE1i:
.long 12

     分配40个字节的栈空间。

_main:
LFB2:
pushl �p
LCFI3:
movl %esp, �p
LCFI4:
subl $40, %esp

    下面的代码是gcc编译器的new的过程,new的过程需要12个字节的栈空间。总之,我们知道new的东西没有放在栈上,只将返回的指针放在栈上。

movl $4, (%esp)                 new一个int所以是4个字节
call __Znwj                   new空间时调用的函数
movl �x, -28(�p)   返回的地址赋给l
movl -28(�p), �x
movl $12, (�x)               12赋值给这个地址,以下可能是在检验是否出错
movb $0, %al
movl -28(�p), �x
movl �x, -20(�p)
testb %al, %al
je L10
movl -28(�p), �x
movl �x, (%esp)
call __ZdlPv

下面是堆上分配数组。

movl $112, (%esp)
call __Znaj
movl �x, _d
movl _d, �x
movl �x, (%esp)               这里将指针赋给esp的原因是后面继续要将这个指针用

                                   于strcpy

g=h

movl -16(�p), �x
movl �x, -12(�p)
e =1

movl $1, -4(�p)
movl $12, (%esp)

     上面的语句将栈空间补满了,下面的语句实际上直接用extern变量,因为在本代码中没有对_b分配内存,所以若是连接的代码也没有这个变量的话将会出现连接错误。

movl _b, �x
movl �x, 4(%esp)
call __ZNSolsEf
     在register变量中直接出现了下面的代码,编译器将有k出现的地方直接用12代替了。这里大概跟宏差不多。

movl $12, 4(%esp)
call __ZNSolsEi

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章