《高质量C++/C编程指南》与《陷阱》的讨论
c++吧
全部回复
仅看楼主
level 9
本文提到的“文章”,“原文”均指《高质量C++/C编程指南》。“本文”是指本篇评论。只有《陷阱》是指《高质量C++/C编程指南》陷阱。
百度空间中《陷阱》一文似乎有个点击这里查看原文的字样,但没链接。我把原文链接贴一下:
http://oss.org.cn/man/develop/c&c++/c/c.htm#_Toc520633988所有 > 开头的行是引用《陷阱》一文的文本。
(*) 文章主要从实际操作的角度出发,因此在一些地方的叙述欠缺严谨性。下文遇到类似情况,不再写出这句评论,代之以“参见(*)”
(**) 考虑到文章的成文年代(2001年)以及当时各编译器对标准的支持程度(特别是VC 6带的,原文很大程度上是基于这个编译器的),加之文章从实际操作角度出发(参见(*)),也就不难理解为什么文中只字未提模板,以及会出现某些明显和标准不符的地方了(例如new的异常)。下文遇到类似情况,不再写出这句评论,代之以“参见(**)”
顺便再吐一句:Windows 98亮瞎狗眼
> 第1章 文件结构
> 每个C++/C程序通常分为两个文件。
> //错误。没有强调翻译单元的概念。
参见(*)。作者的本意是想说明实际操作中通常的组织代码方式。毕竟程序员日常写的是各种xxx.c,xxx.cpp,xxx.h,代码也是存放在一个个这些名字的文件中,而不是“xxx.翻译单元”中。
另外,原文此处不强调某个概念什么时候也是错误了(至于两“个”文件,无视好了)
> 1.2
> 建议1-2-1
> 在C++语法中,类的成员函数可以在声明的同时被定义,并且自动成为内联函数。这虽然会带来书写上的方便,但却造成了风格不一致,弊大于利。建议将成员函数的定义与声明分开,不论该函数体有多么小。
> //这条建议一般不适用于类模板。
原文只字未提模板,原因参见(**)
> 1.4 关于头文件
目前有争议。头文件(假定里面放了函数声明等)有违反DRY原则的嫌疑。所以“但头文件可以导致其它方面——像重构(典型情况如重命名一个函数)的复杂化”
> 第2章 程序的版式
> 这章基本是主观内容。读者需要注意风格是有争议的,其中的“良好风格”或“不良风格”并非公认。
既然是主观内容,那容许鄙人主观评论一下
> 2.8
> 很多……数据成员
> “Biarne Stroustrup ”拼写错误……效率。
无评论。
> 第3章 命名规则
> 这章也基本是主观内容。
好吧,同意,我想说的是,原文全文都是主观内容。
> 3.1
> 规则3-1-1
> 标识符应当直观且可以拼读,可望文知意,不必进行“解码”。
> //这点和此文建议使用的匈牙利命名法有一定程度的矛盾。
原文叙述的不是微软的那种匈牙利命名法(作者也不赞成,例如“‘匈牙利’法最大的缺点是烦琐”、“会让绝大多数程序员无法忍受”),作者他也说了“据考察,没有一种命名规则可以让所有的程序员赞同……而应当制定一种令大多数项目成员满意的命名规则,并在项目中贯彻实施”
> 规则3-1-3
> 命名规则尽量与所采用的操作系统或开发工具的风格保持一致。
> //尽管大部分人应该乐于支持这个观点,不过事实上有时候无法实现。例如同时使用标准库和Windows API 风格的代码。这时倒不妨直接约定允许根据上下文选择要使用的命名风格。要点是,应该让人看出某个名称是用哪个风格命名的,而不至于一眼就混淆来源。
赞同
> 3.2
> 规则3-2-7
> 为了防止某一软件库中的一些标识符和其它软件库中的冲突,可以为各种标识符加上能反映软件性质的前缀。例如三维图形标准OpenGL的所有库函数均以gl开头,所有常量(或宏定义)均以GL开头。
> //在 C++ 中应该考虑是否可以用命名空间代替前缀。
赞同。难道又是因为(**)?
> 第4章 表达式和基本语句
> 我真的发觉很多程序员用隐含错误的方式写表达式和基本语句,我自己也犯过类似的错误。
> //作者似乎没搞清楚“错误”一词的语义。
“隐含错误”一词原文表述不清,意思大概是“容易造成费解和引入Bug”。
> 4.3
> 规则4-3-4
> 不要写成
> if (p== 0) // 容易让人误解p是整型变量
> if (p!= 0)
> //事实上,C++中的NULL典型地就是 int 字面量 0 (考虑到成文时间,不提新标准的空指针类型),和 int 兼容。以 Bjarne Stroustrup 的观点,这样恰恰会使人误以为 NULL 不是整数,因此推荐用 0 而不是 NULL 。
链接失效……
> 4.3.5
> 或者改写成更加简练的return (condition ? x : y);
> //这里的括号是多余的。
个人风格问题
> 4.4
> 这节不是语言本身而是涉及语言实现的内容。以现在的观点来看,优化器会可能会在此做一些工作。当然了解一些相关原理大体上还是有益的。
无评论。
> 第5章 常量
> 常量是一种标识符,它的值在运行期间恒定不变。C语言用
#define来定义常量(称为宏常量)。C++ 语言除了 #
define外还可以用const来定义常量(称为const常量)。
> //谭XX风格的信口开河。这段引文中逗号或者句号之间的内容,没一个能算得上是
正确的

