C缺陷与陷阱学习笔记

 易上手的工具往往功能有限,满足不了高手。得心应手的工具初学时的困难往往超过那些容易上手的工具。
但一旦熟练后,则会觉得那些易上手的工具最后都碍手碍脚。
使其难于做傻事往往使其难于做聪明事。
Chapter 1 词法陷阱

程序中的单个字符孤立起来看并没有什么意义,只有结合上下文才有意义,如p->s = "->";两处的-意义
是不同的。

程序的基本单元是token ,相当于自然语言中的单词。 一个token的意义是不会变的。 而组成token 的字
符序列则随上下文的不同而改变。

token之间的空格将被忽略。


1.1 = 不同于 ==

1.2 &和|不同于&&和||

1.3 词法分析中的贪心法

token分为单字符token和多字符token,如/ 和 == ,当有岐义时,c语言的规则是:每一个token应包括
尽可能多的字符。

另外token的中间不能有空白(空格,制表符, 换行符)
y = x /*p 应写为y = x / *p 或者y = x / (*p);

老编译器允许用=+来代表现在+=的含义。所以它们会将a=-1理解为a=- 1 即a = (a-1);
它们还会将复合赋值语句看成两个token,于是可以处理 a>> =1, 而现代的编译器会报错。


1.4 整型常量

常量前加0代表是8进制。

1.5 字符与字符串

用双引号引起的字符串, 代表的是一个指向无名数组起始字符的指针


a+++++b的含义是什么?

C不允许嵌套注释。


Chapter 2 语法陷阱


2.1 构造函数声明

构造函数声明的规则:按照使用的方式来声明。

任何C声明都由两部分组成:类型及类似表达式的声明符(declarator)。


float *g(), (*h)();
g是一个函数,该函数的返回值类型为指向浮点数的指针。 h是一个函数指针, h所指向函数的返回值为
浮点类型。()的优先级高于*。

因为float (*g)();表示g是一个指向返回值为浮点类型的函数的指针。所以(float (*)())表示一个“指向
返回值为浮点类型的函数的指针”的类型转换符。


一旦我们知道如何声明一个给定类型的变量, 那么该类型的类型转换符就很容易得到了:只需要把声明
中的参量名和声明末尾的分号去掉,再将剩余的部分用一个括号整个“封装”起来即可。

(*(void(*)())0)()表示什么意思呢?

如果fp是一个函数指针, 那么(*fp)()就表示对其所指的函数的调用。简写为fp()。但这只是简写而已。
而*((*fp)())可以简写为*fp()

根据上文(void(*)()) 表示一个“指向返回值为void的函数的指针”的类型。这里不过是对0作强制转换而
已。其实用typedef更好:

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

signal的声明如下:
void (*signal(int, void(*)(int)))(int);
或者用typedef:
typedef void (*HANDLER)(int);
HANDLER signal(int, HANDLER);


2.2 运算符的优先级问题

注意条件运算符优先级比赋值运算符高,书上第22页是错的。
& > ^ > |

2.3 分号

2.4 switch 语句

2.5 函数调用

f();
是个函数调用。而f;则计算函数f的地址。
2.6 else

C语言允许初始化列表中出现多余的逗号。


Chapter 3 语义陷阱


3.1 指针与数组

C语言中只有一维数组, 而且数组的大小必须在编译期间就作为一个常数确定下来。多维数组是通过一维
数组仿真的,因为数组的元素可以是任何对象,当然也可以是数组。

对数组,我们只能做两件事,确定其大小,以及获得指向该数组下标为0的元素的指针。其它的有关数组
的操作,实际上是通过指针进行的。


如果两个指针指向的是同一个数组中的元素,我们可以把这两个指针相减。如果它们指向的不是同一个数
组中的元素,即使它们指向的地址在内存中的位置正好间隔一个数组元素的整数倍,所得的结果仍然是无
法保证其正确性的。

如果在应该出现指针的地方出现了数组名,则数组名就被当作指向该数组下标为0的元素的指针。
int a;
p = a;
int *p;
是对的。但p = &a在ansi C中则是非法的。因为&a 是一个指向数组的指针,而p是一个指向整型变量的指针,
它们的类型不匹配。
由于a[i] 即*(a+i);而a+i即i+a;所以a[i]即i[a];但不推荐后者的写法

int cal[12][31];
int *p;
int i;

i = cal[4][7]等于i = *(cal[4] + 7);也等于i = *(*(cal + 4) +7);


p = cal; 是错误的,类型不匹配,后者是指向数组的指针。


我们来声明指向数组的指针:
int (*ap)[31];
于是我们可以这样写:
int cal[12][31];
int (*monthp)[31];
monthp = cal;

两 个指针不能相加。负数的移位运算不等于相应的乘或除运算。

3.2 非数组的指针

我们要将s和t连接成r.
s = "abc";
t = "efg";
char *r;
strcpy(r,s);
strcat(r,t);
这并不能达到目的。

因为一是不能确定r指向何处, 二是不能保证r所指向的地址处还应该有内存空间可供容纳字符串。

较好的是把第一行改为char r[100];只是这样的话,大小固定了。

正确的应该是:

#include <stdio.h>
#include <ctype.h>

int main (void)
{
char s[10];
char t[10];
char *r;
char *malloc();
r = malloc(strlen(s) + strlen(t) + 1);
if(!r)
{
complain();
exit(1);
}
scanf("%s",s);
/*getchar();*/
scanf("%s",t);
strcpy(r,s);
strcat(r,t);
printf("%s/n",r);
free(r);
}

3.3 作为参数的数组声明

我们没有办法将一个数组作为函数参数直接传递。数组名会被转为指向该数组第一个元素的指针。

int strlen(char s[]){}
与下面的写法完全相同:
int strlen(char* s){}

但其它地方就未必相同了。
下面两 个语句是完全不同的。
extern char *hello;
extern char hello[];

下面则是一样的
main(int argc, char* argv[]){}
main(int argc, char** argv){}

3.4 避免“举隅法”

复制指针并不同时复制指针所指向的数据。

3.5 空指针并非空字符串

把常数0转为指针,则指针不等于任何有效的指针,即 void 指针。其它将整数转为指针得到的结果未定
义。当常数0被转为指针时,这个指针绝对不能被解除引用(dereferenc)。换句话说,当我们将0赋给一个指
针变量时,绝对不能企图使用该指针所指向的内存中存储的内容。

下面的是合法的:
if (p == (char *) 0)
但下面是非法的
if (strcmp(p, (char *) 0) == 0)

如果p是一个空指针,即使printf(p);和printf("%s",p);的行为也是未定义的。

3.6 边界计算与不对称边界

数组的下标如果用入界口加出界口来表达(即10个元素,其下标为0 <= n < 10 ),则元素个数即为上界与下界
之差,即下界。若为空,则上界等于下界。任何情况下上界也永远不可能小于下界。

尽量采用非对称边界法。
一个有N个元素的数组 ,我们可以使用a[N]进行比较和赋值,但不能引用其内容。


3.7 求值顺序

C语言只有四个运算符(&&, ||, ?: , 和 ,)存在规定的求值顺序。另外,分隔函数参数的逗号并非逗号
运算符。例如,在x和y在函数f(x,y)中的求值顺序是未定义的,而在函数g((x,y))是先算x,再算y,y
的值为参数。特别是赋值运算符没有规定求值顺序。


3.9 整数溢出

无符号算术运算中,没有所谓的“溢出”一说。有符号运算中发生溢出,则结果未定义。

下面检测溢出的方法不可靠:
if(a + b <0)
complain();

应该这样:
if((unsigned) a + (unsigned) b >INT_MAX)
complain();

或者这样
if(a > INT_MAX - b)
complain();

3.10 为函数main提供返回值

如果没 有为函数声明返回类型,则默认为int.



free之后最好马上就p = NULL;


Chapter 4 连接



4.1 什么是连接器
连接器通常把目标模块看成是由一组外部对象组成的。 第个外部对象都代表着机器内存中的某个部分,并
通达一个外部名称来识别。因此, 程序中的每个函数和每个外部变量,如果没有被声明为static,就都是一个
外部对象。 某些编译器会对静态函数和静态变量的名称做一定改变,将它们也作为外部对象。

除了外部对象,目标模块还可能包括了对其它模块中的外部对象的引用。


4.2 声明与定义


每个外部变量只能定义一次。

4.3 命名冲突与static修饰符

4.4 形参、实参与返回值

每个函数都要在调用之前进行声明定义,不然返回类型为int
如果一个函数没有float,short或者char类型的参数,在函数声明中完全可以省略类型声明(定义不能省
略)

4.5 检查外部类型

同一个外部变量在不同的地方被声明为不同的类型,这种错误大部分编译器是检不出来的。
char file[]= "/etc/password";

extern char* file;
是不一样的。

4.6 头文件

Chapter 5 库函数


C标准没有定义执行底层I/O操作的read和write函数。
5.1 返回整数的getchar函数

5.2 更新顺序文件

为了与以前的程序保持兼容,一个输入操作不能随后紧跟一个输出操作,反之亦然。如果要同时进行输入
和输出操作,必须在其中插入fseek函数的调用。

FILE *fp;
struct record rec;
while (fread((char *)&rec, sizeof(rec),1,fp) = 1)
{
/* */
if(/* */)
{
fseek(fp, -(long)sizeof(rec), 1);
fwrite((char *)&rec, sizeof(rec), 1,fp);
fseek(fp, 0l,1);
}
}

5.3 缓冲输出与内存分配

#include <stdio.h>

