C++阴暗面

近来一篇<The Dark Side Of C++>在坊间广为转载,作为一个以C++为吃饭家伙的程序员,还是应该下载下来好好读一读的。 总的来讲还是总结的蛮全的,由于个人知识的限制,我读完后将其分为三类:一类是我不以为然的,觉得算不上阴暗面;一类是深有同感,深受其害;而另外一类则是还不理解,需要日后有时间的时候加以研究的。

一、不以为然

  • 不断变更的标准,迫使我们需要不断更新已有代码。
    作者列出了几点其实影响并不是很大(循环变量的scope;头文件后缀;名字空间)。而且,为了标准的进步,偶尔做出的妥协也是应该的吧。
  • 不断变更的style,作者举得例子是:
    Old and busted:
    for (int i = 0; i < n; i++)
    New hotness:
    for (int i(0); i != n; ++i)
      有这个问题吗?有人用第二种方式的吗? 
  • auto_ptr很烂。
    这个,你不用它就是了,而且最近在智能指针加入C++0x大家庭后,应该达成共识了吧
  • iterator可能会失效。
    这个我感觉还好,没遇到过太多问题;
  • iterator对container毫不知情。
    我觉得这是STL设计的一个优点吧,通过iterator解耦算法与容器。
  • vector::at会做边界检查,而operator []不会。
    很好啊,提供两种选择
  • 构造函数与析构函数中的虚函数调用,可能会调用基类的虚函数,甚至是纯虚函数。
    这个不怎么阴暗吧,不要在构造函数与析构函数总调用虚函数应该是个常识,而且,其他语言难道没有这个问题?

二、深有同感

  • 模板中排山倒海式的编译错误,在繁琐无比的错误后面,原因可能仅仅是用错了iterator类型。那个被毙掉的C++ 0x proposal不知是否可以解决此问题。
  • 用C++写的代码不太容易读,函数重载,操作符重载,虚函数重定义,类型重定义,宏定义等等,把代码的真实面貌严严实实的藏在了身后。(当然,这也是为了抽象与一致性),几个例子:
    string a("blah"); // 定义一个string对象
    string a(); //声明一个函数

    a
    && b // 如果&&没被重定义,是短路计算;但若是被重载了,那么可能两个都要计算

    typedef OtherType
    & Type;
    Type a
    = b;
    a.value
    = 23; // 不看到那个typedef,鬼知道b的值会不会被改掉

     另外, baz = foo->bar(3);如此简单的一行代码背后蕴含的无穷可能,也充分体现了C++代码难读的特点。
  • 关于cin为什么typecast到void*,而不是bool的讨论,凸显了C++中剑走偏锋的情况 - 火候不到,一招不慎就很容易伤及自身。
  • 析构函数中不应该抛出异常,我以前只知道一个原因 - 就是在前次异常的栈展开过程中调用析构函数并抛出异常,会导致程序退出。但这里给出了我觉得更有说服力的原因:在delete []数组的时候,前面对象析构抛出异常,会导致数组中其他对象内存泄露。
  • 类成员的初始化顺序由其定义的顺序决定,而不是初始化列表中的顺序 - 这点的确引起了较大的迷惑,也带来了不少bug - 因为C++的行为是反直觉的。
  • 函数调用中,传指针的方式比较明显的告诉你该函数可能会改变这个参数,而引用却没这么明显,语法和传值调用一样,却也可以改变参数值。
  • C++过于强大,过于灵活,很多人无法很好的掌控 - 太多复杂的feature set,要用好它,你可以读个博士了~~~
  • prefix ++的重载语法是:operator++(yourtype&), 为了加以区别,postfix的重载语法有个dummy的int参数:operator++(yourtype&, int dummy)。
    虽然我也没有更好的方法,但我承认这的确很傻。
  • 同样的容器,由于使用了不同的allocator就无法交互了,这可以理解,因为STL中allocator是容器类型的一部分,allocator不同导致容器类型不同 - 但这不得不让我们思考STL用这种方式提供allocator是不是合适。
  • map的operator[]自动添加元素,如果不存在的话。
    因为相比于find和insert,operator []实在是太方便了,这个方便的诱惑的确造成了不少麻烦。
  • 模板中你不得不把>>写成> >。因为>>已经被占用了。
  • 用不用exception,如何用好exception实在是个太大太深的话题,都可以在大学开个博士学位了。其中异常安全中resource leak,deadlock是常见的问题。
  • delete []可以很好的处理退化为指针的数组,如果是类的话调用会调用的析构函数s,因为数组元素的个数可以通过sizeof(memoryblock)/sizeof(type)求出。
  • new []可能会引起int的溢出,如: new double [0x8000000] = malloc (8 * 0x80000000),超过了int的表达范围,溢出~
  • 局部静态变量的初始化不是线程安全的 - 这个问题在多线程环境下的单件模式中尤为常见,一般可以用lock解决,但是每次访问都lock比较费力,所以会用一种double-check lock的方式,但是这种方式由于编译器优化引起的reorder,也会线程不安全,需要使用volatile,或者memory barrier防止优化。这个估计可以另外写篇文章了。
  • 用基类指针操作派生类的数组,p++不是指向下一个元素,而是指向了一个不合适的内存地址。
  • 如果你在派生类中有个函数的名字和基类中的函数名字重复,即使函数原型不一样,其基类中的函数都将在派生类中被隐藏。
    这点的确比较过分!背后有什么原因呢? 

