C语言对话-31.与大虾对话 领悟设计模式
myan(孟岩) 翻译[译者按]本文根据发表在CUJ Expert Forum上的两篇文章编译而成。C/C Users Journal是目前最出色的C/C语言专业杂志特别是在C Report闭刊之后CUJ的地位更加突出。CUJ Expert Forum是CUJ主办的网上技术专栏汇集2000年10月以来C社群中顶尖专家的技术短文并免费公开发布精彩纷呈是每一个C/C学习者不可错过的资料。由Jim Hyslop和Herb Sutter主持的Conversation系列是CUJ Expert Forum每期必备的精品专栏以风趣幽默的对话形式讲解C高级技术在C社群内得到广泛赞誉。译者特别挑选两篇设计模式方面的文章介绍给大家。设计模式方面的经典著作是GoF的Design Patterns。但是那本书有一个缺点不好懂。从风格上讲该书与其说是为学习者而写作的教程范本还不如说是给学术界人士看的学术报告严谨有余生动不足。这一点包括该书作者和象Bjarne Stroustrup这样的大师都从不讳言。实际上Design Pattern并非一定是晦涩难懂的通过生动的例子一个中等水平的C学习者完全可以掌握基本用法在自己的编程实践中使用得到立竿见影的功效。这两篇文章就是很好的例证。本文翻译在保证技术完整性的前提下作了不少删节和修改以便使文章显得更紧凑。人物介绍我 --- 一个追求上进的C程序员尚在试用期聪明但是经验不足。Wendy --- 公司里的技术大拿就坐在我旁边的隔间里C大虾最了不起的是她是个女的她什么都好就是有点刻薄我对她真是又崇拜又嫉妒。I. Virtually Yours -- Template Method模式我在研究Wendy写的一个类。那是她为这个项目写的一个抽象基类而我的工作就是从中派生出一个具象类(concrete class)。这个类的public部分是这样的class Mountie {public:void read( std::istream );void write( std::ostream ) const;virtual ~Mountie();很正常virtual destructor表明这个类打算被继承。那么再看看其protected部分protected:virtual void do_read( std::istream );virtual void do_write( std::ostream ) const;也不过就是一会儿的功夫我识破了Wendy的把戏她在使用template method模式。public成员函数read和write是非虚拟的它们肯定是调用protected部分do_read/do_write虚拟成员函数来完成实际的工作。啊我简直为自己的进步而飘飘然了哈Wendy这回你可难不住我还有什么招数尽管放马过来... 突然笑容在我脸上凝固因为我看到了其private部分private:virtual std::string classID() const 0;这是什么一个private纯虚函数能工作么我站了起来“Wendy你的Mountie类好像不能工作耶它有一个private virtual function。”“你试过了”她连头都不抬。“嗯那倒是没有啦可是想想也不行啊我的派生类怎么能override你的private函数呢” 我嘟囔着。“嗬你倒是很确定啊”Wendy的声音很轻柔“你怎么老是这也不行那也不行的这几个月跟着我你就没学到什么东西吗小菜鸟。”真是可恶啊...“小菜鸟你全都忘了访问控制级别跟一个函数是不是虚拟的根本没关系。判断一个函数是动态绑定还是静态绑定是函数调用解析的最后一个步骤。好好读读标准的3.4和5.2.2节吧。”我完全处于下风只好采取干扰战术。“好吧就算你说的不错我也还是不明白何必把它设为private”“我且问你倘若你不想让一个类中的成员函数被其他的类调用应当如何处理”“当然是把它设置为private的” 我回答道。“那么你去看看我的Mountie类实现特别是write()函数的实现。”我正巴不得逃开Wendy那刺人的目光便转过头去在我的屏幕上搜索很快我找到了void Mountie::write(std::ostream Dudley) const{Dudley classID() std::endl;do_write(Dudley);}嗨最近卡通片真是看得太多了居然犯这样的低级失误。还是老是承认吧“好了我明白了。classID()是一个实现细节用来在保存对象时指示具象类的类型派生类必须覆盖它所以必须是纯虚的。但是既然是实现细节就应该设为private的。”“这还差不多小菜鸟。”大虾点了点头“现在给我解释一下为什么do_read()和do_write()是protected的”这个问题并不难我组织了一下就回答“因为派生类对象需要调用这两个函数的实现来读写其中的基类对象。”“很好很好”大虾差不多满意了“不过你再解释解释为什么我不把它们设为public的”现在我感觉好多了“因为调用它们的时候必须以一种特定的方式进行。比如do_write()函数必须先把类型信息写入再把对象信息写入这样读取的时候负责生成对象的模块首先能够知道要读出来的对象是什么类型的然后才能正确地从流中读取对象信息。”“聪明啊我的小菜鸟”Wendy停顿了一下“就跟学习外国口语一样学习C也不光是掌握语法而已还必须要掌握大量的惯用法。”“是啊是啊我正打算读Coplien的书...”[译者注就是James Coplien 1992年的经典著作Advanced C Programming Style and Idioms]大虾挥了挥她的手“冷静小菜鸟我不是指先知Coplien的那本书我是指某种结构背后隐含的惯用法。比如一个类有virtual destructor相当于告诉你说‘嗨我是一个多态基类来继承我吧’ 而如果一个类的destructor不是虚拟的则相当于是在说‘我不能作为多态基类看在老天的份上别继承我。’”“同样的virtual函数的访问控制级别也具有隐含的意义。一个protected virtual function告诉你‘你写的派生类应该哦可是说是必须调用我的实现。’而一个private virtual function是在说‘派生类可以覆盖也可以不覆盖我随你的便。但是你不可以调用我的实现。’”我点点头告诉她我懂了然后追问道“那么public virtual function呢”“尽可能不要使用public virtual function。”她拿起一支笔写下了以下代码class HardToExtend{public:virtual void f();};void HardToExtend::f(){// Perform a specific action}“假设你发布了这个类。在写第二版时需求有所变化你必须改用Template Method。可是这根本不可能你知道为什么”“呃这个...不知道。”“由两种可能的办法。其一将f()的实现代码转移到一个新的函数中然后将f()本身设为non-virtual的class HardToExtend{// possibly protectedvirtual void do_f();public:void f();};void HardToExtend::f(){// pre-processingdo_f();// post-processing}void HardToExtend::do_f(){// Perform a specific action}然而你原来写的派生类都是企图override函数f()而不是do_f()的你必须改变所有的派生类实现只要你错过了一个类你的类层次就会染上先知Meyers所说的‘精神分裂的行径’。” [译者注参见Scott MeyersEffective C, Item 37绝对不要重新定义继承而来的非虚拟函数]“另一种办法是将f()移到private区域引入一个新的non-virtual函数”class HardToExtend{// possibly protectedvirtual void f();public:void call_f();};“这会导致无数令人头痛的问题。首先所有的客户都企图调用f()而不是call_f()现在它们的代码都不能编译了。更有甚者大部分派生类都回把f()放在public区域中这样直接使用派生类的用户可以访问到你本来想保护的细节。”“对待虚函数要象对待数据成员一样把它们设为private的直到设计上要求使用更宽松的访问控制再来调整。要知道由private入public易由public入private难啊”[译者注这篇文章所表达的思想具有一定的颠覆性因为我们太容易在基类中设置public virtual function了Java中甚至专门为这种做法建立了interface机制现在竟然说这不好一时间真是接受不了。但是仔细体会作者的意思他并不是一般地反对public virtual function只是在template method大背景下给出上述原则。虽然这个原则在一般的设计中也是值得考虑的但是主要的应用领域还是在template method模式中。当然template method是一种非常有用和常用的模式因此也决定了本文提出的原则具有广泛的意义。]II. Visitor模式我正在为一个设计问题苦恼。试用期快结束了我希望自己解决这个问题来证明自己的进步。每个人都记得自己的第一份工作吧也都应该知道在这个时候把活儿做好是多么的重要我亲眼看到其他的新雇员没有过完试用期就被炒了鱿鱼就是因为他们不懂得如何对付那个大虾...别误会我不是说她不好她是我见过最棒的程序员可就是有点刻薄古怪...。现在我拜她为师不为别的就是因为我十分希望能达到她那个高度。我想在一个类层次(class hierarchy)中增加一个新的虚函数但是这个类层次是由另外一帮人维护的其他人碰都不能碰class Personnel{public:virtual void Pay ( /*...*/ ) 0;virtual void Promote( /*...*/ ) 0;virtual void Accept ( PersonnelV ) 0;// ... other functions ...};class Officer : public Personnel { /* override virtuals */ };class Captain : public Officer { /* override virtuals */ };class First : public Officer { /* override virtuals */ };我想要一个函数如果对象是船长(Captain)就这么做如果是大副(First Officer)就那么做。Virtual function正是解决之道在Personnel或者Officer中声明它而在Captain和First覆盖(override)它。糟糕的是我不能增加这么一个虚函数。我知道可以用RTTI给出一个解决方案void f( Officer o ){if( dynamic_castCaptain*(o) )/* do one thing */else if( dynamic_castFirst*(o) )/* do another thing */}int main(){Captain k;First s;f( k );f( s );}但是我知道使用RTTI是公司编码标准所排斥的行为我对自己说“是的虽然我以前不喜欢RTTI但是这回我得改变对它的看法了。很显然除了使用RTTI别无它法。”“任何问题都可以通过增加间接层次的方法解决。”我噌地一下跳起来那是大虾的声音她不知道什么时候跑到我背后“啊哟您吓了我一跳...您刚才说什么”“任何问...”“是的我听清楚了”我也不知道哪来的勇气居然敢打断她“我只是不知道您从哪冒出来的。”其实这话只不过是掩饰我内心的慌张。“哈算了吧小菜鸟”大虾斜着眼看着我“你以为我不知道你心里想什么”她把声音提高了八度直盯着我“那些可怜的C语言门徒才会使用switch语句处理不同的对象类型。你看”/* A not-atypical C program */void f(struct someStruct *s){switch(s-type) {case APPLE:/* do one thing */break;case ORANGE:/* do another thing */break;/* ... etc. ... */}}“这些人学习Stroustrup教主的C语言时最重要的事情就是学习如何设计好的类层次。”“没错”我又一次打断她迫不及待地想让Wendy明白我还是有两下子的“他们应该设计一个Fruit基类派生出Apple和Orange用virtual function来作具体的事情。“很好小菜鸟。C语言门徒通常老习惯改不掉。但是你应该知道通过使用virtual function你增加了一个间接层次。”她放下笔“你所需要的不就是一个新的虚函数吗”“是的。可是我没有权力这么干。”“因为你无权修改类层次对吧”“您终于了解了情况我们没法动它。也不知道这个该死的类层次是哪个家伙设计的...” 我嘀嘀咕咕着。“是我设计的。”“啊...真的这个嘿嘿...”我极为尴尬。“这个类层次必须非常稳定因为有跨平台的问题。但是它的设计允许你增加新的virtual function而不必烦劳RTTI。你可以通过增加一个间接层次的办法解决这个问题。请问Personnel::Accept是什么?””嗯这个...”“这个类实现了一个模式可惜这个模式的名字起得不太好是个PNP叫Visitor模式。”[译者注PNPPoor-Named Pattern, 没起好名字的模式]“啊我刚刚读过Visitor模式。但是那只不过是允许若干对象之间相互迭代访问的模式不是吗”她叹了一口气“这是流行的错误理解。那个V我觉得毋宁说是Visitor还不如说是Virtual更好。这个PNP最重要的用途是允许在不改变类层次的前提下向已经存在的类层次中增加新的虚函数。首先来看看Personnel及其派生类的Accept实现细节。”她拿起笔写下void Personnel::Accept( PersonnelV v ){ v.Visit( *this ); }void Officer::Accept ( PersonnelV v ){ v.Visit( *this ); }void Captain::Accept ( PersonnelV v ){ v.Visit( *this ); }void First::Accept ( PersonnelV v ){ v.Visit( *this ); }“Visitor的基类如下”class PersonnelV/*isitor*/{public:virtual void Visit( Personnel ) 0;virtu

相关新闻

C++11实战:手把手教你写个线程池

C++11实战:手把手教你写个线程池

自从C11 直接内置线程库后,再也不用靠操作系统API裸奔写并发了! 但是呢,这标准库给的多线程支持还是太基础了!想整点高级活儿比如线程池、信号量啥的,还得咱自己动手丰衣足食。 比如面试聊到线程池,面试官…

2026/5/17 3:26:41 阅读更多 →
COMSOL声学—超声波无损检测(三维) 模型介绍:本模型主要利用压力声学、静电、固体力学以及...

COMSOL声学—超声波无损检测(三维) 模型介绍:本模型主要利用压力声学、静电、固体力学以及...

COMSOL声学—超声波无损检测(三维) 模型介绍:本模型主要利用压力声学、静电、固体力学以及压电效应、声结构耦合边界多物理场6个模块。 本模型包括压电单元(PZT-5H)和被检测材料(樟子松)两个部分…

2026/7/4 16:42:28 阅读更多 →
【小程序毕设源码分享】基于springboot+小程序的高校讲座信息APP的设计与实现(程序+文档+代码讲解+一条龙定制)

【小程序毕设源码分享】基于springboot+小程序的高校讲座信息APP的设计与实现(程序+文档+代码讲解+一条龙定制)

博主介绍:✌️码农一枚 ,专注于大学生项目实战开发、讲解和毕业🚢文撰写修改等。全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围:&am…

2026/7/3 7:16:18 阅读更多 →

最新新闻

小型自动进给台钻设计与机械结构详解

小型自动进给台钻设计与机械结构详解

1. 小型自动进给台钻的设计背景与需求分析 在金属加工、木工制作和模型制作等领域,钻孔作业是最基础也最频繁的操作之一。传统手动台钻虽然结构简单,但在批量加工时存在效率低下、钻孔深度不一致等问题。自动进给机构的引入,能够显著提升加工…

2026/7/5 10:19:07 阅读更多 →
知识管理实战:从用户故事驱动KARL框架落地

知识管理实战:从用户故事驱动KARL框架落地

1. 项目概述:当知识管理不再只是IT部门的PPT工程我是Jim Glenn,在Six Feet Up担任KARL Champion——这个头衔听起来有点拗口,但它的实际含义很实在:我不是来写技术文档的,也不是来推动某个特定软件上线的,而…

2026/7/5 10:17:07 阅读更多 →
高速PCB信号完整性:眼图分析与工程实践

高速PCB信号完整性:眼图分析与工程实践

1. 高速PCB设计中的信号完整性挑战 在当今GHz级高速数字电路设计中,信号完整性问题已成为工程师面临的最大挑战之一。当信号速率超过5Gbps时,PCB走线上的传输线效应、阻抗不连续、串扰和抖动等问题会显著影响系统性能。我曾参与过一个25Gbps SerDes接口的…

2026/7/5 10:17:07 阅读更多 →
AI技能安全扫描实战:从威胁模型到CI/CD集成

AI技能安全扫描实战:从威胁模型到CI/CD集成

1. 项目概述:为什么AI技能也需要“安检门”?最近在折腾AI Agent和各类AI编程工具(比如Cursor、GitHub Copilot)时,我发现一个挺有意思的现象:大家热衷于分享和下载各种“技能”(Skills&#xff…

2026/7/5 10:17:07 阅读更多 →
3分钟解锁网易云音乐:NCM转MP3的完全免费解决方案

3分钟解锁网易云音乐:NCM转MP3的完全免费解决方案

3分钟解锁网易云音乐:NCM转MP3的完全免费解决方案 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 你是否曾经遇到过这样的尴尬:在网易云音乐下载了心爱的歌曲,却只能在特定App里播放?车…

2026/7/5 10:15:07 阅读更多 →
RK3576芯片架构与AIoT应用开发全解析

RK3576芯片架构与AIoT应用开发全解析

1. RK3576/RK3576J芯片架构解析 Rockchip RK3576系列是瑞芯微面向AIoT和工业市场推出的高性能应用处理器,采用"44"大小核设计: 4个Cortex-A72性能核心2.2GHz(工业版2.1GHz) 4个Cortex-A53能效核心2.0GHz(工…

2026/7/5 10:15:07 阅读更多 →

日新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/5 0:03:34 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/5 0:03:34 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/5 0:07:38 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/5 0:03:34 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/5 0:03:34 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/5 0:07:38 阅读更多 →

月新闻