您的位置:首页 > 编程语言 > C语言/C++

规范化的C++编程方法备忘录 杂项约定

2010-04-22 09:15 190 查看
1.引子

    对于程序编制人员来说,怎样安排他们的代码成了他们除设计具体算法外的最大的任务之一。和学生时代开发的试验品相比,所写代码还必须在执行正确的前提下(但这是个绝对的前提),保证健壮、可靠、系统收敛的,还必须尽可能的快(但不能以降低稳定为代价)。你还得保证代码看起来“漂亮”,以便让其他人看着舒服——请记住:你写的代码不光是给你看的,是给别人,尤其是给开发测试人员——看的。这里的“其他人”还包括你自己:想想看,当你的程序在开发出来、运行无误很久很久后被查出有问题,你查到了有问题的部分,却已经不记得当时的思路了...挖了个坑把自己埋住。记住:如果不想给自己惹麻烦,最好别偷懒。

    现在将你觉得很上手的程序源代码翻出来,打开,将编译器的警告级别调到最高,编译...决不要忽略任何一处。错了就是错了,没有借口。

2.理想抽象化平台

 .虚处理器

  撇开编译器,现在站在程序设计者的角度想象一下,我们的程序运行起来总满足以下两点之一:1.顺序执行。代码从编码开始就有一定的逻辑顺序,尽管有被编译器和运行中处理器优化机制修改的可能,但逻辑上总是顺序一定的。2.并发执行。这没什么说的,线程和进程。说这些的根本目的是:我们必须得尽力考虑为一个“通用的处理器”编码,尽管它不存在。这样做的好处就是你不会为同时为满足不同的处理器优化而伤透脑筋,而且在硬件、操作系统升级后不会碰到兼容性问题。这个问题在后面会一点点说明,并让你明白对代码安排的影响。

 .虚内存(只适用于用户模式应用的开发)

    这里的虚内存和虚拟内存的概念不太一样,它是针对源程序而言的。编译后的程序会被假设使用一个理想的大内存,但却不考虑这些内存是否真的能被使用。注意虚内存的概念对驱动程序开发基本无用,只有十分有限参考价值。虚内存会被划分为几个特殊区域:代码段、固定大小的块:用于存储静态可写/只读数据、动态存储区块:包括(注意栈就是栈stack,没有“堆栈”!国内的计算机资料...)。

  对于任何指针、引用、跳转,虚内存会给予其以下约定:1.NULL为空地址。指向NULL意味着没有指向任何实际对象。2.从 (void *)-1 到 (void *)(-  [对象大小]) 区域的内存为无效内存。3.除了1、2以外的地址均为可能的有效地址。4.对象被分配地址空间后,其有效起止地址范围不会跨越NULL地址值(即动态分配不会循环)。5.栈的分配在有限大小内永远成功,但很有限;堆的分配永远有失败的可能。

  注意第3点和Windows操作系统实际的地址分配方式的不同(实际上根本就不考虑系统的内存分配)。为了保证对未来的兼容性,除非你的目的就是开发某个特定系统的软件,永远不要假定系统会给你划分什么样的地址范围。例如有人认为7fffffff以上的地址不会被应用程序使用,便用它来判断是否处在系统地址空间中。这样不好(尽管事实真的可行)。实在有此需要,应当使用系统SDK提供的相关API或宏,不要自己去定义他们。

  第5点意味着,每个函数及其内部区块声明的局部对象必须尽可能的小、少,函数嵌套层数必须有界;堆分配必须检查返回合法性。强调安全的程序还要考虑分配溢出问题的处理。而与“成功”相反的结果就是,如果超过限制,异常抛出...这种异常是会永久地破坏最深层调用栈的结构的。不要这样设计。

----------------------------

测试一下:

 - 你有用过递归调用计算菲氏函数或画分形图案的经历吗?那现在你知道了这种设计是不好的。其调用深度动态来看无界。

 - 不要以为只有空闲内存不足才会导致分配堆内存失败!内存碎片化也会导致分配失败!更不要认为Windows不会让堆内存分配失败!驱动和函数钩子也可以让分配失败!

 - 指针是否只有NULL为空?其实还可以定义(T *)-1为检测值。动态内存永远分配不到这里。只要你保证定义的类型大小不止一个字节。

3.关于API的约定

    对于一个指定的API:

  如果没有特殊说明,对基本资源有分配型操作的API遵守动态内存分配的约定——即有可能失败,从而使API返回失败;对基本资源回收型操作的API遵守动态内存回收的约定——即总是会成功(注意不要对已回收的“资源”再次调用回收API,就像不要对已释放内存的指针调用内存释放函数那样)。对基本资源回收型操作的API如果违反了此约定(当然调用的时候必须合法。否则就只剩下你自己的问题了),只应该考虑以下情况:。系统不稳定或受损;。驱动级程序的有意而为,注意这个驱动应当本来就是被设计成与你的应用程序有服务或管理关系的。

  对于API的应用层,比较复杂,应该视具体情况。例如ListBox控件,对已存在的列表项执行删除的操作就可能失败,但你不该认为是“系统不稳定或受损或驱动级程序的有意而为”,它实际上是执行重分配内存块(缩小内存)操作失败所致。这其实并不是违反了约定,其实很好理解,因为ListBox控件不是基本资源。如果你需要强制他们符合约定,最好自己定义和管理相应的数据结构。

  基本资源:系统核心部分有内存、进程内线程、文件和设备(要看情况)、进程索引值等(可以和操作系统原理教材涉及的概念结合起来理解)。还有一类是功能集内定义的基本资源。例如窗口API中的窗口(以HWND句柄为引用标志),GDI中的设备描述(以HDC句柄为引用标志)、各种绘图资源等(位图、刷子、画笔...)。

  API的应用还有很多要注意的地方,后面会提到。

4.GC的困惑。

  使用垃圾收集器可以从理论上根除堆分配管理中的常见经典问题。这些问题是菜鸟的家常便饭,而遗憾的是老鸟也不能保证不范。

  垃圾收集器成立的条件是基于一个基本事实:所有的计算机程序,除了操作系统的最底层代码,都是以函数的形式存在的。于是就意味着在任意时刻,对任何有效对象的任何索引都是以某个函数栈的某个索引符号为根节点的(当然,除了操作系统的最底层以外)。因为索引不能被创建,只能从另一个有效的索引拷贝出来,而栈又是后进先出的...于是垃圾收集器诞生...

  现在的问题是,对于使用.NET开发Windows程序的人来说,GC对于开发基本资源的自动回收机制却不利。很多资源是基于线程的。一个线程创建的资源很多只能在该线程内销毁,而当前的GC,都运行于一个区别于主线程的独立的线程内,且释放也不及时(并不是无用的时候就释放)。如果想在GC里析构他们基本上是不可能的。

5.区分“规定”和“具体实现”

  弄清楚标准规定了什么内容比探索编译系统具体的实现方式更重要。

- 编译器必须服从于标准。所以代码应以适应标准为准,如果编译器对某个条款的支持有问题,应等待或者根本不用这类条款的支持。而不是去适应编译器的具体实现。

- 编译器的实现不属于标准的内容。依赖于这些实现容易导致代码对未来编译系统的兼容性问题。

- 讨论编译器的具体实现应只限于在对性能影响的评估上。

- 代码必须写成优化无关的。程序的业务行为不应该依赖于编译系统具体使用的优化手段。

- 代码应该完全避免写成标准中描述过的未定义行为的。

- 除非是基于标准的条款通过数理逻辑推导出来的定理,否则对于某个问题没有明确的条款规定的情况下,不要做人为的假定。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: