登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

秒大刀 博客

好好学习 天天向上

 
 
 

日志

 
 
 
 

读《C++ 工程实践经验谈》  

2012-06-14 20:42:08|  分类: 读书 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |
读《C++ 工程实践经验谈》 - 秒大刀 - 秒大刀 博客
C++ 工程实践经验谈》,陈硕 著

1.慎用匿名namespace
  1. 如果用匿名namespace遇到问题了,可以用像boost中那样用namespace detail,或者C中的static

2.不要重载全局的::operator new()
  1. 按现代C++的手法RAII来管理内存,很难遇到什么内存方面的错误
  2. 在现实的产品开发中,重载::operator new()乃是下策
  3. 绝对不能在library里重载::operator new()
  4. 在主程序里重载::operator new()作用不大
  5. 专业的除错工具比自己山寨一个内存检查器要靠谱
  6. 在目前的多线程开发中,自己实现一个能打败系统默认的malloc的内存分配器是不现实的
  7. 为单独的class重载::operator new()也不是好的实践。如果某class需要重载member ::operator new(),说明它用到了特殊的内存分配策略,常见的情况是使用了内存池或对象池。如此,应该将这一事实明显的摆出来,而不是改变new语句的默认行为。如,用factory来创建对象
  8. 重载::operator new()或许在某些临时的场合能应个急,但是不应该作为一种策略来使用。如果需要,我们可以从malloc层面入手,彻底而全面的替换内存分配器

3.采用有利于版本管理的代码格式
  1. 多行注释也用//,不用/* */
  2. 一行之定义一个变量更利于版本管理。同样也适用于enum成员的定义,数组的初始化列表等
  3. 构造函数初始化列表的顺序必须和数据成员声明的顺序相同
  4. 避免使用版本控制软件的keyword substitution功能。文件的Id不应该在文件内容中出现,这些metadata跟源文件的内容无关,应该由版本管理软件额外提供
  5. operator重载不利于用文本的方式查找引用

4.二进制兼容性
  1. 二进制兼容性: 在升级库文件的时候,不必重新编译使用这个库的可执行文件或其他库文件,程序功能不被破坏
  2. 直接访问库中字段会有潜在的二进制兼容性问题,因为字段偏移可能改变;而通过成员函数的操作方式是安全的,因为会重定位
  3. 一般不影响二进制兼容性的做法:
  4. 增加新的class
  5. 增加非虚成员函数或static成员函数
  6. 修改数据成员的名称,当然这会造成源码级别的不兼容
  7. 在C++中以虚函数作为接口基本上就跟二进制兼容说拜拜了
  8. Widows下Debug和Release两种模式下的CRT二进制不兼容,Linux不存在这样的问题
  9. 采用静态库可以很好的规避二进制兼容问题
  10. 采用pimpl技法,在头文件中只暴露non-virtual接口,并且class的大小固定为sizeof(Impl*),这样即可随意更新库文件而不影响可执行文件

5.避免使用虚函数作为库的接口
  1. 动态库比静态库节约内存这种优势在今天看来已不太重要
  2. 对于interface作为回调,现代C++中的boost::function+bind是更好的做法
  3. 虚函数作为接口在二进制兼容性方面的本质困难: 一旦发布,不能修改!脆弱与僵硬是以C++虚函数为接口的宿命
  4. COM确实以一种丑陋的方式做到了二进制兼容
  5. Explicit is better than implicit, Flat is better than nested. —《The Zen of Python》
  6. Java实际上把C/C++的linking这一步推迟到class loading的时候来做。就不存在“不能增加虚函数”,“不能修改data member”等问题。在Java里面用面向interface编程远比C++更通用和自然

6.动态库接口的推荐做法
  1. 为了检查二进制兼容性,应该把库代码的暴露情况分清楚。头文件应该有意识的分为用户可见和用户不可见两部分,对于用户可见部分升级时要注意二进制兼容性
  2. 采用pimpl多了一道explicit forward的手续,带来的好处是可扩展性与二进制兼容性。pimpl扮演了编译器防火墙的作用
  3. 因virtual function是bind by value offset,而non-virtual function是bind by name,故non-virtual要比virtual健壮。加载器会在程序启动时做resolution,通过mangled name把可执行文件和动态链接库链到一起。就像使用域名要比IP地址更能适应变化一样
  4. 跨语言的情况下需要暴露C接口,C函数是linux下的万能接口

7.以boost::function和boost::bind取代虚函数
  1. 面向对象的继承就像一条贼船,上去就下不来了
  2. boost::function就像C#中的delegate,可以指向任何函数,包括成员函数
  3. 当用bind把某个成员函数绑定到某个对象上时,我们得到一个closure闭包
  4. 程序库的设计不应该给使用者带来不必要的限制(耦合),而继承是第二强的耦合(最强耦合是友元)。如果一个程序库限制其使用者必须从某个class派生,可能是一个糟糕的设计
  5. 或许closure-based programming将作为一种新的programming paradiam而流行起来

8.带符号证整数的除法与余数
  1. a / b或a % b,当操作数为负时,结果是implementation-defined

9.用异或来交换变量是错误的
  1. C语言的一条语句中,一个变量的值只允许改变一次,像*begin ^= *end ^= *begin ^= *end;和x = x++;之类的行为是未定义的
  2. 这不是一个值得炫耀的技巧,只会丑化劣化代码
  3. 查看编译器生成的汇编代码固然是了解程序行为的一个重要手段,但是千万不要认为看到的东西是永恒真理,它只是一时一地的真相。换了硬件平台或编译器,情况可能会变化。重要的不是为什么版本一比版本二快,而是如何发粘这个事实。不要"guess"而要"benchmark"
  4. 不要想当然的优化,也不要低估编译器的能力
  5. “我喜欢优雅和高效的代码。代码逻辑应当直截了当,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;以某种全局策略一以贯之的处理全部出错情况;性能调校至接近最优,省的引诱别人实施无原则的优化,搞出一团乱码。整理的代码只做好一件事”——《代码整洁之道
  6. 没有原则的优化,甚至根本连优化都不是
  7. 现代处理器上乘法运算和加减法一样快,比除法快一个数量级左右
  8. 现在已经不是那个懂点汇编就能打败编译器的时代了
  9. Making real applications run really fast is something that's donewith the help of a compiler. Modern architectures have reached the point where people can't code effectively in assembler anymore - switching the order of two independent instructions can have a dramatic impact on performance in a modern machine, and the constraints that you need to optimize for are just more complicated than people can generally deal with.
    So for modern systems, writing an efficient program is sort of a partnership. The human needs to careful choose algorithms - the machine can’t possibly do that. And the machine needs to carefully compute instruc-tion ordering, pipeline constraints, memory fetch delays, etc. The two together can build really fast systems. But the two parts aren’t independent: the human needs to express the algorithmin a way that allows the compiler to understand it well enough to be able to really optimize it. ——《The "C is Efficient" Language Fallacy》

10.在单元测试中mock系统调用
  1. 放在类中的静态函数是封闭的; 放在namespace中的全局函数是开放的,可以随时打开往里添加新的函数,而不用改动原来的头文件。这是non-member non-friend函数为接口的优点。

11.iostream的用途与局限
    读《C++ 工程实践经验谈》 - 秒大刀 - 秒大刀 博客
  1. C++ iostream的主要作用是让初学者有一个方便的命令行输入输出试验环境,在真实的项目中很少用到,不必把精力花在深究iostream的格式化与manipulator(格式化操控符)上。iostream的设计初衷是提供一个可扩展的类型安全的IO机制,但是后来莫名其妙的加入了locale和facet等累赘。其整个设计复杂不堪,多重+虚拟继承的结构也很巴洛克,性能方面几无亮点

  2. C的printf类函数局限:
  3. 在同时兼容x86和x64时会很麻烦
  4. 当格式化字符串与参数类型不匹配会出问题
  5. 不可扩展,无法扩展自定义类型
  6. 使用了little language(现在流行叫DSL)来配置。固然有利于紧凑型和灵活性,但损失了一点点效率
  7. C locale的负担。C locale是可以在运行时动态更改的,即使程序只使用默认的"C" locale,仍然要为这个灵活性付出代价
  8. C的格式化字符串可以在外部灵活配置,iostream做不到
  9. 不必深究iostream的格式化方法,只需要用好它最基本的类型安全输出即可。在真的需要格式化的场合,可以考虑snprintf()打印到栈上缓冲,再用ostream输出
  10. iostream不是线程安全的,不适合在多线程程序中做logging
  11. 复杂的设计必然带来复杂的使用规则,而面对复杂的使用规则,用户是可以投票的,那就是:你做你的,我不用!——孟岩
  12. 继承非为复用,乃为被复用
  13. 作者不认为以iostream为基础的上层程序库(比如那些克服iostream格式化方面缺点的库)有多大的实用价值
  14. iostream最大的缺点是臆造抽象——孟岩
  15. 一个基类设计的好,大家才愿意去继承它
  16. iostream在性能方面没有比stdio高多少,在健壮性方面多半不如stdio,在灵活性方面受制于本身复杂设计而难以让使用者自行扩展。目前看来只适合一些简单的要求不高的应用,但是又不得不为它的复杂设计付出运行是代价,总之其定位有点不上不下

12.值语义与数据抽象
  1. 值类型的一个巨大的好处是生命期管理简单
  2. 引用类型一般不能拷贝,只能通过指针或引用来访问
  3. 由于C++只能通过指针或引用来获得多态性,那么在C++里从事基于继承和多态的面向对象编程有其本质的困难——资源管理
  4. 可以借助smart pointer将对象语义转换为值语义,从而轻松解决对象生命期管理
  5. 如果不使用smart pointer,用C++做面向对象编程将会困难重重
  6. 在写一个class的时候,先让它继承boost::noncopyable,几乎总是正确的
  7. C++的强大之处在于“抽象”不以性能损失为代价
  8. 面向对象真正核心的思想是消息传递,“封装继承多态”只是表象
  9. 只在ADT表示一个“数值”的时候才适合重载加减乘除,其他情况下用具名函数为好
  10. 在C++中,提高抽象的层次并不会降低效率

13.再探std::string
  1. g++的std::string采用Copy on Write方式实现。但COW对多线程不友好
  2. VC采用的是短字符串优化(SSO)方式实现,利用string对象本身的空间来存储短字符串

2012-6-15
14.用STL algorithm秒杀几道面试题
  1. 算法在Big-O意义下是最优的,但不一定是运行最快的
  2. 快速排序是数据结构课上就有的内容,但是工业强度的实现是足以在顶级期刊上发论文的

