《C缺陷与陷阱》阅读笔记

《C缺陷与陷阱》

第一章 词法“陷阱”

  • 1.编译器会将程序分解为一个一个符号的部分,这个步骤称为**“词法分析器”**。

  • 2.= 不同于 ==,在下面的代码中,容易进入死循环了:

  • while (c = '' || c == '\t' || c =='\n') {
        c = getc(f);
    }
    

​ 像这种错误,有的编译器会提示程序员,但是有的不会。这样就很容易埋下坑了。

  • 3.&和| 不等于 &&和||

  • 4.整型常量:如果一个整型常量的第一个数字是0,那么该常量将被视作八进制数。

  • 5.字符与字符串:用单引号引起的一个字符实际上代表一个整数,整数值对应于该字符在编译器采用的字符集中的序列值。因此,对于采用ASCII字符集的编译器而言,‘a’的含义与0141(八进制)或者97(十进制)严格一致。用双引号引起的字符串,代表的却是一个指向无名数组起始字符的指针

第二章 语法“陷阱”

  • 1.运算符的优先级问题:对于以下表达式

    if (flag & FLAG != 0) ...
    对于上面的表达式,很容易理解成:1)先判断flag&FLAG;2)再判断其结果是否不为0.
    

但是,这样的理解往往是错误的。由于!=的优先级高于&,所以其实是先判断FLAG != 0;再将其结果与flag进行&操作。

  • 2.在C语言程序中不小心写了一个分号可能造成难以估计的后果:

    if (x[i] > big) ;
        big = x[i];
    

多写了一个分号,导致后面的语句永远会被执行,不受if语句的限制。

第三章 语义“陷阱”

  • 1.C语言中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来。当然,C语言中数组的元素可以是任何类型的对象,当然也可以另外一个数组。

  • 2.对于一个数组,我们只能够做两件事:确定该数组的大小,以及获得指向该数组下标为0的元素的指针。**其他有关数组的操作,哪怕它们看上去是以数组下标进行运算的,实际上都是通过指针进行的。**换句话说,任何一个数组下标运算都等同于一个对应的指针运算,因此我们完全可以依据指针行为定义数组下标的行为。

  • 3.如果一个指针指向的是一个数组的元素,那么给该指针加上一,就相当于让该指针指向数组的下一个元素。

  • 4.定义a[3],那么sizeof(a)的结果是整个数组a的大小,而不是指向数组a的元素的指针的大小。

  • 5.实际上,由于a + i 与 i + a 的含义一样,因此 a[i] 与 i[a] 也具有同样的含义。但是,我们没有理由写后面一种让人摸不着头脑的写法。

  • 6.对于

    int calender[12][31]
    

那么,calender[4] 指的是什么呢?

因为calender是一个有着12个数组类型元素的数组,它的每个数组类型元素又是一个有着31个整型元素的数组,所以 calender[4] 是calender数组的第5个元素,是calender数组中12个有着31个整型元素的数组之一。因此,calender[4]的行为也就表现为一个有着31个整型元素的数组的行为。例如,sizeof(calender[4])的结果是31 * sizeof(int).

  • 7.C语言强制要求我们必须声明数组大小为一个常量。

  • 8.对于:

    char *p, *q;
    p = "xyz";
    q = p;
    

p和q现在是两个指向内存中同一地址的指针。这个赋值语句并没有同时复制内存中的字符。

    1. 对于:

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

上面的代码误把 i < 10 写成了 i <=10; 由于并不存在a[10]设置为0,也就是内存中在数组a之后的一个字的内存被设置为0. 如果这个段内存正好用来放置了变量i, 那么这个循环就变成了死循环了。

第四章 连接

  • 1.C语言中的一个重要思想就是分别编译,即若干个源程序可以在不同的时候单独进行编译,然后在恰当的时候整合到一起。但是,连接器一般是与编译器分离的,它不可能了解C语言的诸多细节。编译器的责任是把C源程序“翻译”成对连接器有意义的形式。

  • 2.典型的连接器是把由编译器生成的若干个目标模块,整合成一个被称为载入模块或可执行文件的实体,该实体能够被操作系统直接执行。

  • 3.连接器通常把目标模块看成是一组外部对象组成的。每个外部对象代表着机器内存中的某个部分,并通过一个外部名称来识别。因此,程序中的每个函数和每个外部变量,如果没有被申明为static, 就都是一个外部对象。由于经过了“名称修饰”,所以它们不会与其他源程序文件中的同名函数或同名变量发生命名冲突了。

  • 4.每个外部对象都必须在程序的某个地方进行定义。因此,如果一个程序中包括了如下语句:

    extern int a;

    那么,这个程序就必须在别的某个地方包括语句:

    int a;

  • 5.命名冲突与static修饰符:

    static int a;

    其含义与下面的语句相同:

    int a;

    只不过, a的作用域限制在一个源文件内,对于其他源文件,a是不可见的。因此,如果若干个函数需要共享一组外部对象,可以将这些函数放到一个源文件中,把它们需要用到的对象也都在用一个源文件中以static修饰符声明。

  • 6.每个外部对象只在一个地方声明。这个声明的地方一般就在一个头文件中,需要用到该外部对象的所有模块都应该包含这个头文件。

第五章 库函数

  • 1.返回整型的getchar()函数。

第六章 预处理器

  • 1.C语言预处理器首先对代码作了必要的转换处理。因此,我们运行的程序实际上并不是我们所写的程序。
  • 2.为什么要使用到预处理器:大多数C语言实现在函数调用时都会带来重大的系统开销。**虽然宏非常有用,但是宏只是对程序的文本起作用。**也就是说,宏提供了一种对组成C程序的字符进行交换的方式。
  • 3.不能忽视宏定义中的空格:#define f (x) ((x) - 1); 在这里,并不是f(x)代表((x) - 1), 而是f代表(x) ((x) - 1);
  • 4.宏并不是函数:宏发生在编译时期,是简单的文本替换。

第七章 可移植性缺陷

  • 1.即使写得最早的两个C语言编译器,他们之间也有着很大的区别。此外,不同系统有不同的需求,因此我们应该能够料到,机器不同则其上的C语言实现也有细微差别。ANSI C标准的发布能够在一定程度上解决问题,但并不是万验灵药。

  • 2.一个常见的错误理解:如果c是一个字符变量,使用(unsigned)c就可得到与c等价的无符号整数。这是会失败的,因为在将字符c转换为无符号整数时,c将首先被转换为int型整数,而此时可能得到非预期的结果。

    正确的方式是使用语句(unsigned char)c, 因为一个unsigned char类型的字符在转换为无符号整数时,无需首先转换为int型整数,而是直接进行转换。

  • 3.内存位置0:某些C语言实现对内存位置0加强了硬件级的读保护,在其上工作的程序如果错误使用了一个null指针,将立即终止执行。其他一些C语言实现对内存位置0只允许读,不允许写。在这种情况下,一个null指针似乎指向的是某个字符串,但其内容通常不过是一堆“垃圾信息”。还有一些C语言实现对内存位置0既允许读,也允许写。在这种实现上面工作的程序如果错误使用了一个null指针,则很可能覆盖了操作系统的部分内容,造成彻底的灾难。

最后:

我们所处的是一个编程环境不断改变的世界,尽管软件看上去不像硬件那么实在,但大多数软件的生命周期却要长于它运行其上的硬件。而且,我们很难预言未来硬件的性能。因此,努力提高软件的可移植性,实际上是延长了软件的生命周期。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值