C++对象模型(3) -- 数据语义学

一、数据成员绑定时机

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef string mytype;
//定义一个类
class A
{
public:
void myfunc(mytype tmpvalue) //mytype = string
{
m_value = tmpvalue; //出错,是把一个string类型给一个整型
}
typedef int mytype;
private:
mytype m_value; //int
};

  (1)编译器对成员函数的解析,是整个类定义完毕后才开始的。因为只有整个类定义完毕后,编译器才能看到类中的成员变量,才能根据实际的需要把出现该成员变量的场合做适当的解释(成员函数中解析成类中的变量类型,全局函数中解析成全局的变量类型)。
  (2)对于成员函数参数,是在编译器第一次遇到整个类型mytype的时候被决定的。所以,mytype第一次遇到的时候,编译器只看到了typedef string mytype,没有看到类中的typedef int mytype。
  (3)为了在类中尽早的看到类型mytype,所以这种类型定义语句typedef,一定要挪到类的最开头定义。(即上述代码中应该把typedef int mytype移到class A的开头处)当后边的成员函数第一次遇到这个类型mytype的时候,它就本着最近碰到的类型的原则来应用最近碰到的类型。

二、进程内存空间布局

  不同的数据在内存中会有不同的保存时机,保存位置。当运行一个可执行文件时,操作系统就会把这个可执行文件加载到内存,此时进程有一个虚拟的地址空间(内存空间),分为:堆栈段、数据段、代码段等。从下面代码中可以看出些许规律。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
int *ptest = new int(120);
int g1;
int g2;
int g3 = 12;
int g4 = 32;
int g5;
int g6 = 0;
static int g7;
static int g8=0;
static int g9 = 10;
void mygfunc()
{
return;
}
//定义一个类
class MYACLS
{
public:
int m_i;
static int m_si; //声明不是定义
int m_j;
static int m_sj;
int m_k;
static int m_sk;
//static void myclsfunc() {}
};
int MYACLS::m_sj = 0; //这才是定义;

int main()
{
int i = 7;;
printf("i地址=%p\n", &i); //i地址=00AFFB94

printf("ptest地址=%p\n", &ptest); //ptest地址=00F9B300
printf("g1地址=%p\n", &g1); //g1地址=00F9B2EC
printf("g2地址=%p\n", &g2); //g2地址=00F9B2F0
printf("g3地址=%p\n", &g3); //g3地址=00F9B000
printf("g4地址=%p\n", &g4); //g4地址=00F9B004
printf("g5地址=%p\n", &g5); //g5地址=00F9B2F4
printf("g6地址=%p\n", &g6); //g6地址=00F9B2F8
printf("g7地址=%p\n", &g7); //g7地址=00F9B304
printf("g8地址=%p\n", &g8); //g8地址=00F9B308
printf("g9地址=%p\n", &g9); //g9地址=00F9B008
printf("MYACLS::m_sj地址=%p\n", &(MYACLS::m_sj)); //MYACLS::m_sj地址=00F9B2FC

printf("mygfunc()地址=%p\n", mygfunc); //mygfunc()地址=00F91433
printf("main()地址=%p\n", main); //main()地址=00F9132A

cout << (void*)mygfunc << endl; //00F91433
return 1;
}

  从上述代码的打印结果来看,不同类型的数据在内存中的存储位置是不同的,同一类型的数据是连续存储的。

三、类中成员变量的布局

  (1)普通成员变量的存储顺序是按照在类中的定义顺序从上到下来的。比较晚出现的成员变量在内存中有更高的地址。类定义中pubic,private,protected的数量,不影响类对象的sizeof。
  (2)某些因素会导致成员变量之间排列不连续,就是边界调整(字节对齐),调整的目的是提高效率,编译器自动调整。调整方式:往成员之间填补一些字节,使用类对象的sizoef字节数凑成一个4的整数倍,8的整数倍。
  (3)为了统一字节对齐问题,引入一个概念叫一字节对齐(不对齐):#pragma pack(1)。把这个语句放到程序最开始处即可。
  (4)有虚函数时,编译器往类定义中增加vptr虚函数表指针(内部的数据成员)。
  (5)成员变量偏移值,就是这个成员变量的地址,离对象首地址偏移多少。

