C++对象模型之虚函数实例讲解

介绍

因为c++只规定了 虚继承/ 虚函数/ 多继承/ 的行为, 但将实现方法留给编译器作者. 所以各个平台的实现并不相同, 得出的结果也不尽相同.

经测试, vs和gcc目前比较统一的情况只有2种 :

  • 无继承+无虚函数
  • 无继承+虚函数

故本文只讨论这2种, 以及了解虚函数和虚继承的含义.

. . .

关于虚函数

当类中声明了虚函数(不管是1个还是多个),那么在实例化对象时,编译器会自动在对象里安插一个指针vPtr指向虚函数表VTable;

关于虚继承

当涉及到虚继承,会增加vbPtr指针指向虚基表vbTable

单继承对象模型

类的继承关系为:
class Derived : public Base

无继承但有虚函数示例

测试环境为Windows/VS, 32位.

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
class A       
{
};

class B
{
char ch;
virtual void func0() { }
};

class C
{
char ch1;
char ch2;
virtual void func() { }
virtual void func1() { }
};


int main(void)
{
cout<<"A="<<sizeof(A)<<endl; //result=1
cout<<"B="<<sizeof(B)<<endl; //result=8
cout<<"C="<<sizeof(C)<<endl; //result=8
return 0;
}

再测试一个复杂点的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct MyStructA{};
struct MyStructB{ int value(); };
struct MyStructC{ virtual ~MyStructC(); int x; };
struct MyStructD{
virtual ~ MyStructD(); virtual void value();
};
struct MyStructE : public MyStructC{
virtual ~MyStructE();
};
struct MyStructF : public MyStructC, public MyStructD{
virtual ~MyStructF();
};

int main(int argc, char* argv[]){
{
std::cout << sizeof(MyStructA) << std::endl; // 1
std::cout << sizeof(MyStructB) << std::endl; // 1
std::cout << sizeof(MyStructC) << std::endl; // 8
std::cout << sizeof(MyStructD) << std::endl; // 4
std::cout << sizeof(MyStructE) << std::endl; // 8
std::cout << sizeof(MyStructF) << std::endl; // 12
getchar(); return 0;
}
}

总结

首先,平时所声明的类只是一种类型定义,它本身是没有大小可言的。 因此,如果用sizeof运算符对一个类型名操作,那得到的是具有该类型实体的大小。
计算一个类对象的大小时的规律:

空类、单一继承的空类、多重继承的空类所占空间大小为:1(字节,下同);
一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间的;因此一个对象的大小≥所有非静态成员大小的总和;

这是为什么呢?
实际上,这是类结构体实例化的原因,空的类或结构体同样可以被实例化,如果定义对空的类或者结构体取sizeof()的值为0,那么该空的类或结构体实例化出很多实例时,在内存地址上就不能区分该类实例化出的实例,,,所以,为了实现每个实例在内存中都有一个独一无二的地址,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址,所以空类所占的内存大小是1个字节。

类对象的大小 =
各非静态数据成员(包括父类的非静态数据成员但都不包括所有的成员函数)的总和 +
虚函数表指针(多继承有几个含有虚函数的父类就有几个虚函数表指针)+虚基表指针(多继承下可能不止一个, 不赘述了, 虚继承的对象的内存布局,在不同编译器实现有所区别) +
编译器因为要内存对齐而额外增加的字节。

详情可参考: 陈皓的 C++ 虚函数表解析

测试常用平台的不同

测试了 Windows10 / VS2015 和 Ubuntu14.04.3 / gcc4.8.4 , 都是64位

测试有多继承的情况

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
#include <iostream>

using std::cout;
using std::endl;

class A
{
};

class B
{
char ch;
virtual void func0() { }
};

class C
{
char ch1;
char ch2;
virtual void func() { }
virtual void func1() { }
};

class D : public A, public C
{
int d;
virtual void func() { }
virtual void func1() { }
};

class E : public B, public C
{
int e;
virtual void func0() { }
virtual void func1() { }
};

int main(void)
{
cout << "A=" << sizeof(A) << endl;
cout << "B=" << sizeof(B) << endl;
cout << "C=" << sizeof(C) << endl;
cout << "D=" << sizeof(D) << endl;
cout << "E=" << sizeof(E) << endl;
return 0;
}

打印对比如下 :

Windows10/VS2015
1
2
3
4
5
6
A=1
B=16
C=16
D=24
E=40
请按任意键继续. . .
Ubuntu14.04.3/gcc4.8.4
1
2
3
4
5
A=1
B=16
C=16
D=16
E=32

测试有虚拟继承的情况

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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
#include <iostream>

using std::cout;
using std::endl;

class Base
{
public:
Base()
{
mBase = 11;
}
virtual void funcA()
{
cout << "Base::funcA()" << endl;
}
virtual void funcX()
{
cout << "Base::funcX()" << endl;
}
protected:
int mBase;
};
class Base1 : virtual public Base
{
public:
Base1() :
Base()
{
mBase1 = 101;
}
virtual void funcA()
{
cout << "Base1::funcA()" << endl;
}
virtual void funcB()
{
cout << "Base1::funcB()" << endl;
}
private:
int mBase1;
};
class Base2 : virtual public Base
{
public:
Base2() :
Base()
{
mBase2 = 102;
}
virtual void funcA()
{
cout << "Base2::funcA()" << endl;
}
virtual void funcC()
{
cout << "Base2::funcC()" << endl;
}
private:
int mBase2;
};

class Base3 : virtual public Base
{
public:
Base3() :
Base()
{
mBase3 = 102;
}
virtual void funcA()
{
cout << "Base3::funcA()" << endl;
}
virtual void funcX()
{
cout << "Base3::funcC()" << endl;
}
private:
int mBase3;
};

class Derived : public Base1, public Base2
{
public:
Derived() :
Base1(),
Base2()
{
mDerived = 1001;
}
virtual void funcD()
{
cout << "Derived::funcD()" << endl;
}
virtual void funcA()
{
cout << "Derived::funcA()" << endl;
}
private:
int mDerived;
};


int main(void)
{
cout << "Derived's size is " << sizeof(Derived) << endl;
cout << "Base's size is " << sizeof(Base) << endl;
cout << "Base1's size is " << sizeof(Base1) << endl;
cout << "Base2's size is " << sizeof(Base2) << endl;
cout << "Base3's size is " << sizeof(Base3) << endl;
return 0;
}

打印对比如下 :

Windows10/VS2015
1
2
3
4
5
6
Derived's size is 80
Base's size is 16
Base1's size is 48
Base2's size is 48
Base3's size is 40
请按任意键继续. . .
Ubuntu14.04.3/gcc4.8.4
1
2
3
4
5
Derived's size is 48
Base's size is 16
Base1's size is 32
Base2's size is 32
Base3's size is 32

测试总结

这两个都还算是比较常用的平台了, 测试之后发现vs和gcc目前比较统一的情况只有2种 :

  • 无继承+无虚函数
  • 无继承+虚函数

参考