C专家编程 第2章 这不是Bug,而是语言特性 2.4 少做之过

    少做之过的特性就是语言应该提供但未能提供的特性,如标准参数处理以及把lint程序错误从编译器中分离出来。
    2.4.1 用户名中若有字母f,便不能收到邮件
    用户名的第二个字母是f,邮件确实无法发送到他们那里
    许多人对ANSI C采用argc、argv的约定向C程序传递参数感到惊奇,但事实就是如此。
    UNIX的约定有所提升,达到了一个标准的层次,但此时却成了这个邮件Bug的原因之一。 
    /*分析方法像试探法。*/
    if (argv[argc - 1][0] == '-' || (argv[argc - 2][1] == 'f')) {
        readmail(argc, argv);
    } else {
        sendmail(argc, argv);
    } 
    /*能正确读取邮件*/ 
    mail -h -d -f -/usr/linden/mymailbox 
    /*读取邮件,而不是发送邮件*/ 
    mail effie Robert
    /*修改方案*/ 
    if (argv[argc - 1][0] == '-' || argv[argc - 2][0] == '-' && (argv[argc - 2][1] == 'f')) {
        readmail(argc, argv);
    } else {
        sendmail(argc, argv);
    } 
    /*能正确读取邮件*/
    mail -h -d -f -/usr/linden/mymailbox
    /*能正确发送邮件*/
    mail effie Robert
    许多操作系统(如VAX/VMS)能够在程序中区分运行时选项和其他参数(如文件名),但UNIX却不能,ANSI C也不能。
  
    软件信条
    Shell参数解析
    不充分的参数解析问题出现在UNIX的许多地方。找出目录中的那些文件是链接文件,你可能输入下面的命令:
    ls -l | grep ->
    缺少重定向的名字,->被shell翻译成重定向符。
    ls -l | grep "->"
    grep先看到减号,然后把整个参数翻译成大于号的一种未知组合形式,然后退出,要解决问题,必须放弃使用ls命令。
    file -h * | grep link
    创建一个文件,文件名以连字符开头,然后去发现无法用rm命令把连字符去掉。
    一种解决方法是给出文件的完整路径名,这样rm就不会把连字符当做选项开关,并依次翻译文件名。
    有些C程序员采用了一种约定,即带“--”的参数表示“从这里开始,没有参数是选项开关,即使它是以连字符开头”。
    一种更好的解决方法是把包袱扔给系统而不是用户,使用参数处理器把参数分成选项开关和非选项开关两种。目前这种简单的argv机制由于使用得太广,因而不可能对它作任何修改。 

    2.4.2 空格---最后的领域
    这里的几个例子,空格从根本上改变了程序的意思或程序的有效性。“\”字符用于对一些字符进行“转义”,包括newline(这里指回车键)。被转义的newline在逻辑上把下一行当做当前行的延续,它可用于连接长字符串。如果在“\”和回车键之间不小心留上一两个空格就会出现问题,\ newline \newline 效果是不一样的。 
    因为你是在寻找无形的东西(在应该是newline的地方出现了一个空格,注意newline并不是一个有形的字符,所以“\”后面有没有空格在实际代码中根本看不出来)。newline在典型情况下用于转义连续多行的宏定义。转义newline的另一种用处是延续一个字符串常量,如下: 
    char a[] = "Hi! How are you? I am quite a \
long string, folded onto 2 lines";
    这种多行字符串常量的问题被ANSI C通过引入相邻字符串常量自动连接的约定得以解决。
    如果所有的空格都弃之不用,也会陷入麻烦
    z = y+++x;
    //correct
    z = y++ + x;
    //error
    z = y+ ++x
    ANSI C规定了一种逐渐为人熟知的“maximal munch strategy”(最大一口策略)这种策略表示,如果下一个标记有超过一种的解释方案,编译器将选取能组成最长字符序列的方案。 
    z = y+++++x;
    唯一有效的编排方式是: 
    z = y++ + ++x;
    它还是会出现编译错误。 

    /*
    ** space.
    */
    #include <stdio.h>
    #include <stdlib.h>
    
    int main( void ){
        int x;
        int y;
        int z;
        
        x = 1;
        y = 2;
        printf( "1:x = %d, y = %d\n", x, y );
        z = x+++y;
        printf( "1:x = %d, y = %d, z = %d\n", x, y, z );
        x = 1;
        y = 2;
        printf( "2:x = %d, y = %d\n", x, y );
        z = x++ +y;
        printf( "2:x = %d, y = %d, z = %d\n", x, y, z );
        x = 1;
        y = 2;
        printf( "3:x = %d, y = %d\n", x, y );
        z = x+ ++y;
        printf( "3:x = %d, y = %d, z = %d\n", x, y, z );
        
        x = 1;
        y = 2;
        printf( "4:x = %d, y = %d\n", x, y );
        /* z = y+++++x;
        ** can't pass compilation:
        ** because of [Error] lvalue required as increment operand
        */
        z = y++ + ++x;
        printf( "4:x = %d, y = %d, z = %d\n", x, y, z );
        
        return EXIT_SUCCESS;
    }
