返回   cpper编程论坛 > Blog > bankrock
注册账号 论坛帮助 会员列表 日历事件 搜索 今日新帖 标记版面已读

为这篇文章评分

It's Not a Bug, It's a Language Feature!

发表于 2008-07-28 01:47 AM 作者: bankrock
标题这句话是维护编程语言尊严的常用语,但有时候Language Feature比Bug还要可怕,今天鄙人就被C/C++的一个Language Feature和devc++耍了一把。
首先来看看这段代码:
C++ 代码:
  1. struct SearchItem
  2. {
  3.     SearchItem(int impedanceSegId, int parentImpedSegId, int startingLoadId)
  4.         : impedSegId(impedanceSegId), parImpedSegId(parentImpedSegId), startLoadId(startingLoadId)
  5.     {  }
  6.  
  7.     int impedSegId, parImpedSegId, startLoadId;
  8. };
  9.  
  10. void GetAccImpedAndMtrxStruct(....)
  11. {
  12.     deque<SearchItem> searchQueue;
  13.      searchQueue.push_back(SearchItem(tree.rootImpedSegId, -1, 0))//1
  14.  
  15.     while (!searchQueue.empty())
  16.     {
  17.         SearchItem& item = searchQueue.front();
  18.         int impedSegId = item.impedSegId;
  19.         int parImpedSegId = item.parImpedSegId;
  20.         int startLoadId = item.startLoadId;   //2
  21.  ...........
横竖看起来都是人畜无害的一段代码,SearchItem是含有三个int变量的类,searchQueue是存储SearchItem元素的搜索队列,//1处push的元素在while语句中会被取出,其三个变量必然是
C++ 代码:
  1. item.impedSegId == tree.rootImpedSegId;
  2. item.parImpedSegId == -1;
  3. item.startLoadId == 0;
可是这里却出现了存储错误,检查的结果是startLoadId值为-1163005939!deque的push_back函数里肯定发生了什么问题,才导致startLoadId从0变成了-1163005939。Debug进入push_back的定义,如下:
C++ 代码:
  1. void
  2.       push_back(const value_type& __x)
  3.       {
  4.     if (this->_M_impl._M_finish._M_cur != this->_M_impl._M_finish._M_last - 1)
  5.       {
  6.         std::_Construct(this->_M_impl._M_finish._M_cur, __x);
  7.         ++this->_M_impl._M_finish._M_cur;
  8.       }
  9.     else
  10.       _M_push_back_aux(__x);
  11.       }
由于deque的默认构造函数分配了一些存储空间,因此_M_cur还没到末尾,if判断为真,程序执行到++this->_M_impl._M_finish._M_cur;时出现了意想不到的情况。这里为了了解++this->_M_impl._M_finish._M_cur;做了什么,将相关的定义罗列如下:
C++ 代码:
  1. template<typename _Tp, typename _Alloc = allocator<_Tp> >
  2.     class deque : protected _Deque_base<_Tp, _Alloc>
  3.  
  4.   template<typename _Tp, typename _Alloc>
  5.     class _Deque_base
  6.     {
  7.        ....
  8. typedef _Deque_iterator<_Tp,_Tp&,_Tp*>             iterator;
  9.       struct _Deque_impl
  10.     : public _Alloc {
  11.      .........
  12.     iterator _M_finish;
  13.       ..........
  14.       };
  15.        _Deque_impl _M_impl;
  16.     };
  17.  
  18.   template<typename _Tp, typename _Ref, typename _Ptr>
  19.     struct _Deque_iterator
  20.     {
  21.     .........
  22.       _Tp* _M_cur;
  23.     .........
  24.      }
可以看出deque<SearchItem>中的this->_M_impl._M_finish._M_cur的类型是SearchItem*,由于SearchItem是12字节的,因此想必++this->_M_impl._M_finish._M_cur会将指针移动12个字节。实际结果却是:
引用:
Before++: this->_M_impl._M_finish._M_cur = 0x700f38
After++: this->_M_impl._M_finish._M_cur = 0x700f40
只增加了8字节!在程序里将SearchItem的大小输出看看,发现确实是12字节:
C++ 代码:
  1. cerr << sizeof(SearchItem) //结果是12
  2.  
编译器居然口是心非!想了半天突然觉察到还有另外一个文件中的算法也定义了相同名字的类,找到其定义如下:
C++ 代码:
  1. struct SearchItem
  2. {
  3.     SearchItem (int searchNodeUid, int parentBranchId)
  4.         : nodeUid(searchNodeUid), parBranchId(parentBranchId)
  5.     {}
  6.  
  7.     int nodeUid;
  8.     int parBranchId;
  9. };
这个是8字节的结构,正好和++操作的结果相符。到这里才恍然大悟,C++的编译模式规定一个类如果不特别指明,它的名字作用域默认是全局的,如果在多个文件中定义了该类,则必须保证类定义完全相同。Devc++居然对两个类定义不做任何检查就用上了,编译器执行指针操作时按照8字节的SearchItem工作,导致startLoadId根本没有被存储到deque中,sizeof操作符却用了12字节的SearchItem!解决的方法很简单,将两个SearchItem加入无名名字空间,冲突就解决了。
Expert C Progamming中将C语言默认的函数名字全局可见这一Language Feature归类为Sin of Commission,也就是C语言不应该做的却做了的事情。C++为了保持兼容性,变本加厉的加入了数量巨大的其他全局外联类型。因此对于确定只用在某个编译单元中的函数,类,类成员函数和产生全局名字的无论其他什么东西,最好用static或者无名名字空间让它们安静的躺在自己的角落里,免得碰上devc++(或者说gcc)这种哑巴编译器搅浑水。
评论 5 Email文章
评论总数 5

评论

旧
polyrandom 的头像
我觉得这个的确挺讨厌的,但是也情有可原。我遇到过更惨烈的。我有一个类,里面有一个成员是用宏来开关的:
代码:
class SomeClass
{
    // ...
#ifdef SOMETHING
    int var_;
#endif //SOMETHING
    // ...
};
有时候我们在多个项目中使用这个头文件,但是每个项目的预定义设置不同。这样一来,同一个类在不同的lib里面可能有不同的layout。一旦出错,调试起来虽然很快,但是最后发现是这种问题会很窝囊的。
发表于 2008-07-30 12:21 AM 作者: polyrandom polyrandom 当前离线
旧
bankrock 的头像
您老这种情况看起来发生的可能性更大,不知道有什么检查的技巧没
发表于 2008-08-04 06:15 PM 作者: bankrock bankrock 当前离线
旧
polyrandom 的头像
出了问题就调试呗。
发表于 2008-08-04 06:23 PM 作者: polyrandom polyrandom 当前离线
旧
还有一个GCC编译器出名的问题

比如:
函数申明如下
std:tring doSomting(someArgs);
定义如下:
std:tring doSomting(someArgs) {
std:tring result;
....
//!!!not return result
}

编译竟然很正常,连警告都没有。

std:tring userName = doSomting(...);

用到userName的时候,就会coredump了。
发表于 2008-10-18 08:41 PM 作者: fixopen fixopen 当前离线
旧
bankrock 的头像
俺也被这个bug折磨过。可能gcc觉得判断函数返回点的覆盖范围太困难了,干脆就不检查了;也有可能是标准将这种行为定义为undefined,这样什么事情都有可能发生(gcc选择的就是什么都不做)。
发表于 2008-10-21 06:58 PM 作者: bankrock bankrock 当前离线
发表评论 发表评论
作者为 bankrock 的最新文章

所有时间均为格林尼治时间 +9。现在的时间是 07:34 PM


Powered by vBulletin® 版本 3.7.0
版权所有 ©2000 - 2009,Jelsoft Enterprises Ltd.
(C) Copy Right All Right Reserved 2001 - 2007

Search Engine Friendly URLs by vBSEO 3.1.0