C陷阱与缺陷

时间:2022-06-03 16:06:11

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中,字符串常量代表了一块包括字符串中所有字符以及一个空字符(`\0`)的内存区域的地址

作为参数的数组声明

避免“举隅法”

空指针并非空字符串

#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处理函数尽可能地简单,并将它们组织在一起


第六章 预处理器

预处理器的重要性:

  1. 将某个特定数量在程序中出现的所有实例统统加以修改

  2. 函数调用会带来重大的系统开销

宏只是对程序的文本起作用

不能忽视宏定义中的空格

预处理器从宏定义中就可以知道宏调用时是否需要参数

宏不是函数

最好在宏定义中把每个参数都用括号括起来

确保宏中的参数没有副作用

宏并不是语句

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函数的取值范围

大小写转换

函数的方法,时间会长一点

宏定义的方法,不能保证传入的参数正确

首先释放,然后重新分配

不推荐

可移植性问题的一个例子