15.C++编译链接模型精要
  1. C++语言的三大约束: 与C兼容、零开销原则、值语义
  2. 与C兼容不仅仅是兼容C的语法,更重要的是兼容C的编译模型与运行模型,能直接使用C语言的头文件和库
  3. C++必须用include头文件的方式机械的将库的接口声明以文本替换的方式载入,再重新parse一遍
    • 如此让编译效率奇低,编译器动辄要parse几万行预处理之后的源码,哪怕源文件只有几百行
    • 头文件包含具有传递性,会引入不必要的依赖
    • 如果头文件的内容组织不当,会造成程序的语义跟头文件包含的顺序有关,也跟是否包含某一个头文件有关
    • 任何头文件改动一点点代码都引起所有直接或间接包含它的源文件重新编译
      • 合理组织源代码,减少开发时rebuild的成本是每个稍具规模的项目必做功课
    • 头文件是在编译时使用,动态库文件是在运行时使用,二者的时间差可能带来不匹配,导致二进制兼容性方面问题
    • 整个程序应该用统一的编译选项。如果程序用到了第三方库,除了拿到头文件和库文件,我们还要拿到当时编译这个库的编译选项,才能安全无误的使用这个程序库。如果用到多个库,而他们的编译选项有冲突,那就麻烦大了!
    • 这些是在“与C兼容”的大前提下不得不做出的妥协
  4. 由于不能将整个源文件的语法树保存在内存中,C语言是按照“单遍编译”来设计的。编译时从头到尾扫描一遍源码,一边解析代码,一边即刻生成目标代码。在单遍编译时,编译器只能看到已经parse过的代码,看不到之后的代码,而且过眼即忘
  5. C++只能通过解析源码来了解名字的含义,不能像其他语言那样通过直接读取目标代码中的元数据来获得所需信息。这意味着要想准确理解一行C++代码的含义,我们需要通读这行代码之前的所有代码,并理解每个符号的定义
  6. C++编译器读到一个函数调用语句时,它必须(也只能)从目前看到的同名函数中选出最佳函数。哪怕后面的代码中出现了更适合的匹配,也不能影响当前的决定
  7. 实现C++重构工具的难度: 重构器对代码的理解必须达到编译器的水准,才能在修改代码时不改变原意
  8. 由于C++新增了不少语言特性,其编译器并不能真正做到像C那样过眼即忘的单遍编译。但是C++必须兼容C的语义,因此编译器不得不装得好像是单遍编译(one parse)一样,哪怕它内部是multiple pass的
  9. C++典型缺陷: 一样东西区分声明和定义,代码放到不同的文件中,这就有可能出现不一致的可能性
  10. 当知道模板会有哪些具现化类型时,可以将模板的实现放到.cpp中而只在.h放必要的声明
  11. 如果编写程序库,那么公开的头文件应该表达模块的接口,必要的时候可以把实现细节放到内部头文件中
  12. COM违反了“软件设计的基本原理”: 改动程序本身或它依赖的库之后应该重新测试,否则测试通过的版本和实际运行的版本根本就是两个东西——《软件测试实践》
  13. 通常我们不能在没有充分测试的情况下升级JVM大版本
  14. linux上C++标准库的版本跟C++编译器直接关联,C标准库的版本跟linux操作系统的版本直接关联。
    • 为了稳妥起见,通常建议用linux发行版自带的gcc版本来编译代码。因为此版本的gcc是linux发行版主要支持的编译器版本,当前kernel和用户态的其他程序也基本是它编译的,如果有什么问题的话早就被人发现了。
    • 选定了操作系统版本,C标准库、C++标准库、C++编译器的版本也就确定了。
    • 升级操作系统后这些编译器和库的版本都会一起变,那时整个程序几乎肯定要重新测试并重新部署上线
  15. linux的共享库(shared library)比Windows的动态链接库在C++编程方面要好得多,对应用程序来说基本可算是透明的,跟使用静态库无区别
    • 一致的内存管理。linux动态库与应用程序共享一个heap,因此动态库分配的内存可以交给应用程序去释放,反之亦可
      • VC采用MDd/MD编译选项也没有问题,参考阅读《VC运行时库
    • 一致的初始化。动态库里的静态对象的初始化和程序其他地方的静态对象一样,不用特别区分对象的位置
    • 在动态库的接口中可以放心的使用class、STL、boost(如果版本相同)
    • 没有dllimport/dllexport的累赘,直接include头文件就能使用
    • dll hell的问题也小得多,因为linux允许多个版本的动态库并存
      • Windows7里有side-by-side assembly,基本解决了dll hell问题,代价是系统里有一个巨大的且不断增长的WinSxS目录
  16. 动态库是有害的: 一旦替换了某个应用程序用到的动态库,先前运行正常的程序使用的将不再是当初build和测试时的代码。结果是程序的性我为变得不可预期。在fix bug和增加feature的同时,还能保证不会损坏现有的应用程序是不可能的——Jeffrey Richter
  17. 静态库的的链接比动态库慢,存在编译库时的环境和应用时的环境不一致的陷阱
  18. 源码编译是王道——但编译时间会很长


  评论这张
 
阅读(3124)| 评论(0)

历史上的今天

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2018