• 《C陷阱和缺陷》读书笔记-其一:词法和语法陷阱


    第一章:词法陷阱

    1、= 与 ==

    赋值运算符"=",表示将等号右侧的值赋值给左侧的变量,注意点在于等号左侧必须为变量,等号右侧可以为变量、常量或者表达式;
    比较运算符"==",判断双等号左右两侧的值是否相等,如果相等则该比较表达式返回true,否则返回false;其中两侧可以为变量、常量或者表达式;

    (1)在if条件或者循环条件语句中,经常会用到比较运算符“==”,判断两个值是否相等,并分别进行操作;如果,此时将比较运算符误写成赋值运算符,则可能导致出现if条件恒为真或者进入死循环;如下程序段:

    int start = 0;
    int end = 100;
    // 预期判断start与end是否相等,相等则退出,实际上,当end为非零值时if条件恒真
    if (start = end) {
    	break;
    }
    

    建议:在比较运算符使用时,将常量写在左侧,利用赋值运算符的左侧必须为变量的原则进行校验,此时若将“==”误写为“=”时,编译器会报错;

    (2)如果在赋值过程中,将“=”误写成“==”,会更令人头疼,编译器很难发现这个错误,且人工排查也难,只能在遇到变量赋值异常的时候,进一步确认变量是否真正赋值成功;

    int i = 0;
    int j = 5;
    if (i != j) {
    	// 预期当i与j不等时,将j的值赋值给i,但实际上比较运算符操作后,i和j的值没有任何变化
    	i == j;
    }
    

    (3)比较运算符和位运算符,也需要注意避免混用:& 和 &&; | 和 ||

    2、 词法分析中贪心法

    当编译器一次读入多个字符时,划分原则:每一个符号应该包含尽可能多得字符。
    即,从左到右一个一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个符号,判断ok则再继续读,直到读入的字符组成的字符串已经不再可能组成一个有意义的符号,也被称为“贪心法”。
    a---b会被解释成(a--) - b,而不是a - (--b)
    y = x/*p /* p指向除数*/会被解释成y = x,注释为/*p /*p指向除数*/

    3、整形常量

    (1)如果一个整形常量的第一个字符时数字0,那么该常量将被视作八进制数。需要注意在数字对其时,避免在数字第一个字符添加0。

    4、字符与字符串

    (1)单引号引起的一个字符实际上代表一个整数,其值为该字符在编译器采用的字符集中的序列值,如在ASCII字符集中,'a'的含义与十进制97严格一致;
    (2)双引号引起的字符串,表示一个指向无名数组的起始字符的指针,且为常量指针,即字符串中内容不能改变,该数组被双引号之间的字符以及衣蛾额外的二进制值为0的字符''初始化。
    (3)简而言之,单引号表示的字符实际上是一个整数,双引号表示的常量字符串实际上是一个指针。

    第二章 语法陷阱

    1、函数声明

    (1)任何变量的声明都由两部分组成:类型以及一组类似表达式的声明符。
    第一步,基本上没啥问题:

    int a;         // 声明一个int型变量a  
    float ff();    // 声明一个返回值为浮点类型的函数,且函数参数为空    
    float *p;      // 声明一个指向浮点数的指针  
    

    第二步,还可以理解:
    注意:()的优先级高于*

    float *g();    // *g()实际上是*(g()),表示声明一个函数,函数的返回值类型为指向浮点数的指针
    float (*h)();  // 表示声明一个指针,该指针是一个函数指针,返回值为浮点型,函数参数为空
    

    (2)将声明转换成该类型的类型转换符:只需要将声明中的变量名和声明末尾的分号去掉,再将剩余的部分用一个括号整个封起来即可。如:

    float (*h)();   // 声明一个指向返回值为浮点类型的函数指针
    (float (*)());   // 表示一个“指向返回值为浮点类型的函数指针”的类型转换符
    

    如何调用函数指针:如上,h为一个函数指针,h则是该指针指向的函数,(h)()就是调用该函数的方式,ANSI C标准允许将其简写为h()。

    (3)(*(void (*)())0)();
    计算机启动,调用首地址为0的位置,如上,void ()() 是一个类型转化符,将0转换为一个指向返回值为void类型且入参为空的函数指针,
    在利用
    运算可以得到对应的函数,(*0)(),即可进行调用。

    (4)void (*signal(int, void(*)(int)))(int);
    信号处理函数,如上上,void (*h)()表示声明一个指向返回值为void类型且参数为空的函数指针h;
    则 void (*signal)(int)表示声明一个指向返回值void类型且参数为int型的函数指针signal;
    如果有,void (*signal())(),可以看做signal()调用后,返回一个函数指针,该指针指向的函数类型为void,且入参为int;
    至此,如果signal如果添加两个入参:一个整形和一个函数指针类型,就变成了标题中形式;
    使用typedef简化函数声明如下:

    typedef viod (*HANDLER)(int);  
    HANDLER sinnal(int, HANDLER);
    

    2、运算符的优先级

    (1)最高优先级:() [] -> .
    (2)其次是单目运算符:! -- ++ - (type) * & sizeof
    (3)再次是双目运算符,三目运算符,最后是逗号
    (4)任何一个逻辑运算符的优先级低于任何一个关系运算符;
    (5)移位运算符的优先级低于算符运算符,高于关系运算符;
    (6)赋值运算符的优先级低于比较运算符;

    3、语句结束标志——分号

    (1)多加分号:在if语句或者while语句的条件判断后多加一个分号,表明当前条件体或者循环体为空:

    if (x[i] > big); 
    {
    	// 获取x[i]的最大值
    	big = x[i];
    }
    

    利用一下缩进可以解决:

    if (x[i] > big) {
    	big = x[i];
    }
    
    while (i > big) {
    	i++;
    }
    

    (2)少加分号:当声明结尾的分号丢失时,编译器可能会将声明的类型作为函数的返回值类型:

    struct logrec {
    	int date;
    	int time;
        int code;
    } 
    
    main()
    {
    	// 此时编译器可能会认为main函数的返回值类型为struct logrec
    }
    

    4、switch语句

    C语言的switch-case结构中,case是真正意义上的标号,程序的控制流程会直通case标号,类似于goto语句;

    如果需要在执行完当前case后立即退出switch,需要在case部分添加break,如果有意遗漏,需要添加注释说明;

    利用case的这种直通特性,有时候可以构造处精巧的程序。

    5、函数调用

    在函数调用时,即使函数不带参数,也需要包括参数列表,如f()。

    6、悬挂的else

    else始终与同一对括号内最近的未匹配的if结合;使用完备的大括号,可以有效避免if-else匹配的错误。

  • 相关阅读:
    【Stanford Machine Learning Open Course】1. 机器学习介绍
    【Stanford Machine Learning Open Course】3. 线性回归问题两种解法:正规方程组解法 & 梯度下降法
    【linux】crontab周期性/定时启动任务
    【python】 IOError: [Errno 32] Broken pipe
    关注性能:循环的耗时及编译优化的影响
    【Linux】shell: 获取时间间隔到毫秒、微秒级别
    【Stanford Machine Learning Open Course】4. 特征优化
    【Stanford Machine Learning Open Course】2. 线性回归问题介绍
    拖延处理技巧汇编摘自《拖延心理学》
    JavaFx版本植物大战僵尸
  • 原文地址:https://www.cnblogs.com/HZL2017/p/14905756.html
Copyright © 2020-2023  润新知