71

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

 5 years ago
source link: http://www.10tiao.com/html/285/201806/2651158198/1.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

(点击上方公众号,可快速关注)


来源:oschina

https://my.oschina.net/editorial-story/blog/1821891


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


网友评论:


@old_big:

更正几个问题:

1. ”Java 和 C++ 都是面向对象的语言,允许对象之间的继承“ -- 对象之间都无法继承,继承的是类。javascript等语言才可以集成对象。

2. ”但是我们的确可以把 Java 的所有函数等同于虚函数,因为 Java 类的所有非 static 函数都可以被子类覆盖“-- java的private实例方法不是虚函数,包访问权限的实力方法在其他包也是无法覆盖,所以此时也不是虚函数。

3. java中private的方法并不是绝对无法被其他对象访问,反射可以,另外还有一个鲜为人知的方式可以在子类上调用而无需反射。


@小果汁儿

C++是个多功能的语言,也就是多范式语言。因为功能多,所以初学者容易滥用功能,出很多问题不知道怎么解决,然后就上网抱怨,从而导致C++现在名声不好。终究归结为一个原因:傻逼太多。C++的面向对象是和其他语言不同的,感兴趣的看一下侯捷的《深入理解C++对象模型》,你就会明白类的对象在内存中怎么存的,继承是怎么继承的。C++的自定义类型有和基本数据类型同等的功能和效率,源于他的运算符重载。要说面向对象是数据结构的重用,那模板就是源码的重用(模板展开,你懂的)。表面的用法没啥探究的,你懂了原理自然豁然开朗。


@飞鸿眉敛

"子类对父类函数覆盖时,扩大可访问性,通常都不是问题。"……我的天,这么大的问题居然说没问题?我的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的权限管理简直是一坨,脚踩西瓜皮,滑到哪里算哪里


【关于投稿】


如果大家有原创好文投稿,请直接给公号发送留言。


① 留言格式:
【投稿】+《 文章标题》+ 文章链接

② 示例:
【投稿】《不要自称是程序员,我十多年的 IT 职场总结》:http://blog.jobbole.com/94148/

③ 最后请附上您的个人简介哈~



看完本文有帮助?请分享给更多人

关注「CPP开发者」,提升C/C++技能


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK