关于C++面向对象设计的访问性问题详解

发布时间 - 2026-01-11 03:07:38    点击率:

前言

最近在看Scott Meyers大神的《Effective C++》和《More Effective C++》,虽然这两本书都是古董级的教参了(当然针对C++11/C++14作者所更新的《Modern Effective C++》英文已经发售了,不过还没中文翻译版本),但是现在看来仍然收益匪浅,而且随着对这个复杂语言了解的深入和实践项目经验的增加,很多东西和作者产生了一种共鸣,以前种种疑惑突然有种拨云雾而见天日、豁然开朗的感觉,也难怪被列为合格C++程序员之必读书目。其实C++确实是个可怕的语言,于是市面上针对这个语言的教参也是聆郎满目层出不穷,当然水平也是参差不齐,像上面所说的Meyers三部曲能够历久弥新,也凸显了这些经典教参的真正价值。

至于最近回归C++本质,主要是觉得现在后台开发的RPC、MQ、分布式系统虽然被称的神乎其神的,但是作为成熟的组件绝大多数公司都可以是直接拿来主义,当然也不可否认其使用经验的可贵,因为最近线上使用这些组件还是遇到或多或少不少问题的,以后可以少走些坑,然而这种东西也是可遇难求的;反而C++语言本身的使用占用了程序员绝大多数的工作内容,从而直接影响到项目的质量和后续的可维护性。在此,侯捷老师的 勿在浮沙筑高台 仍如警世名言响彻在耳,一个合格的程序员其扎实的基本功是多么重要。

C++面向对象的东西太多了:public、protected、private访问和继承,virtual和多态、多继承,外加const、缺省参数、名字查找等,光这些元素的排列组合就可以导出很多种情况,看似灵活多变,但不是每种情况都值得去尝试的。

一、public继承

public继承意味着是”is-a”的关系,每个派生类型对象也是一个基类类型对象,基类支持的操作派生类都支持,只不过派生类比基类更具体化一些而已,否则的话应该将派生类不支持的特性给踢出去,比如:

class Bird { ... };
class FlyingBird: public Bird {
public:
 virtual void fly(); ...
};
class Penguin: public Bird { ... };

所以,总体来说public继承是相对比较严格的契约关系。当然public继承是一个比较笼统的概念,细分下来还包括接口继承、实现继承、接口和实现继承。

如果基类声明了一个pure virtual函数,则其目的是让派生类只继承该函数接口;如果基类声明了一个impure virtual函数,就是让派生类继承该函数的接口和其缺省实现;如果某个成员函数是non-virtual函数,则意味着它不打算在派生类中有不同的行为,即派生类继承该函数接口及一份强制性实现。

对于pure virtual函数的接口声明,基本没有什么意义,而non-virtual成员也显而易见。不过对于impure virtual虚函数,看似提供了缺省实现使用起来会比较方便,而且派生类可以覆盖其实现也比较灵活,但是如果直接使用这种方式,那么如果基类产生了新的派生类,但是恰好派生类忘记对这个impure virtual函数进行override,而其缺省实现又不满足新派生类的行为,那么新派生类对象的调用将会引发问题。所以如果想继承接口,同时又提供缺省实现,那么比较好的方式是将这两个功能进行分离,用一个pure virtual函数提供接口,再用一个non-virtual protected函数提供缺省实现,而让派生类手动确认是否使用该默认行为。

class Airplane {
public: virtual void fly(const Airport& dest) = 0;
protected: void defaultFly(const Airport& dest){ ... }
};

除了上面的方式处理impure virtual的缺省实现,其实也可以将其转换为:仍然使用pure virtual函数声明接口,不同同时也提供其缺省定义,这样派生类在override这个pure virtual接口的时候既可以完全重新定义fly的行为,也可以直接一条语句用基类名字直接调用基类的缺省实现(Airplane::fly),其好处是不用引入一个新的函数名字,缺点是缺省实现成了public的了。

说到此处,应该对C++中接口继承的行为得以了解了。

二、虚函数外的其他选择

前面我们说到了《C++之virtual函数访问性》中谈及了NVI手法,算是对public virtual的一个强有力的替换工具,不过我们知道其本身也用到了虚函数。虚函数具有运行时开销,而且其实现也是编译时间确定运行时候选择,在有些情况下其灵活性还是受限。

相比于虚函数依据派生类型进行行为的定制化之外,Strategy策略模式显得更为的灵活。通过在对象内部保存函数指针(或者更泛化的boost::function函数对象),其行为可以依据具体对象差异化而非派生类型差异化,甚至通过Set接口其行为还可以在运行时候进行变更。虽然Meyers说明如果使用非成员函数,默认将不能访问类的私有成员,否则就需要对封装性进行一定程度的妥协松懈,但是通过boost::function+boost::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的时候没有指明参数缺省值,此时如果客户端以派生类对象方式调用该函数,则发生的是静态绑定,需要显示指定参数值;而如果客户端以指针、引用的新式调用该函数,则发生的是动态绑定,可以不指定其带有缺省值的参数。

class Shape {
public: virtual void draw(ShapeColor color = Red) const = 0; ...
};
class Circle: public Shape {
// 如果以对象模式调用draw,必须指定color参数而不能使用缺省参数
public: virtual void draw(ShapeColor color) const; ... 
};

解决这个问题的一个方式是使用NVI手法,其public non-virtual接口提供默认默认值(且不会被派生类重写),而private virtual不使用默认默认的特性以规避这种可能的不一致性。

3.4 private继承

private继承没有”is-a”的契约关系了,在使用上一个巨大的差异是:编译器不再会自动将一个派生类对象转换为一个基类对象了,这意味着原本接收基类对象的函数参数将不再能够为其传递派生类对象作为实参了(对象、引用、指针类型都不允许,编译器会报基类S是派生类T不可访问的基类);同时由基类继承而来的所有成员,在派生类中都会变成private的访问权限。

private继承意味着只有实现部分被继承,接口部分被全部略去了,所以private继承应当是采用基类的某些功能帮助派生类完善其功能,从某种情况下说具有”has-a”的符合类型,所以除了考虑到派生类需要访问基类protected成员和virtual的因素被牵扯进来,否则应该尽量使用组合类型来代替private继承,而且即使如此,也可以使用下面的手法瞒天过海:

class Timer { 
public: virtual void onTick() const; ... 
};
class Widget {
private:
 class WidgetTimer: public Timer {
 public: virtual void OnTick() const; ...
 };
 WidgetTimer timer;
};

关于protected继承,连Meyers大神都没用过,那么我又何必废脑经去考虑他……

参考

Effective C++

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对的支持。


# c面向对象的三大特性  # 面向对象特性  # 面向对象的特性  # C++面向对象编程之析构详解  # 一篇文章带你了解C++面向对象编程--继承  # C++面向对象之多态的实现和应用详解  # 分享一下8年C++面向对象设计的经验体会  # C++图文并茂轻松进阶面向对象  # 派生类  # 而来  # 绑定  # 类中  # 默认值  # 的是  # 都是  # 好了  # 将会  # 说到  # 大神  # 情况下  # 考虑到  # 重写  # 会报  # 教参  # 转换为  # 强有力  # 高台  # 到该 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: Laravel怎么实现前端Toast弹窗提示_Laravel Session闪存数据Flash传递给前端【方法】  Laravel如何使用Service Provider服务提供者_Laravel依赖注入与容器绑定【深度】  ,交易猫的商品怎么发布到网站上去?  购物网站制作费用多少,开办网上购物网站,需要办理哪些手续?  Windows10电脑怎么查看硬盘通电时间_Win10使用工具检测磁盘健康  Laravel怎么实现支付功能_Laravel集成支付宝微信支付  Laravel如何使用API Resources格式化JSON响应_Laravel数据资源封装与格式化输出  html5源代码发行怎么设置权限_访问权限控制方法与实践【指南】  网站建设要注意的标准 促进网站用户好感度!  百度输入法ai组件怎么删除 百度输入法ai组件移除工具  昵图网官网入口 昵图网素材平台官方入口  Laravel怎么配置自定义表前缀_Laravel数据库迁移与Eloquent表名映射【步骤】  C语言设计一个闪闪的圣诞树  JS中使用new Date(str)创建时间对象不兼容firefox和ie的解决方法(两种)  如何快速完成中国万网建站详细流程?  javascript中闭包概念与用法深入理解  iOS中将个别页面强制横屏其他页面竖屏  Laravel路由Route怎么设置_Laravel基础路由定义与参数传递规则【详解】  Laravel怎么连接多个数据库_Laravel多数据库连接配置  Laravel模型关联查询教程_Laravel Eloquent一对多关联写法  高性能网站服务器部署指南:稳定运行与安全配置优化方案  高防服务器租用首荐平台,企业级优惠套餐快速部署  Windows驱动无法加载错误解决方法_驱动签名验证失败处理步骤  Laravel如何实现模型的全局作用域?(Global Scope示例)  如何用已有域名快速搭建网站?  独立制作一个网站多少钱,建立网站需要花多少钱?  Python函数文档自动校验_规范解析【教程】  ,网页ppt怎么弄成自己的ppt?  JS中页面与页面之间超链接跳转中文乱码问题的解决办法  千库网官网入口推荐 千库网设计创意平台入口  Laravel中间件起什么作用_Laravel Middleware请求生命周期与自定义详解  Laravel如何实现多对多模型关联?(Eloquent教程)  Laravel中的Facade(门面)到底是什么原理  Python高阶函数应用_函数作为参数说明【指导】  Laravel怎么进行数据库回滚_Laravel Migration数据库版本控制与回滚操作  Laravel如何使用Blade组件和插槽?(Component代码示例)  如何用景安虚拟主机手机版绑定域名建站?  如何快速使用云服务器搭建个人网站?  Java Adapter 适配器模式(类适配器,对象适配器)优缺点对比  历史网站制作软件,华为如何找回被删除的网站?  Laravel如何处理表单验证?(Requests代码示例)  佛山企业网站制作公司有哪些,沟通100网上服务官网?  Laravel怎么集成Vue.js_Laravel Mix配置Vue开发环境  Android中Textview和图片同行显示(文字超出用省略号,图片自动靠右边)  智能起名网站制作软件有哪些,制作logo的软件?  Win11怎么关闭资讯和兴趣_Windows11任务栏设置隐藏小组件  HTML5空格和nbsp有啥关系_nbsp的作用及使用场景【说明】  Laravel中间件如何使用_Laravel自定义中间件实现权限控制  HTML透明颜色代码在Angular里怎么设置_Angular透明颜色使用指南【详解】  Python并发异常传播_错误处理解析【教程】