首页 > 编程语言 >C++类继承基础5——继承中的类作用域

C++类继承基础5——继承中的类作用域

时间:2024-03-30 12:58:22浏览次数:15  
标签:调用 继承 派生类 C++ 作用域 Base 基类 fcn

继承中的类作用域

每个类定义自己的作用域,在这个作用域内我们定义类的成员。

当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用城内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。

派生类的作用域位于基类作用域之内这一事实可能有点儿出人意料,毕竟在我们的程中派生类和基类的定义是相互分离开来的。不过也恰恰因为类作用域有这种继承嵌系,所以派生类才能像使用自己的成员一样使用基类的成员。

例如,当我们编写下面代码时:

class Quote
{
string isbn()
{}
};
class Disc_quote:public Quote
{};

class Bulk_quote:public Disc_quote
{
};


Bulk_quote bulk;
cout << bulk.isbn();


名字isbn的解析将按照下述过程所示:

  1. 因为我们是通过Bulk_quote的对象调用isbn的,所以首先在Bulk_quote中查找,这一步没有找到名字isbn。
  2. 因为Bulk_quote是Disc_quote的派生类,所以接下来在Disc_quote中查找,仍然找不到。
  3. 因为Disc_quote是Quote的派生类,所以接着查找Quote;此时找到了名字isbn,所以我们使用的isbn最终被解析为Quote中的isbn。

编译时进行名字查找

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

举个例子,我们可以给Disc_quote 添加一个新成员

class Dise_quote ; public Quote f
publ1c: 
void discount_policy() const
 {}
// 其他成员与之前的版本一致
};

我们只能通过Disc_quote及其派生类的对象、引用或指针使用discount_policy

Bulk_quote bulk;
Bulk_quote *bulkP = &bulk; // 静态类型与动态类型一致
Quote *itemp = bulk; //静态类型与动态类型不一致
bulkp->discount_policy(); // 正确:bulkP的类型是 Bulk_quote*
itemP->discount policy() ; //错误:itemP的类型是Quote*

尽管在bulk 中确实含有一个名为discount_policy的成员,但是该成员对于temp却是不可见的。

itemp的类型是Quote的指针,意味看对discount_policy的搜索将从Quote开始。

显然Quote不包含名为discount_policy的成员,所以我们无法通过Quote的对象、引用或指针调用discount _policy。

名字冲突与继承

和其他作用域一样,派生类也能重用定义在其直接基类或间接基类中的名字,此时定义在内层作用域(即派生类)的名字将隐藏定义在外层作用域(即基类)的名字(参见2.2.4节,第43页):

struct Base
{
Base(): mem(0) {}
protected:
int mem;
};

struct Derived : Base {
Derived(int i): mem(i) {}//用i初始化Derived::mem
// Base::mem进行默认初始化
int get_mem()
{ return mem;}

protected: // 返回 Derived:;mem
int mem;// 隐藏基类中的 mem
};

get_mem中mem引用的解析结果是定义在Derived中的名字,下面的代码

Derived d(42);
cout << d.get_mem() << endl; //打印42

的输出结果将是42。

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

通过作用域运算符来使用隐藏的成员

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

struct Derived:Base
{
int get base_mem()
{ return Base::mem}
//。。。
};

作用城运算符将覆盖掉原有的查找规则,并指示编译器从Base类的作用城开始查找mem。

如果使用最新的Derived版本运行上面的代码,则d.get mem()的输出结果将是0.

除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字名字查找与继承

关键概念:名字查找与继承

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

  1. 首先确定p(或obj)的静态类型。因为我们调用的是一个成员,所以该类型必然是类类型。
  2. 在p(或obj)的静态类型对应的类中查找mem。如果找不到,则依次在直接基类中不断查找直至到达继承链的顶端。如果找遍了该类及其基类仍然找不到,则编译器将报错。
  3. 一旦找到了mem,就进行常规的类型检查以确认对于当前找到的mem,本次调用是否合法。
  4. 假设调用合法,则编译器将根据调用的是否是虚函数而产生不同的代码:

         — 如果mem是虚函数且我们是通过引用或指针进行的调用,则编译器产生的代码将在运行时确定到底运行该虚函数的哪个版本,依据是对象的动态类型。

          一反之,如果mem不是虚函数或者我们是通过对象(而非引用或指针)进行的调用,则编译器将产生一个常规函数调用。

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

如前所述,声明在内层作用域的函数并不会重载声明在外层作用域的函数。

因此,定义派生类中的函数也不会重载其基类中的成员

和其他作用域一样,如果派生类(即内层作用域)的成员与基类(即外层作用域)的某个成员同名,则派生类将在其作用域内隐藏该基类成员。

即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉:

struct Base {

int memfcn();
};

struct Derived : Base {
// 隐藏基类的memfcn
int memfcn(int);
);

Derived d;
Base b;

b.memfcn(); //调用Base::memfcn

d.memfcn(10);//调用 Derived::memfcn

d.memfcn(); //错误:参数列表为空的memfcn被隐藏了

d.Base::memfcn();//正确:调用Base::memfcn

Derived中的memfcn 声明隐藏了 Base中的memfcn声明。

在上面的代码中前两条通用语句容易理解,第一个通过Base对象b进行的调用执行基类的版本;类似的,第二个通过d进行的调用执行Derived的版本;

第三条调用语句有点特殊,d.memfcn()是非法的。为了解析这条调用语句,编译器首先在Derived中查找名字memfcn;因为Derived确实定义了一个名为memfcn的成员,所以查找过程终止。一旦名字找到,编译器就不再继续查找了。Derived中的memfcn版本需要一个int实参,而当前的调用语句无法提供任何实参,所以该调用语句是错误的。

虚函数与作用域

我们现在可以理解为什么基类与派生类中的虚函数必须有相同的形参列表了。

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

class Base {
public:
virtual int fcn();
};

ciass 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 的虚函数 f1
};

D1的fcn函数并没有覆盖Base的虚函数fcn,原因是它们的形参列表不同。

实际上,D1的fcn将隐藏Base的fcn。此时拥有了两个名为fcn的函数:一个是D1从Base继承而来的虚函数fcn;另一个是D1自己定义的接受一个int参数的非虚函数fcn。

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

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

Base bobj; 
D1 dlobj; 
D2 d2obj;

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

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

前三条调用语句是通过基类的指针进行的,因为fcn 是虚函数,所以编译器产生的代码将在运行时确定使用虚函数的哪个版本。判断的依据是该指针所绑定对象的真实类型。

在bp2的例子中,实际绑定的对象是D1类型,而D1并没有覆盖那个不接受实参的fcn, 所以通过bp2进行的调用将在运行时解析为Base定义的版本。

接下来的三条调用语句是通过不同类型的指针进行的,每个指针分别指向继承体系中的一个类型。因为Base类中没有f2(),所以第一条语句是非法的,即使当前的指针碰巧指向了一个派生类对象也无济于事。

为了完整地阐明上述问题,我们不妨再观察一些对于非虚函数 fcn(int)的调用语句:

Base*p1=&d2obj;
D1 *p2=&d2obj;
D2 *p3 = &d2obj;

p1->fcn(42);
// 错误:Base中没有接受一个int的fcn

p2->fcn(42);
//静态绑定,调用D1::fcn(int)

p3->fcn(42); //静态绑定,调用D2::fcn(int)

在上面的每条调用语句中,指针都指向了D2类型的对象,但是由于我们调用的是非虚函数,所以不会发生动态绑定。实际调用的函数版本由指针的静态类型决定。

覆盖重载的函数

和其他函数一样,成员函数无论是否是虚函数都能被重载。

派生类可以覆盖重载函数的0个或多个实例。

如果派生类希望所有的重载版本对于它来说都是可见的,那么它就需要覆盖所有的版本,或者一个也不覆盖。

有时一个类仅需覆盖重载集合中的一些而非全部函数,此时,如果我们不得不覆盖基类中的每一个版本的话,显然操作将极其烦琐。

一种好的解决方案是为重载的成员提供一条using声明语句,这样我们就无须覆盖基类中的每一个重载版本了。

using 声明语句指定一个名字而不指定形参列表,所以一条基类成员函数的using 声明语句就可以把该函数的所有重载实例添加到派生类作用域中。

此时,派生类只需要定义其特有的函数就可以了,而无须为继承而来的其他函数重新定义。

类内using声明的一般规则同样适用于重载函数的名字:基类函数的每个实例在派生类中都必须是可访问的。

对派生类没有重新定义的重载版本的访问实际上是对using声明点的访问。

 

