C++是一种复杂的编程语言,其中充满了各种微妙的陷阱。在C++中几乎有数不清的方式能把事情搞砸。幸运的是,如今的编译器已经足够智能化了,能够检测出相当多的这类编程陷阱并通过编译错误或编译警告来通知程序员。终,如果处理得当的话,任何编译器能检查到的错误都不会是什么大问题,因为它们在编译时会被捕捉到,并在程序真正运行前得到解决。坏的情况下,一个编译器能够捕获到的错误只会造成程序员一些时间上的损失,因为他们会寻找解决编译错误的方法并修正。
  那些编译器无法捕获到的错误才是危险的。这类错误不太容易察觉到,但可能会导致严重的后果,比如不正确的输出、数据被破坏以及程序崩溃。随着项目的膨胀,代码逻辑的复杂度以及众多的执行路径会掩盖住这些bug,导致这些bug只是间歇性的出现,因此使得这类bug难以跟踪和调试。尽管本文的这份列表对于有经验的程序员来说大部分都只是回顾,但这类bug产生的后果往往根据项目的规模和商业性质有不同程度的增强效果。
  这些示例全部都在Visual Studio 2005 Express上测试过,使用的是默认告警级别。根据你选择的编译器,你得到的结果可能会有所不同。我强烈建议所有的程序员朋友都采用高等级的告警级别!有一些编译提示在默认告警级别下可能不会被标注为一个潜在的问题,而在高等级的告警级别下会被捕捉到!
  1.变量未初始化
  变量未初始化是C++编程中为常见和易犯的错误之一。在C++中,为变量所分配的内存空间并不是完全“干净的”,也不会在分配空间时自动做清零处理。其结果是,一个未初始化的变量将包含某个值,但没办法准确地知道这个值是多少。此外,每次执行这个程序的时候,该变量的值可能都会发生改变。这有可能产生间歇性发作的问题,是特别难以追踪的。看看如下的代码片段:
  if(bValue) //doA else //doB
  如果bValue是未经初始化的变量,那么if语句的判断结果无法确定,两个分支都可能会执行。在一般情况下,编译器会对未初始化的变量给予提示。下面的代码片段在大多数编译器上都会引发一个警告信息。
  intfoo() { intnX; returnnX; }
  但是,还有一些简单的例子则不会产生警告:
  voidincrement(int&nValue) { ++nValue; } intfoo() { intnX; increment(nX); returnnX; }
  以上的代码片段可能不会产生一个警告,因为编译器一般不会去跟踪查看函数increment()到底有没有对nValue赋值。
  未初始化变量更常出现于类中,成员的初始化一般是通过构造函数的实现来完成的。
  classFoo { private: intm_nValue; public: Foo(); intGetValue(){returnm_bValue;} };  Foo::Foo() { //Oops,我们忘记初始化m_nValue了 }  intmain() { FoocFoo; if(cFoo.GetValue()>0) //dosomething else //dosomethingelse }
  注意,m_nValue从未初始化过。结果是,GetValue()返回的是一个垃圾值,if语句的两个分支都有可能会执行。
  新手程序员通常在定义多个变量时会犯下面这种错误:
  intnValue1,nValue2=5;
  这里的本意是nValue1和nValue2都被初始化为5,但实际上只有nValue2被初始化了,nValue1从未被初始化过。
  由于未初始化的变量可能是任何值,因此会导致程序每次执行时呈现出不同的行为,由未初始化变量而引发的问题是很难找到问题根源的。某次执行时,程序可能工作正常,下一次再执行时,它可能会崩溃,而再下一次则可能产生错误的输出。当你在调试器下运行程序时,定义的变量通常都被清零处理过了。这意味着你的程序在调试器下可能每次都是工作正常的,但在发布版中可能会间歇性的崩掉!如果你碰上了这种怪事,罪魁祸首常常都是未初始化的变量。
  2.整数除法
  C++中的大多数二元操作都要求两个操作数是同一类型。如果操作数的不同类型,其中一个操作数会提升到和另一个操作数相匹配的类型。在C++中,除法操作符可以被看做是2个不同的操作:其中一个操作于整数之上,另一个是操作于浮点数之上。如果操作数是浮点数类型,除法操作将返回一个浮点数的值:
  floatfX=7; floatfY=2; floatfValue=fX/fY;//fValue=3.5
  如果操作数是整数类型,除法操作将丢弃任何小数部分,并只返回整数部分。
  intnX=7; intnY=2; intnValue=nX/nY;//nValue=3
  如果一个操作数是整型,另一个操作数是浮点型,则整型会提升为浮点型:
  floatfX=7.0; intnY=2; floatfValue=fX/nY;  //nY提升为浮点型,除法操作将返回浮点型值 //fValue=3.5
  有很多新手程序员会尝试写下如下的代码:
  intnX=7; intnY=2; floatfValue=nX/nY;//fValue=3(不是3.5哦!)
  这里的本意是nX/nY将产生一个浮点型的除法操作,因为结果是赋给一个浮点型变量的。但实际上并非如此。nX/nY首先被计算,结果是一个整型值,然后才会提升为浮点型并赋值给fValue。但在赋值之前,小数部分已经丢弃了。
  要强制两个整数采用浮点型除法,其中一个操作数需要类型转换为浮点数:
  intnX=7; intnY=2; floatfValue=static_cast<float>(nX)/nY;//fValue=3.5
  因为nX显式的转换为float型,nY将隐式地提升为float型,因此除法操作符将执行浮点型除法,得到的结果是3.5。
  通常一眼看去很难说一个除法操作符究竟是执行整数除法还是浮点型除法:
  z=x/y;//这是整数除法还是浮点型除法?
  但采用匈牙利命名法可以帮助我们消除这种疑惑,并阻止错误的发生:
  intnZ=nX/nY;//整数除法 doubledZ=dX/dY;//浮点型除法
  有关整数除法的另一个有趣的事情是,当一个操作数是负数时C++标准并未规定如何截断结果。造成的结果是,编译器可以自由地选择向上截断或者向下截断!比如,-5/2可以既可以计算为-3也可以计算为-2,这和编译器是向下取整还是向0取整有关。大多数现代的编译器是向0取整的。
  3.= vs ==
  这是个老问题,但很有价值。许多C++新手会弄混赋值操作符(=)和相等操作符(==)的意义。但即使是知道这两种操作符差别的程序员也会犯下键盘敲击错误,这可能会导致结果是非预期的。
  //如果nValue是0,返回1,否则返回nValue intfoo(intnValue) { if(nValue=0)//这是个键盘敲击错误! return1; else returnnValue; }  intmain() { std::cout<<foo(0)<<std::endl; std::cout<<foo(1)<<std::endl; std::cout<<foo(2)<<std::endl;  return0; }
  函数foo()的本意是如果nValue是0,返回1,否则返回nValue的值。但由于无意中使用赋值操作符代替了相等操作符,程序将产生非预期性的结果:
  0 0 0
  当foo()中的if语句执行时,nValue被赋值为0。if (nValue = 0)实际上成了if (nValue)。结果是if条件为假,导致执行else下的代码,返回nValue的值,而这个值刚好是赋值给nValue的0!因此这个函数将永远返回0。
  在编译器中将告警级别设置为高,当发现条件语句中使用了赋值操作符时会给出一个警告信息,或者在条件判断之外,应该使用赋值操作符的地方误用成了相等性测试,此时会提示该语句没有做任何事情。只要你使用了较高的告警级别,这个问题本质上都是可修复的。也有一些程序员喜欢采用一种技巧来避免=和== 的混淆。即,在条件判断中将常量写在左边,此时如果误把==写成=的话,将引发一个编译错误,因为常量不能被赋值。
  4.混用有符号和无符号数
  如同我们在整数除法那一节中提到的,C++中大多数的二元操作符需要两端的操作数是同一种类型。如果操作数是不同的类型,其中一个操作数将提升自己的类型以匹配另一个操作数。当混用有符号和无符号数时这会导致出现一些非预期性的结果!考虑如下的例子:
  cout<<10–15u;//15u是无符号整数
  有人会说结果是-5。由于10是一个有符号整数,而15是无符号整数,类型提升规则在这里需要起作用了。C++中的类型提升层次结构看起来是这样的:
  longdouble(高) double float unsignedlongint longint unsignedint int(低)
  因为int类型比unsigned int要低,因此int要提升为unsigned int。幸运的是,10已经是个正整数了,因此类型提升并没有使解释这个值的方式发生改变。因此,上面的代码相当于:
  cout<<10u–15u;
  好,现在是该看看这个小把戏的时候了。因为都是无符号整型,因此操作的结果也应该是一个无符号整型的变量!10u-15u = -5u。但是无符号变量不包括负数,因此-5这里将被解释为4,294,967,291(假设是32位整数)。因此,上面的代码将打印出 4,294,967,291而不是-5。
  这种情况可以有更令人迷惑的形式:
  intnX; unsignedintnY; if(nX–nY<0) //dosomething
  由于类型转换,这个if语句将永远判断为假,这显然不是程序员的原始意图!
  C++编译器无法捕捉到的8种错误
  5.delete vs delete []
  许多C++程序员忘记了关于new和delete操作符实际上有两种形式:针对单个对象的版本,以及针对对象数组的版本。new操作符用来在堆上分配单个对象的内存空间。如果对象是某个类类型,该对象的构造函数将被调用。
  Foo*pScalar=newFoo;
  delete操作符用来回收由new操作符分配的内存空间。如果被销毁的对象是类类型,则该对象的析构函数将被调用。
  deletepScalar;
  现在考虑如下的代码片段:
  Foo*pArray=newFoo[10];
  这行代码为10个Foo对象的数组分配了内存空间,因为下标[10]放在了类型名之后,许多C++程序员没有意识到实际上是操作符new[]被调用来完成分配空间的任务而不是new。new[]操作符确保每一个创建的对象都会调用该类的构造函数一次。相反的,要删除一个数组,需要使用 delete[]操作符:
  delete[]pArray;
  这将确保数组中的每个对象都会调用该类的析构函数。如果delete操作符作用于一个数组会发生什么?数组中仅仅只有第一个对象会被析构,因此会导致堆空间被破坏!