[小记]C++阴暗面,笔记,注释

6 篇文章 0 订阅

一、不以为然

  • 不断变更的标准,迫使我们需要不断更新已有代码。 
    作者列出了几点其实影响并不是很大(循环变量的scope;头文件后缀;名字空间)。而且,为了标准的进步,偶尔做出的妥协也是应该的吧。
  • 不断变更的style,作者举得例子是:
    Old and busted:
    for  ( int  i  =   0 ; i  <  n; i ++ )
    New hotness:
    for  ( int  i( 0 ); i  !=  n;  ++ i)
      有这个问题吗?有人用第二种方式的吗? 

【理解】最好还是用 ++i ,i!=n也可以考虑

  • auto_ptr很烂。
    这个,你不用它就是了,而且最近在智能指针加入C++0x大家庭后,应该达成共识了吧

【理解】的确不是很好用,可以考虑smart_ptr

  • iterator可能会失效。
    这个我感觉还好,没遇到过太多问题;

【理解】iterator循环处理中,删除或者添加都有可能会造成iterator失效,例如,vector中添加元素,可能会造成重新分配容器内存,这样原来的iterator肯定就失效了

  • iterator对container毫不知情。
    我觉得这是STL设计的一个优点吧,通过iterator解耦算法与容器。
  • vector::at会做边界检查,而operator []不会。
    很好啊,提供两种选择

【理解】这是at和[]的唯一区别,at会提前做一个便捷检测,当然效率也会下降

  • 构造函数与析构函数中的虚函数调用,可能会调用基类的虚函数,甚至是纯虚函数。
    这个不怎么阴暗吧,不要在构造函数与析构函数总调用虚函数应该是个常识,而且,其他语言难道没有这个问题?

【理解】实例化子类要优先调用父类的构造函数(没有老爸哪有儿子),然后此时如果父类的构造函数里面调用了所谓的虚函数,那么整个逻辑非常奇怪,因为现在儿子都还没有,你去调用什么?怎么去调用还不存在、还未实例化的事物的方法。而且最重要的一个原因是:一般定义虚函数的主要目的是希望该函数由派生类去实现。我现暂且把虚函数的实现者认定为派生类吧,那么也就是说该虚函数其实就是派生类的一个方法,一个类的方法,难免会用到自己本身的属性,而此时派生类还未实例化,请注意,现在才刚开始在实例化派生类它老爸呢。他老爸才刚出生就想调用到儿子的方法,有点奇怪吧。同理析构函数也是这样,析构函数是由外到里一层层析构的,也就是说会先调用派生类的析构函数,也就是先干掉派生类,如果派生类重写的虚函数里面,有调用到派生类被干掉了的数据,而父类又还真调用了“虚函数”,那可就杯了个具了

所以不要在构造函数与析构函数中调用虚函数

二、深有同感

  • 模板中排山倒海式的编译错误,在繁琐无比的错误后面,原因可能仅仅是用错了iterator类型。那个被毙掉的C++ 0x proposal不知是否可以解决此问题。

【理解】的确是难看,不过还好模板用的少

  • 用C++写的代码不太容易读,函数重载,操作符重载,虚函数重定义,类型重定义,宏定义等等,把代码的真实面貌严严实实的藏在了身后。(当然,这也是为了抽象与一致性),几个例子:
    string  a( " blah " );  //  定义一个string对象
    string  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)。
    虽然我也没有更好的方法,但我承认这的确很傻。

【理解】重载 "后++"要利用到 "前++"的重载,模式

前++重载: T& operator++() { ++this->v; return *this;}

后++重载: T& operator++(int) {T tmp=*this; ++(*this); return tmp;}

  • 同样的容器,由于使用了不同的allocator就无法交互了,这可以理解,因为STL中allocator是容器类型的一部分,allocator不同导致容器类型不同 - 但这不得不让我们思考STL用这种方式提供allocator是不是合适。
  • map的operator[]自动添加元素,如果不存在的话。
    因为相比于find和insert,operator []实在是太方便了,这个方便的诱惑的确造成了不少麻烦。

【理解】map和hash_map的[]跟insert有很大的区别

  • 模板中你不得不把>>写成> >。因为>>已经被占用了。

【理解】必然的

  • 用不用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防止优化。这个估计可以另外写篇文章了。

【理解】线程安全的Sington模式

  • 用基类指针操作派生类的数组,p++不是指向下一个元素,而是指向了一个不合适的内存地址。

【理解】很容易出错

  • 如果你在派生类中有个函数的名字和基类中的函数名字重复,即使函数原型不一样,其基类中的函数都将在派生类中被隐藏。
    这点的确比较过分!背后有什么原因呢? 

【理解】这是由于C++的名称查找机制,分为3步:
1。从调用类查找,有名称match的函数
2。如果#1的返回结果多于1,用参数和返回值类型判断,如果无法确定,返回二义性error
3。如果有match的函数,执行accessibility check

三、日后研究

  • 关于名字空间,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,那就不该有问题 - 这个可能和上一点的答案有关。

【理解】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_);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值