C++中的成员变量的内存分配问题

在C\C++中,通常可以把内存理解为4个分区: 栈、堆、全局/静态存储区和常量存储区 。下面我们分别简单地介绍一下各自的特点。
1 栈(stack)

        通常是用于那些在编译期间就能确定存储大小的变量的存储区,用于在函数作用域内创建,在离开作用域后自动销毁的变量的存储区。通常是局部变量函数参数等的存储区。他的存储空间是连续的,两个紧密挨着定义的局部变量,他们的存储空间也是紧挨着的。栈的大小是有限的,通常Visual C++编译器的默认栈的大小为1MB,所以不要定义int a[1000000]这样的超大数组。主要用来存放局部变量, 传递参数, 存放函数的返回地址。.esp 始终指向栈顶, 栈中的数据越多, esp的值越小

2 堆(heap)

        通常是用于那些在编译期间不能确定存储大小的变量的存储区,它的存储空间是不连续的,一般由malloc(或new)函数来分配内存块,并且需要用freedelete)函数释放内存。如果程序员没有释放掉,那么就会出现常说的内存泄漏问题。需要注意的是,两个紧挨着定义的指针变量,所指向的malloc出来的两块内存并不一定的是紧挨着的,所以会产生内存碎片。另外需要注意的一点是,堆的大小几乎不受限制,理论上每个程序最大可达4GB。

3 数据区(.data和.bss

        包括初始化数据区(.data)、未初始化数据区(.bss)两种,和“栈”一样,通常是用于那些在编译期间就能确定存储大小的变量的存储区,但它用于的是在整个程序运行期间都可见的全局变量和静态变量未初始化数据区(BSS):用于存放程序的静态变量,这部分内存都是被初始化为零的;而初始化数据区用于存放可执行文件里的初始化数据这两个区统称为数据区。初始化数据区中包含一个常量存储区(.rodata)保存字符串。

4 代码区(text)

       是个只读区,存放了程序的代码。任何尝试对该区的写操作会导致段违法出错。代码区是被多个运行该可执行文件的进程所共享的

5 总结

        根据上面的内容,分别将栈和堆、全局/静态存储区和常量存储区进行对比,结果如下。

表1 栈和堆的对比

 

存储内容

局部变量

变量

作用域

函数作用域、语句块作用域

函数作用域、语句块作用域

编译期间大小是否确定

大小

1MB

4GB

内存分配方式

地址由高向低减少

地址由低向高增加

内容是否可以修改

 
表2 全局/静态存储区和常量存储区的对比


全局/静态存储区

常量存储区

存储内容

全局变量、静态变量

常量

编译期间大小是否确定

内容是否可以修改

注意:

1)     堆向高内存地址生长

2)     栈向低内存地址生长

3)     堆和栈相向而生,堆和栈之间有个临界点,称为stkbrk。

 

1、一条进程在内存中的映射

    假设现在有一个程序,它的函数调用顺序如下:

main(...) ->; func_1(...) ->; func_2(...) ->; func_3(...),即:主函数main调用函数func_1; 函数func_1调用函数func_2; 函数func_2调用函数func_3。

当一个程序被操作系统调入内存运行, 其对应的进程在内存中的映射如下图所示:

 


注意:

l          随着函数调用层数的增加,函数栈帧是一块块地向内存低地址方向延伸的;

l          随着进程中函数调用层数的减少(即各函数调用的返回),栈帧会一块块地被遗弃而向内存的高址方向回缩

l          各函数的栈帧大小随着函数的性质的不同而不等, 由函数的局部变量的数目决定。

l          进程对内存的动态申请是发生在Heap(堆)里的。随着系统动态分配给进程的内存数量的增加,Heap(堆)有可能向高址或低址延伸, 这依赖于不同CPU的实现,但一般来说是向内存的高地址方向增长的。

l          函数的栈帧:包含了函数的参数(至于被调用函数的参数是放在调用函数的栈帧还是被调用函数栈帧, 则依赖于不同系统的实现)。函数的栈帧中的局部变量以及恢复该函数的主调函数的栈帧(即前一个栈帧)所需要的数据, 包含了主调函数的下一条执行指令的地址

2、  函数的栈帧

    函数调用时所建立的栈帧包含下面的信息:

1)     函数的返回地址。返回地址是存放在主调函数的栈帧还是被调用函数的栈帧里,取决于不同系统的实现;

2)     主调函数的栈帧信息, 即栈顶和栈底;

3)     为函数的局部变量分配的栈空间

4)     为被调用函数的参数分配的空间取决于不同系统的实现。

注意:

l          BSS区(未初始化数据段):并不给该段的数据分配空间,仅仅是记录了数据所需空间的大小

           BSS段的大小从可执行文件中得到 ,然后链接器得到这个大小的内存块紧跟在数据段后面。当这个内存区进入程序的地址空间后全部清零。包含数据段和BSS段的整个区段此时通常称为数据区。所以说BSS不占据程序空间的意思是在编译程序时此段并不存在,在运行时动态创建一个空间,并由系统维护

l          DATA(初始化的数据段):为数据分配空间,数据保存在目标文件中

 

不同类型的变量在内存中的位置:

1,局部变量、函数参数存放在上。静态局部变量,并不是在调用函数时分配,在函数返回时释放,而是像全局变量一样静态分配在.data数据段,但它的作用域只在函数中起作用。

2,堆,给动态分配内存使用。

3,全局变量、静态变量位于.data数据段;未初始化变量则位于.bss未初始化数据段。

4const修饰的全局变量在.rodata只读数据段(const变量在定义时必须初始化,如果未初始化将被设为0或空),只读数据段在.text同一个segment

5,代码段即存储程序文本。指令指针中的指令就从这里取得。这个段一般是可以被共享的:你可以使用两个编辑器(vi)来编辑文本,则它们共享一个代码段。


全局变量初始化和未初始化的区别

我们都知道未初始化的静态变量或全局变量将被初始化为空串或0;未初始化的局部变量的值不确定。

全局变量不初始化,则默认为0,存放在.bss数据段中。

编译器在编译的时候针对这两种情况会产生两种符号放在目标文件的符号表中,对于初始化的,叫强符号,未初始化的,叫弱符号

连接器在连接目标文件的时候,如果遇到两个重名符号,会有以下处理规则:

1、如果有多个重名的强符号,则报错。

2、如果有一个强符号,多个弱符号,则以强符号为准。

3、如果没有强符号,但有多个重名的弱符号,则任选一个弱符号。

基于以上规则,使用以下程序测试

#include <stdio.h>

int global_int;

void setInt();

int main()

{

    printf(“未初始化全局变量: %d\n”, global_int);

    setInt();

    printf(“改变全局变量: %d\n”, global_int);

}

int global_int;

void setInt()

{

    global_int = 2;

}

以上程序正常运行。如果将第2行和第12行的global_int任意一个初始化,程序也正常运行。但将第2行和第12行的gloal_int都初始化,将会出现:error: redefinition of ‘global_int’

所以我们尽量初始化全局变量。

除了连接有区别外,它们存储的位置也不一样:初始化的全局变量被保存在data数据段中;而未初始化的全局变量被保存在.bss数据段中。


 
C++类和成员变量的长度
很多人都知道C++类是由结构体发展得来的,所以他们的成员变量(C语言的结构体只有成员变量)的内存分配机制是一样的。下面我们以类来说明问题,如果类的问题通了,结构体也也就没问题啦。 类分为成员变量和成员函数,我们先来讨论成员变量。 一个类对象的地址就是类所包含的这一片内存空间的首地址,这个首地址也就对应具体某一个成员变量的地址。(在定义类对象的同时这些成员变量也就被定义了)我们来以一段代码说明问题: //类的定义
class K
{
    public:
         K(){k = 12;}
         ~K(){}
    int k;
}; 
//类的使用//... K kTemp;
 printf("%d--%d\n",&kTemp,&kTemp.k);
 printf("%d--%d\n",sizeof(K),sizeof(kTemp.k));
 int *i = (int*)(&kTemp);
 int w = *i;
 printf("%d\n",w);
运行上面的代码,结果如下:
1310588--1310588
4--4
12
很明显, 类的内存大小和其 唯一的成员变量的内存大小是一致的。 内存地址也是一致的。他们甚至可以相互转换。换成结构体结果也是一样。网友可以自己运行上面代码来进行确认。 这个时候,可能有人会提出疑问了。那么 成员函数又如何?上面得代码就好像类没有任何成员函数一样,根本说明不了问题。 呵呵,所有的函数都是存放在 代码区的,不管是 全局函数,还是 成员函数。要是成员函数占用类的对象空间,那么将是多么可怕的事情:定义一次类对象就有成员函数占用一段空间。 我们再来补充一下静态成员函数的存放问题吧: 静态成员函数与一般成员函数的唯一区别就是没有 this指针,因此 不能访问非静态数据成员,就像我前面提到的,所有函数都存放在 代码区,静态函数也不例外。所有有人一看到 static 这个单词就主观的认为是存放在全局数据区,那是不对的
----------第二篇------------
c++是一种面向对象的编程语言,它向下保持了对c的兼容,同时也允许程序员能够自由的操控内存,虽然会带来一些问题,但这不是我们要探讨的问题,略过不表。类是对某种对象的定义,包含变量和方法,也可以理解为现实生活中一类具有共同特征的事务的抽象,他是面向对象语言的基础。所以类是不占有内存的,可是如果类生成实例那么将会在内存中分配一块内存来存储这个类。
类的实例在内存中是如何分配内存的,有什么需要我们注意的,下面将慢慢到来。
    比如下面一个类:
    class A
    {};
    从形式上看,它似乎什么有没有,事实上它不止隐含了一个 构造函数和一个 析构函数,还有一些 操作符重载函数,比如“=”。如果类A被实例话,如A a;在内存会占据多大的空间呢?有人可能会说4,也有人会说0,还有人会说1,说 1的就对了,为什么会是1呢?原因有很多,如果我们定义一个数组A b[10];如果上面是0,这样的局面将会很尴尬,所以A这样一个空类,编译器会给它一个字节来填充。  
    增加一个变量,(字节对齐默认都是4)
   class  A
   {
       public:
         int i;
   }
类A的实例将占据4个字节的内存,sizeof(A) = 4
   变量i 的初值被编译器指定位0xcdcdcdcd。
   再增加一个变量,
   class A
   {
      public:
      int  i;
      int  l;
   }
   此时按照变量生命的先后顺序,i被放在低地址上,l紧随其后。
   实例占用8个字节,sizeof(A) = 4*2 = 8
   如果类里面含有函数:
 
   class A
  {
       public:
       int i;
       int l;
       int add(int x,int y){return (x+y);}
  };
 有些人可能会说类的大小是12,事实上sizeof(A) = 8;
 为什么会这样,这是因为sizeof访问的 程序的数据段,而函数地址则被保存在 代码段内,所以最后的结果是8.

当类里面含有虚函数时,情况会如何呢?
 class A
 {
      public:
         int i;
         int l;
         static int s;
         virtual void Say(){};
         int add(int x,int y){return (x+y)};
 };
 因为含有虚函数,所以类里面将含有一个 虚指针vptr,指向该类的 虚表vtbl,一个指针占用 四字节的地址,所以sizeof(A) = 12
 虚指针放在 类实例地址的最低位置,

比如 A *a = new A;
 我们可以这样给变量i赋值
 int *p = (int *)a;
 p++;
 *p = 1;//把i的值赋为1.
如果类作为派生类,内存将如何分配呢?
这种情况虽然有些复杂,但并不是说不好理解。
他有多少个父类每个父类的大小加起来在加上自身就是sizeof的大小。

首先介绍一下C++中有继承关系的类对象内存的布局: 
在C++中,如果类中有 虚函数,那么它就会有一个 虚函数表的指针__vfptr,在类对象最开始的内存数据中。之后是类中的 成员变量的内存数据。 
对于子类,最开始的 内存数据记录着父类对象的拷贝(包括父类虚函数表指针和成员变量)。 之后是 子类自己的成员变量数据。 
对于子类的子类,也是同样的原理。但是无论继承了多少个子类, 对象中始终只有一个虚函数表指针。 (指向唯一的一个虚函数,由函数动态多态确定)
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值