void main(void)
{
int c;
char buf[BUFSIZ];
setbuf(stdout,buf);
while((c = getchar()) != EOF)
putchar(c);
}
这个是不对的。buf最后一次被清空是在什么时候?答案是在main函数结束之后,作为程序交回控制给操作系
统之前C运行时库所必须进行的清理工作的一部分。但是在此之前buf已经被释放。

解决方法一是加上static 声明。也可以把buf声明完全移到main函数之外。第二种办法是动态分配缓冲区,
在程序中并不主动释放分配的缓冲区


5.4 使用erron检测错误

很多的库函数,特别是那些与操作系统有关的,当执行失败时会通过一个名称为errno的外部变量,通知
程序该函数调用失败。

下面的是错误的:
/*调用库函数*/
if(errno)
/*处理错误*/
因为,在库函数调用没有失败的情况下,并没有强制要求库函数一定要设置errno为0,这样errno的值可能
就是前一个执行失败的库函数设置的值。
下面更正了,可还是错误的:
errno = 0;
/*调用库函数*/
if(errno)
/*处理错误*/

库函数在调用成功时,既没有强制要求对errno清零,但同时也没有禁止设置errno。

下面才是对的:

/* 调用库函数 */
if(返回的错误值)
检查errno

5.5 库函数signal

从理论上说,一个信号可能在C程序执行期间的任何时刻上发生,甚至可能出现在某些复杂的库函数(如
malloc)的执行过程中。因此从安全的角度讲,信号的处理函数不应该调用上述类型的库函数。基于同样的原
因,从signal处理函数中使用longjump退出,通常情况下也是不安全的:因为信号可能发生在malloc 或者其它
库函数开始更新某个数据结构,却又没有最后完成的过程中。因此signal处理函数能够做的安全的事情,似乎
就只有设置一个标志然后返回,期待以后主程序能够检查到这个标志,发现一个信号已经发生。

然而,就算这样做也并不总是安全的。当一个算术运算错误引发一个信号时,某些机器在signal处理函
数返回后还将重新执行失败的操作。因此对于算术运算错误,signal处理函数的惟一安全、可移植的操作就是
打印一条出错消息,然后使用longjump或exit立即退出程序。
当一个程序异常终止时,程序输出的最后几行常常会丢失,原因是缓冲。


Chapter 6 预处理器

6.1 不能忽视空格

6.2 宏并不是函数

6.3 宏并不是语句

#define assert(e) ((void)((e)||_assert_error(_FILE_,_LINE_)))
6.4 宏并不是类型定义

我们没有办法在一个C表达式的内部声明一个临时变量。
避免副作用的一个办法就是再引入一个变量。

在某个上下文中本应需要函数而实际上却用了函数指针,那么该指针所指向的函数将会自动地被取得并替换这
个函数指针。


Chapter 7 可移植性缺陷

7.1 应对C语言标准变更
7.2 标识符名称的限制

c标准所能保证的只是,c实现必须能够区别出前6个字符不同的外部名称,且并没有要求区分大小写。
7.3 整数的大小

一个普通(int)整数足够大以容纳任何数组下标。

字符长度由硬件决定
7.4 字符是有符号整数还是无符号整数

若为有符号,则将其转为int时,应该同时复制符号位,而无符号,则填 0即可。
一个常见的错误是:如果c是一个字符变量,使用(unsigned)c就可得到与c等价的无符号整数。这是错误
的,因为在将字符c转换为无符号整数之前,c将先被转为int型,而此时可能得到非预期的结果。
正确的是使用语句(unsigned char)c,这样就直接转换。
7.5 移位运算符

如果被移位的对象长度是n位,那么移位计数必须大于或等于0,而严格小于n。

即使某些c实现将符号位复制 到空出的位中,有符号整数的向右移位运算也并不等于除以2的某次幂。
(-1)>>1这一般不可能为0,但(-1)/2一般为0.

7.5 内存位置0

NULL指针并不指向任何对象,只能用于赋值或比较运算。

7.7 除法运算的截断

q = a / b;
r = a % b;
C 语言的定义只保证q*b+r==a,以及当a>=0且b>0时,保证|r|<|b|以及r>=0.最好避免a为负值。

7.8 随机数的大小

RAND_MAX

7.9 大小写转换

7.10 首先释放,然后重新分配

注意早期的C实现可以realloc一个已经free了的指示针。

7.11 一个例子

因为字符串常量可以用来表示一个字符数组,所以在数组名出现的地方都可以用字符串常量末端替换。
如:
"0123456789"[n%10]

-n可能溢出,因为最小负数的绝对值大于最大正数的绝对值。所以改亦正数的符号不会有问题,而改变
负数的符号则可能有问题。

void printnum(long n, void (*p)())
{
if(n<0)
{
(*) ('-');
n=-n;
}
if(n>=10)
printnum(n/10,p);
(*p)((int)(n%10) + '0');
}
上面的是有问题的。下面的才是对的:
void printneg(long n, void (*p)())
{
long q;
int r;
q = n / 10;
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);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值