学习C++Prime第十五章第六节--继承中的类作用域

6、继承中的类作用域

  每个类定义自己的作用域,在这个作用域内我们定义类的成员。当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。
  派生类的作用域位于基类作用域之内这一事实可能有点出人意料,毕竟在我们的程序文本中派生类和基类的定义是相互分离开来的。不过也恰恰因为类作用域有这种继承嵌套的关系,所以派生类才能像使用自己的长远一样使用基类的成员。

在编译时进行名字查找
 &ems;一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致(当使用基类的引用或指针时会发生这种情况),但是我们能使用哪些成员仍然是由静态类型决定的。

名字冲突与继承
  和其他作用域一样,派生类也能重用定义在其直接基类或间接基类中的名字,此时定义在内存作用域(即派生类)的名字将隐藏定义在外城作用域(即基类)的名字。

NOTE:派生类的成员将隐藏同名的基类成员。

通过作用域运算符来使用隐藏的成员
我们可以通过作用域运算符来使用一个被隐藏的基类成员:

struct Derived : Base {
	int get_base_mem() {return Base::mem;}
	//...
};

作用域运算符将覆盖掉原有的查找规则,并指示编译器从Base类的作用域开始查找mem。
BEST:除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字。

关键概念:名字查找与继承
  理解函数调用的解析过程对于理解C++的继承至关重要,假定我们调用p->men()(或者obj.mem()),则依次执行以下4个步骤:

  • 首先确定p(或obj)的静态类型。因为我们调用的是一个成员,所以该烈性必然是类类型。
  • 在p(或obj)的静态类型对应的类中查找mem。如果找不到,则依次在直接基类中不断查找直至叨叨继承链的顶端。如果找遍了该类及基类仍然找不到,则编译器将报错。
  • 一旦找到了mem,就进行常规的类型检查以确认对于当前找到的mem,本次调用是否合法。
  • 假定调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码:
    -----如果mem是虚函数且我们是通过引用或指针进行的调用,则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本,依据是对象的动态类型。
    ----- 反之,如果mem不是虚函数或者我们是通过对象(而非引用或指针)进行的调用,则编译器将产生一个常规函数调用。

一如往常,名字查找先于类型检查

  如前所述,声明在内层作用域的函数并不会重载声明在外层作用域的函数。因此,定义派生类中的函数也不会重载其基类中的成员。和其他作用域一样,如果派生类(即内层作用域)的成员与基类(即外层作用域)的某个成员同名,则派生类在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉。

struct Base {
	int memfcn();
};
struct Drrived :Base {
	int memfcn(int);		//隐藏基类的memfcn
};
Derived d; Base b;
b.memfcn();					//调用Base::memfcn
d.memfcn();					//调用Derived::memfcn
d.memfcn();					//错误,参数列表为空的memfcn被隐藏了
d.Base::memfcn();			//正确:调用Base::memfcn

虚函数与作用域

假如基类与派生类的虚函数接受的实参不同,则我们就无法通过基类的引用或指针的派生类的虚函数了。

class Base {
public:
	virtual int fcn();
};
class D1 : public Base {
public:
	//隐藏基类的fcn,这个fcn不是虚函数
	//D1继承了Base::fcn()的定义
	int fcn(int);					//形参列表与Base中的fcn不一致
	virtual void f2();				//是一个新的虚函数,在Base中不存在
};
class D2:public D1{
public:
	int fcn(int);					//是一个非虚函数,隐藏了D1::fcn(int)
	int fcn();						//覆盖了Base的虚函数fcn
	void f2();						//覆盖了D1的虚函数f2
};

D1的fcn函数并没有副高Base的虚函数fcn,原因是它们的形参列表不同。实际上,D1的fcn将隐藏Base的fcn。此时拥有了两个名为fcn的函数:一个是D1从Base继承而来的虚函数fcn;另一个是D1自己定义的接受一个int参数的非虚函数fcn。

通过基类调用隐藏的虚函数

给定上面定义的这些类后,我们来看几种使用其函数的方法:

Base bobj;D1 d1obj;D2 d2obj;

Base *bp1 = &bobj,*bp2 = &d1obj, *bp3 = &d2obj;
bp1->fcn();				//虚调用,将在运行时调用Base::fcn()
bp2->fcn();				//虚调用,将在运行时调用Base::fcn()
bp3->fcn();				//虚调用,将在运行时调用D2::fcn()

D1 *d1p = &dlobj;	D2 *d2p = &d2obj;
bp2->f2();				//错误:Base没有名为f2的成员
d1p->f2();				//虚调用,将在运行时调用D1::f2()
d2p->f2();				//虚调用,将在运行时调用D2::f2()

前三条调用语句是通过基类的指针进行的,因为fcn是虚函数,所以编译器产生的代码将在运行时确定使用虚函数的哪个版本,判断的依据是该指针所绑定对象的真实类型。在bp2的例子中,实际绑定的对象时D1类型,而D1并没有覆盖那个不接受实参的fcn,所以通过bp2进行的调用将在运行时解析为Base定义的版本。
  接下来的三条调用语句是通过不同类型的指针进行的,每个指针分别指向继承体系中的一个类型。因为Base类中没有f2(),所以第一条语句是非法的,即使当前的指针碰巧指向了一个派生类对象也无济于事。

覆盖重载的函数

  和其他函数一样,成员函数无论是否是虚函数都能被重载。派生类可以覆盖重载函数的0个或多个实例。如果派生类希望所有的重载版本对于它来说都是可见的,那么它就需要覆盖所有的版本,或者一个也不覆盖。
  有时一个类仅需要覆盖重载集合中的一些而非全部函数,此时,如果我们不得不覆盖基类中的每一个版本的话,显示操作将及其烦琐。
  一种好的解决方案是为重载的成员提供一条using声明语句,这样我们就无须覆盖基类中的每一个重载版本了。using声明语句指定一个名字而不指定形参列表,所以一条基类成员函数的using声明语句就可以把该函数的所有重载实例添加到派生类作用域中。此时,派生类只需要定义其特有的函数就可以了,而无须为继承而来的其他函数重新定义。
  类内using声明的一般规则同样适用于重载函数的名字,基类函数的每个实例在派生类中都必须是可访问的。对派生类没有重新定义的重载版本的访问实际上是对using声明点的访问。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

工地码哥

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值