C缺陷和陷阱

一、词法陷阱


1.x---y与x- --y是不一样的。
2.int x = 'aaa'; /* char x,在VS中结果是最后一个字符的值 */
   printf("%x\n", x); /* 616161 */
3.C语言不允许嵌套注释
4.写一个测试程序,无论是允许嵌套注释的编译器和不允许的都能够通过编译,但是结果不同。
   /*/*/0*/**/1
   允许的会翻译成这样:/*          /* /0 */      *        */      1,结果是1
   不允许的会翻译成:  /* / */          0 *        /**/          1,结果是0*1=0

5.a-----b会出现编译错误,因为a--不能作为左值,从而不能作为--的操作数


二、语法陷阱


1. 调用首地址为0位置的子例程,将0强制转换成函数指针:(*(void(*)())0)()

2.signal库函数:void (*signal(int, void(*)(int)))(int)

   typedef void (*HANDLER)(int)

   HANDLER signal(int, HANDLER)

3.如果f是一个函数,f;只是计算函数f的地址,却并不会调用该函数。

4.int d[] = {1, 2, 3,};是对的,每一行都已逗号结尾,能方便处理很大的初始化列表。

5.sizeof求有多少个字节,故int a[10][11],sizeof(a) = 10 * 11 * 4。

6.如果a是数组,a+i和i+a一样,所以a[i]和i[a]都是合法的。

7.int d[11][11];int **p = d;是不对的,请看如下代码:

	int calendar[12][31];
	int (*monthp)[31];
	for (monthp = calendar; monthp < & calendar[12]; monthp++) {
		int *dayp;
		for (dayp = *monthp; dayp < &(*monthp)[31]; ++dayp)
			*dayp = 0;
	}

8.修改字符串常量是未定义的行为。

	char * p, * q;
	p = "xyz";
	q = p;
	q[1] = 'Y';//未定义行为

9.空指针并非空字符串

因为有

#define NULL 0

所以0和NULL完全等价,下面的写法完全合法:

if (p == (char*) 0)
但是

if (strcmp(p, (char *)0) == 0)和
printf("%s", p);就不合法了

10.ANSI C标准明确允许引用数组最后一个元素后面那个元素的地址:数组中实际不存在的“溢界”元素的地址位于数组所占内存之后,这个地址可以用于进行赋值和比较,当然不能引用这个元素(非法)

void memcpy(char *dest, const char *source, int k)
{
	while (--k >= 0) *dst++ = *source++;
}
void bufwrite(char *p, int n)
{
	while (n > 0) {
		int k, rem;
		if (bufptr == &buffer[N]) flushbuffer();
		rem = N - (bufptr - buffer);
		k = n > rem ? rem : n;
		memcpy(bufppr, p, k);
		p += k;
		n -= k;
	}
}

11.C语言中只有四个运算符存在规定的求值顺序:(&&  ||  ?:  ,)条件运算符根据判断只算分号分隔的一个。所有其他的运算符都不能保证求值顺序。

12.判断a+b是不是溢出可以用:if ((unsigned)a + (unsigned)b > INT_MAX)或者

if (a > INT_MAX - b)

对称边界下标运算二分:

int * bsearch(int * t, int n, int x)
{
	int lo = 0, hi = n - 1;
	while (lo <= hi) {
		int mid = (hi + lo) / 2;
		if (x < t[mid]) hi = mid - 1;
		else if (x > t[mid]) lo = mid + 1;
		else return t + mid;
	}
	return NULL;
}

指针形式二分:

int * bsearch(int * t, int n, int x)
{
	int * lo = t, * hi = t + n;
	while (lo < hi) {
		int * mid = lo + ((hi - lo) >> 1);
		if (x < *mid) hi = mid;
		else if (x > *mid) lo = mid + 1;
		else return mid;
	}
	return NULL;
}
第四章、连接

1.连接器一般是与C编译器分离的,它不可能了解C语言的诸多细节。然而它却能够理解机器语言和内存布局。

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

3.如果函数f需要调用另一个函数g,而且只有函数f需要调用g,我们可以把f和g放同一个源文件,并且声明g为static。

4.如果亿个寒暑在被定义或声明之前被调用,那么它的返回类型就默认为整型

5.下面的代码当输入0 1 2 3 4时会输出0 0 0 0 0 1 2 3 4

int main()
{
	int i;
	char c;
	for (i = 0; i < 5; ++i) {
		scanf("%d", &c);
		printf("%d ", i);
	}
	printf("\n");
	return 0;
}
c被声明为char类型,而不是int类型,scanf只是将这个指向字符的指针作为指向整数的指针而接受,并且在指针指向的位置存储一个整数。所以字符c附近的内存将被覆盖。本例中它存放的是整数i的低端部分。这样每读入一个数就相当于把i重新设置为0,循环一直进行,当到达文件结束位置后,scanf不再试图突入新的数到c,这时候i才可以正常递增然后种终止循环。

6.检查外部类型

char filename[] = "/etc/passwd";和extern char * filename;在不同的文件中声明,就会报错。第一个语句用filename的值是将得到指向该数组起始元素的指针,但是filename的类型是"字符数组",而不是“字符指针”;第二个声明中,filename被定义为一个指针,这两个对filename的声明使用存储空间的方式是不同的。

正确方法一、char filename[] = "/etc/passwd";和extern char  filename[]

正确方法二、char *filename = "/etc/passwd";和extern char * filename

7.每个外部对象都在同一个地方声明,需要用该对象的模块就包含这个头文件,定义该外部对象的模块也应该包括这个头文件。

