1.无虚函数的单继承
class A
{
public:
A():_ai(1){}
void Afun();
static void static_fun();
static int _val;
private:
int _ai;
};
class B:public A
{
public:
B():_bi(2){}
void Bfun();
private:
int _bi;
};
int main()
{
A a;
B b;
cout<<sizeof(A)<<endl;//4
cout<<sizeof(B)<<endl;//8
return 0;
}
上述代码内存分布:
我们可以调试在内存中看一下:(VS2008调试,本文后续都在此环境)
2.有虚函数的单继承
虚函数是C++多态实现的一种形式,那么,在虚函数中,编译器是怎么知道什么时候调用合适的函数呢?
编译器为每个包含虚函数的类创建一个表(虚函数表/VTABLE)。虚表中存放着类里面虚函数的地址。在每个虚函数的类中,有一个指针指向虚表,称为vpointer(VPTR),这个指针指向虚表。当进行多态调用时,编译器通过VPTR在虚表中通过函数地址来查找函数代码,这样就能正确的调用函数实行动态编译。看下面代码:
class A
{
public:
A():_a(1){}
virtual void fun1(){};
virtual void fun2(){};
private:
int _a;
};
int main()
{
A a;
cout<<sizeof(A)<<endl;
return 0;
}
类A的大小为8说明了A里面还有别的东西,在此假设为VPTR。调试上述代码 ,查看对象a的内存:
可以看到 ,对象a的地址开始4个字节处存放的是VPRT。
把VPRT的那串地址拿出来在内存中look一下,VS2008为小端存储
那我们不妨通过虚表里面的函数地址调用一下虚函数:
typedef void (* pfun)();
void PrintVirtual(pfun *_pfun)
{
while(*_pfun)
{
(*_pfun)();
++_pfun;
}
}
void test()
{
A a;
pfun* p = (pfun*)*((int*)(&a));//拿出对象a前4个字节,把它强转为函数指针
PrintVirtual(p);
}
成功的通过虚表调用了对象a里面的虚函数:
清楚了以上的虚函数表,下面研究有虚函数的单继承,看代码:
class Base
{
public:
Base():_b(1){}
virtual void fun1()
{
cout<<"Base::fun1()"<<endl;
}
virtual void fun2()
{
cout<<"Base::fun2()"<<endl;
}
private:
int _b;
};
class Derive:public Base
{
public:
Derive():_d(2){}
virtual void fun1()
{
cout<<"Derive::fun1()"<<endl;
}
virtual void fun3()
{
cout<<"Derive::fun3()"<<endl;
}
virtual void fun4()
{
cout<<"Derive::fun4()"<<endl;
}
private:
int _d;
};
int main()
{
Base b;
Derive d;
cout<<sizeof(Base)<<endl;//8
cout<<sizeof(Derive)<<endl;//12
return 0;
}
按照前面虚表的知识,上面代码中的理论上派生类Derive的大小是基类大小+派生类成员大小+派生类VPRT = 8+4+4 = 16
,但是上面的结果却是12,这是因为派生类并没有生成自己的虚表。派生类真实的内存布局为:
调试程序,对Derive类对象d的内存布局进行分析:
用上面的打印函数打印验证:
这里,编译器是怎么做的呢?
先将基类虚表里的内容拷贝一份,如果派生类对基类中的虚函数进行重写,使用派生类的虚函数替换相同偏移位置的基类虚函数,如果派生类中增加自己的虚函数,按照其在派生类的声明次序,放在上述虚函数之后。