C陷阱与缺陷笔记

CH1 词法”陷阱”

  1. 第一个问题就是赋值=不同于==

如果出现e1 = e2这种表达式在循环语句中的条件判断部分时,编译器可能会给出警告。

1
if(x = y) foo();

类似这种的语句可以改成下面这种,更方便理解

1
if((x = y) != 0) foo();

经典语录:

作为程序员不能指望靠编译器来提醒,毕竟警告消息可以被忽略

  1. &和| 不同于&& 和||
  2. 词法分析中的“贪心法“

C语言有一个很简单的规则就是:每一个符号会包含尽可能多的字符。

a---ba -- - b含义相同,与a - -- b不同

以及典型的错误: y = x/*p, 不过这个如果有ide,一般是不会犯的。

里面提及老版的C语言允许使用=+来替代+=含义,不过现在应该是不太可能了,不过如果注意规范代码格式,空格应该不会少。

  1. 整型常量

一个整形常量的第一个字符是数字0,那么该常量将被视作8进制数。也就是说10与010不同。这种错误会出现在如下格式中

1
2
3
4
5
6
7
8
struct {
int part_number;
char *description;
} parttab[] = {
046, "left-handed widget",
047, "right-handed widget",
125, "frammis"
};
  1. 字符与字符串

单引号引起的字符实际上代表一个整数,而双引号括起来的一个字符代表一个指针。

以及一个比较有意思的题目是写一个测试程序可以在支持或不支持嵌套注释的编译器下都可以编译。

作者给出的/*/**/"*/"/*"/**/, 若允许嵌套则等效于”/*“,不允许的话则等效于"*/".

Doug McIlroy给出了/*/*/0*/**/1, 若允许嵌套显然则是1,若不允许的话就是0*1为0.若无法理解,请看第一小点里面的一句话。

CH2 语法”陷阱“

  1. 理解函数声明

首先就给出了一个吓人的语句(*(void(*)())0)();,是模拟开机启动时的情形。

构造这类表带是也有一条简单的规则: 按照使用的方法来申明。

先考虑float (*p)();, 这就显然是一个函数指针了,可以得出(void(*)())也是一个函数指针的类型转化符了,对0做类型转换后(*0()), 来完成调用存储位置为0的子例程。

事实上可以利用typedef:

1
2
typedef void (*funcptr)();
(*(funcptr)0)();

作者也举了一个signal.h的例子:

1
void sigfunc(int n) {...}

现在希望申明一个指向sigfunc函数的指针变量sfp。

那么就有void (*sfp)(int);, 不过signal函数返回值类型和sfp返回类型一样,上式也就申明了signal函数: void (*signal(int, void(*)(int)))(int); 同样可以利用typedef:

1
2
typedef void (*HANDLER)(int);
HANDLER signal(int, HANDLER);
  1. 运算符优先级问题

比如if(flags & FLAG != 0) 这种错误,应该改为if((flags & FLAG) != 0), 还有这种错误的r = hi<<4 + low,

具体就看看表.

可以记忆的:

  1. 任何一个逻辑运算符的优先级低于任何一个关系运算符
  2. 移位运算符的优先级比算数运算符要低,比关系运算符高

这些运算符的优先顺序是由于历史原因形成的。

一个典型的错误是:

1
2
while(c=getc(in) != EOF) putc(c, out);
while((c=getc(int)) != EOF) putc(c, out);

下面一句才是对的,因为=的优先级太低了。

还有lint程序种的一个错误:

1
if( (t=BTYPE(pt1->aty)==STRTY) || t==UNIONTY)

本意是先赋值,很遗憾没做到。

  1. 注意作为语句结束标志的分号

if()语句后多了; 和 return 后少了; 这都是沙雕错误。

当然struct{} 后没有分号也同样的智障。

  1. switch语句

对于利用了switch的语句如果不想加break可以加注释

1
2
3
case '\n':
linecount ++;
/* 此处没有break */
  1. 函数调用

一个好好的函数f(), 如果写成f; 一定要好好睡一觉啊。

  1. 悬挂else引发的问题
1
2
3
4
5
6
if (x == 0) 
if(y == 0) error();
else {
z = x + y;
f(&z);
}

这种就算你对齐了else还是跟着最近的if。

CH3 语义“陷阱”

  1. 指针与数组

基础不牢地动山摇。

  1. 非数组的指针
1
2
char *r;
strcpy(r, s);

如果r不是全局的话,那么这是无法运行的,因为不能确定r指向何处。

如果改成char r[100];不能确保r足够大,如果这样的话

1
2
3
4
char *r;
r = (char *)malloc(strlen(s) + strlen(t) + 1); //1不能少
strcpy(r, s);
strcat(r, t);

也同样会失败:1. 可能无法提供请求的内存,这时会返回一个空指针。2. 给r分配的内存在使用完之后应该及时释放。