输出:

    有两个指向int的指针并想对int数据进行除法运算时,代码如下。
    除法运算符“/”与“*”操作符之间缺少空格。它们紧贴在一起,被编译器理解成注释的开始部分,并把它与下一个“*/”之间的所有代码都变成注释的内容。 
    int ratio = *x / *y;
    int ratio = *x/*y;

    /*打算结束注释时却由于意外未能结束*/
    int hashval = 0;
    /*PJW hash function from "Compilers: Principles, Techniques, and Tools"
     *by Aho, Sethi, and Ullman, Second Edition.(*/)
    while (cp < bound) {
        unsigned long overflow;
        hashval = (hashval << 4) + *cp++;
        if ((overflow = hashval & (((unsigned long)OxF) << 28)) != 0) {
            hashval ^= overflow | (overflow >> 24);
        }
        hashval %= ST_HASHSIZE; /*选择起始桶*/ 
        /*
         * 搜索每个表,这次搜索名字。如果失败,保存该字符串?
         * 进入字符串的指针,然后返回它
         */
        for (hp = &st_ihash; ;hp = hp->st_hnext) {
            int probeval = hashval; /*下一个探测值*/ 
        } 
    }
    2.4.3 C++的另一种注释形式
    把该符号以后直至行末的内容均作为注释内容。 
    a //*
    //*/ b
    上面的代码在C语言中表示a/b,但在C++语言中表示a。C风格的注释在C++语言中依然有效。

    2.4.4 编译器日期被破坏
    将源文件的timestamp转换为表示当地格式日期的字符串。
    调用stat()得到UNIX格式的源文件修正时间。
    调用localtime()将其转换成tm结构。
    最后调用strftime()函数,把tm结构转换成以当地日期。
    格式表示的ASCII字符串。
    症状就是表示日期的字符串被破坏
    #include <stdio.h>
    #include <time.h>

    char *localized_time(char *filename) {
        struct tm *tm_ptr;
        struct stat stat_block;
        char buffer[120];
        /*获得源文件的timestamp,格式为time_t*/
        stat(filename, &stat_block);
        /*把UNIX的time_t转换为tm结构,里面保存当地时间*/
        tm_ptr = localtime(&stat_block.st_mtime);
        /*把tm结构转换为以当地日期格式表示的字符串*/
        strftime(buffer, sizeof(buffer), "%a %b %e %T %Y", tm_ptr);
        return buffer;  //program takes place
    } 

    int main() {
        char *p = localized_time("test.txt");
        printf("date = %s\n", p);
        return 0;
    }
    buffer是一个自动分配内存的数组,是该函数的局部变量。当控制流离开声明自动变量(即局部变量)的范围时,由于该变量已被销毁,谁也不知道指针所指向的地址的内容是什么。
    在C语言中,自动变量是在堆栈中分配内存。当包含自动变量的函数或代码退出时,它们所占用的内存便被回收,它们的内容肯定会被下一个所调用的函数覆盖。这一切取决于堆栈中先前的自动变量位于何处,活动函数声明了什么变量,以及写入了什么内容。原先的变量地址的内容既可能被立即覆盖,也可能稍后才能覆盖,这就是日期破坏问题难以发现的原因。 
    //1.返回一个指向字符串常量的指针
    char *func() {
        return "Only works for simple strings";
        /*只适用于简单的字符串*/ 
    } 
    如果字符串常量存储于只读内存区但以后需要改写它时,你也会有麻烦。
    //2.使用全局声明的数组
    char *func() {
        ...
        my_global_array[i] = 
        ...
        return my_global_array;
    } 
    它的缺点在于任何人都有可能在任何时候修改这个全局数组,而且该函数的下一次调用也会覆盖该数组的内容。
    //3.使用静态数组
    char *func() {
        static char buffer[20];
        ...
        return buffer; 
    } 
    这就可以防止任何人修改这个数组。只有拥有指向该数组的指针的函数(通过参数传递给它)才能修改这个静态数组。但是,该函数的下一次调用将覆盖这个数组的内容,所以调用者必须在此之前使用或备份数组的内容。与全局数组一样,大型缓冲区如果闲置不用,是非常浪费内存空间的。
    //4.显式分配一些内存,保存返回的值。 
    char *func() {
        char *s = malloc(120);
        ...
        return s;
    } 
    这个方法具有静态数组的优点,而且在每次调用时都创建一个新的缓冲区,所以该函数以后的
调用不会覆盖以前的返回值。它适用于多线程的代码(在某一时刻具有一个以上的活动线程的程序)。它的缺点在于程序员必须承担内存管理的责任。
    //5.也许最好的解决方法就是要求调用者分配内存来保存函数的返回值。为了提高安全性,调用者应该通知指定缓冲区的大小(就像标准库中fgets()函数所要求的那样)
    void func(char *result, int size) {
        ...
        strncpy(result, "That's be in the data segment, Bob", size);
    } 

    buffer = (char*)malloc(size);
    func(buffer, size);
    ...
    free(buffer);
    如果程序员可以在同一代码块同时进行malloc和free操作,内存管理是最为轻松的。这个解决方案可以实现这一点。
    return local_array;
    /*return local_array
    *"function returns pointer to automatic"(函数返回一个指向自动变量的指针)。*/
     
    2.4.5 lint程序不应该被分离出来
    lint程序能够检测到问题,并向你发出警告。
    把编译器中所有的语义检查措施都分离出来。错误检查由一个单独的程序完成,这个程序被称为lint。这样编译器可以做得更小、更快而且更简单。
    小启发
    早用lint程序,勤用lint程序
    lint程序是软件的道德准则,当你做错事,他会告诉你那里不对。应该始终使用lint程序,按照它的道德标准办事。
    几个真正严重的bug
    实参的类型在函数和调用之间发生了转变
    一个期望接受3个参数的函数实际上只传递它一个参数,该函数从堆栈中再抓两个参数。
    变量在设置(初始化或赋值)前使用。
    经验不断证明,把lint程序作为一个独立的工具通常意味着把lint程序束之高阁。

    2.5 轻松一下---有些特性确实就是Bug 
    当然,任何人都知道从相对论的角度讲,信息也是有质量的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixin_40186813

你的能量无可限量。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值