文档章节

Java vs C++:子类覆盖父类函数时缩小可访问性的不同设计

编辑部的故事
 编辑部的故事
发布于 05/31 15:47
字数 1921
阅读 2130
收藏 15

Java 和 C++ 都是面向对象的语言,允许对象之间的继承。两个语言的继承都设置有允许子类覆盖父类的“虚函数”,加引号是因为 Java 中没有虚函数这一术语,但是我们的确可以把 Java 的所有函数等同于虚函数,因为 Java 类的所有非 static 函数都可以被子类覆盖,这里仅借用“虚函数”这一名词的含义,不深究语言的术语问题。

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 行 **private void sayHello() {**会有编译错误,导致这段代码根本不能通过编译。因为 Java 不允许子类在覆盖父类函数时,缩小函数的可访问性,至于原因,我们可以用一个例子来说明。

例如我们在外部调用时使用下面的代码:

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

假如之前的代码可以通过编译,那么就存在这么一种可能:由于 Java 是运行时绑定,当 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++ 的标准则会明显简化编译器实现,但是对工程而言并不算很好的参考。毕竟,一个明显标注了 private 的函数,无论任何情况都不应该允许在外部被调用。

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

作者介绍

程梁,软件工程师。目前专注于 Angular 项目研发,同时对 Java 服务器端开发、Qt 桌面开发等都有浓厚的兴趣,个人博客 https://www.devbean.net。


本文系作者投稿文章。欢迎投稿。

投稿内容要求

  • 互联网技术相关,包括但不限于开发语言、网络、数据库、架构、运维、前端、DevOps(DevXXX)、AI、区块链、存储、移动、安全、技术团队管理等内容。
  • 文章不需要首发,可以是已经在开源中国博客或网上其它平台发布过的。但是鼓励首发,首发内容被收录可能性较大。
  • 如果你是记录某一次解决了某一个问题(这在博客中占绝大比例),那么需要将问题的前因后果描述清楚,最直接的就是结合图文等方式将问题复现,同时完整地说明解决思路与最终成功的方案。
  • 如果你是分析某一技术理论知识,请从定义、应用场景、实际案例、关键技术细节、观点等方面,对其进行较为全面地介绍。
  • 如果你是以实际案例分享自己或者公司对诸如某一架构模型、通用技术、编程语言、运维工具的实践,那么请将事件相关背景、具体技术细节、演进过程、思考、应用效果等方面描述清楚。
  • 其它未尽 case 具体情况具体分析,不虚的,文章投过来试试先,比如我们并不拒绝就某个热点事件对其进行的报导、深入解析。

投稿方式

重要说明

  • 作者需要拥有所投文章的所有权,不能将别人的文章拿过来投递。
  • 投递的文章需要经过审核,如果开源中国编辑觉得需要的话,将与作者一起进一步完善文章,意在使文章更佳、传播更广。
  • 文章版权归作者所有,开源中国获得文章的传播权,可在开源中国各个平台进行文章传播,同时保留文章原始出处和作者信息,可在官方博客中标原创标签。

© 著作权归作者所有

共有 人打赏支持
编辑部的故事

编辑部的故事

粉丝 1180
博文 251
码字总数 440183
作品 0
深圳
运营/编辑
私信 提问
加载中

评论(22)

飞鸿眉敛
飞鸿眉敛

引用来自“OSC_KulLHJ”的评论

C++的覆盖和重写是两个不同概念,文中的代码个人感觉并不标准,子类重写的函数要么加上virtual要么加上override,内部处理机制是不同的
加不加都可以,问题在于文中关于C++的描述是错的,完全错误的
OSC_KulLHJ
OSC_KulLHJ
C++的覆盖和重写是两个不同概念,文中的代码个人感觉并不标准,子类重写的函数要么加上virtual要么加上override,内部处理机制是不同的
loki_lan
loki_lan

引用来自“old_big”的评论

更正几个问题:
1. ”Java 和 C++ 都是面向对象的语言,允许对象之间的继承“ -- 对象之间都无法继承,继承的是类。javascript等语言才可以集成对象。
2. ”但是我们的确可以把 Java 的所有函数等同于虚函数,因为 Java 类的所有非 static 函数都可以被子类覆盖“-- java的private实例方法不是虚函数,包访问权限的实力方法在其他包也是无法覆盖,所以此时也不是虚函数。
3. java中private的方法并不是绝对无法被其他对象访问,反射可以,另外还有一个鲜为人知的方式可以在子类上调用而无需反射。

引用来自“000JC嚓”的评论

这个鲜为人知的方法是啥

引用来自“old_big”的评论

public class Son extends Father{
  public static void main(String[] args){
    Son son = new Son();
    son.foo();
  }
}

class Father{
  private String name = "father name";
  private void say(){
    System.out.println("say() is called");
  }
  
  public void foo(){
    System.out.println("what is this:" + this.getClass()); // if "this" is Son type, then the follow is on subclass call super private field and method.
    System.out.println(this + ".name = " + this.name);
    this.say();// "this" is Son type, but call super say, so in this way you can call subClass.superMethod and subClass.superField directly
  }
}
this到底是什么?如果是Son类型,那么Son类型上直接调用了Father的私有成员。note: 在父类中可以访问到未来的任意子类信息,因为this是子类,可以通过反射得到子类的信息。

引用来自“zh0”的评论

这里的this 在eclipse里貌似编译不过的,换成super就指定能用了

引用来自“黑子鱼咖”的评论

:sweat:你确定不是你打错地方了吗。这里可以用。还有son调用的 父类的 公共方法 foo() , 而父类的 私有方法是 方法 foo() 自己调用的。而不是 son 直接调用 father 的私有方法啊...

引用来自“old_big”的评论

this.name
this.say()
这两个this都是Son类型。"Son类型".name 和"Son类型".say() , 注意是点号左边的类型是子类型。万万要注意,是点号左边。

引用来自“Shazi199”的评论

你的理解有问题,错在不理解上下文。
首先,这个this是Son的实例,其次,这个this是Father的引用,而不是Son的引用,最后这个foo是Father的public方法,在Father的public方法里调用Father的私有方法有什么可奇怪的。

引用来自“old_big”的评论

`其次,这个this是Father的引用,而不是Son的引用`
这话怎讲? this当然就是Son类型对象的引用

引用来自“Shazi199”的评论

对象是son的实例,this是Father的引用,意即 Father this = new Son()
正解。
飞鸿眉敛
飞鸿眉敛

引用来自“old_big”的评论

我不打算再回复这些comment了,因为这个问题的关键点没有人看清楚,还以为我是在讲子类调用父类public方法。
this到底是谁?这个问题的更深入理解是:this指的一般是当前类的实例,但是有时也不是。用这个特征可以实现某些特别经典的一个设计,但是我就不展开来说了,某些深层问题,反而会让一些人感觉我很low。
你可以回复一下我说的
old_big
old_big
我不打算再回复这些comment了,因为这个问题的关键点没有人看清楚,还以为我是在讲子类调用父类public方法。
this到底是谁?这个问题的更深入理解是:this指的一般是当前类的实例,但是有时也不是。用这个特征可以实现某些特别经典的一个设计,但是我就不展开来说了,某些深层问题,反而会让一些人感觉我很low。
Shazi199
Shazi199

引用来自“old_big”的评论

更正几个问题:
1. ”Java 和 C++ 都是面向对象的语言,允许对象之间的继承“ -- 对象之间都无法继承,继承的是类。javascript等语言才可以集成对象。
2. ”但是我们的确可以把 Java 的所有函数等同于虚函数,因为 Java 类的所有非 static 函数都可以被子类覆盖“-- java的private实例方法不是虚函数,包访问权限的实力方法在其他包也是无法覆盖,所以此时也不是虚函数。
3. java中private的方法并不是绝对无法被其他对象访问,反射可以,另外还有一个鲜为人知的方式可以在子类上调用而无需反射。

引用来自“000JC嚓”的评论

这个鲜为人知的方法是啥

引用来自“old_big”的评论

public class Son extends Father{
  public static void main(String[] args){
    Son son = new Son();
    son.foo();
  }
}

class Father{
  private String name = "father name";
  private void say(){
    System.out.println("say() is called");
  }
  
  public void foo(){
    System.out.println("what is this:" + this.getClass()); // if "this" is Son type, then the follow is on subclass call super private field and method.
    System.out.println(this + ".name = " + this.name);
    this.say();// "this" is Son type, but call super say, so in this way you can call subClass.superMethod and subClass.superField directly
  }
}
this到底是什么?如果是Son类型,那么Son类型上直接调用了Father的私有成员。note: 在父类中可以访问到未来的任意子类信息,因为this是子类,可以通过反射得到子类的信息。

引用来自“zh0”的评论

这里的this 在eclipse里貌似编译不过的,换成super就指定能用了

引用来自“黑子鱼咖”的评论

:sweat:你确定不是你打错地方了吗。这里可以用。还有son调用的 父类的 公共方法 foo() , 而父类的 私有方法是 方法 foo() 自己调用的。而不是 son 直接调用 father 的私有方法啊...

引用来自“old_big”的评论

this.name
this.say()
这两个this都是Son类型。"Son类型".name 和"Son类型".say() , 注意是点号左边的类型是子类型。万万要注意,是点号左边。

引用来自“Shazi199”的评论

你的理解有问题,错在不理解上下文。
首先,这个this是Son的实例,其次,这个this是Father的引用,而不是Son的引用,最后这个foo是Father的public方法,在Father的public方法里调用Father的私有方法有什么可奇怪的。

引用来自“old_big”的评论

`其次,这个this是Father的引用,而不是Son的引用`
这话怎讲? this当然就是Son类型对象的引用
对象是son的实例,this是Father的引用,意即 Father this = new Son()
old_big
old_big

引用来自“old_big”的评论

更正几个问题:
1. ”Java 和 C++ 都是面向对象的语言,允许对象之间的继承“ -- 对象之间都无法继承,继承的是类。javascript等语言才可以集成对象。
2. ”但是我们的确可以把 Java 的所有函数等同于虚函数,因为 Java 类的所有非 static 函数都可以被子类覆盖“-- java的private实例方法不是虚函数,包访问权限的实力方法在其他包也是无法覆盖,所以此时也不是虚函数。
3. java中private的方法并不是绝对无法被其他对象访问,反射可以,另外还有一个鲜为人知的方式可以在子类上调用而无需反射。

引用来自“000JC嚓”的评论

这个鲜为人知的方法是啥

引用来自“old_big”的评论

public class Son extends Father{
  public static void main(String[] args){
    Son son = new Son();
    son.foo();
  }
}

class Father{
  private String name = "father name";
  private void say(){
    System.out.println("say() is called");
  }
  
  public void foo(){
    System.out.println("what is this:" + this.getClass()); // if "this" is Son type, then the follow is on subclass call super private field and method.
    System.out.println(this + ".name = " + this.name);
    this.say();// "this" is Son type, but call super say, so in this way you can call subClass.superMethod and subClass.superField directly
  }
}
this到底是什么?如果是Son类型,那么Son类型上直接调用了Father的私有成员。note: 在父类中可以访问到未来的任意子类信息,因为this是子类,可以通过反射得到子类的信息。

引用来自“zh0”的评论

这里的this 在eclipse里貌似编译不过的,换成super就指定能用了

引用来自“黑子鱼咖”的评论

:sweat:你确定不是你打错地方了吗。这里可以用。还有son调用的 父类的 公共方法 foo() , 而父类的 私有方法是 方法 foo() 自己调用的。而不是 son 直接调用 father 的私有方法啊...

引用来自“old_big”的评论

this.name
this.say()
这两个this都是Son类型。"Son类型".name 和"Son类型".say() , 注意是点号左边的类型是子类型。万万要注意,是点号左边。

引用来自“Shazi199”的评论

你的理解有问题,错在不理解上下文。
首先,这个this是Son的实例,其次,这个this是Father的引用,而不是Son的引用,最后这个foo是Father的public方法,在Father的public方法里调用Father的私有方法有什么可奇怪的。
`其次,这个this是Father的引用,而不是Son的引用`
这话怎讲? this当然就是Son类型对象的引用
飞鸿眉敛
飞鸿眉敛
"子类对父类函数覆盖时,扩大可访问性,通常都不是问题。"……我的天,这么大的问题居然说没问题?我的Base的指针base指向的是Child的对象,但是我通过base调用sayHello函数的时候,我看到的是Base里sayHello函数的访问权限还是要去找Child里sayHello函数的?很显然我会找的是Base里的,C++明显不允许你扩大访问权限,这才是合理的,因为我看到Base的权限是private或者protected,那我的Base的指针访问这个函数就该是private或者protected,不是吗?

而当缩小访问权限时,在C++里比如Base的sayHello是public,但是到Child下该函数的访问权限是private了,使用Base的指针访问该函数,同样也是完全可以的,因为对于Base的指针来说sayHello的访问权限就是public。

综上,很明显C++做到了访问权限的一致性,而且很好的保持了你所说的“工程上的意义”,而Java的权限管理简直是一坨,脚踩西瓜皮,滑到哪里算哪里
zh0
zh0

引用来自“old_big”的评论

更正几个问题:
1. ”Java 和 C++ 都是面向对象的语言,允许对象之间的继承“ -- 对象之间都无法继承,继承的是类。javascript等语言才可以集成对象。
2. ”但是我们的确可以把 Java 的所有函数等同于虚函数,因为 Java 类的所有非 static 函数都可以被子类覆盖“-- java的private实例方法不是虚函数,包访问权限的实力方法在其他包也是无法覆盖,所以此时也不是虚函数。
3. java中private的方法并不是绝对无法被其他对象访问,反射可以,另外还有一个鲜为人知的方式可以在子类上调用而无需反射。

引用来自“000JC嚓”的评论

这个鲜为人知的方法是啥

引用来自“old_big”的评论

public class Son extends Father{
  public static void main(String[] args){
    Son son = new Son();
    son.foo();
  }
}

class Father{
  private String name = "father name";
  private void say(){
    System.out.println("say() is called");
  }
  
  public void foo(){
    System.out.println("what is this:" + this.getClass()); // if "this" is Son type, then the follow is on subclass call super private field and method.
    System.out.println(this + ".name = " + this.name);
    this.say();// "this" is Son type, but call super say, so in this way you can call subClass.superMethod and subClass.superField directly
  }
}
this到底是什么?如果是Son类型,那么Son类型上直接调用了Father的私有成员。note: 在父类中可以访问到未来的任意子类信息,因为this是子类,可以通过反射得到子类的信息。

引用来自“zh0”的评论

这里的this 在eclipse里貌似编译不过的,换成super就指定能用了

引用来自“黑子鱼咖”的评论

:sweat:你确定不是你打错地方了吗。这里可以用。还有son调用的 父类的 公共方法 foo() , 而父类的 私有方法是 方法 foo() 自己调用的。而不是 son 直接调用 father 的私有方法啊...
我看错了,应该是son的对象跟方法调用不了this.say(),而father的可以
Shazi199
Shazi199

引用来自“old_big”的评论

更正几个问题:
1. ”Java 和 C++ 都是面向对象的语言,允许对象之间的继承“ -- 对象之间都无法继承,继承的是类。javascript等语言才可以集成对象。
2. ”但是我们的确可以把 Java 的所有函数等同于虚函数,因为 Java 类的所有非 static 函数都可以被子类覆盖“-- java的private实例方法不是虚函数,包访问权限的实力方法在其他包也是无法覆盖,所以此时也不是虚函数。
3. java中private的方法并不是绝对无法被其他对象访问,反射可以,另外还有一个鲜为人知的方式可以在子类上调用而无需反射。

引用来自“000JC嚓”的评论

这个鲜为人知的方法是啥

引用来自“old_big”的评论

public class Son extends Father{
  public static void main(String[] args){
    Son son = new Son();
    son.foo();
  }
}

class Father{
  private String name = "father name";
  private void say(){
    System.out.println("say() is called");
  }
  
  public void foo(){
    System.out.println("what is this:" + this.getClass()); // if "this" is Son type, then the follow is on subclass call super private field and method.
    System.out.println(this + ".name = " + this.name);
    this.say();// "this" is Son type, but call super say, so in this way you can call subClass.superMethod and subClass.superField directly
  }
}
this到底是什么?如果是Son类型,那么Son类型上直接调用了Father的私有成员。note: 在父类中可以访问到未来的任意子类信息,因为this是子类,可以通过反射得到子类的信息。

引用来自“zh0”的评论

这里的this 在eclipse里貌似编译不过的,换成super就指定能用了

引用来自“黑子鱼咖”的评论

:sweat:你确定不是你打错地方了吗。这里可以用。还有son调用的 父类的 公共方法 foo() , 而父类的 私有方法是 方法 foo() 自己调用的。而不是 son 直接调用 father 的私有方法啊...

引用来自“old_big”的评论

this.name
this.say()
这两个this都是Son类型。"Son类型".name 和"Son类型".say() , 注意是点号左边的类型是子类型。万万要注意,是点号左边。
你的理解有问题,错在不理解上下文。
首先,这个this是Son的实例,其次,这个this是Father的引用,而不是Son的引用,最后这个foo是Father的public方法,在Father的public方法里调用Father的私有方法有什么可奇怪的。
Java多态性详解——父类引用子类对象

面向对象编程有三个特征,即封装、继承和多态。   封装隐藏了类的内部实现机制,从而可以在不影响使用者的前提下改变类的内部结构,同时保护了数据。   继承是为了重用父类代码,同时为实...

LCZ777
2014/08/25
0
0
Java中JNI的使用详解第三篇:JNIEnv类型中方法的使用

上一篇说道JNIEnv中的方法的用法,这一篇我们就来通过例子来看一下这些方法的使用: 首先是第一个例子:在Java代码中定义一个属性,然后再C++代码中将其设置成另外的值,并且输出来 先来看一下...

zhangyujsj
2015/08/23
0
0
【Java】疯狂Java基础(一)——面向对象的特征:继承、封装和多态

一、前言 小编记得,刚接触计算机相关的课程的时候,接触的是c++,c++的老师一上来就说c++是面向对象的,c语言是面向过程的。面向对象比面向过程厉害,是从面向过程发展过来的的。 当时有一个...

kisscatforever
03/28
0
0
多态在 Java 和 C++ 编程语言中的实现比较

众所周知,多态是面向对象编程语言的重要特性,它允许基类的指针或引用指向派生类的对象,而在具体访问时实现方法的动态绑定。C++ 和 Java 作为当前最为流行的两种面向对象编程语言,其内部对...

Jerikc
2013/12/03
0
1
Java程序员如何高效而优雅地入门C++

Java程序员如何高效而优雅地入门Cpp,由于工作需要,需要用C++写一些模块。关于C++ 的知识结构,虽说我有过快速学习很多新语言的经验,但对于C++ 我也算是老手,但也还需要心生敬畏,本文会从...

小欣妹妹
04/23
0
0

没有更多内容

加载失败,请刷新页面

加载更多

OSChina 周二乱弹 —— 哥们之间报恩的想法被上帝实现了

Osc乱弹歌单(2018)请戳(这里) 【今日歌曲】 小小编辑:推荐歌曲《消愁》 《消愁》- 毛不易 手机党少年们想听歌,请使劲儿戳(这里) @过遥 :周一的早上就应该用来补觉,太困了 周末不想...

小小编辑
29分钟前
24
4
MariaDB 服务器在 MySQL Workbench 备份数据的时候出错如何解决

服务器是运行在 MariaDB 10.2 上面的,在使用 MySQL Workbench 出现错误: mysqldump: Couldn't execute 'SELECT COLUMN_NAME, JSON_EXTRACT(HISTOGRAM, '$."number-of-buckets-specified"'......

honeymose
今天
3
0
apache顶级项目(二) - B~C

apache顶级项目(二) - B~C https://www.apache.org/ Bahir Apache Bahir provides extensions to multiple distributed analytic platforms, extending their reach with a diversity of s......

晨猫
今天
7
0
day152-2018-11-19-英语流利阅读

“超级食物”竟然是营销噱头? Daniel 2018-11-19 1.今日导读 近几年来,超级食物 superfoods 开始逐渐走红。不难发现,越来越多的轻食餐厅也在不断推出以超级食物为主打食材的健康料理,像是...

飞鱼说编程
今天
19
1
SpringBoot源码:启动过程分析(二)

接着上篇继续分析 SpringBoot 的启动过程。 SpringBoot的版本为:2.1.0 release,最新版本。 一.时序图 一样的,我们先把时序图贴上来,方便理解: 二.源码分析 回顾一下,前面我们分析到了下...

Jacktanger
昨天
4
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部