Java、C++中子类对父类函数覆盖的可访问性缩小的区别介绍

前言

“Java 和 C++ 中子类对父类函数覆盖的可访问性缩小的问题”的题目看起来比较学术化,但的确是一个容易忽视的问题。本文力求详细阐述这一问题在 Java 以及 C++ 中的区别。

先介绍什么是“子类对父类函数覆盖的可访问性缩小”。对于继承而言,子类可以覆盖父类的“虚函数”——尽管 Java 中没有虚函数这一术语,但可以把 Java 的所有函数都看作虚函数,因为 Java 的所有函数都可以被子类覆盖。这里仅借用“虚函数”这一名词的含义,不深究语言的细节。Java 和 C++ 都允许在覆盖时,改变函数的可访问性。所谓“可访问性”,就是使用 public 、 protected 、 private 等访问控制符进行修饰,用来控制函数能否被访问到。通常可访问性的顺序为(由于 C++ 中没有包的概念,因此暂不考虑包访问控制符,这并不影响这里的讨论):

public > protected > private

以 Java 为例:

class Base {
 protected void sayHello() {
  System.out.println("Hello in Base");
 }
}
class Child extends Base {
 public void sayHello() {
  System.out.println("Hello in Child");
 }
}

注意:这里的 sayHello() 函数。父类 Base 中,该函数使用 protected 访问控制符进行修饰。而子类将其改用 public ,这不会有任何问题。 子类对父类函数覆盖时,扩大可访问性,通常都不是问题。

当子类对父类函数覆盖的可访问性缩小时,Java 和 C++ 采取了不同的策略。

首先以 Java 为例,看下面的代码:

class Base {
 public void sayHello() {
  System.out.println("Hello in Base");
 }
}
class Child extends Base {
 private void sayHello() {
  System.out.println("Hello in Child");
 }
}

上面的代码中,高亮的第 8 行会有编译错误——这段代码根本不能通过编译! Java 不允许子类在覆盖父类函数时,缩小可访问性。 至于原因,我们可以用一个例子来说明。例如我们在类外部写下面的代码:

Base base = new Base();
base.sayHello();
base = new Child();
base.sayHello();

假如之前的代码可以通过编译,那么就存在这么一种可能:当 base 指向 new Base() 时, sayHello() 是可以访问到的,但是当 base 指向 new Child() 时, sayHello() 却无法访问到!在 Java 看来这是一个矛盾,应该避免出现这种问题,因此,Java 从编译器的角度规定我们不能写出上面的代码。

针对 C++,情况又有所区别。来看 C++ 的例子:

class Base {
public:
  virtual void sayHello() {
    std::cout << "Hello in Base";
  }
}
class Child : public Base {
private:
  void sayHello() {
    std::cout << "Hello in Child";
  }
}

这段代码在 C++ 中是完全正确的。注意,这里的子类在覆盖父类函数时, 缩小 了可访问性。如果你没有看出有什么问题,那么我们完全可以在类外部写出下面的代码:

Child child;
child.sayHello(); // 不能通过编译,因为 sayHello() 是 private 的
static_cast<Base&>(child).sayHello(); // 可以通过编译,因为 sayHello() 是 public 的

第 2 行调用是失败的,因为在 Child 中, sayHello() 是 private 的,不能在外部调用。然而,当我们使用 static_cast 将 Child 强制转换成 Base 对象时,事情发生了改变——对于 Base 而言, sayHello() 是 public 的,因此可以正常调用。

针对这一点,C++ 标准的 Member access control 一章中的 Access to virtual functions 一节可以找到如下的例子:

class B {
public:
  virtual int f();
};
class D : public B {
private:
  int f();
};
void f() {
  D d;
  B* pb = &d;
  D* pd = &d;
  pb->f(); // OK: B::f() is public, D::f() is invoked
  pd->f(); // error: D::f() is private
}

对此,C++ 标准给出的解释是:

Access is checked at the call point using the type of the expression used to denote the object for which the member function is called ( B* in the example above). The access of the member function in the class in which it was defined (D in the example above) is in general not known.

简单翻译过来有两条要点:

  • 访问控制是在调用时检查的,也就是说,谁调用了这个函数,就检查谁能不能访问这个函数
  • 类中成员函数的可访问性一般而言是不知道的,也就是说,检查可访问性时,并不能知道这个函数在定义时到底是 public 的还是 private 的,因此也就无法据此检查可访问性

正因如此,C++ 的调用方似乎可以通过一些技巧性转换,“巧妙地”调用到原本无法访问的函数。一个更加实际的例子是:Qt 里面, QObject::event() 函数是 public ,而其子类 QWidget 的 event() 函数则改变成 protected 。具体可以阅读 Qt 的相关代码。

总结来说,在子类覆盖父类函数时,Java 严格限制了子类不能缩小函数可访问性,但 C++ 无此限制。个人认为,从软件工程的角度来说,Java 的规定无疑更具有工程上面的意义,函数的调用也更加一致。C++ 的标准则会明显简化编译器实现,但是对工程而言并不算很好的参考。

PS:C++ 标准的正式版是需要购买的,但是草案可以免费下载。C++ 标准草案的下载地址可以在下面的页面找到: https://isocpp.org/std/the-standard

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

您可能感兴趣的文章:

  • java解析XML Node与Element的区别(推荐)
  • Java集合与数组区别简介及相互转换实例
  • java中timer的schedule和scheduleAtFixedRate方法区别详解
  • 浅谈java项目与javaweb项目导入jar包的区别
  • @PathParam和@QueryParam区别简析
(0)

相关推荐

  • Java集合与数组区别简介及相互转换实例

    数组Array和集合的区别: (1)数组是大小固定的,并且同一个数组只能存放类型一样的数据(基本类型/引用类型) (2)JAVA集合可以存储和操作数目不固定的一组数据.(3)若程序时不知道究竟需要多少对象,需要在空间不足时自动扩增容量,则需要使用容器类库,array不适用. 联系:使用相应的toArray()和Arrays.asList()方法可以回想转换. List和ArrayList的区别 1.List是接口,List特性就是有序,会确保以一定的顺序保存元素. ArrayList是它的实现类

  • 浅谈java项目与javaweb项目导入jar包的区别

    现在的项目基本上都是java web项目,所以导入jar包会出现问题,主要介绍一下java项目与javaweb项目的区别: java项目: 在classLoader加载jar和class的时候,是分开加载的,一般jar导入分两种: 1.在web-inf下的lib中直接引入 2.在user library上引入 无论以上哪种引入,jar包都能加载并且运行,classLoader会智能加载(本地JRE运行) javaweb项目: 不是通过本地的JRE运行的,而是部署到web服务器(比如tomcat,

  • java解析XML Node与Element的区别(推荐)

    对Element和Node有困惑是因为对xml整个结构不了解,以下作为一个简要概述: 以下图为w3c.org网站的xml文档树图: 从上图可以看出,一个xml文档由元素节点,属性节点,文本节点构成,其中bookstore被称为文档元素或根元素,也是一个元素节点 XML DOM是这样规定一个节点的 XML 文档中的每个成分都是一个节点. 整个文档是一个文档节点    即Document节点.在java中Document接口是继承于Node接口,表示整个XML 文档 每个 XML 标签是一个元素节点

  • java中timer的schedule和scheduleAtFixedRate方法区别详解

    timer的schedule和scheduleAtFixedRate方法一般情况下是没什么区别的,只在某个情况出现时会有区别--当前任务没有来得及完成下次任务又交到手上. 我们来举个例子: 暑假到了老师给schedule和scheduleAtFixedRate两个同学布置作业. 老师要求学生暑假每天写2页,30天后完成作业. 这两个学生每天按时完成作业,直到第10天,出了意外,两个学生出去旅游花了5天时间,这5天时间里两个人都没有做作业.任务被拖延了. 这时候两个学生采取的策略就不同了: sch

  • @PathParam和@QueryParam区别简析

    本文主要研究的是@PathParam和@QueryParam区别问题,具体如下. 问题 今天调试一个上传功能,客户端手持机发送数据,在URL中附加一个参数,后台用@PathParam接收,但是报错,无法获取这个参数. url:http://192.168.1.3/web1_service/convert/vict_hj1000?unit=160106 代码 @Path("/vict_hj1000") @POST @Consumes(MediaType.MULTIPART_FORM_DA

  • Java、C++中子类对父类函数覆盖的可访问性缩小的区别介绍

    前言 "Java 和 C++ 中子类对父类函数覆盖的可访问性缩小的问题"的题目看起来比较学术化,但的确是一个容易忽视的问题.本文力求详细阐述这一问题在 Java 以及 C++ 中的区别. 先介绍什么是"子类对父类函数覆盖的可访问性缩小".对于继承而言,子类可以覆盖父类的"虚函数"--尽管 Java 中没有虚函数这一术语,但可以把 Java 的所有函数都看作虚函数,因为 Java 的所有函数都可以被子类覆盖.这里仅借用"虚函数"

  • Java序列化中子类、父类构造函数问题实例分析

    本文实例讲述了Java序列化中子类.父类构造函数问题.分享给大家供大家参考,具体如下: 一 介绍 1.如果父类实现了序列化接口,子类就不需要实现序列化接口. 2.创建对象的时候,递归调用了父类的构造函数. 3.对子类对象进行反序列化操作时,如果其父类没有实现序列化接口,那么其父类的构造函数会被调用,否则不会被调用. 二 实例 package com.imooc.io; import java.io.FileInputStream; import java.io.FileOutputStream;

  • python中子类调用父类函数的方法示例

    前言 本文主要给大家介绍了关于python子类调用父类函数的相关内容,Python中子类中的__init__()函数会覆盖父类的函数,一些情况往往需要在子类里调用父类函数.下面话不多说了,来一起看看详细的介绍: 如下例程里,???处是需要调用父类函数的地方,接下来结合例程具体介绍. # -*- coding:utf-8 -*- class Student: def __init__(self,name): self.name=name def ps(self): print('I am %s'%

  • PHP中子类重载父类的方法【parent::方法名】

    在PHP中不能定义重名的函数,也包括不能再同一个类中定义重名的方法,所以也就没有方法重载.单在子类中可以定义和父类重名的方法,因为父类的方法已经在子类中存在,这样在子类中就可以把从父类中继承过来的方法重写. 子类中重载父类的方法就是在子类中覆盖从父类中继承过来的方法,父类中的方法被子类继承过来不就可以直接使用吗?为什么还要重载呢?因为有一些情况我们必须要覆盖的.例如,有一个"鸟"类,在这个类中定义了鸟的通用方法"飞翔".将"鸵鸟"类作为它的子类,

  • Java编程关于子类重写父类方法问题的理解

    子类重新实现父类的方法称重写:重写时可以修改访问权限修饰符和返回值,方法名和参数类型及个数都不可以修改:仅当返回值为类类型时,重写的方法才可以修改返回值类型,且必须是父类方法返回值的子类:要么就不修改,与父类返回值类型相同.那么,该如何理解呢?为什么要是父类返回值类型的子类? 提出问题:子类必须重写父类所有方法吗? Java,子类不是必须重写父类所有方法的,分为以下两种情况: 父类方法为抽象方法时,子类必须重写(实现)所有父类的抽象方法: 父类方法为普通方法时,子类可以重写父类方法,也可以不重写

  • 浅谈java继承中是否创建父类对象

    1. 调用父类构造方法是真的,但是根本没有创建父类对象,只不过是调用父类构造方法来初始化属性. 如果说调用父类构造方法就等于创建父类对象,那就真的无稽之谈. new指令开辟空间,用于存放对象的各个属/性引用等,反编译字节码你会发现只有一个new指令,所以开辟的是一块空间,一块空间就放一个对象. 然后,子类调用父类的属性,方法啥的,那并不是一个实例化的对象. 在字节码中子类会有个u2类型的父类索引,属于CONSTANT_Class_info类型,通过CONSTANT_Class_info的描述可以

  • python中子类与父类的关系基础知识点

    在对于python中类的使用上,我们分出了子类和父类两种.对于这二者之间的关系,我们可以简单理解为继承.不过python中加入了实例的讨论,那么对于继承后的子类来说,父类的实例是否被继承又是我们所需要思考的问题.下面我们就子类和父类进行简单介绍,然后就二者之间的继承关系重点分析. 1.概念 子类和父类主要描述的是类之间的继承关系,即所属关系.继承的类可在被继承的类的基础上添加格外的参数和行为,新类称为子类,扩展类:被继承的类称为基类.父类或者超类. 2.继承关系 子类与父类的关系是 "is&qu

  • JavaScript 中定义函数用 var foo = function () {} 和 function foo()区别介绍

    某天写代码突然县道这个问题,顺势总结一波 JavaScript 函数和变量声明的"提前"(hoist)行为 简单的说 如果我们使用 匿名函数 var a = {} 这种方式, 编译后变量声明a 会"被提前"了,但是他的赋值(也就是a)并不会被提前. 也就是,匿名函数只有在被调用时才被初始化. 如果使用 function a () {}; 这种方式, 编译后函数声明和他的赋值都会被提前. 也就是说函数声明过程在整个程序执行之前的预处理就完成了,所以只要处于同一个作用域

  • Java中for、while、do while三种循环语句的区别介绍

    本文通过实例讲解给大家介绍Java中for.while.do while三种循环语句的区别,具体详情如下所示: 第一种:for循环 循环结构for语句的格式: for(初始化表达式;条件表达式;循环后的操作表达式) { 循环体; } eg: class Dome_For2{ public static void main(String[] args) { //System.out.println("Hello World!"); //求1-10的偶数的和 int sum = 0; fo

  • jquery选择器中的空格与大于号>、加号+与波浪号~的区别介绍

    概念 空格:$('parent childchild')表示获取parent下的所有的childchild节点 大于号:$('parent > childchild')表示获取parent下的所有下一级childchild 加号:$('pre + nextbrother')表示获得pre节点的下一个兄弟节点,相当于next()方法 波浪号:$('pre ~ brother')表示获取pre节点的后面的所有兄弟节点,相当于nextAll()方法 详解描述 现有代码如下 <meta charset

随机推荐