【转贴】对《高质量程序设计指南--C++/C第二版》的探讨

来源:互联网 发布:苹果app软件降级 编辑:程序博客网 时间:2024/06/07 20:07

《高质量程序设计指南——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页有类似说明)。用地址来初始化一个字符,该字符得到的是随机的值。但愿是笔误。

作者fang_rk,【绝对原稿】
出处:
http://community.csdn.net/Expert/topic/3365/3365921.xml?temp=.5677759
http://community.csdn.net/Expert/topic/3368/3368437.xml?temp=.6728479
----------------------------------------------------------------------
本文适合于《高质量》的读者以及C++初学者【其实并不适合初学者】。
   写这篇文章是出于一个偶尔的原因:读计算机系的女友即将升入研究生三年级,她说想要看看这本书。她是个C/C++门外汉,看此书只是为了应付找工作时可能被问到的题目。我觉得有必要指出这本书中的错误,但显然她没有心思坐下来与我讨论书中的大部分内容。况且她只需要看书中关于测试题的附录以应付考试;况且她认为这本书被广泛流传,成为不少IT企业招聘员工题目的来源,质量应该很好;况且《高质量》作者的不凡经历让我这个普通高校的本科生相形见绌,在这点上她宁愿相信硕士博士也不大会相信我。
我粗略翻了一下,找出30个左右错误,不管此时你是否相信。为了给自己增加筹码,我在文章中引用广为人知的权威书籍中的部分内容,有时候会提供一些代码作实际测试。学习任何编程语言都应该选用符合标准的编译器,VC6对标准C++支持的不是很好,有条件的读者可以选用.NET、C++Builder或者DEV-C++等。
我不是专家,只是一名2.5年的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[])
就算《高质量》中写了四个常用的不算错,也有义务强调一下标准,告诉读者应该避免什么(因为很大一部分是C++初学者,没有鉴别能力);事实上,《高质量》中的void main(void)出现了不下20次,显得非常随意。任何完整的程序都会用到这一点,严重程度:中。

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页有类似说明)。用地址来初始化一个字符,该字符得到的是随机的值。但愿是作者笔误,不过注释中也这么写好象又说不过去。严重程度:中。

P177.    关于指向成员函数的指针,为了与静态成员函数区别,取virtual函数和普通成员函数的地址必须使用”&”运算符。
不知道从哪里得到的结论:取virtual和普通成员函数地址必须”&”,我也没有找到证明这一说法的根据,只能当场测试了:
#include <iostream>
#include <string>
using namespace std;
struct Test
{
Test(int i){value=i;}
int Value()const {return value;}
void Value(int i) {value=i;}
virtual void do_something() const {cout<<"do something in Test/n";}
private: int value;
};
int main()
{
    typedef int (Test::*P1)() const;
    typedef void (Test::*P2)() const;
    P1 p1 = Test::Value;//Borland需要&符号,MS不需要
    P2 p2 = Test::do_something;//BCB和.NET都不需要&取址
    Test T1(123);
    cout<<(T1.*p1)()<<'/t';
    (T1.*p2)();
}
显然两个主流编译器都不同意《高质量》作者的说法,问题严重性不是很大,严重程度:轻。

P193.    提示9-9:不要对枚举变量使用++、--、+=、-=等操作,除非你为它重载了这些运算符。
《C++ Primer》第三版P92:对于枚举类型第二件不能做的事情是,我们不能使用枚举成员进行迭代,如for(open_mode iter=input; iter!=append;++iter);C++不支持在枚举成员之间的前后移动
倒是很想看看如何自定义++/--运算符,但对于两种截然不同的说法,我只能站在Lippman一边。严重程度:中。

P218.    ADT/UDT版式的主要两种形式:(1)将private限定的成员写在前面,public限定的成员写在后面,“以数据为中心”。(2)将public限定的成员写在前面,将private限定的成员写在后面,“以行为为中心”。估计很多C++教科书受到C++之父第一本著作《The C++ Programming Language》的影响,不知不觉采用“以数据为中心”的方式来编写类,这样做不见得有多少道理。
“不见得有多少道理”吗?我可以告诉你一些原因,《深度探索C++对象模型》P88:
//某个foo.h头文件,从某处含入
extern float x;
//程序员的Point3d.h文件
class Point3d{
public:
…    //问题是:被传回的和被设定的x是哪一个x呢?
        float X() const {return x;}
        void X(float new_x) {x=new_x;}//原书中此函数为const函数,恐为笔误

private:    float x,y,z;
};
在C++最早的编译器上,如果在Point3d::X()的两个函数实例中队x进行取用操作,这操作将会指向global x object!这种绑定结果几乎普遍的不在大家的预期之中,并因此导出早期C++的两种防御性程序设计风格:1.把所有的data members放在class声明开头,以保证正确的绑定……
可见,“以数据为中心”有其历史原因。危害性不大,严重程度:轻。

