多重继承
首先给出多重继承的例子:
#include <iostream>
using namespace std;
class Point2d
{
public:
Point2d();
~Point2d();
//...(拥有virtual接口,所以Point2d对象之中会有vptr)
protected:
float _x,_y;
};
class Point3d:public Point2d
{
public:
Point3d();
~Point3d();
//...
protected:
float _z;
};
class Vertex
{
public:
Vertex();
~Vertex();
//...(拥有virtual接口,所以Vertex对象之中会有vptr)
protected:
Vertex *next;
};
class Vertex3d:public Point3d,public Vertex
{
public:
Vertex3d();
~Vertex3d();
//...
protected:
float mumble;
};
在这里,多重继承如果有多态之间的转换,就需要根据其位置来进行计算:
对一个多重派生对象,将其地址指定给“最左端(也就是第一个)base class 的指针”,情况将和单一继承时相同,其二者都有相同的其实地址,而对于第二个或后继的base class 的地址指定操作,则需要将地址进行修改。
例如:
Vertex3d v3d;
Vertex *pv;
则进行这样的操作,pv= &v3d;需要这样的内部转化:
pv = (Vertex*)(((char*)&v3d) + sizeof(Point3d));
Point2d *p2d;
Point3d *p3d;
则进行如下的操作,只需要简单的拷贝地址就行
p2d = &v3d;
p3d = &v3d;
如果有两个指针如下:
Vertex3d *pv3d;
Vertex *pv;
那么下面的指定操作:
//不能简单的转换,得判断pv3d是否是空指针
pv = pv3d;
pv = pv3d ? (Vertex*)((char*) pv3d) +sizeof(Point3d) : 0 ;
虚拟继承
在虚拟继承中,base class 不管在继承中被派生多少次,永远只存在一个实例。
我们看iostream的继承体系:
class ios { ... };
class istream:public ios { ... };
class ostream:public ios { ... };
class iostream :
public istream,public ostream { ... };
则这个多重继承的图表示为:
而在虚拟继承下:
class ios { ... };
class istream:public virtual ios { ... };
class ostream:public virtual ios { ... };
class iostream :
public istream,public ostream { ... };
从这张图,我们可以看到,其istream和ostream这两个类共同指向ios这个基类,但是我怎么从派生类iostream的数据布局上实现这种共享呢?
下面有两种实现方法:(每个编译器的实现方法可能不同)
编译器会在每一个derived class object 中安插一个指针,每个指针指向一个virtual base class,要存取继承得来的virtual base class members,可以使用相关指针间接完成。
下面举个例子说明这种间接完成的方式:
这是Point2d,Point3d,Vertex,Vertex3d的继承体系;
对于Point3d运算符:
void Point3d::operator+=(const Point3d &rhs)
{
_x+=rhs._x;
_y+=rhs._y;
_z+=rhs._z;
};
//在该编译器之下,这个运算符会被内部转换为:
//虚拟c++ 码
__vbcPoint2d->_x += rhs.__vbcPoint2d->_x;
__vbcPoint2d->_y += rhs.__vbcPoint2d->_y;
_z += rhs._z;
通过看下面这张数据结构分布:
你会发现每一个继承的派生类都会对每一个virtual base class 背负一个额外的指针,而随着虚拟继承的加长,则简介存取的次数增加;
第二种实现方式:
通过在虚函数表中放置virtual base class 的位移(而不是地址)
这样上面的运算符代码就转换为:
(this + __vptr__Point3d[-1])->_x +=(&rhs+rhs.__vptr__Point3d[-1])->_x;
(this + __vptr__Point3d[-1])->_y +=(&rhs+rhs.__vptr__Point3d[-1])->_y;
_z += rhs._z;
关于继承以及虚函数,多态的总结:
继承当中有public,protected,private这三种继承;
public继承对于派生类来说,也是公有可以调用的,protected对于基类来说是公有的,但对于派生类来说是私有的,private不用说了,继承了也是私有的,也不可以调用。
继承可以干什么:
比如,我可以将基本功能和强大功能分开,比如在一个mesh中,我就可以将Base_mesh,这个最基本的mesh,其基本功能有计算两点之间的最短距离,计算一个点周围的面,边,存储所有的点,边,面等等;然后派生得到Derived_mesh,这个mesh就可以设置一个专有的功能,通过base_mesh的数据以及方法,来设计一个函数,比如得到更加均匀的网格;这个函数还是需要base_mesh的基本功能来辅助;
可以看出,继承就是在拥有了基类的基本要素之后,进行二次开发,说明继承的类之间是有关系的,如果没有关系,我不知道继承它还有啥用; 我觉得继承则可以更好的面向对象,而不是冗余的单纯把所有功能堆砌到一起,这样并不能塑造不同的特有的对象;继承之后则让其基类更好的被复用;
通过虚函数,我们知道,它可以实现C++ 的一大特性----多态:
通过继承+虚函数,可以从派生类的数据分布看出其基类部分的虚表中的虚函数已经被派生类的相同的函数覆盖,也就是说其指向的地址是派生类的,从而通过基类指针或者引用,从而取得的就是从派生类的函数的地址;
这样做可以有什么用呢?
我们就可以通过基类指针或引用,从而得到派生的不同对象,获取不同的函数;此时就实现了多态;可以知道多态可以实现接口重用。也就是说,不论传递过来的究竟是哪个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法
举个例子:(让我们来看看动物)
class animal
{
public:
animal(string name):_name(name){};
virtual void specialization()=0;
void name()
{
cout << _name << endl;
}
~animal(){};
private:
string _name;
};
class lion:public animal
{
public:
lion(string name):animal(name){};
void specialization(){
cout<<"kill all animal"<<endl;
}
~lion(){};
};
class panda:public animal
{
public:
panda(string name):animal(name){};
void specialization()
{
cout<<"I'm so lovely"<<endl;
}
~panda(){};
};
当有一个函数需要展示你的才能,但是不知道它是哪种动物
void show_yourself(animal *a)
{
a.specialization();
}
我们通过基类的指针,就可以指向特有的动物
lion L(Wow);
show_yourself(&L); //就会输出 "kill all aniamls"
panda P(Cute);
show_yourself(&P); //就会输出 "I'm so lovely"
从而对于此函数show_yourself在编译器其函数的调用地址并不确定,只有在运行其才能知道;此时是运行时多态;
这里提到了运行时多态,也有静态多态;
静态多态在编译期间就可以确定函数的调用地址,其可以通过函数重载和模版(泛型编程)来实现;
这里再补充一下虚函数使用时的特点:
- 派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同
- 基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性
- 只有类的非静态成员函数才能定义为虚函数,静态成员函数和友元函数不能定义为虚函数(因为友元函数并不是成员函数,而静态成员函数被这个类的所有对象共享,并不存在因对象不同而不同,其实它和外面的函数是一样的,并不会传入this指针,它只有一份实例)
- 内联函数也不能定义成虚函数(内联函数的目的是为了减少函数调用时间。它是把内联函数的函数体在编译阶段的时候替换到函数调用处,这样代码运行到这里时候就不需要花时间去调用函数,而虚函数是在运行时确定函数地址,所以不能内联)
- 构造函数不能是虚函数,构造函数是为了构造出实体对象,所以调用构造函数时必须知道是哪个类调用了构造函数
- 析构函数得是虚函数,不然会导致内存泄漏,因为你运行时才知道是哪个类对象,从而才知道应该调用哪个析构函数
更新//2020/3/27
这里可以通过vs的命令行调试功能:可以查看某个类的内存布局
比如D2这个类,很明显这里面有虚继承,还有虚函数
#include <iostream>
using namespace std;
class A2 {
public:
virtual void getinfo()
{
cout << "A" << endl;
}
virtual void getvalue()
{
cout << "value A" << endl;
}
};
class B2 :virtual public A2
{
public:
virtual void getinfo()
{
cout << "B" << endl;
}
};
class C2 :virtual public A2
{
public:
virtual void getvalue()
{
cout << "value C" << endl;
}
};
class D2 : public C2, public B2
{
public:
void getvalue()
{
cout << "value D" << endl;
}
};
我们可以通过vs的命令窗口:
切换到当前源文件,执行命令 cl text.cpp /d1reportSingleClassLayoutD2 ;后面这个D2是类名
可以得到内存布局,虽然每个编译器的实现方式不同,但是我们可以可以看看虚继承的内存布局时什么样子:
这样,一下我们就清楚了,对于虚继承来说,A2只有一份虚表,并且时D2包含A2;
还有下面这种实现方式:
#include <iostream>
using namespace std;
class Base
{
public:
int a;
virtual void fcn1() {};
};
class Derived1 : public Base
{
public:
int b;
virtual void fcn2() {};
};
class Derived2 : public Base
{
public:
int c;
virtual void fcn3() {};
};
class Child : virtual public Derived1, virtual public Derived2
{
public:
int d;
virtual void fcn4() {} ;
};
布局如下: