C陷阱与缺陷(C Traps and Pitfalls)读书笔记

说在前面:为了监督自己养成主动读书获取知识的习惯,特此写一篇博客作为读书笔记,由于是第一次主动正式写读书笔记,免不了语言错误或有不妥的地方,欢迎直接指出,不需要给博主留面子!🤡再来就是我想把这作为一篇即时更新的博客(看多少就在原来基础上继续写笔记),也不知道多久能把它完善😂,但最重要的还是希望自己能坚持吧😁!

目录

第一章 词法“陷阱”

1.1 = 与 ==

 1.2 &(按位与)、 | (按位或) 和 &&(逻辑与) 、 ||(逻辑或)

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


第一章 词法“陷阱”

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·····

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值