几何尺寸与公差论坛

 找回密码
 注册
查看: 1492|回复: 0

【转帖】对《高质量程序设计指南——c++/c(第二版)》的探讨

[复制链接]
发表于 2008-4-7 11:02:57 | 显示全部楼层 |阅读模式
[转帖]对《高质量程序设计指南——C++/C(第二版)》的探讨


引子:
  最近,C/C++值班室里的一位兄弟fang_rk给我来了一封信,信里有一封附件,是他对林锐、韩勇泉两位合著的《高质量程序设计指南--C++/C第二版》一书的一些看法,受他的委托,我把先把他的信件群发给值班室的部分兄弟们讨论,然后于今天转发到坛子里,请坛子里的兄弟们一起审核。共同探讨c/c++的一些细微之处。
请兄弟们在回复之前注意以下几点:
1、本此探讨为纯技术性的探讨,不欢迎对本书或其作者的评论。
2、请自觉保证探讨的完整性,请发出自己经过思考后的回复,为杜绝“up、收藏、学习、接分”之类的回复,本贴为0分贴,如有类似回复,将提请斑竹删除!
3、由于原文太长,我按原文内容分别在c、c++、stl各版发出,增强讨论的针对性。
4、再次重申,本文章为fang_rk兄弟原创,我只是替他搞搞宣传,拉场子而已:-),所有赞誉和或者BS均归他所有,与我无关,呵呵
5、已经收到第一稿的兄弟们请注意,这次转贴的是最新的,如有改动,以本次为准。

下面是fang_rk兄弟的大作,请大家一起审核,呵呵。



《高质量程序设计指南——C++/C(第二版)》的读书心得
        作者:fang_rk
  写这篇文章是出于一个偶尔的原因:读计算机系的女友即将升入研究生三年级,她说想要看看这本书。她是个C/C++门外汉,看此书只是为了应付找工作时可能被问到的题目。我觉得有必要指出这本书中的错误,但显然她没有心思坐下来与我讨论书中的大部分内容,况且我的学历只有本科,人微言轻啊(学历真是太重要了,呵呵)。我只能把自己的读书心得记录下来。
  我在文章中引用广为人知的权威书籍中(都是简体中文版)的部分内容,有时候会提供一些代码作实际测试。学习任何编程语言都应该选用符合标准的编译器,VC6对标准C++支持的不是很好,有条件的读者可以选用.NET、C++Builder或者DEV-C++等。
  本文只针对《高质量》这本书的内容,所谈论的仅仅是技术问题。曾经和书的两位作者联系过,他们都很忙,万不得已不要去打扰他们了。第二作者韩先生可能会抽空整理一份勘误表,虽然觉得到来的比较晚,不过总比没有好,勇气可嘉。
  或许有些人会觉得描述的都是牛角尖的内容,不过每个人的水平都不一样,观点态度也各有不同,仁者见仁,智者见智吧!

P83. main()可以返回任何类型的值,包括void,常见的原型如下:
void main(void);
int main(void);
int main(int argc,char* argv[]);
int main(int argc,char* argv[],char* envp[]);
评注:
  《C++标准程序库》P21页:2.2.9main()的定义式:根据C++标准规格,只有两种main()是可移植的:int main(){…}和int main(int argc,char* argv[]){…}
《Exceptional C++》P81页:void main()是非标准的,因此也是不可移植的。是的,我知道这出现在一些书中,一些作者甚至争辩说”void main()”是符合标准的。不,从来不是,甚至在20世纪70年,在最早的标准C之前都不符合标准。……最好是养成习惯,使用main的如下两个标准的和可移植的声明之一:
  int main()和int main(int argc,char* argv[])
《高质量》有义务强调一下标准,告诉读者应该避免什么。

