虚函数 继承 多态

单继承与Data Members

在C++的继承模型中,base class members和derived class members的排列顺序并为强制规定。不同的编译器可能有不同的布局安排。大部分情况下,base class members会安排在derived class members的前面,但base class是virtual base class(base class存在virtual function)除外。

只有继承没有多态

考虑如下程序:

class Point2d

{

public:

Point2d(float x = 0.0, float y = 0.0)

:x(x),y(y){}

void setX(float newX)

{

x = newX;

}

void setY(float newY)

{

y = newY;

}

float getX()

{

return x;

}

float getY()

{

return y;

}

void operator+=(const Point2d& rhs)

{

x += rhs.getX();

y += rhs.getY();

}

protected:

float x,y;

};

class Point3d : public Point2d

{

public:

Point3d(float x = 0.0,float y = 0.0,float z = 0.0)

:Point2d(x,y),z(z){}

void  setZ(float newZ)

{

z = newZ;

}

float getZ()

{

return z;

}

void operator+=(const Point3d& rhs)

{

Point2d::operator +=(rhs);

z += rhs.getZ();

}

protected:

float z;

};

这样单一继承且没有virtual function的数据布局如下:

加上多态(加上虚函数)

对于Point2d而言,它只是特殊的Point3d,其中的z等于0。这样Point2d,Point3d的重新设计如下,新增部分以红色标出

class Point2d

{

public:

Point2d(float x = 0.0, float y = 0.0)

:x(x),y(y){}

void setX(float newX)

{

x = newX;

}

void setY(float newY)

{

y = newY;

}

float getX()

{

return x;

}

float getY()

{

return y;

}

virtual void setZ(float){}

virtual float getZ()

{

return 0.0;

}

virtual void operator+=(const Point2d& rhs)

{

x += rhs.getX();

y += rhs.getY();

}

protected:

float x,y;

};

class Point3d : public Point2d

{

public:

Point3d(float x = 0.0,float y = 0.0,float z = 0.0)

:Point2d(x,y),z(z){}

void  setZ(float newZ)

{

z = newZ;

}

float getZ()

{

return z;

}

void operator+=(const Point2d& rhs)

{

Point2d::operator +=(rhs);

z += rhs.getZ();

}

protected:

float z;

};

说下虚函数的实现,在大部分编译器中虚函数是通过virtual table和virtual table pointer实现的,二者可以简写为vtbl和vptrs。vtbl通常以函数指针实现。在程序中凡是声明(或者继承)了虚函数者,都有自己的一个vtbl,而其中的值就是该class的各个虚函数实现体的指针。而vptrs的作用就是提供执行期的链接,使每一个object能够找到相应的vtbl。关于vptrs到底放在class object的哪里好?(一般是在class object的最前面或者最后面),不同的编译器有不同的安排。此时class object的布局如下(此图是把vptr放在base class的尾端):

由上图可见base class与derived class之间的转换可以很自然的进行,因为base class和derived
class的object都是从相同的地址开始,唯一的差异只是derived
object比较大,用以容纳自己的non-static data members。如进行一下操作:

Point3d p3d;

Point2d *p = &p3d;

把一个derived class object指定给base class的指针或者reference,该操作并不需要编译器去修改地址,它可以自然地发生。

多重继承

加如一个新的类Vertex如下:

class Vertex

{

public:

//拥有若干virtual接口,所以Vertex对象中会有vptr

protected:

Vertex*
next;

}

class Vertex3d: public Point3d.public Vertex

{

public:

// ….

protected:

float
mumble;

}

现在Point2d,Point3d,Vertex,Vertex3d的继承关系如下

多重继承的问题主要发生于derived class objects和其第二或后继的base class
objects(如本例中的Vertex3d到Vertex之间的转换就属于这中情况)之间的转换,而不是像单重继承那样的转换或是经由其支持的virtual
function做的转换(暂且可以不考虑)。

对一个多重派生对象,将其地址指定给第一个base class的指针情况和单一继承时相同,因为二者都是指向相同的起始地址,需要付出的成本只有地址的指定操作。至于第二个或后继的base class的地址指定操作,需要将地址修改为加上(或减去)介于中间的base class
subobject的大小。

上例的数据布局如下图:

如下例:

Vertex3d v3d;

Vertex* pv;

Point2d* p2d;

Point3d* p3d;

则下面的操作

pv = &v3d; //相当于单重继承的转换

编译器内部的转换可能是这样的:

pv =(Vertex*)( ((char*)&v3d)  + 
sizeof(Point3d) );   //加上sizeof(Point3d的原因是Vertex3d继承了Point3d

而下面的指定操作

p2d = &v3d;

p3d = &v3d;

都只需要简单地拷贝其地址就行了。

C++ Standard 中并未要求base class
Point3d,和Vertex有特定的排列次序。

虚拟继承

多重继承中会碰到如下图的继承关系(钻石型继承),C++中的ios,istream,ostream,iostream就是典型的钻石型继承。

这样的继承出现时, base class 的data members会在derived
class object中都出现,这样derived class object就会出现同样的数据或者function。此时,让base class成为virtual(虚基类),可以消除这样的冗余现象,唯一需要付出的成本是derived
class object内有两指针指向虚基类(如:本例的类A)。

若类A没有任何虚函数,D对象的内存布局可能如下:

D object


B data members


Point to virtual
base class


C data members


Point to virtual
base class


D data members


A data members

若类A存在任何虚函数,D对象的内存布局可能如下:


B data members


vptr


Point to virtual
base class


C data members


vptr


Point to virtual
base class


D data members


vptr


A data members

Virtual
Destructor

最后想说下virtual destructor,我们经常看到基类会把析构函数写成虚函数,这样写还是有道可循的。

如下程序

class Base {

public:

virtual Base() {};

};