8.一个文件有long foo;另一个文件有extern short foo;如果给long类型的foo赋一个37,如果short也是37,那么是小端(低位存低字节),如果为0就是大端(低位存高字节)。

第五章、库函数

1.一个输入操作不能随后直接紧跟一个输出操作,反之亦然。

while (fread((char *)&rec, sizeof(rec), 1, fp) == 1) {
	/*对rec执行某些操作*/
	if (/*rec必须被重新写入*/) {
		fseek(fp, -(long)sizeof(rec), 1);
		fwrite((char *)&rec, sizeof(rec), 1);
		fseek(fp, 0L, 1);//必不可少,因为接下来是循环的fread
	}
}

2.缓冲输出与内存分配:setbuf(stdout, buf)通知I/O库,所有写入到stdout的输出都应该使用buf作为输出缓冲区,直到buf缓冲区被填满或者程序员直接用fflush,buf缓冲区中的内容才实际写入到stdout中,缓冲区的大小由<stdio.h>的BUFSIZ定义。

int main()
{
	int c;
	char buf[BUFSIZ];
	setbuf(stdout, buf);
	while ((c = getchar()) != EOF)
		putchar(c);
	return 0;
}
这个程序是错误的,作为程序交回控制给操作系统之前C运行时库所必须进行的清理工作的一部分,但是在此之前buf字符数组已经被释放!

改进方法1:static char buf[BUSIZ]

改进方法2:setbuf(stdout, malloc(BUFSIZ)),就算malloc失败仍能正常工作。

3.使用errno检测错误

/*调用库函数*/
if (errno)
	/*处理错误*/
这样不对,因为当库函数执行正确的时候,不会把errno置0

errno = 0;
/*调用库函数*/
if (errno)
	/*处理错误*/
也不对,因为像fopen这样的,本身执行正确,但它调用别的库函数改变了errno

因此,再调用库函数时,我们应该首先检测作为错误知识的返回值,确定程序执行已经失败,然后在检查errno来搞清楚错误的原因

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

4.库函数signal,让signal处理函数尽可能简单,不要去调用malloc之类的库函数。

5.当一个程序异常终止时,程序输出的最后几行常常会丢失,是因为异常终止的程序可能没有机会来清空其输出缓冲区,因此,程序生成的输出可能位于内存的某个位置,但却永远不会被写出了,可以在main的第一句加上setbuf(stdoit, (char *)0)

6.为什么前者比后者运行快很多

#include <stdio.h>
int main()
{
	register int c;
	while ((c = getchar()) != EOF)
		putchar(c);
	return 0;
}

#define EOF -1
int main()
{
	register int c;
	while ((c = getchar()) != EOF)
		putchar(c);
	return 0;
}

因为getchar经常被实现为宏,但是库文件中也存在getchar函数,当没有头文件的时候就用的函数调用而不是宏,所以慢很多,同样的依据也完全适用于putchar。

第六章、预处理器

1.对于会引起副作用的宏(如MAX中的++),最好用函数调用替换。

2.#define T1 struct foo *;

  typedef struct foo * T2;

  T1 a, b;//一个指针一个普通

  T2 a, b;//两个指针

3.定义max宏:static int x, y;#define max(a, b) (x = (a), y = (b), x > y ? x : y)

4.(x) ((x) - 1)可能合法,如果x是int或者#define x int,或者是函数指针。

第七章、可移植性缺陷

1.下面这个例子malloc和Malloc是一样的,这样会引起递归调用。

char * Malloc(unsigned n)
{
	char * p, * malloc(unsigned);
	p = malloc(n);
	if (p == NULL)
		panic("out of memory");
	return p;
}

2.一个普通(int类型)整数必须足够大以容纳任何数组下标。

3.字符可能是有符号整数或者无符号整数,与此相关的一个错误的认识是:如果c是一个字符变量,使用

(unsigned)c就可得到与c等价的无符号整数,这是会失败的,因为在将字符c转换成无符号整数时,c将首先被转换为int,这可能得到非预期的结果。正确的方式是使用语句(unsigned char)c,因为一个unsigned char类型的字符在转换为无符号整数时无需首先转换为int型整数,而是直接进行转换。

4.移位运算符:(1)右移位时,如果是无符号数,空位补0,如果是有符号,可能是0,也可能使1。(2)移位位数大于等于0,小于被移位的数的长度。

5.内存位置0。null指针并不指向任何对象,除非是用于赋值和比较运算,出于其他任何目的的使用null指针都是非法的(如strcmp(p, q)其中p和q都是null指针)。某些C预言实现对内存位置0强加了硬件级读保护,在其上工作的程序如果错误使用了一个null指针,就立即终止执行。还有一些只许读,不许写。

int main()
{
	char * p;
	p = NULL;
	printf("%d\n", *p);
	return 0;
}
在禁止读取内存地址0的机器会失败,否则会以10进制打印内存位置0中存储的字符内容。

6.随机数的大小,ANSI C标准定义了RAND_MAX(2^15 - 1)。

7.写一个可移植的atol版本

long atol(char *s)
{
	long r = 0;
	int neg = 0;
	switch (*s) {
		case '-':
			neg = 1;
		case '+':
			++s;
			break;
	}
	while (*s >= '0' && *s <= '9') {
		int n = *s++ - '0';
		if (neg) n = -n;
		r = r * 10 + n;
	}
	return r;
}
第八章、建议与答案

附录A

1.printf,sprintf,fprintf的返回值都是已传送的字符数。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值