1
2
3
4
5
6
7
8
9
char *r;
r = (char *)malloc(strlen(s) + strlen(t) + 1);
if(!r) {
complain();
exit(1);
}
strcpy(r, s);
strcat(r, t);
free(r);
  1. 避免”举隅法”

举隅法(synecdoche)是一种文学修辞上的手段,类似微笑表示喜悦之情,隐喻表示指代物与被指代物相互关系。

其实主要想讲的是指向的是同一个对象,一个修改都会修改。

  1. 空指针并非空字符串

在C语言种将一个整数转换为一个指针,最后得到的结果取决于具体的C编译器实现,除了一个特殊情况0,编译器保证由0转换而来的指针不等于任何有效的指针。

所以有时会有#define NULL 0

但当0被转化成指针使用时,这个指针绝对不能被解引用。

合法:if (p == (cahr *) 0)

不合法: if (strcmp(p, (char *) 0) == 0)

因为strcmp会包括检查它的指针参数所指向内存中的内容的操作。当然空指针也不能printf.

  1. 边界计算与不对称边界

可能是外国人的数学不太行

  1. 求值顺序

主要指if(y != 0 && x/y > tolerance) 这种。

当然还有y[i++]=x[i];这种沙雕错误犯不得,都是看编译器的。

  1. 运算符&&、||和!

与&&不同,&两侧的操作数都必须被求值。

  1. 整数溢出

无符号算术运算中,没有”溢出”这一说法。

当两个非负整型变量a,b。

if (a+b < 0)这样写正确与否得看C编译器。

正确的两种写法:

1
2
if((unsigned)a + (unsigned)b > INT_MAX) complain();
if(a > INT_MAX - b) complain();
  1. 为main函数提供返回值

return 0;或者exit(0);

CH4 连接

该章主要讲的是关于多个文件组合编译的C程序中会出现的错误。不同部分通过一个连接器的程序才合并成一个整体,因为编译器一般只能处理一个文件,所以它不能检测出多个源程序文件会出现的错误。

连接器的输入是一组目标模块和库文件,输出是一个载入模块。对每个目标模块中的每个外部对象,连接器都要检查载入模块,看是否以有同名的外部对象,如果没有,连接器就将该外部对象添加到载入模块,如果有,开始处理命名冲突。

连接器对C语言知之甚少,有很多错误不能检测出来。

  1. 声明与定义

主要提的是extern

  1. 命名冲突与static修饰符

凡是确定好不会被其他文件调用的,那么这个函数应该被声明成static的。

  1. 形参、实参与返回值

不是很懂怎么在调用前不申明函数。我g++没成功链接。

以及一个非常巧妙的错误, 这个错误在不同编译器下的结果是不一样的。

1
2
3
4
5
6
7
8
int main(){
int i;
char c;
for (i = 0; i < 5; i++) {
scanf("%d", &c);
printf("%d", i);
}// 我的mingw是不管输入什么都是输出0
}
  1. 检查外部类型

extern 声明的东西不能和定义时候不一样。

特别是这个错误:

1
2
char filename[] = "/etc/passwd";
extern char* filename;
  1. 头文件

为了解决上述的问题,所以使用头文件。

CH5 库函数

  1. 返回整数的getchar()函数
1
2
3
4
char c;
while((c = getchar()) != EOF) {
putchar(c);
}

这个确实是不知道的一个错误,getchar() 返回的是int类型。在这里赋给char类型的变量会发生”截断”,可能只把低端字节部分赋给了变量c。

  1. 更新顺序文件
1
2
3
4
5
6
7
8
9
10
11
FILE *fp;
struct record rec;
/** ... **/
while(fread((char *)&rec, sizeof(rec), 1, fp) == 1) {
/** some operations **/
if(/** **/ ) {
fseek(fp, -(long)sizeof(rec), 1);
fwrite((char *)&rec, sizeof(rec), 1, fp);
// fseek(fp, 0L, 1);
}
}

当我们想一遍读入文件一边修改文件的时候,一般会用一些操作,但是第二个fseek一般会忘记,第二个fseek起的作用是改变文件的状态。

  1. 缓冲输出与内存分配
1
2
3
4
5
6
7
main() {
int c;
char buf[BUFSIZ];
setbuf(stdout, buf);
while((c = getchar()) != EOF)
putchar();
}

这是一个错误的写法,因为在main函数结束后buf缓冲区才会被最后一次清空,但是这时buf数组已经被释放。

所以要采用static char buf[BUFSIZ];或者把声明放在全局。

第二种是采用动态分配缓冲区,这样就不会主动释放该缓存区。

1
setbuf(stdout, (char *)malloc(BUFSIZ));
  1. 使用errno检测错误

在库函数没有调用失败的情况下,没有强制errno为0,所以要在调用库函数前error=0才能之后再使用。

  1. 库函数signal

信号非常复杂,需要让signal处理函数尽可能的简单。

  1. 练习

程序异常结束的话,常常会丢失最后几行,可以强制不允许对输出进行缓冲setbuf(stdout, (cahr *)0);

stdio.h 对getchar/putchar为宏实现,避免了函数调用的开销。

CH6 预处理器

宏既可以使一段看上去完全不合语法的代码成为一个有效的C程序,也能使一段看上去无害的代码成为一个可怕的怪物。

  1. 不能忽视宏定义中的空格

典型的错误#define f (x) ((x)-1)

  1. 宏并不是函数

典型的错误#define abs(x) x>0?x:-x, 如果计算abs(a-b)就能发现错误。当宏展开时可能也会造成语句非常长,而且如果有i++这种形式也会炸。

1
2
#define putc(x,p) \
(--(p)->_cnt>=0?(*(p)->_ptr++=(x)):_flsbuf(x, p))

x在:左右两侧,因此不会有什么影响,但是p虽然是一个用于描述文件的内部数据结构的指针,但依然可能有求值两次的危险。

  1. 宏并不是语句
1
#define assert(e) if(!e) assert_error(__FILE__,__LINE__)

这样的宏看上去没有什么问题,那我们来创造问题

1
2
if(x > 0 && y > 0) assert(x > y);
else assert(y > x);

现在就会出现问题,这边的else会与最近的if匹配。事实上应该这样

1
#define assert(e) ((void)((e)||_assert_error(__FILE__,__LINE__)))
  1. 宏并不是类型定义
1
2
3
4
#define T1 struct foo *
typedef struct foo *T2;
T1 a, b;
T2 a, b;

这里就会发现又出现了问题。

课后习题解

1
2
3
4
5
static int max_temp1, max_temp2;
#define max(p, q) (max_temp1=(p),max_temp2=(p),\
max_temp1>max_temp2?max_temp1:max_temp2)
#define x int
(x) ((x)-1)

CH7 可移植性缺陷

程序的生命周期往往超过预期。

  1. 标识符名称的限制
1
2
3
4
5
char * Malloc(unsigned n) {
char *p = (char *)malloc((unsigned) n);
if(p == NULL) panic("out of memory");
return p;
}

如果遇到一个不区分大小写的编译器,就会一直递归下去。

  1. 整数的大小

typedef long tenmil来声明所有此变量,最坏的情况就改一下long。

  1. 字符是有符号整数还是无符号整数

这个问题出现在char 转化为int时,由此有一个意外的错误(unsigned)c来获得一个与c等价无符号整数,应该是(unsigned char) c

  1. 移位运算符

主要两个问题:

  1. 在向右移位时,空出的位是由0填充还是符号位

无符号数填充0,有符号数填充符号位

  1. 移位计数允许的取值范围

必须大于或等于0,严格小于被移位的对象长度。

  1. 内存位置0

误用null指针都是未定义的,有可能会覆盖操作系统部分内容。

  1. 除法运算时发生的截断

  2. 随机数的大小

  3. 大小写转换

函数还是宏定义的抉择。

  1. 首先释放然后重新分配

  2. 可以执行的一个例子

1
2
3
4
5
6
7
8
9
10
void printnum(long n, void (*p)()) {
if(n < 0) {
(*p)('-');
n = -n;
}
if(n >= 10) {
printnum(n/10, p);
(*p) ((int)(n%10) + '0');
}
}

问题在于5 + ‘0’ 是否一定等于’5’, 这种大多数情况下应该是对的

不过改成(*p)("0123456789"[n % 10]); 就不会有什么毛病了。

除此之外,因为基于2的补码的计算机一般允许表示的负数取值范围要大于整数取值范围,然后作者就又搞了一个操作,感觉没啥意思的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void printneg(long n, void (*p)()) {
long q = n / 10;
int r = n % 10;
if(r > 0) {
r -= 10; q++;
}
if(n <= -10) printneg(q, p);
(*p) ("0123456789"[-r]);
}

void printnum(long n, void (*p)()) {
if(n < 0) {
(*p) ('-');
printneg(n, p);
} else printneg(-n, p);
}

CH8 建议

  1. 不要说服自己相信皇帝的新装
  2. 直截了当表面意图
  3. 考察最简单的特例
  4. 使用不对称边界
  5. 注意潜伏在暗处的bug,减少使用生僻的语言特性
  6. 防御性编程

附录A printf, varargs, stdarg

可变域宽与精度

1
2
3
#define NAMESIZE 14
int name;
printf("...%.*d...\n", NAMESIZE, name);

关于varargs与stdarg并不想看。

文章目录
  1. 1. CH1 词法”陷阱”
  2. 2. CH2 语法”陷阱“
  3. 3. CH3 语义“陷阱”
  4. 4. CH4 连接
  5. 5. CH5 库函数
  6. 6. CH6 预处理器
  7. 7. CH7 可移植性缺陷
  8. 8. CH8 建议
  9. 9. 附录A printf, varargs, stdarg
|