C陷阱与缺陷
第一章 词法“陷阱”
符号(token):指的是程序的一个基本组成单元,起作用相当于一个句子中的单词
=不同于==
&和|不同于&&和||
词法分析中的“贪心法”
C语言规则:每个符号应该包含尽可能多的字符
整型常量
整形常量的第一个字符是数字0,那么这个常量将被视作八进制数
字符和字符串
用单引号引起的一个字符实际上代表一个整数,整数值对应于该字符在编译器采用的字符集中的序列值。
用双引号引起的字符串,代表的却是一个指向无名数组起始字符的指针。
第二章 词法“陷阱”
理解函数声明
构造表达式的规则:按照使用的方式来声明。
硬件调用首地址为0位置的子例程
(*(void(*)())0)();
第一步,0为地址,对0地址取内容:
(*0)()
第二步,*必须要一个指针来做操作数,这个指针还应该是函数指针,因此对0作类型转换,
转换后类型为:“指向返回值为void类型的函数的指针:
void (*)()0
从而得到:
(*(void(*)())0)();
若用typedef解决
typedef void (*funcptr)();
(*(funcptr)0)();
运算符的优先级
运算符 | 结合性 |
---|---|
() [] -> . | 自左向右 |
! ~ ++ -- - (type) * & sizeof | 自右向左 |
* / % | 自左向右 |
+ - | 自左向右 |
<< >> | 自左向右 |
< <= > >= | 自左向右 |
== != | 自左向右 |
& | 自左向右 |
^ | 自左向右 |
&& | 自左向右 |
| ?: | 自右向左 |
| assignnments | 自右向左 |
| , | 自左向右 |
-
任何一个逻辑运算符的优先级低于任何一个关系运算符
-
移位运算符的优先级比算术运算符要低,但是比关系运算符要高
注意作为语句结束标志的分号
if(...);
xx = xx;
switch语句
break别遗漏
函数调用
“悬挂”else引发的问题
else始终与同一对括号内最近的未匹配的if结合
第三章 语义“陷阱”
指针和数组
-
C语言只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来。
-
对于数组,我们能:确定数组的大小,获得只想数组下标为0的元素的指针
-
其他的数组操作,看上去是以数组下标进行的,实际是指针进行的
非数组的指针
在C中,字符串常量代表了一块包括字符串中所有字符以及一个空字符(` `)的内存区域的地址。
作为参数的数组声明
避免“举隅法”
空指针并非空字符串
#define NULL 0
绝不能被解除引用
边界计算与不对称边界
求值顺序
运算符&&、||和!
整数溢出
如果算术运算符的一个操作数是有符号数,另一个是无符号数,那么有符号数会被转换为无符号数,”溢出“也不可能发生。但是,当两个操作数都是有符号数时,”溢出“就有可能发生。
为函数main提供返回值
第四章 连接
一个c程序由多个分别编译的部分组成,这些部分通过一个叫做连接器的程序合并成一个整体。
什么是连接器
分别编译->整合
连接器的输入是一组目标模块和库文件,输出是一个载入模块
声明和定义
extern
命名冲突与static修饰符
不同源文件之间相同的变量和源文件与库文件之间的命名冲突
static是一个能够减少命名冲突的有用工具
形参、实参和返回值
检查外部类型
两个不同源文件中的同名定义,类型不同时
头文件
规则:每个外部对象只在一个地方声明
第五章 库函数
返回整数的getchar函数
getchar函数在一般情况下返回的是标准输入文件中的下一个字符,当没有输入时返回EOF(一个在头文件stdio.h中定义的值,不同于任何一个字符)
更新顺序文件
FILE *fp;
fp = fopen(file, "r+");
如果同时要进行输入和输出操作,必须在其中插入fseek函数调用。
缓冲输出和内存分配
程序输出有两种方式:一种是即时处理方式,另一种是先暂存起来,然后再大块写入的方式。
控制写操作产生的数据输出量通过库函数setbuf实现
setbuf(stdout, buf);
所有写入到stdout的输出都应该使用buf作为输出缓冲区,直到buf缓冲区被填满或者调用fflush,buf缓冲区的内容才实际写入到stdout中。缓冲区大小由系统头文件<stdio.h>中的BUFSIZ定义
使用errno检测错误
很多库函数,当执行失败时会通知一个名称为errno的外部变量,通知程序该函数调用失败。
当调用库函数时,应该先检测作为错误提示的返回值,确定程序执行已经失败,然后再检查errno来搞清楚出错原因。
/*调用库函数*/
if (返回的错误值)
检查 errno
库函数signal
signal库函数作为捕获异步事件的一种方式
#include
signal(signal type, handler function);//处理一个特定的信号,参数信号类型,事件处理函数
signal处理函数能做的安全的事情似乎只有设置一个标置然后返回,期待主程序能够检查这个标志,发现一个信号已经发生。
对于算术运算错误,signal处理函数的唯一安全、可移植的操作就是打印一个出错消息,然后使用longjmp或exit立即退出程序
让signal处理函数尽可能地简单,并将它们组织在一起
第六章 预处理器
预处理器的重要性:
-
将某个特定数量在程序中出现的所有实例统统加以修改
-
函数调用会带来重大的系统开销
宏只是对程序的文本起作用
不能忽视宏定义中的空格
预处理器从宏定义中就可以知道宏调用时是否需要参数
宏不是函数
最好在宏定义中把每个参数都用括号括起来
确保宏中的参数没有副作用
宏并不是语句
assert宏,参数是一个表达式,如果为0,就使程序终止执行,并给出一条适当的出错信息。_FILE_和_LINE_是内建于C语言预处理器中的宏,会被扩展为所在文件的文件名和所处代码行的行号。
#define assert(e) ((void)((e)||_assert_error(__FILE__, __LINE__)))
||运算符对两侧的操作数依次顺序求值
宏并不是类型定义
宏的常见用途,使多个不同变量的类型可在一个地方说明
第七章 可移植性缺陷
应对C语言标准变更
函数声明的方式在不同标准中的移植性
标识符名称的限制
有些C语言不区分大写字母和小写字母
整数的大小
short int long
-
3种类型的整数其长度是非递减的
short型整数容纳的值肯定能够被int型整数容纳,int型整数容纳的值也肯定能够被long型整数容纳。
-
一个普通(int类型)整数足够大以容纳任何数组下标
-
字符长度由硬件特性决定
字符是有符号整数还是无符号整数
unsigned char
移位运算符
有符号整数的向右移位运算也不等同于除以2的某次幂。
内存位置0
null指针并不指向任何对象
除法运算时发生的截断
保证非负
C语言只保证性质1
随机数的大小
RAND_MAX,rand函数的取值范围
大小写转换
函数的方法,时间会长一点
宏定义的方法,不能保证传入的参数正确
首先释放,然后重新分配
不推荐