P261.    拷贝构造函数的参数必须是同类对象的引用,而不能是值对象。如果允许定义值传递的拷贝构造函数,就会与引用传递的构造函数产生二义性。参见14-5:
class A{
public:    A(const A copy){…}    //(1)
            A(const A& other){…}    //(2)
};
A a;
A b=a;    //这到底应该调用(1)还是调用(2)?无法决断!
又是一个严重错误。表达式(1):A(const A copy){…}的参数是值拷贝,值拷贝就意味着拷贝构造。在拷贝构造函数中调用拷贝构造函数,是一个无限循环递归调用自己的过程,当然被标记为错误!按照《高质量》作者的说法,如果不提供(2),那么(1)就会表现得很好,作者为什么不自己试试呢?严重程度:重。

P262—265    类String.的设计
class String
{
public:
        String(const char* str=NULL);
        String(const String& copy);
~String();
String& operator=(const String& assign);
private:
        size_t    m_size;
        char        *m_data;
};
//String的析构函数
String::~String(void){
delete [] m_data;    //由于m_data是内部数据类型,也可以写成delete m_data;
m_data=NULL;    //释放m_data内存之后应当立即置为NULL,这是良好的习惯
}
//赋值函数
String& String::operator=(const String& other){
        if(this != &other){
            delete [] m_data;            //释放原有的内存资源
int len=strlen(other.m_data);    //分配新的内存资源,并复制内容
m_data=new char[len+1];
strcpy(m_data,other.m_data);
m_size=len;
}
return *this;
}
让我们看看《Effective C++》P23页条款5中的举例:
string *stringPtr1 = new string;
string *stringPtr2 = new string[100];
delete stringPtr1;    //删除单一对象
delete [] stringPtr2;    //删除整个对象数组
如果你在stringPtr1前加了"[]"会怎样呢?答案是:那将是不可预测的;如果你没在stringPtr2前没加上"[]"又会怎样呢?答案也是:不可预测。而且对于象int这样的固定类型来说,结果也是不可预测的,即使这样的类型没有析构函数。所以,解决这类问题的规则很简单:如果你调用new时用了[],调用delete时也要用[]。如果调用new时没有用[],那调用delete时也不要用[]。
“内部数据类型的delete [] p;可以写为delete p;”这一说法在《高质量》好几个地方出现,不可饶恕。对于析构中的m_data=NULL;是没有必要的,因为对象即将销毁。赋值函数基本正确,但是没有考虑异常安全性:当delete [] m_data;释放原有的内存资源后万一分配新的内存资源(m_data=new char[len+1];)失败,就导致了String内部状态不一致,造成了两次delete [] m_data;最小的修改方法是先分配新资源到一个局部变量,成功后再释放旧的资源接手新的。一个更常用的手法是给出一个不会抛出异常的swap的成员函数用于交换(比如在这个例子中只要交换m_size和m_data),在赋值函数中先用拷贝构造函数产生一个局部变量,然后和本身(*this)交换。详细可参考《Exceptional C++》P50。异常并不是只和throw/try/catch关联着。严重程度:重。

P275.    由于数字常量本身没有类型,它直接用作参数时将自动进行类型转换(称为隐式类型转换)。示列15-2(为省略篇幅稍有修改):
#include <iostream>
void output(int x){std::cout<<”output int “<<x<<’/n’;}
void output(float x){std::cout<<”output float “<<x<<’/n’;}
void main(){
        output(0.5);    //错误!不明确的调用,因为自动类型转换
}
数字常量本身是有类型的,比如9的类型是int,0.5的类型是double。涉及到函数重载,需要一个比较复杂的匹配过程。《C++ Primer》第三版P381页开始写的很清楚。简单的说是按照精确匹配/类型提升/类型转换/用户自定义转换的步骤进行的。其中类型提升中是有优先级别的(比如某个enum类型可以提升到int,也能提升到long,那么优先考虑提升到int),类型转换中没有优先级别(比如long可以转换到char,也能转换到int,两者级别一样,如果出现了这样的情况,那么编译出错告诉你模棱两可)。认为“数字常量本身没有类型,直接用作于参数将自动进行类型转换”纯属无稽之谈。之所以调用output(0.5)会出错就是属于类型转换:0.5是double类型,double可以转换到int,也能转换到float,但是两个转换一样的好(或者说一样的不好),因此导致编译无法通过。可以在上述基础上增加void output(double x){std::cout<<"output double "<<x<<'/n';}来看看是否还是不明确的调用。严重程度:重。

