C/C++编程:继承与数据成员

1059 篇文章 286 订阅

在C++继承模型中:

  • 派生类对象 = 自己的成员 + 基类成员
  • 对于派生类成员和基类成员的排序次序:大部分编译器中,基类成员总是先出现,但虚基类除外(一般来说,任何规则在遇上虚基类之后都会被打破)

没有继承

class Point2d{
public:
private:
	float x, y;
};

class Point3d{
public:
private:
	float x, y, z;	
};

在这里插入图片描述

对象布局图如下,在没有虚函数的情况下,它们和C struct完全一样
在这里插入图片描述

只要继承不要多态

一般来说,具体继承(是相对虚拟继承来说的)并不会增加空间或者存取时间上的额外负担

class Point2d{
public:
    Point2d(float x = 0.0, float y = 0.0)
        : _x(x), _y(y) {};

    float x() { return _x;}
    float y() { return _y;}

    void x(float newX){_x = newX; }
    void y(float newY){_y = newY; }

    void operator += (const Point2d & rhs){
        _x += rhs._x;
        _y += rhs._y;
    }


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) {};

    float z() { return _z;}
    void z(float newZ){_z = newZ; }

    void operator += (const Point3d & rhs){
        Point2d::operator+=(rhs);
        _z += rhs._z;
    }


protected:
    float _z;
};

在这里插入图片描述
单一继承而且没有虚函数时的数据布局:
在这里插入图片描述

加上多态

如果要处理一个坐标点,而不在乎它是个Point2d还是Point3d实例,可以在继承关系中添加一个虚函数接口

class Point2d(){
public:
    Point2d(float x = 0.0, float y = 0.0)
        : _x(x), _y(y) {};

//x和y的存取操作和上面相同。由于对于不同维度的点,这些函数的操作固定不变,所以不必设计为虚函数
    float x() { return _x;}
    float y() { return _y;}

    void x(float newX){_x = newX; }
    void y(float newY){_y = newY; }
 
    // 加上z的保留空间
    virtual float z(){return 0.0;}
    virtual void z(float){}


    virtual void operator += (const Point2d & rhs){
        _x += rhs._x;
        _y += rhs._y;
    }


protected:
    float _x, _y;
}

只有当我们试图以多态的方式处理2d或者3d坐标点时,在设计中导入一个虚接口才合理。也就是说:

void foo(Point2d &p1, Point2d &p2){
	p1 += p2;
}

其中p1和p2可能是2d也可能是3d坐标点。这并不是先前任何设计所能支持的。这样的弹性,就是面向对象设计的中心。支持这样的弹性,势必给Point2d类带来空间和存取时间的额外负担:

  • 导入一个和Point2d有关的虚函数表,用来存放它所声明的每一个虚函数的地址,这个表的元素数目一般是被声明的虚函数的数目,在加上一个或者两个slots(用以支持runtime type identification)
  • 在每一个类对象中导入一个vptr,提供运行期的链接,使每一个对象能够找到相应的虚函数表
  • 加强构造函数,使它能够为vptr设定初值,让它指向类所对应的虚函数表。这可能意味着在派生类和每一个基类的构造函数中,重新设定vptr的值。其情况视编译器的优化的积极性而定。
  • 加强析构函数,使它能够抹除指向类相关的虚函数表vptr。要知道,vprt很可能已经在派生类析构函数中被设定为派生类的虚函数地址。析构函数的调用次数是反向的:从派生类到基类
class Point3d : public Point2d{
    Point3d(float x = 0.0, float y = 0.0, float z = 0.0)
            : Point2d(x, y), _z(z) {};

    float z() { return _z;}
    void z(float newZ){_z = newZ; }

    void operator += (const Point2d & rhs){  // 注意,这里是Point2d 而不是Point3d
        Point2d::operator+=(rhs);
        _z += rhs._z;
    }
protected:
    float _z;
};

上面最大的好处是可以把operator+=运用在一个Point2d和Point3d对象上

Point2d pd2(2.1, 3.0);
Point3d pd3(3.1, 5.2, 3.4);
pd2 += pd3

vptr放置在类对象的哪里会最好

在cfont编译器中,它被放置在类对象的尾端,用以支持下面的继承类型:

  • 优点:
    • 单一继承提供了一种自然多态的形式,是关于类体系中的基类和派生类之间的准换。把一个派生类指定给基类的指针或者引用时(不管继承深度有多深),并不需要编译器去调停或者修改地址,它可以很自然的发生,而且提供了最佳指向效率。
    Point3d p3d;
    Point2d *p = &p2d;
    
    • 把vptr放在类对象的尾端,可以保留基类C结构体的对象布局
struct no_virts{
	int d1, d2;
};

class has_virts : public no_virts{
	public:
		virtual void foo();
	private:
		int d3;
}; 

no_virts *p = new has_virts;

在这里插入图片描述
到了C++2.0,开始支持虚拟继承和抽象基类,并且由于OO的兴起,某些编译器开始把vptr放在类对象的头部。

  • 优点:帮助”多重继承时,通过指向类成员的指针调用虚函数“
  • 缺点:
    • 和C不兼容了
    • 当把vptr放在类对象的起始处时,如果基类没有虚函数而派生类有,那么单一继承的自然多态就会被打破,此时,把一个派生类转换为基类,就需要编译器调整地址(因为vptr的插入原因)
      在这里插入图片描述

多重继承

多重继承的复杂度在于派生类和上一个上上一个…基类之间的非自然关系。看个例子:

class Point2d{
	public:
		// ... 有
	protected:
		float _x, _y;
};

class Point3d : public Point2d{
	public:
		// ... 有
	protected:
		float _z;
};

class Vertex{
	public:
	protected:
		Vertex *next;
};

class Vertex3d : 
	public Point3, public Vertex{
	public:
	protected:
		float mumble;
};

在这里插入图片描述
在这里插入图片描述

多重继承的问题主要发生:

  • 在派生类对象和其第2或者后继的基类对象之间的转换:
extern void mumble(const Vertex&);

Vertex3d v;
mumble(3); // 将一个Vertex3d 转换为一个Vertex。这是不自然的
  • 经由其所制作的虚函数机制做转换

那么对于第一个问题:

  • 对一个多重派生对象,将其地址指定给第一个(最左边)基类的指针,情况和单一继承相同,因为二者都指向相同地址,需要付出的只有地址的指定操作而已。
  • 对于第二或后继的基类的地址指定操作,需要修改地址:加上(或者减去,如果downcast的话)介于中间的基类子对象大的大小。

比如:

Vertex3d  v3d;
Vertex  *pv;
Point2d *p2d;
Point3d *p3d;
  • 对于:
pv = &v3d;
  • 内部转换为:
pv = (Vertex*)((char*)v3d + sizeof(Point3d));
  • 对于:
p2d = &v3d;
p3d = &v3d;
  • 只需要简单的拷贝地址就可以了。

已经知道:

Vertex3d *pv3d
Vertex *pv;
  • 对于下面指定操作:
pv = pv3d;
  • 不能够只是简单的被转换为:
pv = (Vertex*)((char*)pv3d + sizeof(Point3d));
  • 因为如果pv3d为0,pv将获得sizeof(Point3d)的值。所以,对于指针,内部转换应该有一个条件测试:
pv = pv3d 
	 ? (Vertex*)((char*)pv3d + sizeof(Point3d))
	 :0

至于引用,则不需要针对可能的0值做防卫,因为引用不可能引用到无物(no object)

如果要存取第二个(或者后继)基类中的一个数据成员,将会是怎样的情况?需要付出额外的成本吗?

  • 不需要。成员的位置在编译时就固定了,因此存取成员只是一个简单的offset运算,就像单一继承一样简单----不管是经由指针、引用还是对象来存取

虚拟继承

多重继承的一个副作用是,他必须支持某种形式的shared subobject继承。比如:

class ios{};
class istream : public ios{};
class ostream : public ios{};
class istream : 
	public istream, public ostream {};

不管istream还是ostream都内含一个ios subobject。然而在iostream的对象布局中,我们只需要单一一份ios subobject就好。解决方法是引入虚拟继承

class ios{};
class istream : public virtual ios{};
class ostream : public virtual  ios{};
class istream : 
	public istream, public ostream {};

在这里插入图片描述
虚拟继承技术要求必须能够找到一个有效的方法:

  • 将istream和ostream各自维护的一个ios subobject,折叠成一个由iostream维护的单一ios subobject,
  • 并且还可以保存基类和派生类的指针(或者引用)之间的多态指定操作。

一般的实现方法如下:

  • 类如果内含一个或多个虚基类子对象,将被分割成两部分:一个不变局部和一个共享局部
  • 不变局部中的数据,不管后继如何演化,总是有固定的偏移量(从对象的开头算起),所以这一部分的数据可以被直接存取
  • 共享局部表现的就是虚基类子对象。这部分的数据,其位置会因为每次的派生操作而变化,所以它们只可以被间接存取。

各家编译器实现技术的差役就在于共享局部间接存取的方法不同。常用的有三种策略。

看个例子:

class Point2d{
public:
protected:
	float _x, _y;
};

class Vertex : public virtual Point2d{
public:
protected:
	Vertex *next;
};

class Point3d : public virtual Point2d{
public:
protected:
	float _z;
};

class Vertex3d : public Vertex, public Point3d{
public:
protected:
	float memble;
};

在这里插入图片描述
一般的布局策略是先安排好派生类的不变部分,然后再建立其共享部分。

但是这里有一个问题:怎么存取类的共享部分呢?

  • cfront编译器会在每一个派生类对象中安插一些指针,每一个指针指向一个虚基类
  • 要存取继承到的虚基类成员,可以用指针间接完成。

看个例子:

void Point3d::operator+=(const Point3d &rhs){
	_x += rhs._x;
	_y += rhs._y;
	_z += rhs._z;
};

在cfront策略下,这个运算符会被内部转化为:

__vbcPoint2d->_x += rhs.__vbcPoint2d->_x;
__vbcPoint2d->_y += rhs.__vbcPoint2d->_y;
_z += rhs._z;

而派生类和基类之间的转换:

Point2d *p2d = pv3d;

会变成:

Point2d *p2d = pv3d ? pv3d->vbcPoint2d:0;

这个实现模型有两个缺点:

  • 每一个对象必须针对每个虚基类背负一个额外的指针。然而理想上我们却希望类对象有固定的负担,不因为其虚基类的数目而变化。这应该怎么解决呢?
  • 由于虚拟继承串链的加长,导致间接存取层次增加。比如如果有3层虚拟衍化,就需要3次间接存取(经由三个虚基类指针)。然而理想上我们希望有固定的存取时间。这应该怎么解决呢?

第一个问题一般有两种解决方法:

  • VS编译器引入虚基类表。每一个类对象如果有虚基类,就会由编译器安插一个指针,指向这个虚基类表。
  • 在虚函数表中放置虚基类的偏移量。比如下图:
    在这里插入图片描述

第二个问题的解决方法:
-拷贝所有的nested virtual base class指针,放到派生类对象中。这就解决了“固定存取时间”的间隔,虽然付出了一些空间上的代价
在这里插入图片描述
一般来说,虚基类最有效的一种运用形式是:一个抽象的虚基类,没有任何数据成员

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值