⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 编程精粹.txt

📁 Microsoft 编写优质无错C 程序秘诀
💻 TXT
📖 第 1 页 / 共 5 页
字号:


·10·

    void *memcpy(void *pvTo, void *pvFrom, size_t size) {
        byte *pbTo = (byte *)pvTo;
        byte *pbFrom = (byte *)pvFrom;
        assert(pvTo != NULL && pvFrom != NULL);
        while(size-- > 0)
            *pbTo++ = *pbFrom++;
        return(pvTo);
    }

    assert是个只有定义了DEBUG 才起作用的宏,如果其参数的计算结果为假,就中止程序的执行。因此在上面的程序中,任何一个指针为NULL都会引发assert。

    assert并不是一个仓促拼凑起来的宏,为了不在程序的交付版本和调试版本之间引起重要的差别,需要对其进行仔细的定义。宏assert不应该弄乱内存,不应该对未初始化的数据进行初始化,即它不应该产生其他的副作用。正是因为要求程序的调试版本和交付版本行为完全相同,所以才不把assert作为函数,而把它作为宏。如果把assert作为函数的话,其调用就会引起不期望的内存或代码的对换。要记住,使用assert的程序员是把它看成一个在任何系统状态下都可以安全使用的无害测试手段。

    读者还要意识到,一旦程序员学会了使用断言,就常常会对宏assert进行重定义。例如,程序员可以把assert定义成当发生错误时不是中止调用程序的执行,而是在发生错误的位置转入调试程序。assert的某些版本甚至还可以允许用户选择让程序继续运行,就仿佛从来没有发生过错误一样。

    如果用户要定义自己的断言宏,为不影响标准assert的使用,最好使用其它的名字。本书将使用一个与标准不同的断言宏,因为它是非标准的,所以我给它起名叫做ASSERT,以使它在程序中显得比较突出。宏assert和ASSERT之间的主要区别是assert是个在程序中可以随便使用的表达式,而ASSERT则是一个比较受限制的语句。例如使用assert,你可以写成:

    if(assert(p != NULL), p->foo != bar)
    ...

但如果用ASSERT试试,就会产生语法错误。这种区别是作者有意造成的。除非打算在表达式环境中使用断言,否则就应该将ASSERT定义为语句。只有这样,编译程序才能够在它被错误地用到表达式时产生语法错误。记住,在同错误进行斗争时,每一点帮助部有助于错误的发现。我们为什么要那些自己从来用不着的灵活性呢?

    下面是一种用户自己定义宏ASSERT的方法:

    #ifdef DEBUG
        void _Assert(char *, unsigned); /*  原型  */
    #define ASSERT(f)                   \
        if(f)                           \
            NULL;                       \
        else                            \
            _Assert(__FILE__, __LINE__)
    #else
        #define ASSERT(f) NULL
    #endif


·11·

    从中我们可以看到,如果定义了DEBUG ,ASSERT将被扩展为一个if语句。if语句中的NULL语句让人感到很奇怪,这是因为要避免if不配对,所以必需要有else语句。也许读者认为在_Assert 调用的闭括号之后需要一个分号,但并不重要。因为用户在使用ASSERT时,会给出一个分号。

    当ASSERT失败时,它就使用预处理程序根据宏__FILE__和__LINE__所提供的文件名和行号参数调用_Assert 。_Assert 在标准错误输出设备stderr上打印一条错误信息,然后中止:

    void _Assert(char *strFile, unsigned uLine) {
        fflush(stdout);
        fprintf(stderr, "\nAssertion failed: %s, line %u\n", strFile, uLine);
        fflush(stderr);
        abort();
    }

    在执行Abort 之前,需要调用fflush将所有的缓冲输出写到标准输出设备stdout上。同样,如果stdout和stderr都指向同一个设备,fflush(stdout)仍然要放在fflush(stderr)之前,以确保只有在所有的输出都送到stdout之后,fprintf 才显示相应的错误信息。

    现在如果用NULL指针调用memcpy,ASSERT就会抓住这个问题,我通常所用的编译程序的assert会显示出如下信息:

    Assertion failed: string.c, line 153

这给出了assert于ASSERT的另一点不同,标准宏assert除了给出上述信息之外,还显示出已经失败了的测试条件。例如对这个问题,我通常所用的编译程序的assert会显示出如下信息:

    Assertion failed: pvTo != NULL && pvFrom != NULL
    File string.c, line 153

    在错误信息中包括测试表达式的唯一麻烦是每当使用assert时,它都必须为_Assert产生一条该条件对应的正文形式打印消息,但问题是,编译程序要在哪儿存贮这个字符串呢?Macintosh,DOS 和Windows 上的编译程序通常在全局数据区存贮字符串,但在Macintosh 上,通常把最大的全局数据区限制为32K ,在DOS 和Windows 上限制为64K 。因此对于象Microsoft Word和Execl 这样的大程序,断言字符串立刻会占掉这块内存。

    关于这个问题存在一些解决的办法,但最容易的办法是在错误信息中省去测试表达式字符串。毕竟只要查看了string.c的第153 行,就会知道出了什么问题以及相应的测试条件是什么。


·12·

    如果读者想了解标准宏assert的定义方法,可以查看所用编译系统的assert.h文件。ANSI C 标准在其基本原理部分也谈到了assert,并且给出了一种可能的实现。P. J. Plauger 在其“The Standard C Library”一书中,也给出了一种略微不同的标准assert的实现。

    不管断言宏最终是用什么样方法定义的,都要使用它来对传递给相应函数的参数进行确认。如果在函数的每个调用点都对其参数进行检查,错误很快就会被发现。断言宏的最好作用是使用户在错误发生时,就可以自动地把它们检查出来。

    ┏━━━━━━━━━━━━━━━┓
    ┃要使用断言对函数参数进行确认。┃
    ┗━━━━━━━━━━━━━━━┛

“无定义”意味着“要避开”

    如果读者停下来读读ANSI C中memcpy函数的定义,就会看到其最后一行说:“如果在存储空间相互重叠的对象之间进行了拷贝,其结果无定义”。在其它的书中,对此的描述有点不同。例如在 P. J. Plauger和Jim Brodie的“Standard C”中相应的描述是:“可以按任何次序访问和存储这两个数组的元素。”

    总之,这些书都说如果依赖于以按特定方式工作的memcpy,那么当使用相互重叠的内存块调用该函数时,你实际上是做了一个编译程序不同(包括同一编译程序的不同版本),结果可能也不同的荒唐的假定。

    确实有些程序员在故意地使用无定义的特性,但我想大多数的程序员都会很有头脑地避开任何的无定义特性。我们不应该效仿前一部分程序员。对于程序员来说,无定义的特性就相当于非法的特性,因此要利用断言对其进行检查。倘若本想调用memmove ,却调用了memcpy,难道你不想知道自己槁错了吗?

    通过增加一个可以验证两个内存块绝不重叠的断言,可以把memcpy加强如下:

    /*  memcpy—拷贝不重叠的内存块  */
    void *memcpy(void *pvTo, void *pvFrom, size_t size) {
        byte *pbTo = (byte *)pvTo;
        byte *pbFrom = (byte *)pvFrom;
        ASSERT(pvTo != NULL && pvFrom != NULL);
        ASSERT(pbTo >= pbFrom + size || pbFrom >= pbTo + size);
        while(size-- > 0)
            *pbTo++ = *pbFrom++;
        return(pvTo);
    }

    读者可能会认为上面的加强不太明显,怎么只用了一行语句就完成了重叠检查呢?其实只要把两个内存块比作两辆在停车处排成一行等候的轿车,就可以很容易明白其中的道理。我们知道,如果一辆车的后保险打在另一辆车的前保险杠之前,两辆车就不会重叠。上面的检查实现的就是这个思想,那里pbTo和pbFrom是两个内存块的“后保险杠”,pbTo + size和pbFrom + size分别是位于其相应“前保险杠”之前的某个点。就是这么简单。


·13·

    顺便说一句,如果读者还没有认识到重叠填充的严重性,只要考虑pbTo等于pbFrom + 1并且要求至少要移动两个字节这一情况就清楚了。因为在这种情况下,memcpy的结果是错误的。

    所以从今以后,要经常停下来看看程序中有没有使用无定义的特性。如果程序中使用了无定义的特性就要把它从相应的设计中去掉,或者在程序中包括相应的断言,以便在使用了无定义的特性时,能够向程序员发出通报。

    这种做法在为其他的程序员提供代码库(或操作系统)时显得特别重要。如果读者以前曾经为他人提供过类似的库,就知道当程序员试图得到所需的结果时,他们会利用各种各样的无定义特性。更大的挑战在于改进后新库的发行,因为尽管新库与老库完全兼容。但总有半数的应用程序在试图使用新库时会产生瘫痪现象。问题在于新库在其“无定义的特性”方面,与老库并不100%兼容。

    ┏━━━━━━━━━━━━━━━━━━━━━━━━━┓
    ┃          要从程序中删去无定义的特性,            ┃
    ┃或者在程序中使用断言来检查出无定义特性的非法使用。┃
    ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛

△不要让这种事情发生在你的身上

    在1988年晚些时候,Microsoft 公司的摇钱树DOS 版Word被推迟了三个月,明显地影响了公司的销售。这件事情的重要原因,是整整六个月来开发小组成员一直认为他们随时都可以交出Word。

    问题出在Word小组要用到的一个关键部分是由公司中另一个小组负责开发的。这个小组一直告诉Word小组他们的代码马上就可以完成,而且该小组的成员对此确信不疑。但他们没有意识到的是,在他们的代码中充斥了错误。

    这个小组的代码与Word代码之间一个明显的区别是Word代码从过去到现在一直都使用断言来调试代码,而他们的代码却几乎没有使用断言。因此,其程序员没有什么好的办法可以确定其代码中的实际错误情况,错误只能慢慢地暴露出来。如果他们在代码中使用了断言,这些错误本该在几个月之前就被检查出来。▲

大呼“危险”的代码

    尽管我们已经到了新的题目,但我还是想再谈谈memcpy中的重叠检查断言。对于上面的重叠检查断言:

    ASSERT(pbTo >= pbFrom + size || pbFrom >= pbTo + size);

    假如在调用memcpy对这个断言测试的条件为真,那么在发现这个断言失败了之后,如果你以前从来没有见过重叠检查。不知道它是怎么回事,你能想到发生的是什么差错吗?我想我大概想不出来。但这并不是说上面的断言技巧性太强,清晰度不够,因为不管从哪个角度看这个断言都很直观。然而,直观并不等于明显。



·14·

    请相信我的话,很少比跟踪到了一个程序中用到的断言,但却不知道该断言的作用这件事更令人沮丧的了。你浪费了大量的时间,不是为了排除错误,而只是为了弄清楚这个错误到底是什么。这还不是事情的全部。更有甚者程序员偶尔还会设计出有错的断言。所以如果搞不清楚相应断言检查的是什么,就很难知道错误是出现在程序中,还是出现在断言中。幸运的是,这个问题很好解决。只要给不够清晰的断言加上注解即可。我知道这是显而易见的事情,但令人惊奇的是很少有程序员这样做。为了使用户避免错误的危险,程序员们经历了各种磨难,但却没有说明危险到底是什么。这就好比一个人在穿过森林时,看到树上钉着一块上书“危险”红字的大牌了。但危险到底是什么?树要倒?废矿井?还是大脚兽?除非告诉人们危险是什么或者危险非常明显,否则这个牌子就起不到帮助人们提高警觉的作用,人们会忽视牌子上的警告。同样,程序员不理解的断言也会被忽视。在这种情况下,程序员会认为相应的断言是错误的,并把它们从程序中去掉。因此,为了使程序员能够理解断言的意图,要给不够清楚的断言加上注解。

    如果在断言的注解中还注明了相应错误的其它可能解法,效果更好。例如在程序员使用相互重叠的内存块调用memcpy时,就是这样做的一个好机会。程序员可以利用注解指出此时应该使用memmove ,它不但能够正好完成你想做的事情,而且没有不能重叠的限制:

    /*  内存块重叠吗?如果重叠。就使用memmove   */
    ASSERT(pbTo >= pbFrom + size || pbFrom >= pbTo + size);

    在写断言注解时,不必长篇大论。一般的方法是使用经过认真考虑过的简短问句,它可能比用一整段的文字系统地解释出每个细节的指导性更强。但要注意,不要在注解中建议解决问题的办法,除非你能够确信它对其他的程序员确有帮助。做注解的人当然不想让注解把别人引入歧途。

    ┏━━━━━━━━━━━━━━━━━━━━━━┓
    ┃不要浪费别人的时间—清晰地说明不明显的断言。┃
    ┗━━━━━━━━━━━━━━━━━━━━━━┛

△不是用来检查异常的

    当程序员刚开始使用断言时,有时会错误地利用断言去检查可能的异常,而不去检查非法的情况。看看在下面的函数strdup中的两个断言:

    /*  strdup—为字符串分配一个副本  */
    char * strdup(char *str) {
        char *strNew;
        ASSERT(str != NULL);
        

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -