C++对象模型--C++对象模型

何为C++对象模型?


 

C++对象模型可以概括为以下2部分:

  1. 1       语言中直接支持面向对象程序设计的部分
  2. 2       对于各种支持的底层实现机制

语言中直接支持面向对象程序设计的部分,如构造函数析构函数虚函数继承(单继承、多继承、虚继承)、多态等等,这也是组里其他同学之前分享过的内容。第一部分这里我简单过一下,重点在底层实现机制。

c语言中,数据处理数据的操作(函数)是分开来声明的,也就是说,语言本身并没有支持数据和函数之间的关联性。在c++中,通过抽象数据类型(abstract data typeADT),在类中定义数据和函数,来实现数据和函数直接的绑定。

概括来说,C++类中有两种成员数据:staticnonstatic;三种成员函数:staticnonstaticvirtual

clip_image002[4]

如下面的Base类定义:

#pragma once
#include<iostream>
using namespace std;
class Base
{
public:
    Base(int);
    virtual ~Base(void);
 
    int getIBase() const;
    static int instanceCount();
    virtual void print() const;
 
protected:
 
    int iBase;
    static int count;
};

  Base类在机器中我们如何构建出各种成员数据和成员函数的呢?

基本C++对象模型


 

在介绍C++使用的对象模型之前,介绍2种对象模型:简单对象模型(a simple object model)、表格驱动对象模型(a table-driven object model)。

简单对象模型(a simple object model

clip_image004[4]

所有的成员占用相同的空间(跟成员类型无关),对象只是维护了一个包含成员指针的一个表。表中放的是成员的地址,无论上成员变量还是函数,都是这样处理。对象并没有直接保存成员而是保存了成员的指针。

表格对象模型(a table-driven object model

clip_image006[3] 

这个模型在简单对象的基础上又添加了一个间接层。将成员分成函数和数据,并且用两个表格保存,然后对象只保存了两个指向表格的指针。这个模型可以保证所有的对象具有相同的大小,比如简单对象模型还与成员的个数相关。其中数据成员表中包含实际数据;函数成员表中包含的实际函数的地址(与数据成员相比,多一次寻址)。

C++对象模型

clip_image008[3]

这个模型从结合上面2中模型的特点,并对内存存取空间进行了优化。在此模型中,non static 数据成员被放置到对象内部,static数据成员, static and nonstatic 函数成员均被放到对象之外。对于虚函数的支持则分两步完成:

  1. 1.      每一个class产生一堆指向虚函数的指针,放在表格之中。这个表格称之为虚函数表(virtual tablevtbl)。
  2. 2.      每一个对象被添加了一个指针,指向相关的虚函数表vtbl。通常这个指针被称为vptrvptr的设定(setting)和重置(resetting)都由每一个class构造函数析构函数拷贝赋值运算符自动完成。

另外,虚函数表地址的前面设置了一个指向type_info的指针,RTTIRun Time Type Identification)运行时类型识别是有编译器在编译器生成的特殊类型信息,包括对象继承关系,对象本身的描述,RTTI是为多态而生成的信息,所以只有具有虚函数的对象在会生成

这个模型的优点在于它的空间和存取时间的效率;缺点如下:如果应用程序本身未改变,但当所使用的类的non static数据成员添加删除或修改时,需要重新编译。

模型验证测试

为了验证上述C++对象模型,我们编写如下测试代码。

验证实验一:

//Base.h

#pragma once
#include <iostream>

using namespace std;

class Base
{
public:
	Base(void);
	virtual ~Base(void);
	void print() const;
	virtual void print_virtual() const;
};

  基类的头文件,声明类。

//Base.cpp

#include "Base.h"

Base::Base(void)
{
	cout << "Base::Base()" << endl;
}

Base::~Base(void)
{
	cout << "Base::~Base()" << endl;
}

void Base::print() const
{
	cout << "Base::print()" << endl;
}

void Base::print_virtual() const
{
	cout << "Base::print_virtual()" << endl;
}

  基类的cpp文件,对类作具体的实现。

注意:最后面的仍然是函数的指针,并不是函数最终在内存的存放的位。

测试代码:

void test_base_model()
{
	Base b1;

	cout << "对象b1的起始内存地址:" << &b1 << endl;

	//根据虚函数表前面的指向对象类型的指针,获取当前对象的类型信息
	cout << "type_info信息存放的内存地址:" << ((int*)*(int*)(&b1) - 1) << endl;

	RTTICompleteObjectLocator str = *((RTTICompleteObjectLocator*)*((int*)*(int*)(&b1) - 1));	//获取类对象的名称
	string classname(str.pTypeDescriptor->name);
	cout << classname << endl;

	//虚函数表里的虚函数的地址
	cout << "虚函数表地址:" << (int*)(&b1) << endl;
	cout << "虚函数表中第1个函数的占位符的地址:" << (int*)*(int*)(&b1) <<endl;

	cout << "这个指针指向了真正的析构函数的代码,但是cout的结果是虚函数表中析构函数表项里的内容" << (int*)*((int*)*(int*)(&b1)) << endl;

	cout << "虚函数表中第二个函数占位符的地址:" << ((int*)*(int*)(&b1) + 1) << endl;
	typedef void(*Fun)(void);
	Fun pFun = (Fun)*((int*)*(int*)(&b1) + 1);
	pFun();
	b1.print_virtual();
	cout << endl;
}

  上面代码中所说的函数占位符的地址意思就是:这个函数所对应的虚函数表中那个表项的内存地址,是函数指针存放的内存单元的地址。

验证试验二:

对于增加成员变量iBase的类来说,验证的方法是

cout << "推测数据成员iBase地址:" << ((int*)(&b1) +1) << endl;
cout << "通过地址取值iBase的值:" << *((int*)(&b1) +1) << endl;
cout << "Base::getIBase(): " << b1.getIBase() << endl;	获取iBase的函数

  

根据C++对象模型,实例化对象b1的起始内存地址,即虚函数表地址。

l  虚函数表的中第1个函数地址是虚析构函数地址;

l  虚函数表的中第2个函数地址是虚函数print()的地址,通过函数指针可以调用,进行验证;

l  推测数据成员iBase的地址,为虚函数表的地址 + 1((int*)(&b1) +1)

l  静态数据成员和静态函数所在内存地址,与对象数据成员和函数成员位段不一样;

注意:本测试代码及后面的测试代码中写的函数地址,是对应虚函数表项的地址,不是实际的函数地址。实际的函数地址是虚函数表项的内容,内容就是函数指针。


在继续之前,首先科普一下cout指针的知识,这样才能在测试程序中很好的理解:

看下面的代码:

void main(void)
{
	int a = 10;
	int *p = &a;

	cout << a << endl;
	cout << p << endl;
	cout << *p << endl;
	cout << &a << endl;
}

  执行结果:

从代码和执行的结果可以看出cout的实现原理:cout<<后面接一个变量的标识符,然后把这个标识符转换成对应的内存地址的门牌号,然后输出这个门牌号里存储的数据。

特别地:cout<<一个指针的时候,要弄明白两件事,输出的结果是这个指针所指向的内存单元的门牌号码,同时还要弄明白这个指针变量的实际存放在内存的哪个内存单元里面。例如:cout<<p这个的输出是p指向的内存单元的门牌号,也就是a的门牌号,但是p的实际存放位置在p对应的内存空间中。


看一个复杂的模型:

clip_image020[3]

  1. &d表示这个类对象的指针,cout<<(&d)输出的是这个对象的内存地址,这个地址原来存放在了符号表中。
  2. (int*)(&d)表示一个指向int类型的指针,但是这个指针的起始地址和&d是一样的,只是类型改变了。cout<<((int*)(&d))输出的值和cout<<&d是一样的。
  3. *(int*)(&d)表示一个指向虚函数表的指针,这个指针指向的是虚函数表的起始地址,cout<<这个指针的结果是这个指针所指向的内存单元的门牌号。同时,存储这个指针变量的内存单元是_vptr_Derived,所以得到的输出结果是存储在Derived对象中的,也就是_vptr_Derived的值。
  4. (int*)*(int*)(&d)+1指向虚函数表中第二个slot的指针,cout<<的结果是指向内存单元的门牌号。这个指针是计算得到的,没有内存单元存放这个指针变量的值。
  5. *((int*)*(int*)(&d)+1)这个指针指向void Derived::print(void) const这个函数的函数指针,但是cout这个指针的结果是这个指针所指向的内容的门牌号,但是这个指针的值实际存放的内存位置是虚函数表中的第二个slot的内容。

C++对象模型中加入单继承


 

不管是单继承、多继承,还是虚继承,如果基于“简单对象模型”,每一个基类都可以被派生类中的一个slot指出,该slot内包含基类对象的地址。这个机制的主要缺点是,因为间接性而导致空间和存取时间上的额外负担;优点则是派生类对象的大小不会因其基类的改变而受影响

如果基于“表格驱动模型”,派生类中有一个slot指向基类表,表格中的每一个slot含一个相关的基类地址(这个很像虚函数表,内含每一个虚函数的地址)。这样每个派生类对象汗一个bptr,它会被初始化,指向其基类表。这种策略的主要缺点是由于间接性而导致的空间和存取时间上的额外负担;优点则是在每一个派生类对象中对继承都有一致的表现方式,每一个派生类对象都应该在某个固定位置上放置一个基类表指针,与基类的大小或数量无关。第二个优点是,不需要改变派生类对象本身,就可以放大,缩小、或更改基类表

不管上述哪一种机制,“间接性”的级数都将因为集成的深度而增加。C++实际模型是,对于一般继承是扩充已有存在的虚函数表;对于虚继承添加一个虚函数表指针

无重写的单继承

无重写,即派生类中没有于基类同名的虚函数。

基类和派生类的代码:

//Base.h
#pragma once
#include<iostream>
using namespace std;

class Base
{
public:
	Base(int);
	virtual ~Base(void);
	virtual void print(void) const;

protected:
	int iBase;
};

  

//Base.cpp
#include "Base.h"

Base::Base(int i)
{
	iBase = 1;
	cout << "Base_1::Base()" << endl;
}

Base::~Base(void)
{
	cout << "Base::~Base()" << endl;
}

void Base::print(void) const
{
	cout<<"Base_1::print(), iBase " << iBase << endl;
}

  

//Derived.h
#pragma once
#include "base.h"

class Derived : public Base
{
public:
	Derived(int);
	virtual ~Derived(void);
	virtual void derived_print(void);

protected:
	int iDerived;
};

  

//Derived.cpp
#include "Derived.h"

Derived::Derived(int i) : Base(0)
{
	iDerived = i;
	cout<<"Derived::Derived()"<<endl;
}

Derived::~Derived(void)
{
	cout<<"Derived::~Derived()"<<endl;
}

void Derived::derived_print()
{
	cout<<"Derived::derived_print()"<<iDerived<<endl;
}

  

  Base、Derived的类图如下所示:

clip_image013[3]

Base的模型跟上面的一样,不受继承的影响。Derived不是虚继承,所以是扩充已存在的虚函数表,所以结构如下图所示:

clip_image015[3]

为了验证上述C++对象模型,我们编写如下测试代码。

void test_single_inherit_norewrite()
{
	Derived d(9999);

	cout << "对象d的起始内存地址:" << &d << endl;

	//获取类型信息
	cout << "type_info信息的地址:" << ((int*)*(int*)(&d) - 1) << endl;	//cout得到一个地址,就输出这个地址里存放的内容
	RTTICompleteObjectLocator str = *((RTTICompleteObjectLocator*)*((int*)*(int*)(&d) - 1));
	string classname(str.pTypeDescriptor->name);
	cout << classname << endl;

	//获取虚函数信息

	cout << "虚函数表地址:" << (int*)(&d) << endl;
	cout << "虚函数表中第1个函数占位符的地址:" << (int*)*(int*)(&d) << "即析构函数在虚函数表中占位符的地址" << endl;
	cout << "虚函数表中第2个函数占位符的地址:" << ((int*)*(int*)(&d) + 1) << endl;
	typedef void(*Fun)(void);
	Fun pFun = (Fun)*((int*)*(int*)(&d) + 1);
	pFun();
	d.print();
	cout << endl;

	cout << "虚函数表中第3个函数占位符的地址:" << ((int*)*(int*)(&d) + 2) << endl;
	pFun = (Fun)*((int*)*(int*)(&d) + 2);
	pFun();
	d.derived_print();
	cout << endl;

	//获取成员变量的信息
	cout << "推测成员变量iBase的地址:" << (int*)(&d) + 1 << endl;
	cout << "通过地址取得的iBase的值:" << *((int*)(&d) + 1) << endl;

	cout << "推测成员变量iDerived地址:"  << (int*)(&d) + 2 << endl;
	cout << "通过地址取得的iDerived的值:" << *((int*)(&d) + 2) << endl;

}

  为了支持RTTICompleteObjectLocator必须引入一个头文件,这个头文件中定义了一些结构体,这些结构体封装了类的相关信息。

//type_info.h
#pragma once

typedef unsigned long DWORD;
struct TypeDescriptor
{
	DWORD ptrToVTable;
	DWORD spare;
	char name[8];
};
struct PMD
{
	int mdisp;  //member displacement
	int pdisp;  //vbtable displacement
	int vdisp;  //displacement inside vbtable
};
struct RTTIBaseClassDescriptor
{
	struct TypeDescriptor* pTypeDescriptor; //type descriptor of the class
	DWORD numContainedBases; //number of nested classes following in the Base Class Array
	struct PMD where;        //pointer-to-member displacement info
	DWORD attributes;        //flags, usually 0
};

struct RTTIClassHierarchyDescriptor
{
	DWORD signature;      //always zero?
	DWORD attributes;     //bit 0 set = multiple inheritance, bit 1 set = virtual inheritance
	DWORD numBaseClasses; //number of classes in pBaseClassArray
	struct RTTIBaseClassArray* pBaseClassArray;
};

struct RTTICompleteObjectLocator
{
	DWORD signature; //always zero ?
	DWORD offset;    //offset of this vtable in the complete class
	DWORD cdOffset;  //constructor displacement offset
	struct TypeDescriptor* pTypeDescriptor; //TypeDescriptor of the complete class
	struct RTTIClassHierarchyDescriptor* pClassDescriptor; //describes inheritance hierarchy
};

  测试结果:

注意:有一个点需要说明一下,从代码和执行的结果中可以看出通过函数指针调用的函数和通过对象调用的函数,打印的成员变量的值是不一样的,具体的原因是C++在调用成员函数的时候,会把某一个对象传递给一个成员函数隐藏的函数参数this指针。这样,这个成员函数就知道了去操作哪一个对象的数据,但是通过函数指针调用成员函数的话,就没有一个对象来初始化这个成员函数中的this指针。所以通过函数指针调用成员函数是找不到具体的操作对象的,所以打印的值是一个随机值。


有重写的单继承

派生类中重写了基类的print()函数。

//Derived_Overwrite.h
#pragma once
#include "base.h"

class Derived_Overrite : public Base
{
public:
    Derived_Overrite(int);
    virtual ~Derived_Overrite(void);
    virtual void print(void) const;
 
protected:
    int iDerived;
};

  

//Derived_Overwrite.cpp

#include "stdafx.h"
#include "Derived_Overwrite.h"


Derived_Overrite::Derived_Overrite(int i):Base(0)
{
	iDerived = i;
	cout<<"Derived_Overwrite::Derived_Overwrite()"<<endl;
}

Derived_Overrite::~Derived_Overrite(void)
{
	cout<<"Derived_Overwrite::~Derived_Overwrite()"<<endl;
}

void Derived_Overrite::print(void) const
{
	cout<<"Derived_Overwrite::print(), iDerived " << iDerived << endl;
}

  Base、Derived_Overwrite的类图如下所示:

clip_image018[3]

重写print()函数在虚函数表中表现如下:

clip_image020[3]

为了验证上述C++对象模型,我们编写如下测试代码。

void test_single_inherit_rewrite()
{
	Derived_Overwrite d(11111);
	cout << "对象d的起始内存地址:" << &d << endl;
	cout << "虚函数表地址:" << (int*)(&d) << endl;

	//虚函数信息
	cout << "虚函数表中第1个函数占位符的地址:" << (int*)*(int*)(&d) << "即析构函数在虚函数表中占位符的地址" << endl;
	cout << "虚函数表中第2个函数占位符的地址:" << ((int*)*(int*)(&d) + 1) << endl;

	typedef void(*Fun)(void);
	Fun pFun = (Fun)*((int*)*(int*)(&d) + 1);
	pFun();
	d.print();
	cout << endl;

	cout << "虚函数表中第3个函数占位符的内容:" << *((int*)*(int*)(&d) + 2) << endl;
	cout << endl;


	cout << "推测数据成员iBase地址:" << ((int*)(&d) +1) << "通过地址取得的值:" << *((int*)(&d) +1) << endl;
    cout << "推测数据成员iDerived地址:" << ((int*)(&d) +2) << "通过地址取得的值:" << *((int*)(&d) +2) << endl;
}

  代码执行结果:

特别注意下,前面的模型虚函数表中最后一项没有打印出来,本实例中共2个虚函数,打印虚函数表第3项为0其实虚函数表以0x0000000结束,类似字符串以’’结束


C++对象模型中加入多继承


 

从单继承可以知道,派生类中只是扩充了基类的虚函数表。如果是多继承的话,又是如何扩充的?

1)        每个基类都有自己的虚表。

2)        子类的成员函数被放到了第一个基类的表中。

3)        内存布局中,其父类布局依次按声明顺序排列。

4)        每个基类的虚表中的print()函数都被overwrite成了子类的print ()。这样做就是为了解决不同的基类类型的指针指向同一个子类实例,而能够调用到实际的函数。

clip_image023[3]

上面3个类,Derived_Mutlip_Inherit继承自Base、Base_1两个类,Derived_Mutlip_Inherit的结构如下所示:

clip_image025[3]

为了验证上述C++对象模型,我们编写如下测试代码。

void test_multip_inherit()
{
    Derived_Mutlip_Inherit dmi(3333);
    cout << "对象dmi的起始内存地址:		" << &dmi << endl;
    cout << "虚函数表_vptr_Base地址:	" << (int*)(&dmi) << endl;
    cout << "_vptr_Base — 第1个函数地址:	" << (int*)*(int*)(&dmi) << "	即析构函数地址" << endl;
    cout << "_vptr_Base — 第2个函数地址:	" << ((int*)*(int*)(&dmi) + 1) << "	";
    typedef void(*Fun)(void);
    Fun pFun = (Fun)*(((int*)*(int*)(&dmi)) + 1);
    pFun();
    cout << endl;
    cout << "_vptr_Base — 第3个函数地址:	" << ((int*)*(int*)(&dmi) + 2) << "	";
    pFun = (Fun)*(((int*)*(int*)(&dmi)) + 2);
    pFun();
    cout << endl;
    cout << "_vptr_Base — 第4个函数地址:	" << *((int*)*(int*)(&dmi) + 3) << "【结束】	";
    cout << endl;
    cout << "推测数据成员iBase地址:		" << ((int*)(&dmi) +1) << "	通过地址取得的值:" << *((int*)(&dmi) +1) << endl;
 
 
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_INTENSITY | FOREGROUND_GREEN);
    cout << "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++" << endl;
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_INTENSITY | FOREGROUND_RED);
    cout << "虚函数表_vptr_Base1地址:	" << ((int*)(&dmi) +2) << endl;
    cout << "_vptr_Base1 — 第1个函数地址:	" << (int*)*((int*)(&dmi) +2) << "	即析构函数地址" << endl;
    cout << "_vptr_Base1 — 第2个函数地址:	" << ((int*)*((int*)(&dmi) +2) + 1) << "	";
    typedef void(*Fun)(void);
    pFun = (Fun)*((int*)*((int*)(&dmi) +2) + 1);
    pFun();
    cout << endl;
    cout << "_vptr_Base1 — 第3个函数地址:	" << *((int*)*(int*)((int*)(&dmi) +2) + 2) << "【结束】	";
    cout << endl;  
    cout << "推测数据成员iBase1地址:	" << ((int*)(&dmi) +3) << "	通过地址取得的值:" << *((int*)(&dmi) +3) << endl;
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_INTENSITY | FOREGROUND_GREEN);
    cout << "++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++" << endl;
    SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), FOREGROUND_INTENSITY | FOREGROUND_RED);
    cout << "推测数据成员iDerived地址:	" << ((int*)(&dmi) +4) << "	通过地址取得的值:" << *((int*)(&dmi) +4) << endl;
}

  输出结果如下图所示:

