1. 词法“陷阱”

  • = 不同于 == , 可以通过if( 1 == a )来避免
  • & | 不同于 && ||

  • 词法分析中的“贪心法”

    编译器将程序分解成符号的方法是,从左到右一个字符一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。这个处理策略被称为“贪心法”,也称为“大嘴法”
    举例:
    a---b 等价于 a-- - b
    y = x/*p 编译器理解为一段注释的开始,改为 y = x / *p
    a++++b的含义是((a++)++)+b,但是a++的结果不是左值,故该式错误
    老版本的C语言中允许使用 =+ 来代表 += 的含义。所以 a=-1; 被理解为 a =- 1; 即 a -= 1; 而程序员的原意可能是 a = -1;

  • 整型常量 10不等于010

  • 字符与字符串
    1. 用单引号引起的一个字符实际上代表一个整数,整数值对应于该字符在编译器采用的字符集中的序列值。如'a'为0141或97
    2. 用双引号引起的字符串,代表的却是一个指向无名数组起始字符的指针。
      所以
      char *slash = '/'; 编译错误
      printf('\n'); 会在程序运行的时候产生难以预料的错误,而不会给出编译器诊断信息。
    3. 整型数的存储空间可以容纳多个字符,因此有的C编译器允许在一个字符常量中包括多个字符。如'yes'代替"yes"不会被编译器检测到。前者的含义并没有准确定义。
  • 某些C编译器允许嵌套注释 。写一个测试程序:无论是对允许嵌套注释的编译器,还是对不允许嵌套注释的编译器,该程序都能正常通过编译,但含义却不同。

    /*/*0*/**/1
    允许嵌套注释,解释为 /* /* /0 */ * */ 1 即 1
    不允许嵌套注释,解释为 /* / */ 0* /**/ 1 即 0*1

2. 语法“陷阱”

  • 理解函数声明

    1. float *g(), (*h)();
      ()的结合优先级高于*,故g是一个函数,其返回值为float*;h是一个函数指针,h所指向函数的返回值为float
    2. 知道如何声明一个给定类型的变量,那么该类型的类型转换符可以通过以下方式得到:
      只需要把声明中的变量名和声明末尾的分号去掉,再将剩余的部分用一个括号整个“封装”起来即可。
    3. 例子:从 float (*h)(); 得到 (float (*)()) 表示一个“指向返回值为float的函数的指针”的类型转换符

    void (*signal(int , void(*)(int)))(int);

  • 运算符的优先级问题

    if( flag & FLAG != 0 ) ==> if( (flag & FLAG) != 0 ) 运算符 != 的优先级高于 &
    r = hi<<4 + low; ==> r = (hi<<4) + low; 运算符 + 的优先级高于 <<
    算术5个 > 移位2个 > 关系6个 > 按位3个( &|^, 不包括~ ) > 逻辑2个( && ||, 不包括! )
    注意:上面的移位也属于按位运算符,此外,按位运算符还有~

  • 注意作为语句结束标志的分号 struct logrec{};
  • switch语句 case穿透 break;

  • 函数调用

    如果f是一个函数
    f();
    f; 计算函数f的地址,却并不调用

  • “悬挂”else引发的问题:else始终与同一对括号内最近的未匹配的 if 结合;代码缩进

  • C语言允许初始化列表中出现多余的逗号,如

    int days[] = { 31, 28, 31, 30, 31, 30,                 31, 31, 30, 31, 30, 31,               };

    原因:每一行都是以逗号结尾的,这种相似性能够方便自动化的程序设计工具的处理。