四、类中成员变量的存取

  (1)静态成员变量,可以当做一个全局量,但是它只在类的空间内可见。引用时用类名::静态成员变量名。静态成员变量只有一个实体,保存在可执行文件的数据段。
  (2)非静态成员变量(普通的成员变量)存放在类的对象中。存取通过类对象(类对象指针)来操作。例如:对于普通成员的访问,编译器是把类对象的首地址加上成员变量的偏移值。

五、单一继承下的数据成员布局

  (1)一个子类对象,所包含的内容,是他自己的成员,加上他父类的成员的总和。从偏移值看,父类成员先出现,然后才是孩子类成员。
  (2)引入继承关系后,可能会带来内存空间的额外增加(字节对齐)。所以不能用memcpy内存拷贝把父类对象的内容直接往子类对象里拷贝。

六、带有虚函数的类中数据成员布局

单个类带虚函数的数据成员布局

  类中引入虚函数时,会有额外的成本付出:
  1)编译的时候,编译器会产生虚函数表。
  2)对象中会产生虚函数表指针vptr,用以指向虚函数表。
  3)增加或者扩展构造函数,增加给虚函数表指针vptr赋值的代码,让vptr指向虚函数表。
  4)如果多重继承,比如你继承了2个父类,每个父类都有虚函数的话,每个父类都会有vptr,那继承时,子类就会把这两根vptr都继承过来,如果子类还有自己额外的虚函数的话,子类与第一个基类共用一个vptr。
  5)析构函数中也被扩展增加了虚函数表指针vptr相关的赋值代码,这个赋值代码似乎和构造函数中代码相同。

单一继承父类带虚函数的数据成员布局

  代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Base
{
public:
int m_bi;
virtual void mybvirfunc() {}
};
class MYACLS :public Base
{
public:
int m_i;
int m_j;
MYACLS()
{
int abc = 1; //方便加断点
}
~MYACLS()
{
int def = 0;//方便加断点
}
};

int main()
{
cout << sizeof(MYACLS) << endl; //16
printf("MYACLS::m_bi = %d\n", &MYACLS::m_bi); //MYACLS::m_bi = 4
printf("MYACLS::m_i = %d\n", &MYACLS::m_i); //MYACLS::m_i = 8
printf("MYACLS::m_j = %d\n", &MYACLS::m_j); //MYACLS::m_j = 12

MYACLS myobj;
myobj.m_i = 3;
myobj.m_j = 6;
myobj.m_bi = 9;

return 1;
}

  从打印结果可以看出,当父类中有虚函数的时候,指向父类虚函数表的虚函数表指针会占用内存开头的四个字节,紧接着是父类的成员变量,最后才是子类的成员变量。

单一继承父类不带虚函数的数据成员布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Base
{
public:
int m_bi;
};
class MYACLS :public Base
{
public:
int m_i;
int m_j;
MYACLS()
{
int abc = 1; //方便加断点
}
~MYACLS()
{
int def = 0;//方便加断点
}
};

int main()
{
cout << sizeof(MYACLS) << endl; //12
printf("MYACLS::m_bi = %d\n", &MYACLS::m_bi); //MYACLS::m_bi = 0
printf("MYACLS::m_i = %d\n", &MYACLS::m_i); //MYACLS::m_i = 4
printf("MYACLS::m_j = %d\n", &MYACLS::m_j); //MYACLS::m_j = 8

MYACLS myobj;
myobj.m_i = 3;
myobj.m_j = 6;
myobj.m_bi = 9;

return 1;
}

  从打印结果可以看出,如果父类中没有虚函数,则父类的成员变量就放到了内存开头处,之后是子类的成员变量。
  但是如果子类中有虚函数而父类中没有,内存布局又会发生变化,即数据在内存空间中的分布分别是:父类成员、子类的虚函数表指针、子类成员。代码及打印结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Base
{
public:
int m_bi;
};
class MYACLS :public Base
{
public:
int m_i;
int m_j;
virtual void myvirfunc() {}
MYACLS()
{
int abc = 1; //方便加断点
}
~MYACLS()
{
int def = 0;//方便加断点
}
};