标签:调用,继承,派生类,C++,作用域,Base,基类,fcn
From: https://blog.csdn.net/2301_80224556/article/details/137170046

相关文章

  • 快递员的烦恼【华为OD机试JAVA&Python&C++&JS题解】
    一.题目-快递员的烦恼快递公司每日早晨,给每位快递员推送需要送到客户手中的快递以及路线信息,快递员自己又查找了一些客户与客户之间的路线距离信息,请你依据这些信息,给快递员设计一条最短路径,告诉他最短路径的距离。注意:不限制快递包裹送到客户手中的顺序,但必须保证都送......
  • 园区参观路径【华为OD机试JAVA&Python&C++&JS题解】
    一.题目-园区参观路径园区某部门举办了FamilyDay,邀请员工及其家属参加;将公司园区视为一个矩形,起始园区设置在左上角,终点园区设置在右下角;家属参观园区时,只能向右和向下园区前进;求从起始园区到终点园区会有多少条不同的参观路径;输入描述:第一行为园区长和宽;后面每一行表示......
  • L2-046 天梯赛的赛场安排 团体程序设计天梯赛-练习集 c++ 易懂 模拟
    天梯赛使用OMS监考系统,需要将参赛队员安排到系统中的虚拟赛场里,并为每个赛场分配一位监考老师。每位监考老师需要联系自己赛场内队员对应的教练们,以便发放比赛账号。为了尽可能减少教练和监考的沟通负担,我们要求赛场的安排满足以下条件:每位监考老师负责的赛场里,队员人数不得......
  • 【C++】C到C++的入门知识
    目录1、C++关键字2、命名空间2.1命名空间的定义2.2命名空间的使用2.2.1 加命名空间名称及作用域限定符2.2.2使用using将命名空间中某个成员引入2.2.3使用usingnamespace命名空间名称引入3、C++输入&输出4、缺省参数 4.1缺省参数的概念4.2缺省参数的分类......
  • 双向链表C++
    今天写了双向链表..........写的头好晕..........看来链表还是要多加练习这个双向链表完成了增删改查,并且最后销毁链表环境VScode#include<iostream>#include<cstring>usingnamespacestd;//结点类classNode{public:stringip;//客户端ipstringn......
  • leetcode-面试经典150题-42-接雨水(双指针c++)
    第一遍做的时候(没有看题解)我想到的思路就是遍历每一个凹下去的部分,计算能接到的雨水数量,然后累加,left,right分别是凹点的左右边界下面是代码:classSolution{public:inttrap(vector<int>&height){intn=height.size();intans=0;for(int......
  • C++堆详细讲解
    介绍二叉堆是一种基础数据结构,主要应用于求出一组数据中的最大最小值。C++的STL中的优先队列就是使用二叉堆。堆的性质: 1.堆是一颗完全二叉树;2.堆分为大根堆和小根堆(这里不讨论那些更高级的如:二叉堆,二叉堆,左偏树等等)3.大根堆满足每个节点的键值都小于等......
  • C++项目——集群聊天服务器项目(七)Model层设计、注册业务实现
    在前几节的研究中,我们已经实现网络层与业务层分离,本节实现数据层与业务层分离,降低各层之间的耦合性,同时实现用户注册业务。网络层专注于处理网络通信与读写事件业务层专注于处理读写事件到来时所需求的各项业务数据层专注于与底层数据库间进行增删改查。数据库中有User、Fr......
  • 继承特点、访问特点、方法重写1
    1.继承的特点:①子类可以继承父类的属性和行为,但是子类不能继承父类的构造器;  //子类有自己的构造器,父类构造器用于初始化父类的对象②java是单继承模式,一个类只能继承一个直接父亲;③java不支持多继承、但是支持多层继承;④java所有的的类都是Object的子类;//Java中的所......
  • 大海捞针 Skia(C++) 第 4.1 期(特别篇):将绘制结果输出到窗口
    前言由于本人(我)没有系统学习过图形学,无法提供准确的术语表达,如果哪位大佬看到我的一些错误,还请友善指出!第四期之后,我一直纠结于应该讲些什么。图形学的东西我真的学的不多,未来也不是很想走这个方向。但是我仍然希望通过我的一些绵薄之力为一些苦苦寻找关于Skia资料的兄弟们提供......