提前篇:
在内存中类的一个对象中存储的信息并不包括静态成员、成员函数、虚函数。
对于静态成员是当作全局变量来处理的,放在全局区,所以可以通过类名直接访问。
对于成员函数和静态成员函数也都当作全局函数来处理,通过类名来进行调用,具体细节在另一篇博客说明。
对于类的虚函数,类将维持一个一个虚函数表(vtbl),每个表项是一个指向虚函数具体代码的地址。在类的每个对象中将有一个虚函数指针(vptr)指向虚函数表(vtbl)。
所以类的每个对象将持有类的成员变量和vptr。
如果一个类没有成员变量和虚函数,那么这个类大小将为1(不涉及继承,继承情况下子类将嵌套含有基类的成员变量),为1是因为编译器为了使类对象具有指定唯一的内存第一会为类安插一个char。
如下代码:
#include<iostream>
#include<vector>
using namespace std;
class A
{
//virtual void func(){};
};
class B
{
int b;
};
class C
{
virtual void func(){};
};
class D:public A
{
};
class E:public C
{
};
int main()
{
cout<<sizeof(A)<<endl;//output 1
cout<<sizeof(B)<<endl;//output 4
cout<<sizeof(C)<<endl;//output 8
cout<<sizeof(D)<<endl;//output 1
cout<<sizeof(E)<<endl;//output 4
int k;
cin>>k;
return 0;
}
既然类对象只含有数据成员和vptr,那么类在内存中如何布局,又如何存取本类以及基类的成员变量,如何执行虚函数产生多态效应。
下面讨论在无继承、单重继承、多重继承、虚继承四中情况下进行讨论。
1:无继承的情况,和struct的类似
可以直接通过地址偏移取得各个成员
代码测试:
#include<iostream>
#include<cstdio>
using namespace std;
class A
{
public:
int m1;
int m2;
char m3;
A():m1(0),m2(1),m3('A'){}
};
int main()
{
A a;
int *p = &a.m1;
cout<<"address of a"<<&a<<endl;
cout<<p<<endl;
cout<<*p<<endl;
printf("address of m1 %p\n",&A::m1);
cout<<(*p )<<endl;
printf("address of m2 %p\n",&A::m2);
cout<<(++p )<<endl;
cout<<(*p )<<endl;
printf("address of m3 %p\n",&A::m3);
cout<<(++p )<<endl;
cout<<(char)(*p )<<endl;
int k;
cin>>k;
return 0;
}
输出如下:
address of a003FF8A0
003FF8A0
0
address of m1 00000000
0
address of m2 00000004
003FF8A4
1
address of m3 00000008
003FF8A8
A
可见在class中成员变量是连续存放的,可通过&classname::memderdatename 取得成员变量相对于类对象地址的偏移量(注:书上说对象地址+成员变量偏移量-1才是真正的成员变量地址,但我在VS2012中取得偏移量并不用-1)。
这里涉及一个问题:如果A的成员变量为char,int,char 和int ,char ,char,类的大小是不同的,因为后者会把两个char放到同一个4Byte空间内,所以大小应为12,8,可以写代码测试下。
2:单继承无多态的情况
派生类将顺序内含各个基类的成员变量
#include<iostream>
#include<cstdio>
using namespace std;
class A
{
public:
int a;
A():a(0){}
};
class B:public A
{
public:
int b;
B():b(1){}
};
class C:public B
{
public:
int c;
C():c(2){}
};
int main()
{
C c;
int *p = &c.a;
cout<<*p<<endl;
++p;
cout<<*p<<endl;
++p;
cout<<*p<<endl;
int k;
cin>>k;
return 0;
}
内存模型大致如下:
int a |
int b |
int c |
3:单继承包含多态
这种情况下只需在无多态的情况下加入vptr指针即可(同时需强化类的一些功能,加入vtbl、为每个类对象加入vptr、加强构造和析构函数),每个类对象有一个vptr即可,vptr可在内存顶部或底部。
(加入vptr后为什么需强化构造和析构函数将在关于构造函数的博客中说明)
代码测试:
#include<iostream>
#include<cstdio>
using namespace std;
class A
{
public:
int a;
virtual void func(){ cout<<"class A"<<endl;};
A():a(0){}
};
class B:public A
{
public:
int b;
B():b(1){}
};
class C:public B
{
public:
int c;
C():c(2){}
};
typedef void (*func)();
int main()
{
C c;
cout<<"sizeof C:"<<sizeof(c)<<endl;
cout<<"sizeof"<<sizeof(int*)<<endl;
cout<<"size of int"<<sizeof(int)<<endl;
int *p = (int*)&c;
func pfun = (func)( *( (int*)(*p) ) );
pfun();
return 0;
}
输出结果如下(VS2012):
sizeof C:24
sizeof8
size of int4
A
(在g++环境下测试)可见在g++中将vptr放在了对象内存中的顶部(),A和B的两个数据成员共占8Byte,C的数据成员c占4Byte,对齐后后占8Byte',所以c对象大小为24.
内存模型大致如下:
vptr |
a | b |
c | |
计算成员变量的地址也可以通过地址偏移直接取得。
4:多重继承下的内存模型
加入多重继承后每个类对象只有一个vptr已经不能符合要求,比如对如下继承体系:
B含有一个来自A的vptr,c含有一个vptr,为了同时支持A体系和C体系的多态,需要两个vptr,而内存模型将以最左端的类为起始地址
大致如下:
测试代码:
#include<iostream>
#include<cstdio>
using namespace std;
class A
{
public:
int a;
virtual void func(){cout<<"A"<<endl;}
A():a(0){}
};
class B:public A
{
public:
int b,b1;
B():b(1),b1(10){}
};
class C
{
public:
int c;
virtual void func1(){cout<<"C"<<endl;}
C():c(2){}
};
class D:public B,public C
{
public:
int d;
D():d(4){}
};
typedef void (*func)();
int main()
{
D d;
cout<<"sizeof C:"<<sizeof(d)<<endl;
int *p = (int*)&d;
func pfun1 = (func)( *( (int*)(*p) ) );
pfun1();
p += 6;
func pfun2 = (func)( *( (int*)(*p) ) );
pfun2();
return 0;
}
输出如下:
sizeof C:40
A
C
计算成员变量的地址也可以通过地址偏移直接取得,但通过指针取地址需对对象地址做防NULL测试(书上这么说的)。对于类B和C需偏移数个基类的的地址。
5:虚拟继承
对于我们需要解决某个类继承的多个基类中,某些基类又都同时继承了同一个类,虚继承要求只能包含一个共同基类,例如以下继承结构:
对象内存布局:
如何保证在虚继承下每个D类对象中只包含一个A对象。
class A
{
public:
int a;
virtual void func(){cout<<"A"<<endl;}
A():a(1){}
};
class B:virtual public A
{
public:
int b;
virtual void func(){cout<<"B"<<endl;}
//virtual void funcb(){cout<<"BBB"<<endl;}
B():b(2){}
};
class C:virtual public A
{
public:
int c;
virtual void func(){cout<<"C"<<endl;}
C():c(3){}
};
class D:public B,public C
{
public:
int d;
D():d(4){}
};
我在g++中测试的结果比较奇怪.
当B或者C不覆写A的func函数时,A的虚函数表中存在func函数指针,而且D的func函数与A相同.
当B和C中一个覆盖func函数,A的虚函数表中不存在func函数指针,D的func函数为被覆盖的函数
当B和C都覆盖了func函数时,A的虚函数表中不存在func函数指针,并且D的func函数为B覆盖后的函数.