zoukankan      html  css  js  c++  java
  • <编程精粹:编写高质量C语言代码> 读书笔记

    0.规则
    <The Elements of Programming Style>
    <The Elements of Style>

    1.假想的编译程序
    (1)使用编译器提供的所有的可选警告设施

    增强类型静态检查的能力
    eg: void* memchr(const void* str, int ch, int size);
    那个调用该函数时,即使互换其字符ch和大小size参数,编译器也不会发出警告

    但是如果在函数原型中使用更加精确的类型,就可以增强原型提供的错误检查能力
    void* memchr(const char* str, unsigned char ch, size_t size);

    注:引入无符号数可以增强类型检查能力,但是也导致 无符号数带来的隐式转换错误(有符号数必须转为无符号数)

    (2)使用lint等静态检查工具来检查编译器漏掉的错误
    (3)如果有单元测试,就进行单元测试

    2.自己设计并使用断言

    /* memcpy v1: 拷贝不重叠的内存块 */
    void* memcpy(void* to, const void* from, size_t size)
    {
        void* pto = (char*)to;
        const void* pfrom = (const char*)from;
        if (pto == NULL || pfrom == NULL) {
            fprintf(stderr, "Bad args in memcpy
    ");
            abort();
        }
        while (size-- > 0)
            *pto++ = *pfrom++;
        return pto;
    }
    /* memcpy v2: 拷贝不重叠的内存块 */
    void* memcpy(void* to, const void* from, size_t size)
    {
        void* pto = (char*)to;
        const void* pfrom   = (const char*)from;
    
    #ifdef DEBUG
        if (pto == NULL || pfrom == NULL) {
            fprintf(stderr, "Bad args in memcpy
    ");
            abort();
        }
    #endif
        while (size-- > 0)
            *pto++ = *pfrom++;
        return pto;
    }

    既要维护程序的 release 版本,又要维护程序的 debug 版本
    利用#ifdef DEBUG 调试宏这种方法的关键是保证调试代码不会在最终产品中出现

    /* memcpy v3: 拷贝不重叠的内存块 */
    void* memcpy(void* to, void* from, size_t size)
    {
        void* pto = (char*)to;
        const void* pfrom = (const char*)from;
        assert(pto != NULL && pfrom != NULL);
        while (size-- > 0)
            *pto+= = *pfrom++;
        return pto;
    }


    尽可能多的使用断言,及早发现错误:
    必须使用断言对函数的每个指针参数进行检查
    必须立即使用断言对获取的资源(malloc获取的指针, fopen获取的FILE指针, 打开的数据库连接,获取的文件描述符等等)进行安全检查

    /* memcpy v4: 拷贝不重叠的内存块 */
    void* memcpy(void* to, const void* from, size_t size)
    {
        void* pto = (char*)to;
        const void* pfrom = (const char*)from;
        assert(pto != NULL && pfrom != NULL);
        assert(pto >= pfrom + size || pfrom >= pto + size);  /* 检查是否重叠 */
        while (size-- > 0)
            *pto++ = *pfrom++;
        return pto;
    }

    在程序中使用断言检查语法中未定义行为特性的非法使用

    3.为子系统设防
    内存管理程序,可能犯的错误:
    a.分配一个内存块并使用未经初始化的内容
    b.释放一个内存块但继续引用其中的内容
    c.调用realloc对一个内存块进行扩展,因此原来的内容发生了存储位置的变化,但程序引用的仍是原来存储位置的内容
    d.分配一个内存块后立即"失去"了它,因为没有保存指向所分配内存块的指针
    e.读写操作越过了所分配内存块的边界
    f.没有对错误情况进行检查

    /* new_memory v1 : 分配一个内存块 */
    int new_memory(void** ptr, size_t size)
    {
        unsigned char** p = (unsigned char**)ptr;
        *p = (unsigned char*)malloc(size);
        return (*p != NULL);
    }

    然后如下调用:
    if (new_memory(&block, 32))
    成功,block指向所分配的内存块
    else
    不成功,block等于NULL

    根据ANSI标准,调用malloc存在两处未定义行为,必须加以处理:
    a.分配长度为0时,结果未定义
    b.malloc分配成功,返回的内存块的内容未定义,可以是0,也可以是随意的信息

    /* new_memory v2: 分配一个内存块 */
    /* 加上内存块大小的检查和内存块的填充初始化 */
    #define INIT_VALUE 0xA3
    int new_memory(void** ptr, size_t size)
    {
        unsigned char** p = (unsigned char**)ptr;
        assert(ptr != NULL && size != 0);
        *p = (unsigned char*)malloc(size);
    #ifdef DEBUG
        {
            if (*p != NULL)
                memset(*p, INIT_VALUE, size);
        }
    #endif
        return (*p != NULL);
    }

    在程序的调试版本中保存额外的信息,就可以提供更强的错误检查
    只要相应的 release 版本能够满足要求,就可以在debug版本加入尽可能多的调试代码来检查错误

    4.对程序进行逐条跟踪
    5.糖果机界面

    /* strdup: 为一个字符串建立副本 */
    char* strdup(const char* str)
    {
        char* newstr = (char*)malloc(strlen(str) + 1);
        assert(newstr);
        strcpy(newstr, str);
        retrun newstr;
    }

    不要把错误标志和有效数据混杂在一起返回

    int resize_memory(void** ptr, size_t newsize)
    {
        unsigned char** p = (unsigned char**)ptr;
        unsigned char* presize = (unsigned char*)realloc(*p, newsize);
        if (presize != NULL)
            *p = presize;
        return (presize != NULL);
    }

    一个函数只干一件事,编写功能单一的函数,而不是多功能集一身的函数
    反面教材: void* realloc(void** ptr, size_t size);
    该函数改变先前已分配的内存块大小:
    a.如果新请求大小小于原来长度,realloc释放该块尾部多余的内存空间,返回的ptr不变
    b.如果新请求大小大于原来长度,扩大后的内存块有可能被分配到新地址处,该块的原有内容被拷贝到新的位置,返回的指针指向扩大后的内存首地址,并且新内存块扩大部分未经初始化
    c.如果满足不了扩大内存块的请求,realloc返回NULL,当缩小内存块时,总是成功的
    d.如果ptr == NULL 则realloc的作用相当于调用 malloc(size);
    e.如果ptr != NULL 且 size == 0 则realloc的作用相当于调用 free(ptr);
    f.如果ptr == NULL 且 size == 0 则realloc结果未定义

    在允许大小为0的参数时要特别小心,一开始就要为函数的输入选择严格的定义,并最大限度地利用断言
    为了程序的易读性和扩充性,不要使用布尔类型作为函数的参数类型

    6.风险事业
    ANSI并没有标准化 char,int,long 这样的基本数据类型
    ANSI没有标准化 基本数据类型的原因:C语言产生于70年代,等到标准化时已经有了20多年写出来的代码基,定义严格的标准将会使大量现存代码无效

    char* strcpy(char* pto, const char* pfrom)
    {
        char* ptr = pto;
        while ((*pto ++ = *pfrom++) != '')
            NULL;
        return ptr;
    }

    上述代码在任何编译系统上都可以正确工作

    int strcmp(const char* left, const char* right)
    {
        for (NULL; *left = *right; left++, right++) {
            if (left == '')
                return 0;
        }
        return ((*left < *right) ? -1 : 1);
    }

    上述代码由于最后一行的比较操作而失去了可移植性。修改strcmp,只需声明 left 和 right 为 unsigned char 指针,或者直接在比较中先使用强制转型

    (*(unsigned char*)left < *(unsigned char*)right)
    for (unsigned char ch = 0; ch <= UCHAR_MAX; ch++)
        array[ch] = ch;

    如果 ch = UCHAR_MAX 时,执行最后一次循环,循环之后,ch增加为 UCHAR_MAX + 1, 这将引起ch上溢为0,因此该循环变成了无限循环

    if (n < 0)
        n = -n;

    这段代码可能会出现bug. 在二进制补码系统中,数据类型的表达范围不是对称的,例如 char [-128, 127) 如果n正好为最小负数,则 n = -n 则会上溢

    7.编码中的假象
    不要引用不属于你的未知存储区,"引用"意味着不仅读而且要写,这样可能会和别的进程产生不可思议的相互作用

    /* unsign_to_str: 将无符号数转换为字符串 */
    void unsign_to_str(unsigned u, char* str)
    {
        char* start = str;
        while (u > 0) {
            *str++ = (u % 10) + '';
            u /= 10;
        }
        *str = '';
        reverse_string(start);
    }

    上述代码 是反向顺序导出数字,确正向顺序建立字符串,所以需要 reverse_string 来重排数字顺序

    void unsign_to_str(unsigned u, char* str)
    {
        assert(u < UMAX);
        /* 将每一位数字从后往前存储, 字符串足够大以便能存储 u 的最大可能值 */
        char* ptr = &str[5]; /* 假设 u <= 65536 */
        *ptr = '';
        while (u > 0) {
            *(--ptr) = (u % 10) + '';
            u /= 10;
        }
        strcpy(str, ptr);
    
    }

    函数能正确工作是不够的,还必须能够防范程序员产生明显的错误
    尽量慎用静态(或全局)存储区传递数据

    紧凑的C代码并不能保证得到高效的机器代码,首先应该考虑的是代码的正确性和可读性

    8.剩下来的就是态度问题

  • 相关阅读:
    sql增删改查-转载
    委托和事件 链接
    三层架构-转载
    ToList()方法
    Invoke--转载
    C# 6.0新特性---语法糖
    索引器
    HBase学习总结(1)
    教程-关于Owner和Parent的区别
    问题-在TreeView使用时,发现选中的树节点会闪烁或消失
  • 原文地址:https://www.cnblogs.com/wwwjieo0/p/3517549.html
Copyright © 2011-2022 走看看