一、词法陷阱
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的返回值都是已传送的字符数。