int main()
{
cout << sizeof(MYACLS) << endl; //16
printf("MYACLS::m_bi = %d\n", &MYACLS::m_bi); //MYACLS::m_bi = 0
printf("MYACLS::m_i = %d\n", &MYACLS::m_i); //MYACLS::m_i = 8
printf("MYACLS::m_j = %d\n", &MYACLS::m_j); //MYACLS::m_j = 12

MYACLS myobj;
myobj.m_i = 3;
myobj.m_j = 6;
myobj.m_bi = 9;

return 1;
}

  如果子类和父类中都有虚函数,则子类和父类共用一个虚函数表指针,即它俩的地址一样,都在内存的开头处,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Base
{
public:
int m_bi;
virtual void mybvirfunc() {}
};
class MYACLS :public Base
{
public:
int m_i;
int m_j;
virtual void myvirfunc() {}
MYACLS()
{
int abc = 1; //方便加断点
}
~MYACLS()
{
int def = 0;//方便加断点
}
};

int main()
{
cout << sizeof(MYACLS) << endl; //16
printf("MYACLS::m_bi = %d\n", &MYACLS::m_bi); //MYACLS::m_bi = 4
printf("MYACLS::m_i = %d\n", &MYACLS::m_i); //MYACLS::m_i = 8
printf("MYACLS::m_j = %d\n", &MYACLS::m_j); //MYACLS::m_j = 12

MYACLS myobj;
myobj.m_i = 3;
myobj.m_j = 6;
myobj.m_bi = 9;

return 1;
}

  总结:不管是父类还是子类,只要含有虚函数,则虚函数指针就位于对应的所有数据成员的前面(父类指针对应父类成员,子类指针对应子类成员),如果父类和子类都含有虚函数,则它俩的虚函数表指针共用一块内存,位于内存空间的开头处。

多重继承且父类都带虚函数的数据成员布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class Base1
{
public:
int m_bi;
virtual void mybvirfunc() {}

Base1()
{
printf("Base1::Base1()的this指针是:%p!\n", this);
}
};
class Base2
{
public:
int m_b2i;
virtual void mybvirfunc2() {}

Base2()
{
printf("Base2::Base2()的this指针是:%p!\n", this);
}
};
class MYACLS :public Base1,public Base2
{
public:
int m_i;
int m_j;

virtual void myvirfunc() {} //虚函数
MYACLS()
{
int abc = 1; //方便加断点
printf("MYACLS::MYACLS()的this指针是:%p!\n", this);
}
~MYACLS()
{
int def = 0;//方便加断点
}
};
int main()
{
cout << sizeof(MYACLS) << endl; //24
printf("MYACLS::m_bi = %d\n", &MYACLS::m_bi); //MYACLS::m_bi = 4
printf("MYACLS::m_b2i = %d\n", &MYACLS::m_b2i); //MYACLS::m_b2i = 4
printf("MYACLS::m_i = %d\n", &MYACLS::m_i); //MYACLS::m_i = 16
printf("MYACLS::m_j = %d\n", &MYACLS::m_j); //MYACLS::m_j = 20

MYACLS myobj; //Base1::Base1()的this指针是:004FFC24!
//Base2::Base2()的this指针是:004FFC2C!
//MYACLS::MYACLS()的this指针是:004FFC24!
myobj.m_i = 3;
myobj.m_j = 6;
myobj.m_bi = 9;
myobj.m_b2i = 12;

MYACLS *pmyobj = new MYACLS(); //Base1::Base1()的this指针是:008F11D8!
//Base2::Base2()的this指针是:008F11E0!
//MYACLS::MYACLS()的this指针是:008F11D8!
pmyobj->m_i = 3;
pmyobj->m_j = 6;
pmyobj->m_bi = 9;
pmyobj->m_b2i = 12;

return 1;
}

  (1)通过this指针打印,我们看到访问Base1成员不用跳 ,访问Base2成员要this指针要偏移(跳过)8字节。
  (2)我们看到偏移值,m_bi和m_b2i偏移都是4。
  (3)this指针,加上偏移值就的能够访问对应的成员变量,比如m_b2i = this指针+偏移值。
  结论:我们要访问一个类对象中的成员,成员的定位是通过:this指针(编译器会自动调整)以及该成员的偏移值,这两个因素来定义。这种this指针偏移的调整都需要编译器介入来处理完成。

