说在前面:为了监督自己养成主动读书获取知识的习惯,特此写一篇博客作为读书笔记,由于是第一次主动正式写读书笔记,免不了语言错误或有不妥的地方,欢迎直接指出,不需要给博主留面子!🤡再来就是我想把这作为一篇即时更新的博客(看多少就在原来基础上继续写笔记),也不知道多久能把它完善😂,但最重要的还是希望自己能坚持吧😁!
目录
1.2 &(按位与)、 | (按位或) 和 &&(逻辑与) 、 ||(逻辑或)
第一章 词法“陷阱”
1.1 = 与 ==
相信以上两个符号都是不少朋友在初学时一踩再踩的坑(我就这样,写代码的稍一分神就进坑里了)😂。“==”作为双目运算符,用于条件判断中,若两边表达式的值相等则为真(1),反之为假(0);而若是在条件表达式中把“==”写成了“=”,表达式将会永真。若用于if else条件语句中,则if 条件下的语句段总是可行;若用于while循环中,则会造成死循环······
相信每个人对其都有不同程度的感悟,这里就不再赘述
1.2 &(按位与)、 | (按位或) 和 &&(逻辑与) 、 ||(逻辑或)
这个相对于=和==来说不容易混淆,注意在使用时做好区分就行。(具体使用的方法可参考笔者C语言学习笔记-逐步剖C专栏中的操作符部分)
1.3 词法分析中的“贪心法”
作者说明了C编译器对连续出现的单字符的判断原则。即对于连续出现的单字符如“ / ”,“ * ”,C编译器需要判断是将其作为两个分别的符号对待,还是合起来作为一个符号对待。C把它归纳为了一个简单的原则:每一个符号应该包含尽可能多的字符。即编译器在分解符号时,从左到右对字符进行逐个读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已读入字符组成的字符串是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断 ,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。
个人觉得比较重要还是下面作者给出的这个例子:
y=x/*p;
你认为这句语句的本意是什么呢?应该是想表达x除以p所指向的对象的值。但编译器会把“ /* ”理解为一段注释的开始,而不断读入字符。直到“ */ ”出现为止。虽然编译器会直接出现整套的“/* */”让你填内容或显示注释的绿色标记,但我们还是应该在开始就把语句意思表达准确,而主动避免此类“二义性”问题。下面是修改后的写法:
y = x / (*p);
看起来是不是舒畅了许多呢?养成好的代码风格很重要!👍
1.4 整型常量
如果一个整型常量的第一个数字是0,那么该常量将被视作八进制数,对此作者提醒了一种需要注意的情况:有时在上下文中为了格式对齐的需要,可能无意将十进制数写成了八进制数。
1.5 字符与字符串
单引号用于引起一个字符,实际上代表一个整数,整数值对应于该字符在编译器采用的字符集中的序列值(基本上都是ASCII字符集);
双引号用于引起字符串,代表一个指向无名数组起始字符的指针,该数组被双引号之间的字符及一个额外的二进制值为0的字符 '\0' 初始化。
要注意避免两这的混用:
char* str = '/' ;
对以上语句,在编译时会生成错误信息,因为 '\' 并不是一个字符指针;
printf('\n');
printf("\n");
对以上语句,若用第一条语句代替了正确的第二条,程序运行时会产生难以预料的错误,而不会给出编译器诊断信息
整型数的存储空间(一般位16位或32位)可以容纳多个字符(一般为8位),故有些C编译器允许一个单引号引起的字符常量中包含多个字符 。如:
'yes'
大多数编译器会理解为:一个整数值,由'y' 'e' 's'所代表的整数值按特定编译器实现中定义的方式组合得到。下面为该书作者在不同编译器测试后的注明:
第二章 语法陷阱
2.1 理解函数声明
通过阅读这一小节,我第一次了解到了“函数指针”这个概念,具体的学习笔记打算单独写写,这里就不详细剖析了。回到书中,作者刚开始就抛出了一个语句:
(*(void (*)())0) ();
刚开始看到直接一脸问号,但跟着作者思路逐个拆分来分析其实也不算很难
任何C变量的声明都有两部分来组成:类型及一组类似表达式的声明符
如最简单也是我们最熟悉的声明变量:
float a; //声明a是一个浮点型变量
float *pf //声明pf是一个指向浮点型变量的指针
float ff() //声明ff是一个返回值为浮点类型的函数
与我们平常声明变量理解不同的是:把变量作为“表达式”,如上float a就可以理解为:当对其求值时,表达式a的类型为浮点数类型
因为是表达式,所以我们可以任意加括号,即:
flaot ((a));
理解为:对其求值时,((a))的类型为浮点型,则可得a也为浮点型,即也就是说,float ((a)) 其实是等价于 float a的。
那么把如上声明像表达式一样组合起来,便可理解函数指针
float (*pf) (); //pf是一个指针,指向返回值类型为浮点型的函数
float *g(); //g是一个函数,其返回值是一个指向浮点型的指针
//相似的还有:
int *arr [3]; //arr是一个数组,每个元素都是指向整型的指针
int (*p)[3]; //p是一个指针,指向有三个整型元素的数组
再联系一下类型转换符的使用:如(int)a,表示将a强制转换为int 型。我们就可将上述声明“封装”起来成为一个类型转换符
延上例,因为 float (*pf) (); 中pf表示一个指向返回值为浮点型的函数的指针,所以
(float(*)())
//去掉变量名和分号
表示一个 “指向返回值为浮点型的函数的指针” 的类型转换符(但我觉得可能也只是在理解层面上的,实际可能不能很好地使用,博主本人还没研究明白怎么具体用,先挖个坑)
有了如上预备知识,就可分析作者刚开始给出的声明了
首先,假设fp是一个函数指针,那么根据指针的使用方式,(*fp)()就是调用该函数的方式
(注:虽ANSI C允许将其简写为 fp(),但只需知道可以这么做就行,不推荐这么做)
接着,这里先补充一下作者一开始提到设计那句语句的目的:为了模拟开机启动时的情形(硬件将调用首地址为0的子例程),设计一个C语句以显式调用子例程。所以C编译器能够理解我们大脑中对于类型的认知,那么我们可以写出:
(*0)()
//理解为:通过0位置上的指针,调用相应的子例程
但实际上,运算符 " * " 在此种情况下需要一个函数指针来作为操作数。故我们就需对0进行类型转换,转换后的类型可描述为 “指向返回值为void类型的函数的指针”
则根据之前的介绍,我们可通过以下语句来完成存储位置为0的子例程的调用
void (*fp) ();
(*fp)();
//默认fp初始化为0,这种写法不值得提倡
再结合之前类型转换符的相关说明,我们只需在变量声明中将变量名去掉即可实现对常数进行类型转换。因此,将常数0转型为 “指向返回值为void类型的函数的指针” 类型可以写成:
(void(*)())0
最后用如上语句替换fp就得到一开始的那条语句,即:
(*(void(*)())0)();
最后作者提到他在解决这个问题时,C语言还没有typedef声明,用 typedef 无疑能使表述更清晰:
typedef void(*fp)();
(*(fp)0)();
虽然这看起来和我们平常写代码的思路与风格大有不同,也确实可能很少会用到,但我觉得将其作为知识了解转化为我们的写代码的“内功”的一部分还是很有价值的👍
持续更新ing·····