转载自http://www.youranshare.com/blog/sid/92.html
在C++
中,结构体和类它们都是有构造函数、析构函数和成员函数的,他们两者的根本区别就是:结构体中访问控制默认是public
的,而类中默认的访问控制是private
的。对于C++
中的结构体而言,public
、protected
、private
的访问都是在编译期进行检查的,当越权访问的时候,编译过程中会给出此类的错误并给与提示,在编译成功后,程序在执行的过程中不会有任何的检查和限制,这一点你可以通过类的指针偏移做一下测试。因此在反汇编中,C++
中的结构体与类没有分别,两者的原理是相同的,只是类型名称不同。
说一下对象内存的布局
对于类和对象的关系想必你已经很了解了,类是一个抽象的概念,对象是一个实例化的具体存在的。这里我们以一个简单的类,谈一下类与对象的关系:
#include <iostream>
using namespace std;
class CNumber{
public:
CNumber(){
m_One = 1;
m_Two = 2;
}
int GetNumberOne(){
return m_One;
}
int GetNumberTwo(){
return m_Two;
}
private:
int m_One;
int m_Two;
};
int main()
{
CNumber num;
cout << num.GetNumberOne() << endl;
//尝试着用指针访问
cout << *(int*)(void*) &num << endl;
cout << *((int*)(void*)&num + 1) << endl;
return 0;
}
我们知道类的私有成员是不能被直接访问到的,但是我们在程序运行后通过指针的偏移还是依然可以读取内存的数据,就像结构体一样,如图所示我们的运行结果:
可以发现,将对象的指针强制转换成我们需要的数据类型,然后通过指针在内存中的偏移可以访问到对象的私有成员!这也就说明了C++
中类保护属性是编译的时候进行检测的!
C++
中类的成员变量和结构体一样,也是按照顺序依次放到内存中的,先定义的数据成员放到地地址,后定义的数据成员放到高地址处,但是对象的大小只包含数据成员,类的成员函数属于执行代码,不属于类对象的数据。
你可以通过sizeof
获取到CNumber
对象的大小,它们的大小都是8Byte
,这8Byte
字节是由类中的两个数据成员主城,它们都是int
类型,格子的长度都为4Byte
。从内存的布局上来看,类与一个数组非常相似,都是由多个数据成员组成,但是类的能力要远远大过数组。类的成员变量类型定义非常广泛,除了本身对象之外,任何已知的数据类型都可以在内种作为成员变量进行定义。
为什么在类中不能定义自身的对象呢?因为类需要在申请内存的过程中计算出自身的实际大小,用于实例化对象。如果你在类中定义了自身的对象变量,那么在计算类中各个数据成员长度时,又会因为回到自身导致无限递归下去,而这个递归没有出口,所以不能在类中定义自身的对象成员变量。但是指针是可以的,因为指针的长度是确定的,也就是相当于一个常量值,因此定义自身的指针是不会影响到类本身大小的计算的。根据上面所说的东西,我们是否可以定义一个公式用于描述类的对象长度呢?也许你会认为下面的公式可以用于计算对象的长度:
对象长度= sizeof(成员1) + …+szieof(成员2)+…+sizeof(成员n)
这里我明确的告诉你,这个公式是错误的,对象大小的计算远远没有那么简单,即是我们抛开虚函数和继承的原因,仍然有三种情况能够推翻此公式:
空类
内存对齐问题
静态数据成员
当出现上面的情况时,类的对象长度的计算就需要小心了:
空类: 就是说类内部没有任何数据成员,但是实际上类对象的长度可是为1
字节
内存对齐: 在VC++6.0
中,类和结构体中的数据成员是根据它们在类或者结构体中出现的顺序来依次申请内存的,但是由于内存对齐的原因,它们并不会一定像数组那样内存是连续排列的,由于数据类型不同,因此占用的内存空间大小也会不同,在申请内存的时候会按照一定的规则.
我们以一个结构体为例子来看看这个结构体中成员变量地址的排列:
struct TagTest{
char a;
int b;
};
如图所示,可以看到a
,b
的地址相差为4
字节,这也就是说a
,b
这两个变量是没有连续的分配在内存中的,这中现象就是内存地址对齐导致的.
在为结构体和类中的数据成员分配内存的时候,结构体中的当前数据成员类型长度为M
,指定对齐值为N
(编译器指定的,例如为8Byte
),那么实际上的对齐值q = Min(M,N)
,也就是说这个数据成员所在的地址必须为q
的倍数,所以在上面的结构体中,数据成员b
,它的对齐值为q=Min(4,8)
也就是4
,所以b
的地址必须为4
的倍数,虽然前面的数据成员的地址a
为18086720
,b
只是占用了1
个字节,但是b
的地址需要为4
的倍数,那么就需要在数据成员a
后面偏移3
个字节的位置放置b
,如图所示的a
和b
的内存分布:
下面我们来分析一下a
,b
的地址分配过程:
首先, 开始分配a
的地址,a
为一个char
类型,它的对齐值M=1
,在VC++6.0
中编译器的默认对齐值为8
,那么q = Min(1,8)
,也就是q=1
,地址需要是1
的倍数,所以直接分配地址就行了。例如分配了地址为18086720
(十进制)
其次,开始分配b
的地址,b
是一个int
类型对齐值为4Byte
,那么q = Min(4,8)
,也就是q = 4
,它所在的地址需要是4
的倍数,如果此时我们直接将b
分配在a
的后面,那么b
的地址将会是18086721
,不满足对齐的地址,所以需要往后偏移3
个字节,对应的地址就是18086724
,这个地址满足对齐值4
,所以系统给b
分配的地址为18086724
,分配完毕。
通过上面的分配我们可以看到,这个结构体一共占用了8Byte
,而不是我们认为的5
字节。另外值得说的是a
后面的那3
个字节里面可不是填充的0x00
,一般系统都会填充0xCC
。
看完上面的东西,有些童鞋肯定感觉到了一个问题:“结构体本身也是一种数据类型,既然是数据类型,就像int
一样,结构体本身也是有对齐的吧“.
答案是肯定的,数据类型都是有对齐值的,数据结构本身也是一种类型,与int
基本类型无差别,当然也存在对齐值的问题,下面我给出一个数据结构:
struct STest{
double da; // 8字节大小
int ib; // 4字节大小
short sc; // 2字节大小
};
这个数据结构STest
的大小是16
而不是14
,为什么?
这是因为结构体本身也是一种数据类型,当然它也有对应的字节对齐处理,这里我们将会讨论一下对齐值对结构体整体大小的影响,如果按照VC++6.0
默认的8
字节对齐,那么对于一个结构体来说它的对齐值依然满足公式q=Min(M,N)
(VC++6.0
中N=8
),但是需要注意的是这里的M
应该是结构体中的数据成员类型的最大值,就像结构体STest
,它的对齐值按照最大的数据成员double da
,对齐值也就是 8Byte
,这样一来可以计算出结构体STest
的对齐值为8
,所以编译器在STest
的最后一个成员short sc
后面有增加了2
个字节用于填充结构体,使得整体大小为16
字节,这样就满足了对齐的要求。
通过上面的介绍可以看出,结构体的对齐值是根据结构体内部最大的成员长度动态调整的,可不是固定的8
字节,也可以是4
字节。
在C++
中,虽然存在默认的对齐值,但是这个默认值也是可以修改的,我们可以使用预编译指令 #pragma pack(N)
来指定对齐大小,例如我们下面的代码,将对齐值设置为1
字节:
#pragma pack(1)
struct STest{
double da; // 8字节大小
int ib; // 4字节大小
short sc; // 2字节大小
};
运行结果如下图所示,这里的sizeof
计算出的结构就是 14
字节了:
经过预编译指令后,将对齐值调整为1
字节,根据对齐规则,q=Min(4,1)
得出对齐值为1
字节,既然是1
字节,辣么就不用想了,直接就是14
字节的大小了。
但是要注意的是,你使用#pragma pack(N)
设置的对齐值可不一定会生效的,这是因为q=Min(M,N)
,要知道你的N
要是太大的话~~q
最终还是等于M
的,就像你设置对齐值为128
,根本没啥用嘛= =
,对齐值的计算流程总的来说:将设定的对齐值与结构体中最大的基本类型数据成员的长度进行比较,取两者之间的较小者。
当结构体中以数组作为成员的时候,将会根据数组 元素类型 的长度计算对齐值,而不是按照数组的整体大小去计算。
结构体含有数组类型的对齐
当结构体中以数组作为成员的时候,将会根据 数组元素 的长度计算对齐值,而不是根据数组的整体长度来计算,例如下面的代码:
Struct{
Char cChar; // 占用一个字节内存
Char cArray[4]; // 占用多少字节内存呢?
Short sShort; // 应该占用2字节内存
}
按照对齐的规定,cChar
与cArry
它们都是char
类型的数据,内存对齐没有缝隙,不需要插入空白的数据。但是当cArray
与sShort
对齐的时候,cChar
与cArray
已经在内存中占用了5
个字节,此时按照结构体中当前的数据类型short
进行对齐的时候,就需要在cArray
后面在插入一个字节就OK
了,其结构如下图所示:
结构体内部的数据成员已经对齐了,下面就是处理结构体本身的对齐值问题了。根据结构体中的数据成元类型得到,最大的数据成员sShort
占2
个字节,其余成员都是1字节的大小,在默认的情况下,对齐值为8
,根据公式q = Min(M,N)
计算得出该结构体的对齐值为2
,而此时结构体的总大小为8
字节,也就是说无需填入数据即可满足对齐要求.
当结构体中出现结构体类型的数据成员的时候,不会将嵌套的结构体类型的整体长度参与到对齐值的计算中,而是以嵌套定义的结构体所使用的对齐值进行对齐计算,如下面的代码所示:
struct tagOne
{
char cChar; // 占用1字节
char cArray[4]; // 占用5字节
short sShort; // 占用2字节
};
struct tagTwo
{
int nInt; // 占用4字节
tagOne one; // 占用8字节
};
在上面的结构体中,虽然tagOne
占用了8
字节大小,但是由于其对齐值为2
,所以在计算tagTwo
的对齐值的时候参数one
的对齐值是2
,所以根据对齐计算的公式q=Min(M,N)
得出数据结构tagTwo
的对齐值为4
,占用了12
个字节!而不是以8
对齐占用16
字节。
静态数据成员
当类中的数据成员被修饰为静态成员的时候,对象的计算方法又会发生变化。因为虽然静态数据成员是在类内部进行定义,但是它与静态局部变量是类似的,存放的位置和全局变量一致。只是编译器增加了作用域的检查,在作用域外不可见,同类对象将共享有静态数据成员的空间.