C++编译器无法捕捉到的8种错误
作者:zacklin 发布时间:[ 2016/10/25 10:21:11 ] 推荐标签:测试开发技术 C++
6.复合表达式或函数调用的副作用
副作用是指一个操作符、表达式、语句或函数在该操作符、表达式、语句或函数完成规定的操作后仍然继续做了某些事情。副作用有时候是有用的:
1x=5;
赋值操作符的副作用是可以地改变x的值。其他有副作用的C++操作符包括*=、/=、%=、+=、-=、<<=、>& gt;=、&=、|=、^=以及声名狼藉的++和—操作符。但是,在C++中有好几个地方操作的顺序是未定义的,那么这会造成不一致的行为。比如:
voidmultiply(intx,inty) { usingnamespacestd; cout<<x*y<<endl; } intmain() { intx=5; std::cout<<multiply(x,++x); }
因为对于函数multiply()的参数的计算顺序是未定义的,因此上面的程序可能打印出30或36,这完全取决于x和++x谁先计算,谁后计算。
另一个稍显奇怪的有关操作符的例子:
intfoo(intx) { returnx; } intmain() { intx=5; std::cout<<foo(x)*foo(++x); }
因为C++的操作符中,其操作数的计算顺序是未定义的(对于大多数操作符来说是这样的,当然有一些例外),上面的例子也可能会打印出30或36,这取决于究竟是左操作数先计算还是右操作数先计算。
另外,考虑如下的复合表达式:
if(x==1&&++y==2) //dosomething
程序员的本意可能是说:“如果x是1,且y的前自增值是2的话,完成某些处理”。但是,如果x不等于1,C++将采取短路求值法则,这意味着++y 将永远不会计算!因此,只有当x等于1时,y才会自增。这很可能不是程序员的本意!一个好的经验法则是把任何可能造成副作用的操作符都放到它们自己独立的语句中去。
7. 不带break的switch语句
另一个新手程序员常犯的经典错误是忘记在switch语句块中加上break:
switch(nValue) { case1:eColor=Color::BLUE; case2:eColor=Color::PURPLE; case3:eColor=Color::GREEN; default:eColor=Color::RED; }
当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!
正确的方法是按照如下方式书写:
switch(nValue) { case1:eColor=Color::BLUE;break; case2:eColor=Color::PURPLE;break; case3:eColor=Color::GREEN;break; default:eColor=Color::RED;break; }
break语句终止了case语句的执行,因此eColor的值将保持为程序员所期望的那样。尽管这是非常基础的switch/case逻辑,但很容易因为漏掉一个break语句而造成不可避免的“瀑布式”执行流。
8. 在构造函数中调用虚函数
考虑如下的程序:
classBase { private: intm_nID; public: Base() { m_nID=ClassID(); } //ClassID返回一个class相关的ID号 virtualintClassID(){return1;} intGetID(){returnm_nID;} }; classDerived:publicBase { public: Derived() { } virtualintClassID(){return2;} }; intmain() { DerivedcDerived; cout<<cDerived.GetID();//打印出1,不是2! return0; }
在这个程序中,程序员在基类的构造函数中调用了虚函数,期望它能被决议为派生类的Derived::ClassID()。但实际上不会这样——程序的结果是打印出1而不是2。当从基类继承的派生类被实例化时,基类对象先于派生类对象被构造出来。这么做是因为派生类的成员可能会对已经初始化过的基类成员有依赖关系。结果是当基类的构造函数被执行时,此时派生类对象根本还没有构造出来!所以,此时任何对虚函数的调用都只会决议为基类的成员函数,而不是派生类。
根据这个例子,当cDerived的基类部分被构造时,其派生类的那一部分还不存在。因此,对函数ClassID的调用将决议为 Base::ClassID()(不是Derived::ClassID()),这个函数将m_nID设为1。一旦cDerived的派生类部分也构造好时,在cDerived这个对象上,任何对ClassID()的调用都将如预期的那样决议为Derived::ClassID()。
注意到其他的编程语言如C#和Java会将虚函数调用决议为继承层次深的那个class上,算派生类还没有被初始化也是这样!C++的做法与这不同,这是为了程序员的安全而考虑的。这并不是说一种方式一定好过另一种,这里仅仅是为了表示不同的编程语言在同一问题上可能有不同的表现行为。
结论
因为这只是这个系列文章的第一篇,我认为以新手程序员可能遇到的基础问题入手会比较合适。今后这个系列的文章将致力于解决更加复杂的编程错误。无论一个程序员的经验水平如何,错误都是不可避免的,不管是因为知识上的匮乏、输入错误或者只是一般的粗心大意。意识到其中有可能造成麻烦的问题,这可以帮助减少它们出来捣乱的可能性。虽然对于经验和知识并没有什么替代品,良好的单元测试可以帮我们在将这些bug深埋于我们的代码中之前将它们捕获。
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系SPASVO小编(021-61079698-8054),我们将立即处理,马上删除。
相关推荐
更新发布
功能测试和接口测试的区别
2023/3/23 14:23:39如何写好测试用例文档
2023/3/22 16:17:39常用的选择回归测试的方式有哪些?
2022/6/14 16:14:27测试流程中需要重点把关几个过程?
2021/10/18 15:37:44性能测试的七种方法
2021/9/17 15:19:29全链路压测优化思路
2021/9/14 15:42:25性能测试流程浅谈
2021/5/28 17:25:47常见的APP性能测试指标
2021/5/8 17:01:11热门文章
常见的移动App Bug??崩溃的测试用例设计如何用Jmeter做压力测试QC使用说明APP压力测试入门教程移动app测试中的主要问题jenkins+testng+ant+webdriver持续集成测试使用JMeter进行HTTP负载测试Selenium 2.0 WebDriver 使用指南