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

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

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

  if (x == 1 && ++y == 2)    // do something

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

7)不带breakswitch语句

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

  switch (nValue)   {    case 1: eColor = Color::BLUE;    case 2: eColor = Color::PURPLE;    case 3: 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)   {    case 1: eColor = Color::BLUE; break;    case 2: eColor = Color::PURPLE; break;    case 3: eColor = Color::GREEN; break;    default: eColor = Color::RED; break;   }

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

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

考虑如下的程序:

  class Base   {   private:    int m_nID;   public:    Base ()    {    m_nID = ClassID ();    }    // ClassID 返回一个 class 相关的 ID 号    virtual int ClassID () { return 1;}    int GetID () { return m_nID; }   };   class Derived: public Base   {   public:    Derived ()    {    }    virtual int ClassID () { return 2;}   };   int main ()   {    Derived cDerived;    cout << cDerived.GetID (); // 打印出1,不是2!    return 0;   }

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

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

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

结论

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:http://127.0.0.1/wywygy.html