clip_image027[3]

对于多继承,看下面一个面试题:

class ClassA
{
public:
    virtual ~ ClassA(){};
    virtual void FunctionA(){};
};
class ClassB
{
public:
   virtual void FunctionB(){};
};
class ClassC : public ClassA,public ClassB
{
    public:
};
ClassC aObject;
ClassA* pA=&aObject;
ClassB* pB=&aObject;
ClassC* pC=&aObject;

  在这个题目中PA和PC的地址是一样的,但是PB的地址要比其他的两个的地址大。三个的地址依次是:

0x7fff502f5aa8
0x7fff502f5ab0
0x7fff502f5aa8

如何访问成员?

前面介绍了C++对象模型,下面介绍C++对象模型的对访问成员的影响。其实清楚了C++对象模型,就清楚了成员访问机制。下面分别针对数据成员和函数成员是如何访问到的,给出一个大致介绍。

对象大小问题

其中:3个类中的函数都是虚函数

l  Derived继承Base

l  Derived_Virtual虚继承Base

void test_size()
{
    Base b;
    Derived d;
    Derived_Virtual dv;
    cout << "sizeof(b):	" << sizeof(b) << endl;
    cout << "sizeof(d):	" << sizeof(d) << endl;
    cout << "sizeof(dv):	" << sizeof(dv) << endl;
}

  输出如下:

