Java vs C++:子类覆盖父类函数时的不同设计
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(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 具体情况具体分析,不虚的,文章投过来试试先,比如我们并不拒绝就某个热点事件对其进行的报导、深入解析。
投稿方式
重要说明
- 作者需要拥有所投文章的所有权,不能将别人的文章拿过来投递。
- 投递的文章需要经过审核,如果开源中国编辑觉得需要的话,将与作者一起进一步完善文章,意在使文章更佳、传播更广。
- 文章版权归作者所有,开源中国获得文章的传播权,可在开源中国各个平台进行文章传播,同时保留文章原始出处和作者信息,可在官方博客中标原创标签。
新闻来源
CryptoCurrency | CNY | Change 1h | Change 24h | Change 7d |
---|---|---|---|---|
Bitcoin | 0.58 % | 1.10 % | 12.10 % | |
Ethereum | 0.74 % | 4.71 % | 7.76 % | |
Tether | --- | 0.03 % | 0.26 % | |
USD Coin | 0.19 % | 0.45 % | 0.44 % | |
BNB | 2.20 % | 3.04 % | 26.90 % | |
Binance USD | 0.38 % | 0.60 % | 0.69 % | |
Cardano | 0.42 % | 4.87 % | 0.22 % | |
XRP | 0.74 % | 0.44 % | 13.05 % | |
Solana | 1.27 % | 1.81 % | 28.37 % | |
Polkadot | 0.08 % | 15.21 % | 37.87 % | |
Dogecoin | 0.07 % | 5.76 % | 12.58 % | |
Dai | 0.16 % | 0.37 % | 0.52 % | |
Shiba Inu | 2.36 % | 8.57 % | 35.74 % | |
TRON | 0.04 % | 0.43 % | 3.05 % | |
Wrapped Bitcoin | 0.89 % | 1.38 % | 3.42 % | |
Avalanche | 1.24 % | 9.58 % | 126.95 % | |
LEO Token | 0.33 % | 1.29 % | 7.54 % | |
Lido Staked Ether | 3.41 % | 12.96 % | 26.05 % | |
Litecoin | 1.07 % | 7.23 % | 35.68 % | |
FTX Token | 3.93 % | 19.20 % | 58.37 % | |
Chainlink | 0.87 % | 1.57 % | 4.71 % | |
Polygon | 0.79 % | 0.08 % | 13.45 % | |
Crypto.com Coin | 0.20 % | 4.39 % | 9.05 % | |
Stellar | 0.41 % | 6.68 % | 0.28 % | |
OKB | 1.75 % | 9.87 % | 78.32 % | |
Near | 0.11 % | 6.96 % | 68.08 % | |
Algorand | 1.63 % | 5.73 % | 6.03 % | |
Bitcoin Cash | 0.17 % | 13.74 % | 49.71 % | |
Ethereum Classic | 0.65 % | 2.71 % | 3.48 % | |
Monero | 0.15 % | 7.91 % | 5.83 % |