文档章节

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

编辑部的故事
 编辑部的故事
发布于 2018/05/31 15:47
字数 1921
阅读 2187
收藏 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 具体情况具体分析,不虚的,文章投过来试试先,比如我们并不拒绝就某个热点事件对其进行的报导、深入解析。

投稿方式

重要说明

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

© 著作权归作者所有

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

编辑部的故事

粉丝 1219
博文 255
码字总数 457655
作品 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就指定能用了

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

😓你确定不是你打错地方了吗。这里可以用。还有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就指定能用了

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

😓你确定不是你打错地方了吗。这里可以用。还有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就指定能用了

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

😓你确定不是你打错地方了吗。这里可以用。还有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就指定能用了

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

😓你确定不是你打错地方了吗。这里可以用。还有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就指定能用了

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

😓你确定不是你打错地方了吗。这里可以用。还有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 走向C++

本文不涉及一些微妙蛋疼的语法比较, 关注的是宏观方面, 当然后期逐步更新如果, 如果觉得必要, 可能会加上. 议题之一: 初始化的比较 1. CPP基类的任何类构造函数会默认调用父类的不带参数的构...

晨曦之光
2012/03/09
38
0
Java 走向C++

本文不涉及一些微妙蛋疼的语法比较, 关注的是宏观方面, 当然后期逐步更新如果, 如果觉得必要, 可能会加上. 议题之一: 初始化的比较 1. CPP基类的任何类构造函数会默认调用父类的不带参数的构...

晨曦之光
2012/03/09
82
0
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
2018/03/28
0
0

没有更多内容

加载失败,请刷新页面

加载更多

postgres预写式日志的内核实现详解-heap2类型

导读: postgres预写式日志的内核实现详解-概述 postgres预写式日志的内核实现详解-wal记录结构 postgres预写式日志的内核实现详解-wal记录写入 postgres预写式日志的内核实现详解-wal记录读...

movead
29分钟前
1
0
ToolBar控件在C#开发APP中的使用方式【附案例源码】——Smobiler移动开发平台

控件说明 底部工具栏控件。 效果演示 其他效果 该界面为仿淘宝UI制作的一个简单的UI模板,源码获取方式请拉至文章末尾。 特色属性 属性 属性说明 Direction(相对布局) 容器主轴方向。 Flex...

amanda112
40分钟前
1
0
模块

AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等...

gtandsn
46分钟前
2
0
代码之外的生存指南,这6本书助你提升软实力

上期盟主向大家推荐了6本技术类书籍,引起了热烈反响。那么,工作之余,还有哪些好书能够为你打开更多的精彩世界呢?本期,多位知名企业的技术大咖将继续为您带来好书推荐,在新的一年里,为...

安卓绿色联盟
50分钟前
4
0
5分钟用Jitpack发布开源库

作者: 菜刀文 Demo:https://github.com/helen-x/JitPackReleaseDemo 项目开发中会用到很多开源库, 他们一般通过Maven/Gradle依赖进来的. 演而优则唱,开发越来越溜以后, 你是否也蠢蠢欲动,想发...

SuShine
55分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部