class Derived : public
Base {};

Base* pd = new
Derived;

但是,当你写下delete pd;时,如果base class 的destructor不是virtual的,其结果是未定义的。实际上执行时通常会发生的是derived对象的成分没有被销毁,于是会造成诡异的“局部销毁”对象,这会形成资源泄露。

上面如有不足不对的地方,望各路大神补充指正。

时间: 2024-10-24 21:48:37

虚函数 继承 多态的相关文章

C++ Primer 学习笔记_34_面向对象编程(5)--虚函数与多态(二):纯虚函数、抽象类、虚析构函数、动态创建对象

C++ Primer 学习笔记_34_面向对象编程(5)--虚函数与多态(二):纯虚函数.抽象类.虚析构函数.动态创建对象 一.纯虚函数 1.虚函数是实现多态性的前提 需要在基类中定义共同的接口 接口要定义为虚函数 2.如果基类的接口没办法实现怎么办? 如形状类Shape 解决方法 将这些接口定义为纯虚函数 3.在基类中不能给出有意义的虚函数定义,这时可以把它声明成纯虚函数,把它的定义留给派生类来做 4.定义纯虚函数: class <类名> { virtual <类型> <函

C++ Primer 学习笔记33_面向对象编程(4)--虚函数与多态(一):多态、派生类重定义、虚函数的访问、 . 和-&gt;的区别、虚析构函数、object slicing与虚函数

C++ Primer学习笔记33_面向对象编程(4)--虚函数与多态(一):多态.派生类重定义.虚函数的访问. . 和->的区别.虚析构函数.object slicing与虚函数 一.多态 多态可以简单地概括为"一个接口,多种方法",前面讲过的重载就是一种简单的多态,一个函数名(调用接口)对应着几个不同的函数原型(方法). 更通俗的说,多态行是指同一个操作作用于不同的对象就会产生不同的响应.或者说,多态性是指发出同样的消息被不同类型的对象接收时有可能导致完全不同的行为. 多态行分

虚继承和虚函数继承

虚继承主要用于菱形 形式的继承形式 虚继承是为了在多继承的时候避免引发歧义, 比如类A有个就是a,B继承了A,C也继承了A,当D多继承B,C时,就会有歧义产生了,所以要使用虚拟继承避免重复拷贝. 虚函数继承是解决多态性的,当用基类指针指向派生类对象的时候,基类指针调用虚函数的时候会自动调用派生类的虚函数,这就是多态性,也叫动态编联 虚函数继承: class A { virtual void fun() {cout < <'A' < <endl;}; }; class B : pub

C++中虚函数和多态

1.C++中的虚函数 C++中的虚函数的作用主要是实现了多态的机制.关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数.这种技术可以让父类的指针有"多种形态",这是一种泛型技术.所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法.比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议. 对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Ta

虚继承与虚函数继承

虚继承主要用于菱形 形式的继承形式. 虚继承是为了在多继承的时候避免引发歧义,比如类A有个就是a,B继承了A,C也继承了A,当D多继承B,C时,就会有歧义产生了,所以要使用虚拟继承避免重复拷贝.虚函数继承是解决多态性的,当用基类指针指向派生类对象的时候,基类指针调用虚函数的时候会自动调用派生类的虚函数,这就是多态性,也叫动态编联 虚函数继承:class A{    virtual void fun() {cout < <'A' < <endl;};};class B : publi

你好,C++(37)上车的人请买票!6.3.3 用虚函数实现多态

6.3.3  用虚函数实现多态 在理解了面向对象的继承机制之后,我们知道了在大多数情况下派生类是基类的“一种”,就像“学生”是“人”类中的一种一样.既然“学生”是“人”的一种,那么在使用“人”这个概念的时候,这个“人”可以指的是“学生”,而“学生”也可以应用在“人”的场合.比如可以问“教室里有多少人”,实际上问的是“教室里有多少学生”.这种用基类指代派生类的关系反映到C++中,就是基类指针可以指向派生类的对象,而派生类的对象也可以当成基类对象使用.这样的解释对大家来说是不是很抽象呢?没关系,可以

C++ Primer 学习笔记_35_面向对象编程(6)--虚函数与多态(三):虚函数表指针(vptr)及虚基类表指针(bptr)、C++对象模型

C++ Primer 学习笔记_35_面向对象编程(6)--虚函数与多态(三):虚函数表指针(vptr)及虚基类表指针(bptr).C++对象模型 一.虚函数表指针(vptr)及虚基类表指针(bptr) C++在布局以及存取时间上主要的额外负担是由virtual引起的,包括: virtual function机制:用以支持一个有效率的"执行期绑定": virtual base class:用以实现多次在继承体系中的基类,有一个单一而被共享的实体. 1.虚函数表指针 C++中,有两种数据

&lt;C++&gt; 类(3):初始化列表 常函数和常量对象 虚函数与多态(包括纯虚函数)

一.初始化列表(初始化列表中必须有的两个内容) 1.类中const的成员变量: ①特点:不能修改 必须初始化 在构造函数后面加冒号 格式为:":变量名(值)" 也就是说 常量必须在初始化列表中初始化 ②执行顺序:构造函数先执行初始化列表 然后执行函数中的内容 1 #include<iostream> 2 using namespace std; 3 4 class CPerson 5 { 6 public: 7 const int a; 8 public: 9 CPerso

c++ 内存分配 虚函数实现多态等

看到了很好的解释,先mark下,有空整理. 虚函数实现多态与多重继承乌索普的回答 很有收获,毕竟我觉得多态才是面向对象的灵魂. 这样的设计也让人得以理解为什么虚函数会像成员变量一样与对象绑定. C++成员函数在内存中的存储方式 原文地址:https://www.cnblogs.com/zsl96/p/8732662.html