• C语言缺陷与陷阱读书笔记(三)


    语义陷阱

    3.1 指针和数组###

    任何一个数组下标运算都等同于一个对应的指针运算,因此完全可以依据指针行为定义数组下标的行为。
    数组名被当作该数组下标为0的元素的指针。

    sizeof(a)的结果是整个数组的大小,而不是指向数组a的元素的指针的大小。

    a即数组a中下标为0的元素的引用,同理(a+1)是数组a中下标为1的元素的引用,以此类推,*(a+i)即数组a中下标为i的元素的引用,简记为a[i].

    带方括号的下标形式很明显要比完全用指针来表达简便得多。

    声明指向数组的指针的方法:

    int calendar[12][31];
    int (*monthp)[31];
    monthp=calendar;
    

    monthp将指向数组calendar的第一个元素,也就是数组calendar的12个有着31个元素的数组类型元素之一。

    以指针monthp以步进的方式遍历数组calendar:

    int (*monthp)[31];
    for(monthp=calendar;monthp<&calendar[12];monthp++)
           //处理一个月份的情况
    

    处理指针monthp所指向的数组的元素:

    int (*monthp)[31];
    for(monthp=calendar;monthp<&calendar[12];monthp++)
    {
          int *dayp;
          for(dayp=*monthp;dayp<&(*monthp)[31];dayp++)
                  *dayp=0;
    }
    

    3.2 非数组的指针

    在C语言中,字符串常量代表了一块包括字符串中的所有字符以及一个空字符('')的内存区域的地址。

    char *r, *malloc();
    //加1是因为strlen返回参数中的字符串所包含的字符数目并不包括空字符,故加1
    r=malloc(strlen(s)+strlen(t)+1);
    //malloc函数分配失败检测
    if(!r)
    {
        
        complain();
        exit(1);
    }
    
    
    strcpy(r,s);
    strcat(r,t);
    
    //释放内存
    free(r);
    

    3.3 作为参数的数组声明

    使用数组名作为参数,那么数组名会立刻被转换为指向该数组第一个元素的指针.
    下面两个语句作用相同:

    char hello[]="hello";
    printf("%s
    ",hello);
    printf("%s
    ",&hello[0]);
    

    下面两个函数作用相同:

    main(int argc,char * argv[]);
    main(int argc,char ** argv);
    

    3.4 避免举隅法

    举隅法(synecdoche):以含义更宽泛的词语来替代含义相对较窄的词语,或者相反。
    经常会混淆指针与指针所指向的数据

    char *p, *q;
    //p的值是一个指向有'x','y','z'和''四个字符组成的数组的起始元素的指针。
    p="xyz";
    //此时,复制指针后q和p是两个指向内存同一地址的指针,但是没有复制内存中的字符。
    q=p;
    

    赋值指针并不同时复制指针所指向的数据。

    3.5 空指针并非空字符串

    当常数0倍转换为指针使用时,不能使用该指针所指向的内存中存储的内容。

    if (strcmp(p,(char *) 0) == 0)....
    

    上述语句会报错,因为其试图使用0转换为指针后的存储的内容。

    3.6 边界计算与不对称边界

    栏杆错误,也称“差一错误(off-by-one error),要避免栏杆错误,需要运用一下两个通用原则:

    1.首先考虑最简单情况下的特例,然后将所得的结果外推

    2.仔细检查边界,绝不掉以轻心

    运用:

    计算假如整数x满足边界条件x>=16且x<=37,那么此范围内x的可能取值个数有多少个?

    根据原则一,最简单的情况是求x>=16且x<=16,此时x的取值个数为1个,因此,当上界与下界重合时,此范围满足条件的整数序列只有一个元素。

    再将结果外推,假定下界为1,上界为h。如何满足上界与下界重合,即h-1=0,那个满足条件的整数序列就有h-1+1个元素,因此本例中答案为37-16+1=22。

    可以通过另外一种方式来解决,上例中x>=16且x<=37,等同于x>=16且x<38。这里的上界是”出界点“,即不包括在取值范围之中;下界是”入界点“,即包括在取值范围之中。我们可以得出以下三个结论:

    1.取值范围的大小就是上界与下界之差。

    2.如果取值范围为空,那么上界等于下界。

    3.即使取值范围为空,上界也永远不可能小于下界。

    不对称边界适用于C语言,对数组赋值时可以写成:

    int a[10],i;
    for (i=0; i<10 ; i++)
            a[i]=0;
    

    对于处理缓冲区时,可以将上界视作某序列中第一个被占用的元素,把下界视作序列中第一个被释放的元素。比如:

    #define N 1024
    //缓冲区的大小
    static char buffer[N];
    //指针变量,指向缓冲区的当前位置
    static char *bufptr;
    //初始化指针变量
    bufptr=buffer;
    
    void bufwrite(char *p,int n)
    {
    //进行n次迭代
    while(--n>=0)
    {
        //k为每次移动的字符数,rem为缓冲区剩下可容纳的字符数
        int k,rem;
        //这里运用了不对称边界
        if(bufptr==&buffer[N])
            //将缓冲区的内容写出,并重置指针
            flushbuffer();
            
        rem=N-(bufptr-buffer);
        //控制边界大小
        k=n>rem?rem:n;
        //输出k的字符,从缓冲区的第一个字符开始复制
        memcpy(bufptr,p,k);
        //将指针bufptr的地址向前移动k个字符,使其仍然指向第一个未被占用的字符
        bufptr+=k;
        //输入字符串指针p前移k个字符
        p+=k;
        //将待转移的字符数减去k
        n-=k;
        
        *bufptr++=*p++;
    }
    }
    
    //每次移动k的字符
    void memcpy(char *dest,const char *source,int k)
    {
    while(--k>=0)
        *dest++=*source++;
    }
    

    另外一个计数的例子:
    此程序按照一定顺序生成一些整数,并将这些整数按列输出,程序的输出可能包括若干页的整数,每页包括NCOLS列,每列包括NROWS个元素,每个元素就是一个待输出的整数。

    //最后一列不必缓存,直接输出
    #define  BUFSIZE (NROWS*(NCOLS-1))
    static int buffer[BUFSIZE];
    static int *bufptr=buffer;
    
    //缓冲区满时打印数据
    void print(inti n)
    {
    if(bufptr==&buffer[BUFSIZE])
    {
        static int row=0;
        int *p;
        for(p=buffer+row;p<bufptr;p+=NROWS)
            printnum(*p);
    
        //打印当前行的最后一个元素
        printnum(n);
        //另起新的一行
        printnl();
    
        if(++row==NROWS)e
        {
            printpage();
            //重置当前行号
            row=0;
            //重置指针
            bufptr=buffer;
        }
    }
    else
        *bufptr++=n;
    }
    
    
    
    //打印缓冲区所有剩余元素
    void flush()
    {
    int row;
    //计算缓冲区中剩余的数目
    int k=bufptr-buffer;
    if(k>NROWS)
        k=NROWS;
    if(k>0)
    {
        for(row=0;row<k;row++)
        {
            int *p;
            for(p=buffer+row;p<bufptr;p+=NROWS)
                printnum(*p);
            printnl();
        }
    
        printpage();
    }
    }
    

    3.7 求值顺序

    C语言中的某些运算符总是以一种已知的,规定的顺序来对其操作数进行求值,但是不是所有都是如此。
    运算符&&和运算符||对于保证检查操作按照正确的顺序执行至关重要。

    if (y !=0 && x/y > tolerance)
             complain();
    

    保证仅当y非0时才进行求值操作。

    i=0;
    while (i<n) 
           y[i] = x[i++];
    

    上述代码并不能保证y[i]的地址在i的自增操作之前被求值,因为赋值运算符并不保证任何求值顺序,应该改为:

    i=0;
    while(i<n)
          y[i]=x[i];
          i++;
    

    或者改为:

    for (i=0;i<n;i++)
              y[i]=x[i];
    

    3.9 整数溢出

    整数溢出的情况出现在有符号运算中。
    下面的代码是检查a+b是否溢出:

    if (a + b <0)
           complian();
    

    其中a,b都是非负整型变量。a和b进行相加时,内部寄存器的状态可能为“溢出”而不是负,导致不能正常检查。

    如何解决:
    1.将a和b强制转换为无符号整数。

    if ((unsigned)a + (unsigned) b >INT_MAX)
            complain();
    

    其中INT_MAX是一个已定义常量,代表可能的最大整数值。

    2.通过类型自动转换来进行

    if (a >INT_MAX-b)
            complain();
    

    3.10 为函数main提供返回值

    函数main与其他函数一样,如果并未显式声明返回类型,那么函数返回类型默认为整型。但是main函数的返回值可以告知系统程序调用执行成功或者失败。

    #include <stdio.h>
    
    main()
    {
        printf("hello world
    ");
        return 0;
    }
  • 相关阅读:
    (转)PHP获取今天、昨天、明天的日期
    (转)META http-equiv="refresh" 实现网页自动跳转
    (转)PHP中文处理 中文字符串截取(mb_substr)和获取中文字符串字数
    (转)Apache2 httpd.conf 配置详解 (二)
    Process finished with exit code 129
    Importing image to python :cannot import name 'imread'
    CUDA运行时错误 --- CUDA_ERROR_LAUNCH_FAILED: unspecified launch failure
    Recommendation system
    php手动实现ip2long和long2ip
    git将某个分支的代码完全覆盖另一个分支
  • 原文地址:https://www.cnblogs.com/luyoujun/p/4827869.html
Copyright © 2020-2023  润新知