设为首页 加入收藏

TOP

C++编译器无法捕捉到的8种错误(二)
2014-11-24 13:29:47 来源: 作者: 【 】 浏览:5
Tags:编译器 无法 捕捉 错误
的版本,以及针对对象数组的版本。new 操作符用来在堆上分配单个对象的内存空间。如果对象是某个类类型,该对象的构造函数将被调用。


delete 操作符用来回收由 new 操作符分配的内存空间。如果被销毁的对象是类类型,则该对象的析构函数将被调用。


现在考虑如下的代码片段:


这行代码为 10 个 Foo 对象的数组分配了内存空间,因为下标[10]放在了类型名之后,许多 C++ 程序员没有意识到实际上是操作符 new[]被调用来完成分配空间的任务而不是 new。new[]操作符确保每一个创建的对象都会调用该类的构造函数一次。相反的,要删除一个数组,需要使用 delete[]操作符:


这将确保数组中的每个对象都会调用该类的析构函数。如果 delete 操作符作用于一个数组会发生什么?数组中仅仅只有第一个对象会被析构,因此会导致堆空间被破坏!


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


副作用是指一个操作符、表达式、语句或函数在该操作符、表达式、语句或函数完成规定的操作后仍然继续做了某些事情。副作用有时候是有用的:


赋值操作符的副作用是可以永久地改变x的值。其他有副作用的 C++ 操作符包括*=、/=、%=、+=、-=、<<=、>>=、&=、=、^=以及声名狼藉的++和—操作符。但 是,在 C++ 中有好几个地方操作的顺序是未定义的,那么这就会造成不一致的行为。比如:


因为对于函数 multiply ()的参数的计算顺序是未定义的,因此上面的程序可能打印出 30 或 36,这完全取决于x和++x谁先计算,谁后计算。


另一个稍显奇怪的有关操作符的例子:


因为 C++ 的操作符中,其操作数的计算顺序是未定义的(对于大多数操作符来说是这样的,当然有一些例外),上面的例子也可能会打印出 30 或 36,这取决于究竟是左操作数先计算还是右操作数先计算。


另外,考虑如下的复合表达式:


程序员的本意可能是说:“如果x是1,且y的前自增值是 2 的话,完成某些处理”。但是,如果x不等于1,C++将采取短路求值法则,这意味着++y将永远不会计算!因此,只有当x等于 1 时,y才会自增。这很可能不是程序员的本意!一个好的经验法则是把任何可能造成副作用的操作符都放到它们自己独立的语句中去。


7)不带breakswitch语句


另一个新手程序员常犯的经典错误是忘记在 switch 语句块中加上 break:


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


考虑下如上的代码,如果 nValue 为 1 时会发生什么。case 1 满足,所以 eColor 被设为 Color::BLUE。继续处理下一个语句,这又将 eColor 设为 Color::PURPLE。下一个语句又将它设为了 Color::GREEN。最终,在 default 中将其设为了 Color::RED。实际上,不管 nValue 的值是多少,上述代码片段都将把 eColor 设为 Color::RED!


正确的方法是按照如下方式书写:


break 语句终止了 case 语句的执行,因此 eColor 的值将保持为程序员所期望的那样。尽管这是非常基础的 switch/case 逻辑,但很容易因为漏掉一个 break 语句而造成不可避免的“瀑布式”执行流。


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


考虑如下的程序:


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


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


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


结论


因为这只是这个系列文章的第一篇,我认为以新手程序员可能遇到的基础问题入手会比较合适。今后这个系列的文章将致力于解决更加复杂的编程错误。 无论一个程序员的经验水平如何,错误都是不可避免的,不管是因为知识上的匮乏、输入错误或者只是一般的粗心大意。意识到其中最有可能造成麻烦的问题,这可 以帮助减少它们出来捣乱的可能性。虽然对于经验和知识并没有什么替代品,良好的单元测试可以帮我们在将这些 bug 深埋于我们的代码中之前将它们捕获。


首页 上一页 1 2 下一页 尾页 2/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇使用GDB调试JNI代码 下一篇Linux USB subsystem --- USBFS d..

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容:

·哈希表 - 菜鸟教程 (2025-12-24 20:18:55)
·MySQL存储引擎InnoDB (2025-12-24 20:18:53)
·索引堆及其优化 - 菜 (2025-12-24 20:18:50)
·Shell 中各种括号的 (2025-12-24 19:50:39)
·Shell 变量 - 菜鸟教 (2025-12-24 19:50:37)