1、类的继承中内存布局
C++的多态性一般是在类的继承过程中,通过动态绑定实现的。即通过基类的对象指针或者引用绑定子类对象的地址或者对象本身,在运行时通过所指向或者引用的实际对象,调用该对象的方法实体。
在介绍继承之前,先简单的说明一个类对象的内存布局结构,一个类内部有数据成员与函数成员。除了静态数据外,所有的数据成员都是存放在类对象的内存布局中,而成员函数则有不同。成员函数可以分为普通成员函数(no static),静态成员函数(static),和虚函数(virtual)。普通成员函数并不放在类对象的内存布局中;静态函数属于类所有,在整个派生过程中,只有一个实例,且只能调用静态数据成员;如果类有虚函数,那么会在对象的内存布局内产生一个虚拟函数表,用于存放各个虚函数指针。
class CShape;
std::ostream &operator<<(std::ostream &, const CShape &);
class CShape
{
public:
CShape(void);
CShape(const int lineWidth);
void SetLineWidth(const int lineWidth);
int GetLineWidth() const;
virtual ~CShape(void);
virtual double GetArea(void) const ;
virtual double GetCircumference(void) const ;
friend std::ostream & operator<<(std::ostream &,const CShape &);// 友元函数无法继承
private:
int m_nLineWidth;
static int m_nCShapeNum;// 静态数据成员在派生体系中只有一个实例
};
它们在对象的内存布局示意如下:
1.1非虚成员函数布局
CShape sh;
在编译处理后是这样调用
GetLineWidth(&sh);
由上面的调用知,类的普通成员函数(非virtual)与其它一般的非类的成员函数的调用实现是一样的。
1.2 虚函数布局
unsigned int *ptr = (unsigned int *)*((unsigned int *)&sh);
简要分析:&sh获取对象地址,然后通过强制转换((unsigned int *)(&sh))转换为该对象第一个unsigned int 数据的地址也即vfptr的地址,然后通过解引用操作*((unsigned int *)(&sh))得到该内存中数据(4字节地址值),该值只是一个值,不具有地址意义,所以再通过强制转换(unsigned int *)转换为一个指针,该指针指向虚函数表的首地址。虚函数表中存放的是各个虚函数的地址。虚拟函数表不存放在对象内存空间中,布局示意如下图所示。
代码中的ptr指针获取得到是虚拟函数表的首地址,即ptr[0]是第一个CShape对象中第一个虚拟函数的地址值,即析构函数地址。有了地址值就可以通过相应的函数指针调用类对象的的虚函数。
typedef void(*pFun)(void);
pFun pf = (pFun)ptr[1];
pf();// 调用CShape的GetArea()函数
1.3 继承中对象的内存布局
用一个CMyRect类派生自CSahpe类,CMyRect 类的定义如下。class CMyRect : public CShape
{
public:
CMyRect(void);
CMyRect(const double ,const double n);
void SetWidth(const double);
void SetHeight(const double);
const double GetWidth(void) const ;
const double GetHeigth(void) const ;
void SetLineWidth(const int lineWidth);//覆盖基类的非虚函数
// 友元函数不能继承,所以在派生类之中要重新实现
friend std::ostream & operator<<(std::ostream &, const CMyRect &);
// 以下是基类的虚拟函数,重写
~CMyRect(void);
//double GetArea(void) const ;
double GetCircumference(void) const;
private:
double m_dWidth;
double m_dHeight;
};
首先分析派生类CMyRect 类对象的大小。CMyRect 类派生自基类CShape,所以它对象内存中含有一个int 型的m_lineWidth数据(4字节), 同时一个虚函数表指针(4字节),另外含有新增加的两个double类型成员(2*8字节),所以CMyRect对象的大小是24 字节。在VS中的结构显示如下所示:
派生类的对象内存布局示意如图所示。
由以上分析知,任何派生类对象当中都含有一个它的直接基类对象的内存结构(subobject),所以考虑以下代码。
CShape baseSh = rec;
CMyRect rec3 = sh;// error,如果没有定义转换的话
第一行代码中,把一个派生类的对象赋值给个基类,从上面的对象内存布局分析中可以看出,这个操作是可以的,但会把派生类中非基类的成员直接去掉,只保留基类的成员;第二行代码中,把一个基类类对象给派生类赋值,如果派生类没有定义基类从派生类的转换方法的话,该行代码是的非法的,直接编译不过;如果定义了相应的转换的话,那么也会造成赋值后的派生对象中自有的成员是处于一个不确定的状态。
CShape *pBase = &rec;
CShape &ref = rec;
上面两行代码的示意如图所示:
pBase->m_nLineWidth = 100;// ok,
pBase->m_dWidth = 1000.0;// error
pBase->GetLineWidth();// 调用基类的方法
pBase->GetArea();// 调用派生类的方法
当基类指针或者引用的对象是派生类对象时,基类指针如果调用的是非虚函数,那么调用是基类的方法实体,如果是虚函数的话,调用方法的实体是派生类的方法,这样就实现了虚拟函数的多态性。
2、虚函数多态机制分析
在C++中对象本身并不支持多态,要实现多态则需要通过基类的指针或引用,指向或者引用派生类对象,并通过虚函数实现。
如果一个基类成员函数当中含有一个虚函数,编译器在编译时会在其类的对象的内存布局中,为其产生一个指向虚拟函数表的指针(vptr),该表中记录提其所有虚函数的实际地址值,同时该虚函数在基类的所有派生类中都是虚函数。基类当中都实现三个虚函数,在派生类中都重写了,编译后查看基类对象其内存分布图如下:
从上面中看出,在基类对象中指向虚拟函数表的指针为vfptr该值为0X013888C,即表明虚拟函数表首地址址,通过该地址值可以获取其各个虚函数实现。
再查看派生类的内存分布图如下:
图可以看出派生类的虚拟函数表中存放的是派生类虚函数的地址值(因为派生类全部实现了基类的所有虚函数),如果通过基类的指针指向的是派生类的对象的话,则通过该指针调用的它所指向对象的方法,实现了多态。
pBase->GetArea(); // 调用实际是vfprt[1]();调用的是派生类的方法;
pBase->GetLineWidth();// 调用的是基类的方法。
在其派生当中,把GetArea()方法的声明和实现全部注释后,再查看其内存布局如下
上图知,如果派生类当中没有实现基类当中的虚函数,则编译器在派生类对象的虚拟函数表中相应的位置上(vfptr[1])放置是基类的虚函数地址(CShape::GetArea()),即pBase->GetArea();调用是基类的方法。
由以上分析,C++类实现多态的根本原因类的对象内存布局中有一个虚拟函数表在存在。