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,那就不该有问题 - 这个可能和上一点的答案有关。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· [AI/GPT/综述] AI Agent的设计模式综述