目录

-1. 前言

第0章. 导读

第1章. 词法“陷阱”

1.1 = 不同与 ==

问题的解决:

1.2 & 与 | 不同于 && 与 ||

&& 与 || 的短路运算

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

1.4 整形常量

1.5 字符与字符串

总结:

难的是那些我们已经了解的东西,如何“运用之妙。存乎一心”。

上面的这句话,正是出自:《C陷阱与缺陷》,我认为其道出了我们学习编程中最重要的一点!真正困难,真正重要的东西,往往是我们在学习完一门编程语言后,如何将他自如的运用起来。我相信这一点一定困扰了许许多多的新手程序员甚至是一些老鸟程序员。

而《C陷阱与缺陷》这本书籍呢,是C语言相关的非常非常经典的一本书籍,所以我准备将我读后的感受以及思考通过博客的方式跟大家分享!本文主要内容有7章,我会用尽量只用7篇以内的文章来跟大家分享。好!现在就让我们开始阅读这本经典书籍吧!


-1. 前言

首先让我们来看看,作者在“中文序”对这本书籍的定位:

第0章. 导读

在导读部分呢,作者主要是谈论了本书的内容方面,我们主要谈论什么问题? —- 计算机严格地按照我写明的程序来执行,但结果却并不是我们真正希望的!更进一步,本书讨论限定在C语言程序中可能产生这类错误的方式。

作者在这里也附上了一段代码,我们可以看出,这段代码的目的是初始化一个N元数组,但是在当时的很多C语言编译器中如出现死循环的问题。详细会在后文讨论。

对于上面的问题,我这里已经有一篇博客是来谈论这个问题,我在这里附上链接大家可以先看一下:

C/C++ 一道nice公司关于循环,数组和栈区的笔试题-CSDN博客

第1章. 词法“陷阱”

这里有一个重点,也是一个引子!就是说我们平常在看一个句子的时候,是不会看每一字母的,而是看字母构成的单词。而在程序中就是说,单个语句是没有意义的,而结合他的上下文来看的!

* 上面这个符号,在C语言中三种比如说:1、c = a * b; 中,* 是一个 乘以符号2、int *p = NULL; 中 * 是一个指针的标识3、/* 中 * 是C语言注释的构成的一部分

说白了就是,上下文的不同,会让一个相同符号,有完全不同的意思!在这里又一次举了一个例子,说在C语言当中呢,在编译器的视角会忽略空白字符(空格,水平制表符,回车符),就是说下面的两端代码意义是完全相同的。

int a= 10;int a=10;
1.1 = 不同与 ==

第一种情况,我们说它是赋值符号,第二种情况,我们说它是 关系判断 符号

我在这里举两个例子,是我们在刚刚学习 if … else 的时候,十分容易犯的错误!

#include int main(){int a = 10;if (a = 20){printf("a = 20\n");}return 0;}

这里在运行起来的时候,我们发现,他是会输出“a = 20”的, 但是我们觉得:a是10, 应该是不会输出的呀?

问题其实就 a = 20 ,我们 将 == 误用成 = ,将判断的意思变成了赋值,所以就会输出 a = 20 的语句了。

#include #include int main(){int* p = (int*)malloc(sizeof(int));if (p = NULL){printf("out of memory\n");}*p = 10;printf("*p = %d\n", *p);return 0;}

问题也是和上面的一样的。同样是将 == 误用成了 = ,造成了野指针访问的问题!

为什么我们在刚刚学习的时候容易犯这种错误呢?因为这种问题,其实是不属于语法错误,编译器在编译后不会告诉我有问题,所以我们就容易犯这种错误了!

问题的解决:

那我们该如何解决的类的问题呢?

#include int main(){int a = 10;if (20 == a) // if (NULL == p){printf("a = 20\n");}return 0;}

我们将常量放在左边,变量放在右边,假如说我们在这个里不小心将 == 写成了 = ,因为我们不能将做个变量赋值给一个常量,所以在编译期间,编译器就会报出这个问题!

在编译器上就会报出这样的错误,提醒我们来修改我们的代码!

当我们真的需要在 if() 括号中完成赋值操作再判断 的时候,我们可以这样写

if((x = y) != 0){// ...}

这里呢,我给大家留一个疑问,看看这样写是否正确,是否能完成我们的上面的需求呢?

if(x = y != 0){// ...}
1.2 & 与 | 不同于 && 与 ||

这个陷阱大家其实在学习完C语言语法后,其实是懂得都懂。

& 和 | 符号属于是 位元素符号

&& 和 || 符号属于是 逻辑运算符号

在这里给大家举个例子看一下:

#include int main(){int a = 10;int b = 4;int c = a & b;// 00000000 00000000 00000000 00001010// 00000000 00000000 00000000 00000100// 00000000 00000000 00000000 00000000 -- 0int d = a | b;// 00000000 00000000 00000000 00001010// 00000000 00000000 00000000 00000100// 00000000 00000000 00000000 00001110 -- 14int e = a && b;int f = a || b;printf("c = %d d = %d\n", c, d);printf("e = %d f = %d\n", e, f);return 0;}

在这里呢,我再给大家提一个知识点:

&& 与 || 的短路运算
#include #include int main(){int a = 0;int b = 1;int v = a && b++;int x = b || a++;printf("a = %db = %d\n", a, b);return 0;}

大家可以考虑一下,最终这里的 a 和 b 分别是多少呢?

这里其实就涉及到了 && 和 || 逻辑运算时候的短路了。

&& 它要求 两个 元素 都为 真 它才是真

|| 它要求只要 有一个元素为真 他就为真

所以说:

v -> a 为 假之后,后面的 b++ 就不会执行了

x -> b 为 真之后,后面的 a++ 就不会执行了

所以最终a和b的结果是没有发生变化的!

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

这里呢就是说,编译器的视角,当我们的程序中有 / * 两个符号的读入,编译器是,将这两个符号单独分开的看作两个当都的符号呢,还是将两个符号合起来看成一个 /* 符号呢?

C语言编译器的处理方法呢就是:对于每一个符号,应该尽可能多的包含多的字符。

解释一下,在编译器的视角,当读入一个字符的时候,如果该字符可能构成一个符号,就再读入一个字符,之后判断一下,读入的两个字符组成的字符串可不可能是一个符号的组成部分,如果可能就再读入一个字符,如此往复的判断,直到读入的字符组成的字符串不再可能组成一个有意义的符号

我在这里给大家举一个实际的例子,

while (1){}面对这里的 一个 while 编译器一定不会将它看作 一个 'w' 'h' 'i' 'l' 'e'这样就完全没意义了在编译器看来,有一个 'w'的时候,它就会考虑会不会是 "while" 的组成部分就会接着读入 'h' 'i' 'l' 'e'但是中间一旦有一个其他的符号,就破坏了前面说的规则!编译器就不会将他看作 "while"了

这样的方法,策略,我们就称它为 贪心法的走法,尽让一个字符串长。

有一个注意的点:除了字符串或者常量字符串,其他符号中间是不能出现 空格 的

这里我们再看一个作者的例子:

a --- b和a-- -b 完全等价和a - -- b 不等价也是说明了 构成符号的字符中间是不能加空格的!

在这个地方,我的观点呢就是:对于编译器的贪心法,我们不要贪心,不要吝啬我们的括号!

再给大家分析一个案例:

int main(){int a = 10;int* p = &a;int x = 100;int v = x/*p;return 0;}

在这地方,我们的本意是 用 x 除以 解引用p,但是这里的 /* 却被看成了 注释的开头。解决方案有两个

第一就是加上括号,第二呢,就是根据贪心法的条件,除了字符串和字符常量,符号的中间是不能有空格的。

int main(){int a = 10;int* p = &a;int x = 100;int v = x / *p;int v = x/(*p);return 0;}

这两种方案都是可以的!

1.4 整形常量

在编程过程中,我们有一些常用的进制,分别是 十进制,二进制,八进制,十六进制

其中二进制是0b打头,八进制是0打头,十六进制0x打头

有一种这样的情况,我们定义一个数组,其中的数都是三位的整数,只有一个数是两位,我们就想使用一个0来给这个数占位

int arr[] = { 123,234,345,015 };

这个时候呢,这里的015其实上就已经不是 15了,而是八进制下的015,对应十进制的13。

还有一些陷阱,就是将一个地址,直接给到一个指针变量的陷阱和C++环境下的多态函数会产生二义性,也在这里说一下吧;

int Max(int a, int b){return a > b " />1.5 字符与字符串 

我们在刚刚学习字符和字符串的时候,很容易写出来这样的代码:

char x = "A";或者char* pstr = 'x';

这里给大家强调一下:单个字符是使用' ' 单引号, " "字符串使用 双引用,而想'abc'这样的,只能说是它既不是一个字符,也不是一个字符串。

再注意一点,C语言中,是不可以将一个空字符赋值给一个变量的,如下:

char ch = ''; // err

但是是可以将一个空串赋值给一个字符串的,因为一个空的字符串可以表示为只有一个'\0'

char *pch = "";

在这里呢,我给大家强调一个问题即可:C语言中字符串的结束标志是'\0',常量字符串编译器在他其有效字符之后加上 '\0'。

最后对于这个字符,再说一点,'\123' '\x12' 是一个单个的字符,分别表示的三位的八进制和两位的十六进制,他们也是转义字符的一种,表示的就是其ASCII码,而在编译器的视角来看呢,由单引号引起来的是一个整数,由双引号引起来的是一个地址!

这也就解释了:

printf("\n");printf('\n');

第一个是正确的,第二个是错误的,至于第二个为什么是错误的,可以参考我写的另一篇文章。

浅谈进程的虚拟地址空间划分-CSDN博客

总结:

到这里!本书第一章的划重点就结束啦!希望大家可以在完这篇文章之后去真正的读一下这本书。很经典!

而对于后面的部分,我也会慢慢给大家都更新出来!如果想要接着看我分享《C陷阱与缺陷》后面的部分的话,希望可以点个关注嗷~