现在的位置: 首页 > 综合 > 正文

由一个问题引发的思考

2013年08月30日 ⁄ 综合 ⁄ 共 3121字 ⁄ 字号 评论关闭
    记不请是那个大哥的blog了,名字好像叫做“用了10年,才知道C++才懂了一点“。俺觉得这话说的太实在了,正如TAOUP中4.2节所言:”C++ is anti-compact—the language's designer has admitted that he doesn't expect any one programmer to ever understand it all.“

    尽管本猪向来对于C++持以战战兢兢、诚惶诚恐的态度,今天在水木上看到某位网友的问题,还是脑子暂时短路了 @_@

发信人: One0 (Born to be Binary), 信区: CPlusPlus
标  题: subclass能够放宽member的access control吗
发信站: 水木社区 (Mon Feb 
18 18:07:27 2008), 站内

我记得是不能的,但是以下代码在suse的gcc version 
3.3.5下编译过了:

class A {
protected:
  
virtual void foo() {};
}
;

class B: public A {
public:
 
void foo() { };
}
;


    本猪看完后的直觉是:我不喜欢这段程序,它应该是合法的,但我更希望编译器认为它非法报错!

    拿GCC 4.1.2 测试的结果果然是合法的

    为什么直觉让我不喜欢这段程序呢?因为Derived Class中声明的foo()和Base Class中的同名虚函数foo() 具备不同级别的控制访问

    不过本猪还是有些沾沾自喜:毕竟我还是能回答该网友的这个疑问的。

    要弄清楚这个问题,其实就是搞清楚C++中的函数解析和名字查找规则。

    考虑下面的代码:编译器是依据怎样的机制找到合适、合法的foo()函数来进行调用呢?

B b;
b.foo();


   编译器基本是这个干的(非准确描述):

    1。以函数的调用上下文为起点,进行名字查找(name lookup)

    例如对“a.foo();”这句,编译器会根据a的类型为 Class B而以ClassB的类域(这词我生造的,不知道标准里有没有class domain这个专用名词)为起点进行查找:是否当前查找范围内至少包含一个名为foo的函数?(这个阶段还不考虑参数、访问权限等乱七八糟的东西,只考虑函数名)。

    如果有,就此打住,记录下当前查找范围内的所有候选者,跳往第2步;

    如果没有,扩大查找范围,将其父类也加入查找范围,重新开始名字查找,如此循环下去,如果最终也没找到,那么编译器会尽职尽责的告诉你"名字并未找到

    2。OK,现在我们已经确定了有若干个名为foo的候选者,至于哪个才是合适的,则需要函数重载机制(overload resolution)来确定。

    如果只有一个候选者,直接跳到第3步;否则根据参数个数和参数类型为标准,设法找的唯一的最佳匹配者;注意这里的唯一是非常重要的,如果存在无法确定最优的两个匹配者,则编译器会告诉你“二义性调用

    3。好,进行到这一步说明我们有一个匹配的foo函数,对这个函数进行可访问性检查(Accessibility checking),例如foo()是否是父类的private成员?

    好了,现在就可以回答哪位网友的问题了:这里发生的不是虚函数的Overriding,而是简单粗暴的名字隐藏(Name Handing)

    Derived Class B中包含名为foo的函数,因此上面第一步的名字查找根本不进一步查看Base Class A中是否也有foo的成员函数;后面的函数重载和可访问性检查的操作对象也就只限定在B中的这个foo上,而这个例子中参数匹配,可访问性也合法(B::foo()的访问控制为public),因此顺利通过编译器的检查。

    诺,编译器根本忽视了到A::foo()的存在!

    所以该网友据此怀疑到:Derived Class可以放宽对Base Class中成员函数的访问控制

    不,并非如此。

    听起来很奇怪吧,好,再试一下如下代码

  A * pa =new B;
pa
->foo();

   A a;
   a.foo();

   


    我想任何一本C++的教材上都会用类似的代码来讲述用用虚函数来达到运行时多态的特性的。然而,对于这个例子,这两句代码是无法通过编译的,编译器又会尽职的告诉你:"error: 'virtual void A::foo()' is protected"

    怎么回事,前面的代码"似乎"合法的放宽了foo()的访问权限,怎么这里编译器又提示错误的原因是因为protected?

    问题的实质是:通常意义上谈到的不允许提升访问权限是限定在这个问题中的:通过虚函数和基类指针(或引用)进行多态操作。 脱离了虚函数和多态,访问权限的提升不具备实际意义。

    想想"pa->foo() "的解析过程吧:编译器知道指针pa的理性为 A*,因此以Class A作为查找起点,找到一个候选对象,并且是唯一最佳匹配,但是无法通过可访问性检查:foo被声明为protected,而foo的调用者不是Class A或其子类的成员函数以及友员函数。

    这里顺便提一下(尽管和下面的内容无直接关系),再回头看一看前面的三段式的函数解析步骤,是不是觉得少了些什么?对了,根本没提到和虚函数相关的内容,事实也不需要对虚函数进行区别对待。那么虚函数独特的多态特性是在哪里和如何实现的呢?答案是不是在解析阶段,而是在调用阶段

    (下述文字未经过对编译器源码级阅读的证实,仅是从常见实现出发作出的合理推测)。 

    我们知道,具有虚函数的类有相应的Vtable,每个虚函数在类对应的Vtable中都有表项,每个表项记录了虚函数的函数体的地址。而具有虚函数的类的每个对象自构造时都有编译器隐式的加入一个Vptr,Vptr指向该类的Vtable;另一方面,在调用类的非静态成员函数时,编译器也会隐式的为函数传入一个表示对象实例的this指针。

     调用虚函数的特别之处在于,首先通过this指针得到Vptr(依赖于编译器实现),然后通过Vptr得到Vtable,进而得到函数体对应的地址,然后调用位于该地址的函数。
   
     对于pa->foo()这样的调用,当pa指向对象a和对象b是,前一半的解析过程都是一样的,如果能通过解析机制,那么在前者的情况,得到的函数体地址指向 A::foo(),而对于后者,则指向B::foo()。      

    抽象的的说,Vtable和Vptr为函数调用引入了一层间接性,而正如CS领域一句著名格言所说的:“计算机科学的所有问题,都可以通过引入一层间接性得到解决”,正是通过引入间接性,虚函数的多态特性得以实现。
   

     我在开始写道:我知道最初的代码是合法的,但我更希望编译器认为它非法。因为这段代码是很典型的合法不合理:puclic继承的意图是通过一致的接口来实现基类的可替换性,而这里的基类和子类不具备可替换性:a.foo()非法,b.foo()合法。

    如果,我是说如果,编译器在这种情况下报错,写这种程序的人就可能仔细审查自己的代码,从而发现设计上的不合理之处了。

    然而,C++秉承了C的理念:充分相信程序员,他们知道自己在做什么。可惜我觉得事实可能往往想反:由于C++的复杂性,程序员更倾向于不知道自己在作什么;或者他们以为自己在作什么而实际上作的是另外一件事情,这更糟糕。

    说来说去一句话,C++实在很让人生畏。

    有生之年,希望能把C++标准通读一边,6rz
   
 
   

 

抱歉!评论已关闭.