因为Base中包含虚函数表指针,所有size为4;Derived继承Base,只是扩充基类的虚函数表,不会新增虚函数表指针,所以size也是4;Derived_Virtual虚继承Base,根据前面的模型知道,派生类有自己的虚函数表及指针,并且有分隔符(0x00000000),然后才是虚基类的虚函数表等信息,故大小为4+4+4=12。

#pragma once
class Empty
{
public:
    Empty(void);
    ~Empty(void);
};

Empty p,sizeof(p)的大小是多少?事实上并不是空的,它有一个隐晦的1byte,那是被编译器安插进去的一个char。这将使得这个class的两个对象得以在内中有独一无二的地址

数据成员如何访问(直接取址)

跟实际对象模型相关联,根据对象起始地址+偏移量取得。

静态绑定与动态绑定

程序调用函数时,将使用那个可执行代码块呢?编译器负责回答这个问题。将源代码中的函数调用解析为执行特定的函数代码块被称为函数名绑定(binding,又称联编)。C语言中,这非常简单,因为每个函数名都对应一个不同的额函数。在C++中,由于函数重载的缘故,这项任务更复杂。编译器必须查看函数参数以及函数名才能确定使用哪个函数。然而编译器可以再编译过程中完成这种绑定,这称为静态绑定(static binding),又称为早期绑定(early binding

然而虚函数是这项工作变得更加困难。使用哪一个函数不是能在编译阶段时确定的,因为编译器不知道用户将选择哪种类型所以,编译器必须能够在程序运行时选择正确的虚函数的代码,这被称为动态绑定(dynamic binding),又称为晚期绑定(late binding

使用虚函数是有代价的,在内存和执行速度方面是有一定成本的,包括:

l  每个对象都将增大,增大量为存储虚函数表指针的大小;

l  对于每个类,编译器都创建一个虚函数地址表;

l  对于每个函数调用,都需要执行一项额外的操作,即到虚函数表中查找地址。

虽然非虚函数比虚函数效率稍高,单不具备动态联编能力。

函数成员如何访问(间接取址)

跟实际对象模型相关联,普通函数(nonstatic、static)根据编译、链接的结果直接获取函数地址;如果是虚函数根据对象模型,取出对于虚函数地址,然后在虚函数表中查找函数地址。

多态如何实现?


 

多态的实现

多态(Polymorphisn)在C++中是通过虚函数实现的。通过前面的模型【参见“有重写的单继承”】知道,如果类中有虚函数,编译器就会自动生成一个虚函数表,对象中包含一个指向虚函数表的指针。能够实现多态的关键在于虚函数是允许被派生类重写的,在虚函数表中,派生类函数对覆盖(override)基类函数。除此之外,还必须通过指针或引用调用方法才行,将派生类对象赋给基类对象。

上面2个类,基类Base、派生类Derived中都包含下面2个方法:

    void print() const;

    virtual void print_virtual() const;

这个2个方法的区别就在于一个是普通成员函数,一个是虚函数。编写测试代码如下:

void test_polmorphisn()
{
    Base b;
    Derived d;
   
    b = d;
    b.print();
    b.print_virtual();
 
    Base *p;
    p = &d;
    p->print();
    p->print_virtual();
}

  

根据模型推测只有p->print_virtual()才实现了动态,其他3调用都是调用基类的方法。原因如下:

l  b.print();b.print_virtual();不能实现多态是因为通过基类对象调用,而非指针或引用所以不能实现多态。

l  p->print();不能实现多态是因为,print函数没有声明为虚函数(virtual,派生类中也定义了print函数只是隐藏了基类的print函数。

为什么析构函数设为虚函数是必要的

析构函数应当都是虚函数,除非明确该类不做基类(不被其他类继承)。基类的析构函数声明为虚函数,这样做是为了确保释放派生对象时,按照正确的顺序调用析构函数。

从前面介绍的C++对象模型可以知道,如果析构函数不定义为虚函数,那么派生类就不会重写基类的析构函数,在有多态行为的时候,派生类的析构函数不会被调用到(有内存泄漏的风险!)。

例如,通过new一个派生类对象,赋给基类指针,然后delete基类指针。

void test_vitual_destructor()
{
    Base *p = new Derived();
    delete p;
}

  如果基类的析构函数不是虚函数:

注意,缺少了派生类的析构函数调用。把析构函数声明为虚函数,调用就正常了:

原文地址:https://www.cnblogs.com/stemon/p/4665493.html