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<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的权限管理简直是一坨,脚踩西瓜皮,滑到哪里算哪里

推荐↓↓↓
C语言与C++编程
上一篇:年代甚远,GCC 9 不再支持较早版本的 ARM 微架构 下一篇:Python的C拓展