虚基类相关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Grand //爷爷类
{
public:
};
class A1 : virtual public Grand
{
public:
};

class A2 : virtual public Grand
{
public:
};

class C1 :public A1, public A2
{
public:
int m_c1;
};

int main()
{
cout << sizeof(Grand) << endl; // 1
cout << sizeof(A1) << endl; // 4
cout << sizeof(A2) << endl; // 4
cout << sizeof(C1) << endl; // 12

return 1;
}

  (1)虚基类就是让Grand类只被继承一次,防止二义性问题。
  (2)有虚基类,就有虚基类表vbtable(virtual base table)、虚基类表指针vbptr(virtual base table pointer)。
  (3)空类sizeof(Grand)=1好理解。虚继承之后,A1,A2里就会被编译器插入一个虚基类表指针,这个指针,有点成员变量的感觉。A1,A2里因为有了虚基类表指针,因此占用了4个字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Grand //爷爷类
{
public:
int m_grand;
};
class A1 : virtual public Grand
{
public:
int m_a1;
};

class A2 : virtual public Grand
{
public:
int m_a2;
};

class C1 :public A1, public A2
{
public:
int m_c1;
};

int main()
{
cout << sizeof(Grand) << endl; // 1
cout << sizeof(A1) << endl; // 4
cout << sizeof(A2) << endl; // 4
cout << sizeof(C1) << endl; // 12
//内存分布情况
C1 c;
c.m_grand = 2; // 21-24字节
c.m_a1 = 3; // 5-8字节
c.m_a2 = 4; // 13-16字节
c.m_c1 = 5; // 17-20字节

return 1;
}

  根据内存分析可以看出:1-4字节和9-12字节应该存放的就是两个虚基类表指针,其中1-4字节是vbptr1(继承自A1),9-12字节是vbptr2(继承自A2)。其余的数据分布如上述程序中所示,需要注意的是:爷爷类里的成员分布在内存的最后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class Grand //爷爷类
{
public:
int m_grand;
};

class Grand2 //爷爷类
{
public:
int m_grand2;
};

class A1 : virtual public Grand,virtual public Grand2
{
public:
int m_a1;
};

class A2 : virtual public Grand
{
public:
int m_a2;
};

class C1 :public A1, public A2
{
public:
int m_c1;
};

int main()
{
cout << sizeof(Grand) << endl; // 4
cout << sizeof(A1) << endl; // 16(3个int数据+1个虚基类表指针)
cout << sizeof(A2) << endl; // 12(2个int数据+1个虚基类表指针)
cout << sizeof(C1) << endl; // 28
//内存分布情况
C1 c;
c.m_a1 = 1; // 5-8字节
c.m_a2 = 2; // 13-16字节
c.m_c1 = 3; // 17-20字节
c.m_grand = 4; // 21-24字节
c.m_grand2 = 5; // 25-28字节

return 1;
}

  由此,可以判断出,一个类同时虚继承多个父类,只会产生一个虚基类指针(A1)。1-4字节放的是A1的虚基类指针,9-12字节放的是A2的虚基类指针。其他的数据分布如程序所示。