三、日后研究

  • 关于名字空间,C++有过什么大的更改么?
    这个估计要查查《C++语言的设计与演化》了
  • 用C++写出好的库基本是不可能的。
    我看到很多人,包括牛人都说过这个,但是不知有没有给过一个列表,C++中那些缺点使其写出好的库成为不可能,哪些语言可以,为什么?
  • 我们不应该在构造函数中抛出异常,因为:Exceptions in constructor don’t unwind the constructor itself。
    这个不太理解,据我所知,在构造函数中抛出异常是构造函数报错的一个方法,因为构造函数本身不返回任何值。
  • 抛出异常时:Does not even clean up local variables!
    不理解,我们的RAII不就是利用local对象的析构来做内存管理的吗。
  • assert(s[s.size()] == 0); works if s is a const std::string, butis undefined if it is not const
    在VC2008上试了一下,没问题。为什么会这么说,为什么? 
  • If you call delete when you should have called delete[], the pointer wilbe off by sizeof(int), leading to heap corruption and possibly code execution.
    不懂。
  • If you call delete[] when you should have called delete, some randomdestructors will be called on garbage data, probably leading to code execution.
    为什么,delete[]会去计算该数组中有几个元素,而答案应该是1,那就不该有问题 - 这个可能和上一点的答案有关。
posted @ 2011-05-20 11:55 lzprgmr 阅读(821) 评论(12) 编辑 收藏

 回复 引用 查看   
#1楼 2011-05-20 21:28 hoodlum1980      
•vector::at会做边界检查,而operator []不会。
------------------------------------------
后者显然是C++的优点么,充分体现了C++的强大和自由啊。咋会被诟病呢。当然这个就是有引起越界的危险性。

 回复 引用   
#2楼 2011-05-21 07:25 hahahah2222[未注册用户]
你还要好好学习C++,很多是比较基本的知识
 回复 引用   
#3楼 2011-05-21 07:55 hahahah2222[未注册用户]
”如果你在派生类中有个函数的名字和基类中的函数名字重复,即使函数原型不一样,其基类中的函数都将在派生类中被隐藏。“
这是由于C++的名称查找机制,分为3步:
1。从调用类查找,有名称match的函数
2。如果#1的返回结果多于1,用参数和返回值类型判断,如果无法确定,返回二义性error
3。如果有match的函数,执行accessibility check

 回复 引用 查看   
#4楼[楼主] 2011-05-21 09:17 lzprgmr      
@hahahah2222
你这个解释相当于:C++不支持这个,因为C++的查找机制不支持这个 ~ ok,这个结论我们已经知道了。 我想重要的是:为什么C++要设计这样的查找机制,基类的函数不被hidden的话会有什么问题

 回复 引用 查看   
#5楼[楼主] 2011-05-21 09:19 lzprgmr      
@hahahah2222
引用hahahah2222:你还要好好学习C++,很多是比较基本的知识

多谢提醒, 也请提携一下,把你认为很多比较基本的,我没理解的知识帮我理一理,不甚感激

 回复 引用 查看   
#6楼 2011-05-21 18:50 islet8      
>>不断变更的style
>>有这个问题吗?有人用第二种方式的吗?
虽然!=和<的区别我也不以为然,但i++和++i的区别还是有意义的,for循环里的i++“可能”会被编译器优化成++i,但毕竟写代码依赖编译器的优化是很不靠谱的陋习。

>>iterator可能会失效
迭代的时候没碰到过erase么?虽然这种情况是得尽量避免,但有时候不得不面对。我前段时间刚好碰到了离谱的情况,一个消息中心,用容器保存所有发过来的消息,然后依次遍历处理,在处理的过程中会调用其他模块来处理这个消息,其他模块又会经过无数次的各种调用,在某个很深的子调用中发了一个消息给这个消息中心,然后stl就“有可能”乱套了,正是因为iterator乱了。所以iterator失效这个问题其实是非常