参见(*)
> 5.2
> 规则5-2-1
> 在C++程序中只使用 const常量而不使用宏常量,即const常量完全取代宏常量。
> //这是有问题的。事实上很多情况下 const 只能让编译器被修饰的对象当做只读变量,而非编译期的真正意义的常量进行处理。与 #define 的符号常量(字面量)相比,只读变量受到了一些限制,例如不能作 case 的标号。
完全?大概真的不可能。改成“尽量”也许好一点?
> 第6章 函数设计
> C语言中,函数的参数和返回值的传递方式有两种:值传递(pass by value)和指针传递(pass by pointer)。
> //错误。形式上, C 语言函数参数只按值传递。所谓的指针传递是按值传递的一种,只是传递参数的类型是指针而已。
C只有按值传递。刻意区分这两者的目的,作者说了是为了帮助新手区分指针和引用。
> 6.1
> 规则6-1-1 关于参数省略名称
主观内容,不过,个人认为养成不省略名称的习惯,通常要比习惯省略名称要好,绝大部分函数的参数名称都是有意义的。
> 6.2
> 规则6-2-3
> 不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return语句返回。
> //在C++中可以不使用此规则而使用异常(在 C 中理论上也可以类似地使用 setjmp/longjmp ,但容易造成语义不明确,实际上基本不用)。
《陷阱》补充一下基本完善了
> 6.3
本节是《陷阱》一文中仅有的几处值得讨论的地方。
> 规则6-3-1
> 在函数体的“入口处”,对参数的有效性进行检查。
> //在函数接口语义明确的情况下并非是必需的。例如 C 标准库 中以及 POSIX 标准中的许多函数。
1. “并非是必须的”不是万能否定句。得说明为什么有它不会带来好处,去掉它也不会带来害处
2. 根据下文,这里的“有效性检查”就是指使用断言assert。当然,要使用得正确。和没有用相比,对程序正确性的好处是显而易见的。对于某些设计(例如契约式设计),断言是必不可少的用于检查前置条件的手段,而且也不会损失效率(release)
> 规则6-3-2
> 在函数体的“出口处”,对return语句的正确性和效率进行检查。
> //同样不是必需的。此外检查可能损失效率。
看看下文说的“检查”,明明就是自己复查下代码,对于return语句留个心眼,防止写出有问题或者效率低下的代码出来。根本不是“同样”的“检查”。
当然,下面几条注意事项是不是有道理就是另一回事了。
> 要搞清楚返回的究竟是“值”、“指针”还是“引用”。
> //注意返回对象的语义,但是刻意区分“值”和“指针”是不必要的,严格上是错误的——它们根本就不是可以比较的一类概念。
前面已经说过。
> 建议6-4-2
> 函数体的规模要小,尽量控制在50行代码之内。
> //尽管为数不多,有些特殊情况,如编译器的某些分析程序,是明显的反例。
人家都说了“尽量”啦,何况只是建议。
> 第7章 内存管理
> “640Kought to be enough for everybody
> —Bill Gates 1981”
> //和本章主题无关。
和本文主题无关
> 7.1
> 内存分配方式有三种
> //有误。内存分配具体方式由实现决定,语言只限制存储类。
参见(**) ,说的是VC 6吧。
> 7.2
> 漏了重复释放内存的错误(这通常会引起程序崩溃)。
根据实现,可能会引起安全问题
顺便说下,原文只字未提安全问题。 例如缓冲区溢出什么的,最起码应该提醒一下strcat这种函数用起来要特别小心吧。
> 规则7-2-1
> 用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。
> //对 C++ 而言是错误的。 ISO C++ 关于内存分配失败的默认行为是抛出 std::bad_alloc 异常。如果要使分配失败不抛出异常,使用 nothrow 版本,或者设置实现相关的编译选项。
参见(**) ,VC 6惹的祸
> 规则7-2-5
> 用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
> //不一定必要。例如指针是自动变量,在退出所在的块作用域被自动释放时。
确实不一定必要。
后文基本同意,没有新评论就不列了,只列举出有评论的内容。
> 10.2
> 规则10-2-1
> 若在逻辑上A是B的“一部分”(a partof),则不允许B从A派生,而是要用A和其它东西组合出B。
> //错误。尽管一般组合能够胜任这项工作,但并非绝对。可以使用private继承完成相同的任务,并且提供覆盖成员函数的特性。这是组合无法完成的。
首先要区分接口继承和实现继承。
从OO的设计角度考虑,如果按照设计原则是组合的,应当尽量使用组合。因为私有继承是实现继承,而非接口,违反了针对接口编程的设计原则。
组合当然也能做到继承做不到或者麻烦的,比如运行时动态改变组合的对象(指针)。
当然,如果是作为一种技巧来实现一些其他方法很难实现的东西,那各种继承还是很有用处的, 不需要刻意不用。
最后扯一句“头文件中#ifndef
#define #
endif”吧,这是原文最莫名其妙的地方之一了。大概作者希望我们能心有灵犀,意识到他就是指头文件开头和结尾的那个include guard吧……
2013年04月10日 22点04分 1
level 13
神马情况 @幻の上帝 ...
2013年04月10日 22点04分 2
level 15
本文提到的“原文”同样是指《高质量C++/C编程指南》。
原文链接已经修正。
(*)“以实际经验出发”并不和内容有误矛盾。即便实际经验是符合事实的,对于不够有经验的读者还是有相当的危害。
(**)文章中指出的错误大多是和公认的常识相悖,即便标准能够提供更精确的解释。即便没有标准,错误也不应该出现。
1.“作者的本意是想说明实际操作中通常的组织代码方式。”
↑虽然比较合理的可能的原意能够被有经验的用户通过下文猜出来,但是这个表述无疑是错的。即便不管标准定义,“程序”的概念在C++中是清晰的。声称一个程序通常组织为“两个文件”,是显然的误导。
1.2.1
这里需要补充,实际是否应该使用这种风格取决于具体需要。
类模版因为分离定义导致代码过于复杂所以往往不适用,但如果能保证可维护性也不是不能用(如libstdc++拆分的.tcc)。
而除了模版外inline函数也适用。
甚至有实用的不用inline或非模版函数的header-only库(如CImg——虽然我内部依赖组织得不怎么样,嘛,题外话了)。
3.1
命名原则应该足够清晰。
需要指出,只强调“直观可以拼读”往往不是最佳实践。当一个“直观”标识符长度过长时就不能使用:
(1)实现不支持的情况。在ANSI C89只规定较少的标识符长度支持。在C++中这种情况的影响较少。
(2)大量出现,过长的标识符影响阅读时。一般拟定通用的缩写,在文档(至少注释)说明。
3.2
没记错的话namespace加入C++是在90年代前期提出的,2001年的主流编译器支持应该不成问题(VC6也支持)。这不是一个很容易实现错而需要避免使用的特性。
考虑到即便是很久以后不少用户对namespace的使用也有因为各种原因有意无意的回避反而导致放弃使代码更清晰潜力(例如box2d和旧版FLTK),在这里补充是有必要的。
4.3
已修正。
链接是BS的个人主页。失效似乎是最近几个月的事情。
5
由于历史原因,“常量”的用法比较混乱,具体含义往往需要通过上下文推测。这里对标题的解释不充分显著地增强了误导的可能性。
这里是非常基础的内容,不应该有放任明显混淆这样的低级错误。
“完全”当然不可能。是否“尽量”,也要看需求。例如有时需要用于条件编译的编译时确定的整数值,const在这里无能为力。
6.
没找到在哪里说“帮助新手区分指针和引用”。出处?
帮助新手区分指针和引用完全不必要也不应该使用这种有问题的说法。
6.3.1
(1)的确需要补充。
(2)原文没有指出就是assert。
从契约式设计上来说,用assert是一种常见的实现,但使用在运行时抛出异常等其它手段有时也是可接受的手法。
而C++11补充的static_assert(以及之前的模拟)也算,尽管适用范围受限。(题外话,assert就没法constexpr了,这点大概算是C++11的缺陷。)
6.3.2
这点确实是错误。已补充修正。
不过需要另外指出,“临时变量”的说法是有问题的。C++中的变量是指声明引入的对象或引用,临时变量中的“变量”很多时候无法作为这个含义。从下文来看,是“临时对象”之误。
这样,就有一个硬伤。表达式中使用模拟类类型的对象初始化得到的一个非类类型右值,不是临时对象。
return int(x + y); // 注意:不创建临时对象。
至于后面“已经说过”的问题,重点是刻意区分“值”的意义。这又是一个混乱的概念,尽管严格意义上相对单一。不应该在此处引入增加误导可能。(我很怀疑原作者是否有意识到这点。)
7.
和本文无关,不过提一下,根据wikiquote,这句不是Bill Gates说的。
7.1
这点是基础概念问题,和具体实现无关。C语言也需要讨论类似问题,虽然说法不同。
10.2
从OOD的角度来看是这样没有错,但注意文章强调的是编码而不是设计上的质量,说的是OOP。
从OOD到OOP的映射过程不是单一简单的,毕竟使用的不是建模语言。尤其是对C++来说,不少语义和OOD所强调的抽象不相吻合,也不能方便地实现。
简化OOD到OOP的过程或许正是Java之流体现价值的重点之一。然而,这样的选择是以牺牲可能实现的灵活性为代价的。而这种灵活性正是OOP用户欠缺与需要重视之处。
这样的使用可以说是“一种技巧”,但类似地也有其它一些特性,如class-scope using。C++为什么不抛弃这些看起来不符合OO方法学的特性?我认为主要并不是兼容性的问题,而是在设计上鼓励语言的用户有权利选择自由灵活的表达方式。
就这里的问题而言,private继承的含义是“用……(基类)实现”,即便OOP角度上不是典型的组合,同样可以是OOD中组合的映射结果。
2013年04月11日 01点04分 3
level 1
可以帮忙解一下题么
2019年04月12日 07点04分 4
1