关于C++面向对象设计的访问性问题详解 前言最近在看Scott Meyers大神的《Effective C》和《More Effective C》虽然这两本书都是古董级的教参了(当然针对C11/C14作者所更新的《Modern Effective C》英文已经发售了不过还没中文翻译版本)但是现在看来仍然收益匪浅而且随着对这个复杂语言了解的深入和实践项目经验的增加很多东西和作者产生了一种共鸣以前种种疑惑突然有种拨云雾而见天日、豁然开朗的感觉也难怪被列为合格C程序员之必读书目。其实C确实是个可怕的语言于是市面上针对这个语言的教参也是聆郎满目层出不穷当然水平也是参差不齐像上面所说的Meyers三部曲能够历久弥新也凸显了这些经典教参的真正价值。至于最近回归C本质主要是觉得现在后台开发的RPC、MQ、分布式系统虽然被称的神乎其神的但是作为成熟的组件绝大多数公司都可以是直接拿来主义当然也不可否认其使用经验的可贵因为最近线上使用这些组件还是遇到或多或少不少问题的以后可以少走些坑然而这种东西也是可遇难求的反而C语言本身的使用占用了程序员绝大多数的工作内容从而直接影响到项目的质量和后续的可维护性。在此侯捷老师的 勿在浮沙筑高台 仍如警世名言响彻在耳一个合格的程序员其扎实的基本功是多么重要。C面向对象的东西太多了public、protected、private访问和继承virtual和多态、多继承外加const、缺省参数、名字查找等光这些元素的排列组合就可以导出很多种情况看似灵活多变但不是每种情况都值得去尝试的。一、public继承public继承意味着是”is-a”的关系每个派生类型对象也是一个基类类型对象基类支持的操作派生类都支持只不过派生类比基类更具体化一些而已否则的话应该将派生类不支持的特性给踢出去比如123456classBird { ... };classFlyingBird:publicBird {public:virtualvoidfly(); ...};classPenguin:publicBird { ... };所以总体来说public继承是相对比较严格的契约关系。当然public继承是一个比较笼统的概念细分下来还包括接口继承、实现继承、接口和实现继承。如果基类声明了一个pure virtual函数则其目的是让派生类只继承该函数接口如果基类声明了一个impure virtual函数就是让派生类继承该函数的接口和其缺省实现如果某个成员函数是non-virtual函数则意味着它不打算在派生类中有不同的行为即派生类继承该函数接口及一份强制性实现。对于pure virtual函数的接口声明基本没有什么意义而non-virtual成员也显而易见。不过对于impure virtual虚函数看似提供了缺省实现使用起来会比较方便而且派生类可以覆盖其实现也比较灵活但是如果直接使用这种方式那么如果基类产生了新的派生类但是恰好派生类忘记对这个impure virtual函数进行override而其缺省实现又不满足新派生类的行为那么新派生类对象的调用将会引发问题。所以如果想继承接口同时又提供缺省实现那么比较好的方式是将这两个功能进行分离用一个pure virtual函数提供接口再用一个non-virtual protected函数提供缺省实现而让派生类手动确认是否使用该默认行为。1234classAirplane {public:virtualvoidfly(constAirport dest) 0;protected:voiddefaultFly(constAirport dest){ ... }};除了上面的方式处理impure virtual的缺省实现其实也可以将其转换为仍然使用pure virtual函数声明接口不同同时也提供其缺省定义这样派生类在override这个pure virtual接口的时候既可以完全重新定义fly的行为也可以直接一条语句用基类名字直接调用基类的缺省实现(Airplane::fly)其好处是不用引入一个新的函数名字缺点是缺省实现成了public的了。说到此处应该对C中接口继承的行为得以了解了。二、虚函数外的其他选择前面我们说到了《C之virtual函数访问性》中谈及了NVI手法算是对public virtual的一个强有力的替换工具不过我们知道其本身也用到了虚函数。虚函数具有运行时开销而且其实现也是编译时间确定运行时候选择在有些情况下其灵活性还是受限。相比于虚函数依据派生类型进行行为的定制化之外Strategy策略模式显得更为的灵活。通过在对象内部保存函数指针(或者更泛化的boost::function函数对象)其行为可以依据具体对象差异化而非派生类型差异化甚至通过Set接口其行为还可以在运行时候进行变更。虽然Meyers说明如果使用非成员函数默认将不能访问类的私有成员否则就需要对封装性进行一定程度的妥协松懈但是通过boost::functionboost::bind这个强有力的工具使用继承体系中的成员函数也是十分方便的。此处本人感觉虽然设计模式被奉为C开发的经典但是随着Modern C在标准上引入更多的特性和功能C的开发将必定变的更加友好直观也不被过于墨守那古典23式了毕竟绝大多数的设计模式都通过继承来实现的不可避免的增加了程序开发和维护的复杂度。三、继承体系来的其他问题好了轻松愉快的东西结束了下面是C史上的黑暗时刻了。3.1 继承而来名字的可见性C具有一套名字查找的规则总体来说就是从局部到外围从派生类到基类从内层名字空间到全局名字空间的查找顺序。由于到此为止我们没有说明函数重载的情况所以你此时仍然安之若素对于public non-virtual函数我们不去重写对于virtual函数我们可以override这一切安好但是一旦考虑到相同函数名的重载问题C有一套理论就会让你晕乎了C防止在应用程序库或者应用框架中建立的新的派生类被附带从疏远的基类中继承而来的重载函数所以在继承的时候C不会将基类的名字自动导入到派生类中。好了这就说明之前继承而来的接口其实也是在使用的时候在派生类作用域中没有找到该符号而在基类中找到该符号后满足调用的而如果你在基类中定义了其某个重载版本(无论是virtual还是non-virtual)的时候C在名字查找的时候就在你的派生类作用域中找到该名字了然后进行类型检查和重载但是重载的版本只限于在派生类中出现的版本基类的版本不参与重载所以在派生类中想增加还是改写无论virtual还是non-virtual函数的重载版本第一件事是使用using声明将基类符号的所有版本声明到派生的名字作用域中然后再干其他的。3.2 绝不重新定义继承而来的non-virtual函数C的non-virtual函数都是编译期静态绑定的其名字查找从其指针的静态类型开始。任何情况下都不要重新定义一个继承而来的non-virtual函数否则其调用的版本决定于其指针静态类型这与public继承is-a的一致性关系相互违背。3.3 绝不重新定义继承来的缺省参数值因为上面说到我们不应该重新定义一个继承而来的non-virtual函数所以到这里我们可以说绝对不要重新定义一个继承而来带缺省参数值的virtual函数的参数默认值。其原因是virtual函数是动态绑定的而缺省参数是静态绑定的。所以如果基类和派生类的参数默认值不一致则使用引用、指针调用发生参数默认值静态绑定和调用函数体动态绑定将会非常的诡异所以需要避免这种情况。还有就是如果虚函数参数再基类指定的参数缺省值而派生类override的时候没有指明参数缺省值此时如果客户端以派生类对象方式调用该函数则发生的是静态绑定需要显示指定参数值而如果客户端以指针、引用的新式调用该函数则发生的是动态绑定可以不指定其带有缺省值的参数。1234567classShape {public:virtualvoiddraw(ShapeColor color Red)const 0; ...};classCircle:publicShape {// 如果以对象模式调用draw必须指定color参数而不能使用缺省参数public:virtualvoiddraw(ShapeColor color)const; ...};解决这个问题的一个方式是使用NVI手法其public non-virtual接口提供默认默认值(且不会被派生类重写)而private virtual不使用默认默认的特性以规避这种可能的不一致性。