P86.声明就是在向系统介绍名字(而一个名字就是一块内存区的别名)……名字的类型有两个用途:……二是教导编译器如何解释它所代表的内存区(大小)……
评注:
  《C++ Primer》第三版P508页:我们也可以声明一个类但是不定义它。例如:
      class Screen;//Screen类的声明
  这个声明向程序引入了一个名字Screen,指示Screen为一个类类型。但是我们只能以有限的方式使用已经被声明但还没有被定义的类类型。如果没有定义类,那么我们就不能定义该类类型的对象,因为类类型的大小不知道,编译器不知道为这种类类型的对象预留多少存储空间。
我们很常见的就是前置声明,不必写出整个类的定义。

P87.示例5-4最后 x&&y;//逻辑表达式是可执行语句,独立使用时被编译器忽略。
评注:
  x&&y是逻辑表达式,如果x为假,那么不再判断y;如果x为真,必须判断y。按照《高质量》的说法,下列的程序不会显示任何内容:
#include <iostream>
using namespace std;
int a(int i){cout<<"call a\n";return i&1;}
int b(int i){cout<<"call b\n";return i%10==0;}
int main()
{
int i;
cin>>i;
a(i) && b(i);
}
“独立使用的时候可能被编译器忽略”也比“独立使用时被编译器忽略”说的恰当。

P91.void指针可以作为通用指针,因为它可以指向任何类型的对象。
评注:
《More Exceptional C++》P205:尽管一个void*的大小足以保存任何对象指针的值,但它不一定适合保存一个函数指针,在某些平台上,一个函数指针比一个对象指针要大。
我对“任何类型”的理解是也包含了函数,觉得这种说法不严格。

P103.C++/C中不存在if/elseif/elseif/…/else的结构,如:
if(…){…}
else if(…){…}
else if(…){…}
else{…}
C++/C把这种结构转换为switch结构。参见P106的switch结构。
评注:
  C++/C不支持if/else if/else if/…/else的结构——第一次看到有人这么说。事实上if/else判断和switch各有优势:switch中的case只能是可转换成整数类型的常量表达式,对于非整数类型(比如字符串类型)、非常量表达式(与变量比较)以及非等价判断(比如大于某个数值)不能采用switch结构。

P104.根据布尔类型(bool)的语义,0为“假”(记为FALSE),任何非0值都是“真”(记为TRUE)。TRUE的值究竟是什么并没有统一的标准。例如Visual C++将TRUE定义为1,而Visual Basic则将TRUE定义为-1。所以不要将布尔变量flag直接与TRUE或者1、0进行比较比较。下列if语句都属于不良风格:if(flag!=TRUE) if(flag==TRUE)……
评注:
  标准C++中的布尔值只有两个:true和false(均为小写字母),而不是TRUE和FALSE(可能是某个编译器定义的宏)。不要把某个编译器和某种语言混淆在一起,比如有些人还认为VC++就是C++。C++中的bool和VB有什么关系呢?

P105.假设有两个浮点变量x和y,精度定义为EPSILON=1e-6,……正确的比较方式:if(abs(x-y)<=EPSILON),同理x与零值比较的正确方式为:if(abs(x)<=EPSILON)……
评注:
  把精度EPSILON定义为1e-6是否有恰当?《C++标准程序库》P61页表4.2例举了class numeric_limits<>的所有成员,其中epsilon()的意思是“1和最接近1的值之间的差距”。因此我认为获取精度的最恰当的方法是:
#include <iostream>
#include <limits>
using namespace std;
int main()
{
cout<<numeric_limits<float>::epsilon()<<'\t'<<numeric_limits<double>::epsilon();
}
在Windows2000中用BCB6和.NET2003的结果均为:1.19209e-07和2.22045e-16。至于和0比较的方法,我的看法是:不要去和0比较,实在需要,可以这么写if(!(0<x)&& !(x<0)){…}至于书中P299页的16-1举例中写了:double a,double; if(b==0)……,呵呵

P108.循环结构基本上可以分为确定循环、不确定循环和无限循环,他们分别又可以叫做计数器控制的循环、标志控制的循环和死循环。
评注:
  第一次看到把三种循环分的这么细,也第一次听说这些新名词,看了头晕。个人感觉这样分类过于死板,对初学者没好处。

P123.你可以取一个const符号常量的地址:对于基本数据类型的const常量,编译器会重新在内存中创建它的一个拷贝,你通过其地址访问到的就是这个拷贝而非原始符号常量;而对于构造类型的const常量,它成了编译时不允许修改的变量,因此如果你能绕过编译器的静态类型安全检查机制,就可以在运行时修改其内存单元。示例6-2:
const long lng=10;
long* pl=(long*)&lng;//取常量的地址
*pl=1000;//“迂回修改”
cout<<*pl<<endl;//1000,修改拷贝!
cout<<lng<<endl;//10,原始常量并没有变!
评注:
  《C++ Primer》第三版P84页:……这并不意味着我们不能间接地指向一个const对象,只意味着我们必须声明一个指向常量的指针来做这件事。例如: const double *pc=0;const double minWage=9.60;pc=&minWage;
一方面,对T类型的常量取得地址应该使用const T*而不是T*:
#include <iostream>
using namespace std;
int main()
{
const long lng=10;
const long* PL=&lng;
cout<<(void*)&lng<<'\t'<<(void*)PL;
}
另一方面所谓的“迂回修改”的结果,比较恰当的说法是“未定义行为”,就像《C++程序设计语言》特别版中说的:具体实现很可能为了保护它的值不被破坏而使用某种特殊形式的存储,无法保证在所有的实现上都给出同样的可预见的结果。《高质量》的这个举例似乎在暗示读者:通过“迂回修改”不会有什么麻烦,而且总能修改成功。个人感觉这是一个比较严重的错误。

P145.用register修饰的变量会被直接加载到CPU寄存器中,如果寄存器足以容纳得下它的话。
评注:
  《C++ Primer》第三版P336:在函数中频繁被使用的自动变量可以用register声明。如果可能的话,编译器会把该对象装载到机器的寄存器中。……关键字register对编译器来说只是一个建议,有些编译器可能忽略该建议,而是使用寄存器分配算法找出最合适的候选放到机器可用的寄存器中。
很明显,register只是请求,是否采纳可能采用很复杂的算法,而不是简简单单 “寄存器足以容纳的下”。虽然不是很严重,不过似乎不大严谨。

P151.递归函数为什么能够进行下去?(3)函数堆栈是自动增长的,只要内存足够,它就会按需增长,直到耗尽内存为止。
评注:
  我找不到什么内容来反驳这个观点,但是递归函数的递归深度只和内存容量有关吗?和编译器无关吗?和编译器的选项设置无关吗?和操作平台无关吗?……哪位知道的话说一下。

P168.提示8-7:虽然数组自己知道它有多少个元素……
P170.C++/C为什么要把数组传递改写为指针传递呢?第二个原因:出于性能考虑,如果把整个数组中的元素全部传递进去,不仅需要大量的时间来拷贝数组……
评注:
  《C++ Primer》第三版P20页:而且,数组类型本身没有自我意识,他不知道自己的长度。……在C++中,数组不同于整数类型和浮点数类型。它不是C++语言的一等公民。数组是从C语言中继承过来的,它反映了数据与对其进行操作的算法的分离,而这正是过程化设计的特征。《C++程序设计语言》特别版P85:数组不具有自描述性,因为并不保证与数组一起保存着这个数组的元素个数。

P173.提示8-16:当你使用字符指针来引用一个字符变量的时候,千万要当心,因为C++/C默认char*表示字符串。例如:char ch=”a”;//用“a”来初始化字符变量ch
评注:
  “a”的含义是依次包含’a’,’\0’的两个连续内存单元的首地址(《C陷阱与缺陷》P12页有类似说明)。用地址来初始化一个字符,该字符得到的是随机的值。但愿是笔误。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

QQ|Archiver|小黑屋|几何尺寸与公差论坛

GMT+8, 2024-5-9 14:30 , Processed in 0.036958 second(s), 19 queries .

Powered by Discuz! X3.4 Licensed

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表