>>构造函数与析构函数中的虚函数调用
这个问题还是有点困扰我的,因为间接调用虚函数不容易发现,但容易犯 - -|||

>>assert(s[s.size()] == 0);
挂上vs的程序会自动将各种变量/内存清值(0或诸如0xcccccc等),包括内存分配/销毁的内部也被接管,和ctrl+f5跑起来的独立程序的运行环境是很不一样的,而这条最大的问题就是,怎么能利用数组越界的返回值来做断言呢?把越界当作一种合法的操作?
我们游戏已经深受数组越界之苦了,程序直接飞掉不留任何dump,下个项目不会再允许使用原生数组。

>>If you call delete[] when you should have called delete
VC编译器对new[]会在分配的内存块之前的4个字节里保存这个数组的大小,所以new int和new int[1]真正分配的内存块结构是不一样的,delete[]会去读数组内存前的这个int值,而本来该delete的内存块用delete[]后,结局自然悲催了。

 回复 引用 查看   
#7楼[楼主] 2011-05-21 21:36 lzprgmr      
@islet8
恩,您指出的前面几条自然因人而异,没有绝对。

对于这两条:
1. asert(s[s.size()] == 0)的关键点在于const与非const的区别,是不是编译器对const的做了特殊的操作,为什么。

2. 无论是new还是new[],最后都是转调malloc,而该函数的标准实现应该是在分配内存的前4个字节加上分配的字节数,所以我觉得关键点可能不在于此。

 回复 引用 查看   
#8楼 2011-05-22 14:37 islet8      
@lzprgmr
1. 一个问题如果是基于错误的假设,那又有什么意义追究它的推论呢?就像如果有人问“为什么我在人少的地方从别人口袋里掏钱会被抓,人挤人的场合去掏就不容易被发现呢”一样,基于越界访问的前提,得出一个想不通的结论,在这上钻牛角尖,就算能找到解答,好像价值也不高啊。
2. 嗯,我上面说的是不太对,new和new[]的确都会最终转为malloc,并在用户内存块前后有额外的空前存放大小及权限等信息,但new/new[]除了会调malloc外,还会调用构造器(new[]会调用每个元素的构造器)。而new和new[]分配的额外空间格式应该有区别(如果结构一致的话,C++就没有必要在标准中区分new和new[]了(我想应该是最初制定C++标准时为了效率等考量)),这个还没查到明确的文章,我的猜测是用户空间之后有若干个字节(可能是8个)保存了这块用户内存的大小,而new[]会在用户内存前再加4个字节保存元素个数。如果确实是这样的一个结构的话,那再参看原文42页的伪码和汇编码就能理解为什么43页的两条是这样说的了。

 回复 引用 查看   
#9楼 2011-05-24 17:07 Anders06      
for (int i(0); i != n; ++i)
有这个问题吗?有人用第二种方式的吗?

没人用还定义成这样,岂非更误导人?:)

 回复 引用   
#10楼 2011-07-13 00:58 gzm55[未注册用户]
new[]/delete[] 在内存管理上和 new/delete 有点差别

new T(xx):
p=malloc(sizeof T);
new(p) T(xx);
return p
delete p:
p->~T();
free(p);

new T[N]:
p=malloc(sizeof T * N)
T *q=p + _magic_; /* _magic_ 依赖编译器 */
/* do something on p[0 .. _magic_ -1] */
new(q+0) T(xx);
...
new(q+N-1) T(xx);
return q; // *NOT* p

delete[] q:
/* get N from p[0 .. _magic_ -1] */
(q+N-1)->~T(); ... (q+0)->~T();
free(q - _magic_);

 回复 引用   
#11楼 2011-07-31 11:21 Neuron Teckid[未注册用户]
更蛋疼的语法
<code>string(a);</code>
这坑爹的是在定义一个名为 <code>a</code> 的 <code>string</code>...

<code>at</code> 与 <code>operator[]</code>: 如果 <code>at</code> 改名为 <code>try_get</code> 这样很明显看起来会抛异常的名字就好了.

多重模板结尾被视为右移操作符在 0x 中解决了 :D

"
不理解,我们的RTTI不就是利用local对象的析构来做内存管理的吗。
"
博主这句话中的 RTTI 是不是应该是 RAII?

 回复 引用 查看   
#12楼[楼主] 2011-08-06 20:32 lzprgmr      
引用Neuron Teckid:
"
不理解,我们的RTTI不就是利用local对象的析构来做内存管理的吗。
"
博主这句话中的 RTTI 是不是应该是 RAII...


多谢,已改正

发表评论

昵称: [登录] [注册]

主页:

邮箱:(仅博主可见)

评论内容:

  登录  注册

[使用Ctrl+Enter键快速提交评论]

0 2051704 olpUF08j+CY=

黄将军