P291.    介绍类型转换函数的时候:
class MyString{
public:    MyString(size_t size,char c=’/0’);
//…
private:    char *m_data;
};
构造函数无意中起了类型转换的作用:把一个size_t类型的整数转换为MyString对象。尤其是当定义这样的函数的时候:void f(MyString str);如下的调用f(100);编译器会暗中转换为f(MyString(100));这极容易与程序中的参数为int、long、float等的函数f()混淆并产生二义性。
还是重载问题,如果存在f(int),那么f(100)就会调用f(int),这属于精确匹配,至于f(100)调用f(MyString)则是用户自定义转换。最前面有f(int)精确匹配,其次有f(long)类型提升,后面还有f(float)类型转换,哪里轮的上你的f(MyString)?根本不会产生二义性。严重程度:中。
#include <iostream>
using namespace std;
class MyString{
public:    MyString(size_t size,char c='/0')
{cout<<"call MyString ctor.size="<<size<<'/n';}
};
void f(int x){cout<<"f(int) "<<x<<'/n';}
void f(long x){cout<<"f(long) "<<x<<'/n';}
void f(float x){cout<<"f(float) "<<x<<'/n';}
void f(MyString){cout<<"f(MyString)/n";}
int main()
{
    f(100);
}

P293.    介绍const成员函数的时候说,任何不会修改数据成员的函数都应该声明为const类型。
介绍const成员函数的时候没有介绍mutable显然有失公允和全面。《C++ Primer》第三版P520:为了允许修改一个类的数据成员,即使它是一个const对象的数据成员,我们可以把该数据成员声明为mutable。mutable数据成员总可以被更新,即使是在一个const成员函数中。严重程度:轻。

P307.    介绍异常:因此,如果你真的不得不从析构函数内抛出异常的话,你应该首先检查一下看当前是否有一个未捕获的异常正要被处理;如果没有,说明该析构函数的调用并非有一个外部异常引起,而是正常的销毁;于是你可以抛出一个异常让程序来捕获,否则不要抛出任何异常。
涉及到比较高深的主题了,并非三言两语说的清楚,参见《More Exceptional C++》P126页:未捕获的异常。

P326.    
#include <set>
void main(){
        using std::set::iterator;
        int a[100]={10};
std::set<int> iSet(a,a+99);
        //…
iterator iter=iSet.find(25);
}
用.NET编译该程序,error C2955: “std::set” : 使用类模板需要模板参数列表;把using std::set::iterator;修改为using std::set<int>::iterator;即可。该页两次出现了一样的错误,不可谓笔误。严重程度:中。

P335.    提示17-2:类模板的成员函数的定义必须放在头文件。
《C++ Primer》第三版P697:在分离编译模式下,类模板定义和其inline成员函数都被放在头文件中,而非inline成员函数和静态数据成员函数被放在程序文本文件中……
鉴于分离模式尚没有被广泛支持,严重程度:轻。

P370.    简化版本auto_ptr
template<typename T>
class auto_ptr{
public:
        explicit auto_ptr(T* p):m_ptr(p){}
auto_ptr(const auto_ptr<T>& copy):m_ptr(copy.m_ptr){}

~auto_ptr(){delete m_ptr;}
private:T* m_ptr;
};
虽然是一个简化版本,但很遗憾,还是写错了:auto_ptr<int> a1(new int(1));auto_ptr<int>a2(a1);这样a1和a2的析构函数对同一个指针进行两次delete。也谈不上对auto_ptr的常见错误使用给出提醒了。标准auto_ptr的实现可以参考《C++标准程序库》P38页。如此明显的错误却视而不见,严重程度:重。

P382.    提示19-3:显然,对顺序容器中的元素进行排序是徒劳的(除非有特殊需要),它的概念决定了排序对查找(定位)其中任一元素的平均效率没有任何贡献,永远是O(N),应为你只能顺序访问它。
对顺序容器排序没有意义的话,对什么排序有意义呢?vector是不是顺序容器?对vector排序没有任何意义吗?对排序后的vector只能进行顺序查找吗?严重程度:中。

P384.    建议19-1:如果你在创建一个容器时能够预先估计出它将可能存放的最大元素数目,那么你就可以给它预先分配足够数量的存储空间,……
并非每个容器都能预先分配存储空间,比如list和deque就没有reserve成员函数。是否能够调用reserve函数在编译的时候即可知道,严重程度:轻。