3. 语义“陷阱”

  • 指针与数组

    1. C语言中只有一维数组,且数组的大小必须编译时确定。然而,数组的元素可以是任何类型的对象,当然也可以是另外一个数组,故可以仿真一个多维数组。
    2. 对于一个数组,只能做两件事:通过sizeof 确定该数组的大小;获得指向数组下标为0的元素的指针。换句话说,任何一个数组下标运算都等同于一个对应的指针运算。
    3. 例子: int calendar[12][31]; 如果calendar不是用于sizeof 的操作数,而用于其他场合,则calendar总是被转换为一个指向calendar数组起始元素的指针
      如果两个指针指向同一个数组中的元素,则两指针相减有意义
      a+i 等同于 i+a,故 a[i] 等同于 i[a]
  • 非数组的指针

    在C语言中,字符串常量代表了一块包括字符串中所有字符以及一个空字符的内存区域的地址。
    char *r; strcpy(r,s); 错误,r无法提供容纳字符串的内存空间
    char r[100]; strcpy(r,s); 字符数组的大小必须足以容纳包括'\0'在内的字符串s

  • 作为参数的数组声明

    C语言中会自动地将作为参数的数组声明转换为相应的指针声明。
    然而,extern char *hello; 却与 extern char hello[]; 有着天壤之别

  • 空指针并非空字符串

    当我们将0赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容。
    if( p == (char*) 0 ) ... // ok
    if( strcmp(p, (char*)0 ) == 0 ) ... // error,库函数strcmp的实现中会包括查看它的指针参数所指向内存中的内容的操作。
    如果p是一个空指针,printf(p); 和 printf("%s", p); 的行为也是未定义的

  • 边界计算与不对称边界

    a[10] ==> [0,10) ,10-0=10即为元素数量,避免栏杆问题

  • 求值顺序

    C语言中只有四个运算符存在规定的求值顺序: && || ?: ,

  • 整数溢出

    1. 在无符号算术运算中,没有“溢出”一说:所有的无符号运算都是以2的n次方为模
    2. 如果算术运算符的两个操作数,一个无符号,一个有符号,那么有符号整数会被转换为无符号,故“溢出”也不可能发生
    3. 当两个操作数都是有符号整数时,“溢出”可能发生,且其结果是未定义的,作任何假设都是不安全的。
    4. 例子:
      假定a和b是两个非负整型变量,需要检查a+b是否会“溢出”,想当然的 if( a+b<0 ) complain();是错误的,因为关于结果的任何假设都不可靠
      if( (unsigned)a + (unsigned)b > INT_MAX ) complain(); 注意是unsigned int可以存放的最大值是INT_MAX的近两倍,所以大于号左边不会溢出;另外,根据上面的第2点,其实只要对加号两边其中的一个操作数进行强制转换即可
      或者 if( a > INT_MAX-b) complain():
  • 为函数main提供返回值,return 0; 或 exit(0);

    大多数C语言实现都通过函数main的返回值来告知操作系统该函数的执行是成功还是失败。如果main函数并不返回任何值,那么有可能看上去执行失败。

4. 连接

  • 什么是连接器

    1. 连接器一般与C编译器分离,它不知道C语言的诸多细节,然而它却能够理解机器语言和内存布局。编译器的责任是把C源程序“翻译”成对连接器有意义的形式,这样连接器就能够“读懂”C源程序了。
    2. 典型的连接器把编译器或汇编器生成的若干个目标模块,整合成一个被称为载入模块可执行文件的实体,该实体能够被操作系统直接执行。其中,某些目标模块是直接作为输入提供给连接器的;而另外一些目标模块则是根据连接过程的需要,从包括有类似printf函数的库文件中取得的。也就是说,连接器的输入是一组目标模块和库文件,输出是一个载入模块.
    3. 连接器通常把目标模块看出是由一组外部对象组成的。每个外部对象代表着机器内存的某个部分,并通过一个外部名称来识别。程序中的每个函数和每个外部变量,如若没被声明为static,就都是一个外部对象。经过static的“名称修饰”,则不会与其他源文件中的同名函数或同名变量发生命名冲突。当不同的目标模块中包含相同名称的外部对象,此时,连接器需要处理命名冲突。
    4. splint静态程序分析工具
      静态程序分析是指使用自动化工具软件对程序源代码进行检查,以分析程序行为的技术,应用于程序的正确性检查、安全缺陷检测、程序优化等。它的特点就是不执行程序,相反,通过在真实或模拟环境中执行程序进行分析的方法称为“动态程序分析”。
  • 声明与定义

    extern关键字

  • 命名冲突与static修饰符

    如果一个函数仅仅被同一个源文件中的其他函数调用,就应该将该函数声明为static

  • 形参、实参与返回值

    函数未显式指定返回值,则假定其返回值为 int

  • 检查外部类型

    定义处long n; 但是,声明处 extern int n;
    定义处 char filename[] = "/etc/passwd"; 但是,声明处 extern char *filename; 修改,统一改成都使用char [] 或 char *

  • 头文件

    全局变量,函数原型,结构体定义,#define,typedef

5. 库函数

  • C语言没有定义输入/输出语句,通过库函数实现输入/输出操作
  • 返回整数的 getchar 函数,否则,无法和 EOF(int类型)比较
  • 缓冲输出,函数setbuf(stdout, buf); 常量BUFSIZ
  • 使用外部变量errno检测错误

    char *strerror(int errnum);
    void perror(const char *s);

  • 库函数signal

6. 预处理器

  • 不能忽视宏定义中的空格:这一规则不适用于宏调用,只对宏定义适用

  • 宏不是函数

    将宏定义中的每个参数都用括号括起来
    确保宏中的参数没有副作用,如不能使用自增操作符++

  • 宏并不是语句

    #define assert(e) if(!e) assert_error(__FILE__, __LINE__) 避免如此使用,避免造成#define替换后if 配对出错

  • 宏并不是类型定义(使用#define创建的“新类型”当含有指针* 时会出问题),类型定义使用typedef

7. 可移植性缺陷

  • 整数大小

    3种整型类型short,int,long其长度非递减;ANSI标准要求short和int至少16位,long至少32位

  • 字符是有符号整数还是无符号整数

    1. 只有在需要把一个字符值转换为一个较大的整数时,这个问题才变得重要起来
    2. 编译器在转换char类型到int类型时,需要作出选择:将字符作为有符号还是无符号数处理?前者,应同时复制符号位;后者,编译器只需要在多余的位上直接填充0即可。
    3. 如果一个字符的最高位是1,编译器是将该字符当作有符号数,还是无符号数呢?这关系到一个8位字符的取值范围是-128到127,还是0到255。而这一点,又反过来影响到程序员对哈希表或转换表等的设计方式。
    4. 如果编程者关注一个最高位是1的字符其数值究竟是正还是负,可以讲这个字符声明为无符号字符(unsigned char),确保在将该字符转换为整数时都只需将多余的位填充为0即可。
    5. 一个常见的错误认识是:如果c是一个字符变量,使用(unsigned)c就可得到与c等价的无符号整数。这是会失败的,因为在将字符c转换为无符号整数时,c将首先被转换为int型整数,而此时可能得到非预期的结果。正确的方式是使用(unsigned char)c,因为一个unsigned char类型的字符在转换为无符号整数时无需首先转换为int型整数,而是直接进行转换。
  • 内存位置0

    所有对NULL指针的操作都是未定义的

  • 大小写转换

    使用库函数toupper和tolower,而不要使用(c+'A'-'a')之类的,因为在EBCDIC字符集中,字母并不是连续存储的,所以'A'与'a'可能并不是像在ASCII字符集中一样差值固定为32,而是64

  • 移位运算符

    1. 右移时,如果被移位的对象是有符号数,那么C语言实现既可以用0填充(逻辑右移),也可以用符号位填充(算术右移)
    2. 移位操作的位数必须>=0,而严格小于n。加上这个限制条件,能够在硬件上高效地实现移位运算
  • 除法运算时发生的截断

    q = a / b
    r = a % b

《C陷阱与缺陷》读书笔记的更多相关文章

  1. csapp读书笔记-并发编程

    这是基础,理解不能有偏差 如果线程/进程的逻辑控制流在时间上重叠,那么就是并发的.我们可以将并发看成是一种os内核用来运行多个应用程序的实例,但是并发不仅在内核,在应用程序中的角色也很重要. 在应用级 ...

  2. CSAPP 读书笔记 - 2.31练习题

    根据等式(2-14) 假如w = 4 数值范围在-8 ~ 7之间 2^w = 16 x = 5, y = 4的情况下面 x + y = 9 >=2 ^(w-1)  属于第一种情况 sum = x ...

  3. CSAPP读书笔记--第八章 异常控制流

    第八章 异常控制流 2017-11-14 概述 控制转移序列叫做控制流.目前为止,我们学过两种改变控制流的方式: 1)跳转和分支: 2)调用和返回. 但是上面的方法只能控制程序本身,发生以下系统状态的 ...

  4. CSAPP 并发编程读书笔记

    CSAPP 并发编程笔记 并发和并行 并发:Concurrency,只要时间上重叠就算并发,可以是单处理器交替处理 并行:Parallel,属于并发的一种特殊情况(真子集),多核/多 CPU 同时处理 ...

  5. 读书笔记汇总 - SQL必知必会(第4版)

    本系列记录并分享学习SQL的过程,主要内容为SQL的基础概念及练习过程. 书目信息 中文名:<SQL必知必会(第4版)> 英文名:<Sams Teach Yourself SQL i ...

  6. 读书笔记--SQL必知必会18--视图

    读书笔记--SQL必知必会18--视图 18.1 视图 视图是虚拟的表,只包含使用时动态检索数据的查询. 也就是说作为视图,它不包含任何列和数据,包含的是一个查询. 18.1.1 为什么使用视图 重用 ...

  7. 《C#本质论》读书笔记(18)多线程处理

    .NET Framework 4.0 看(本质论第3版) .NET Framework 4.5 看(本质论第4版) .NET 4.0为多线程引入了两组新API:TPL(Task Parallel Li ...

  8. C#温故知新:《C#图解教程》读书笔记系列

    一.此书到底何方神圣? 本书是广受赞誉C#图解教程的最新版本.作者在本书中创造了一种全新的可视化叙述方式,以图文并茂的形式.朴实简洁的文字,并辅之以大量表格和代码示例,全面.直观地阐述了C#语言的各种 ...

  9. C#刨根究底:《你必须知道的.NET》读书笔记系列

    一.此书到底何方神圣? <你必须知道的.NET>来自于微软MVP—王涛(网名:AnyTao,博客园大牛之一,其博客地址为:http://anytao.cnblogs.com/)的最新技术心 ...

  10. Web高级征程:《大型网站技术架构》读书笔记系列

    一.此书到底何方神圣? <大型网站技术架构:核心原理与案例分析>通过梳理大型网站技术发展历程,剖析大型网站技术架构模式,深入讲述大型互联网架构设计的核心原理,并通过一组典型网站技术架构设计 ...

随机推荐

  1. 【算法题】- 求和等于K子数组

    一整数(有正有负)数组,用尽量少的时间计算数组中和为某个整数的所有子数组 public class SumK { public static void main(String[] args) { in ...

  2. Backbone Backbone-localStorage demo

    <!doctype html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  3. 使用Windows Azure PowerShell远程管理Windows Azure虚拟机

    对于Windows Azure,如果你还在使用windowsazure.com门户来管理虚拟机,那就显得不怎么高上大了.Windows Azure PowerShell 是一个功能强大的脚本环境,可用 ...

  4. POJ 3468 A Simple Problem with Integers (伸展树区间更新求和操作 , 模板)

    伸展数最基本操作的模板,区间求和,区间更新.为了方便理解,特定附上一自己搞的搓图 这是样例中的数据输入后建成的树,其中的1,2是加入的边界顶点,数字代表节点编号,我们如果要对一段区间[l, r]进行操 ...

  5. 《Linux内核设计与实现》读书笔记 - 目录 (完结)【转】

    转自:http://www.cnblogs.com/wang_yb/p/3514730.html 读完这本书回过头才发现, 第一篇笔记居然是 2012年8月发的, 将近一年半的时间才看完这本书(汗!! ...

  6. 使用Myeclipse完成Hibernate的逆向工程

    前面已经提到过Hibernate的开发流程一般有两种: 1.由Domain object > mapping > db 2.由db开始,用工具生成生成mapping 和Domain obj ...

  7. android利用数字证书对程序签名

     签名的必要性 1.  防止你已安装的应用被恶意的第三方覆盖或替换掉. 2.  开发者的身份标识,签名可以防止抵赖等事件的发生. 开发Android的人这么多,完全有可能大家都把类名,包名起成了一个同 ...

  8. JavaScript判断浏览器类型及版本

    JavaScript是前端开发的主要语言,我们可以通过编写JavaScript程序来判断浏览器的类型及版本.JavaScript判断浏览器类型一般有两种办法,一种是根据各种浏览器独有的属性来分辨,另一 ...

  9. dom4j创建格式化的xml文件

    import java.io.File;import java.io.FileInputStream;import java.io.FileNotFoundException;import java. ...

  10. IOS中控制器的重要方法使用

    1.屏幕即将旋转的时候调用(控制器监控屏幕旋转) - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfac ...