C++编译器无法捕捉到的8种错误

来源:互联网 发布:avi封装软件 编辑:程序博客网 时间:2024/06/14 15:58

这是篇转载文章,出处:点击打开链接,翻译自点击打开链接 ---- 伯乐在线 — 陈舸

    一般情况下,现在的编译器可以帮助程序员寻找到解决编译错误的方法并修正。但是还是存在这些BUG难以跟踪和调试,这些都会导致严重的后果,比如不正确的输出、数据破坏、以及程序崩溃。以下的示例都在VS2005 Express上测试过,(作者同时在vs2010上进行了测试)根据自己选择的编译器,你得到的结果可能会有所不同。我强烈建议所有的程序员朋友都采用最高等级的告警级别!有一些编译提示在默认告警级别下可能不会被标注为一个潜在的问题,而在最高等级的告警级别下就会被捕捉到!


1)变量未初始化

    变量初始化是C++编程中最为常见和易犯的错误之一。在C++中,为变量所分配的内存空间并不干净,其结果就是一个未初始化的变量包含某个值,但没办法准确知道这个值是多少:
m_nValue从未初始化过。结果就是,GetValue()返回的是一个垃圾值,if语句的两个分支都有可能会执行。在VS2010下,程序编译通过,输出的是else分支里面的内容。
    当你在调试器下运行程序时,定义的变量通常都被清零处理过了。这意味着你的程序在调试器下可能每次都是工作正常的,但在发布版中可能会间歇性的崩掉!如果你碰上了这种怪事,罪魁祸首常常都是未初始化的变量。

2)整数除法

C++中的大多数二元操作都要求两个操作数是同一类型。如果操作数的不同类型,其中一个操作数会提升到和另一个操作数相匹配的类型。很多新手会尝试写出如下代码:
这里的本意是nX/nY将产生一个浮点型的除法操作,因为结果是赋给一个浮点变量。但实际上并非如此。nX/nY首先被计算,结果是一个整型值,然后才会提升为浮点型并赋值给fValue。但赋值之前小数点已经丢弃。所以这里必须对右边的表达式进行类型转换。

3)= vs ==

这是个老问题,但很有价值。许多C++新手会弄混赋值符号和相等操作符号的意义。
比如,if (nValue = 0)实际上就成了 if (nValue)。结果就是if条件为假,执行else下的代码。

4)混用有符号和无符号数

之前提到,如果操作数是不同类型,其中一个操作数将提升自己的类型以及匹配另一个操作数。当混用有符号和无符号数时则会导致出现一些非预期性的结果!考虑如下的例子:
有人或说是-5。由于10是个有符号整数,而15是无符号整数,类型提升规则在这里就会起作用了。
这又是另一个把戏,10u-15u = -5u? 但是无符号变量不包括负数,因此-5在这里会被补码。

5)delete vs delete[]

许多C++程序员忘记了关于new和delete操作符实际上有两种形式:针对单个对象的版本,以及针对对象数组的版本。new操作符用来在堆上分配单个对象的内存空间。如果对象时个类类型,该对象的构造函数将被调用。
delete操作符用来回收由new操作符分配的内存空间。如果被销毁的对象时类类型,则该对象的析构函数将被调用。
这行代码为10个Foo对象的数组分配了内存空间,因为下标[10]放在了类型名之后,许多C++程序员没有意识到实际上是操作符new[]被调用来完成分配空间的任务而不是new。new[]操作符确保每一个创建的对象都会调用该类的构造函数一次。相反的,要删除一个数组,需要使用delete[]操作符。

6)复合表达式或函数调用的副作用

副作用是指一个操作符、表达式、语句或函数在该操作符、表达式、语句或函数完成规定的操作后仍然继续做了某些事情。
有副作用的C++操作符包括*=、/=、%=、+=、-=、<<=、>>=、&=、|=、^=以及声名狼藉的++和—操作符。但是,在C++中有好几个地方操作的顺序是未定义的,那么这就会造成不一致的行为。比如:
因为对于函数multiply()的参数的计算顺序是未定义的,因此上面的程序可能打印出30或36(在vs2010里输出的是36,这说明编译器执行的是有操作数先计算),这完全取决于x和++x谁先计算,谁后计算。
再看下一个复合表达式:
程序员的本意可能是说:“如果x是1,且y的前自增值是2的话,完成某些处理”。但是,如果x不等于1,C++将采取短路求值法则,这意味着++y将永远不会计算!因此,只有当x等于1时,y才会自增。这很可能不是程序员的本意!一个好的经验法则是把任何可能造成副作用的操作符都放到它们自己独立的语句中去。

7)不带break的switch语句

当switch表达式计算出的结果同case的标签值相同时,执行序列将从满足的第一个case语句处执行。执行序列将继续下去,直到要么到达switch语句块的末尾,或者遇到return、goto或break语句。其他的标签都将忽略掉!

8)在构造函数中调用虚函数

考虑如下程序:

在这个程序中,程序员在基类的构造函数中调用了虚函数,期望它能被决议为派生类的Derived::ClassID()。但实际上不会这样——程序的结果是打印出1而不是2。当从基类继承的派生类被实例化时,基类对象先于派生类对象被构造出来。这么做是因为派生类的成员可能会对已经初始化过的基类成员有依赖关系。结果就是当基类的构造函数被执行时,此时派生类对象根本就还没有构造出来!所以,此时任何对虚函数的调用都只会决议为基类的成员函数,而不是派生类。

根据这个例子,当cDerived的基类部分被构造时,其派生类的那一部分还不存在。因此,对函数ClassID的调用将决议为Base::ClassID()(不是Derived::ClassID()),这个函数将m_nID设为1。一旦cDerived的派生类部分也构造好时,在cDerived这个对象上,任何对ClassID()的调用都将如预期的那样决议为Derived::ClassID()。

注意到其他的编程语言如C#和Java会将虚函数调用决议为继承层次最深的那个class上,就算派生类还没有被初始化也是这样!C++的做法与这不同,这是为了程序员的安全而考虑的。这并不是说一种方式就一定好过另一种,这里仅仅是为了表示不同的编程语言在同一问题上可能有不同的表现行为。



0 0