P385.    只有符合下述要求的对象才能够作为STL容器的元素:(1)可默认构造和拷贝构造。
P387.    对于关联式容器,在定位元素的时候要调用元素类型的operator==运算符。
“可默认构造”并不是作为STL容器元素的必需条件,关联式容器也没有使用operator==来定位元素。《Exceptional C++》第2章节引导你如何设计一个stack容器,最初的设计中把“可默认构造”作为元素的一个重要条件,随着设计的演化最终把这个条件去掉了(P57页)。有些类是没有缺省构造的,或者说缺省构造出来的元素是没有意义的。至于关联式容器的排序法则默认采用operator<,判断两个元素是否等价采用if(!(x<y) && !(y<x)),根本不是operator==。P401介绍STL的“拆半”查找使用了operator==也是错误的,STL没有采用这种比较。文章末尾有一段程序(稍微长一点,放在此处打破了连贯性)反驳了这个“想当然”的说法,读者也可以自己封装一个跟踪类来看看关联式容器的操作进行了哪些比较。《泛型编程与STL》有详细的论述。科技工作者应该求真务实,而不是信口开河。严重程度:中。

P393.    示例19-4
void main(){
        //…
        vector<int>::const_iterator p=ages.begin();
        for(int i=0;i<10;++i)    ages.push_back(5);        //每次插入都会引起内存重分配
        //…
}
如果vector每次push_back都会引起内存重新分配的话,你还敢使用吗?一旦需要重新分配内存,vector会预留一部分可用空间以避免频繁的重新分配/复制。严重程度:中。

P399.    提示19-18:在使用带有输出迭代器参数的泛型算法(如merge,copy等)时,一定要给输出容器(目标容器)分配足够的空闲空间,否则操作结果不可预测。示例19-10
void main(){
        list<int> li;    vector<int> vi;
        for(int c=0;c<10;c++) li.push_back( c );
        vi.reserve(li.size());                    //预留空间
        copy(li.begin(),li.end(),vi.begin());    
}
预留空间只是避免重新分配内存,并不会使得容器中的元素数量发生变化。上面的例子中vi.reserve(li.size());无法挽救运行结果错误的命运(可以在程序末尾添加copy(vi.begin(),vi.end(),ostream_iterator<int>(cout,"/t")); 来查看结果)。挽救方法一是把vi.reserve(li.size());修改为vi.resize(li.size());二是采用copy(li.begin(),li.end(),back_inserter(vi));严重程度:中。

P410.    STL容器总结:vector内部数据结构:数组,程序员可以使用reserve()成员函数来管理内存。deque内部数据结构是数组。
C++标准没有规定某某容器采用什么数据结构,而是提出了统一的接口以及必须达到的性能。《泛型编程与STL》一书中说vector采用连续存储区,但连续存储区不等同于数组。“使用reserve()成员函数来管理内存”显然很不严谨,reserve只能预留空间,算不上管理内存,真正的内存管理由allocator负责。deque的数据结构也不是简简单单的数组,可以参考《STL源码剖析》。严重程度:轻。
附:用于说明禁用默认构造,还是可以使用于STL,禁用operator==还是可以用于关联式容器,也能用于拆半查找
#include <iostream>
#include <algorithm>
#include <set>
#include <iterator>
#include <vector>
#include <cstdlib>
#include <ctime>
using namespace std;
template<class T>
class MyTestType
{
public:
    explicit MyTestType(const T& t):value(t){}
    MyTestType(const MyTestType& rhs):value(rhs.value){}
    MyTestType& operator=(const MyTestType& rhs){value=rhs.value;return *this;}
    bool operator<(const MyTestType& rhs) const{return value<rhs.value;}
    const T& Value() const{return value;}
private:
    T value;
    MyTestType();//禁用缺省构造函数
    bool operator==(const MyTestType& rhs) const;//禁用operator==
};

int main()
{
    typedef MyTestType<int> T;
    vector<T> V1;
    typedef vector<T>::const_iterator VTCI;
    typedef set<T>::const_iterator STCI;
    srand((unsigned) time(0));
    for(int i=0;i<20;++i) V1.push_back(T(rand()%15));//还是可以使用于STL
    cout<<"V1:/n";
    for(VTCI begin_iter=V1.begin(),end_iter=V1.end();begin_iter!=end_iter;++begin_iter)
        cout<<begin_iter->Value()<<'/t';
    set<T> S1(V1.begin(),V1.end());//还是可以用于关联式容器
    cout<<"/nS1:/n";
    for(STCI begin_iter=S1.begin(),end_iter=S1.end();begin_iter!=end_iter;++begin_iter)
        cout<<begin_iter->Value()<<'/t';
    cout<<'/n';
    sort(V1.begin(),V1.end());
    cout<<"V1 after sort:/n";
    for(VTCI begin_iter=V1.begin(),end_iter=V1.end();begin_iter!=end_iter;++begin_iter)
        cout<<begin_iter->Value()<<'/t';
    //也能用于拆半查找
    vector<T>::iterator LB= lower_bound(V1.begin(),V1.end(),T(10));
    if(LB!=V1.end())
cout<<"Find "<< LB->Value() <<" first time at index "<< LB-V1.begin()<<'/n';
}

原创粉丝点击