C陷阱与缺陷读书笔记

时间:2023-03-09 19:59:08
C陷阱与缺陷读书笔记

2.1理解函数声明

这一章仔细分析了(*(void(*)())0)();这条语句的含义,并且提到了typedef的一种函数指针类型定义的用法。

我们经常用到的typedef用法是用于指定结构体的类型,比如单链表的结点经常这么定义

typedef struct {
int data;
struct node *next;
} Node;

实质上是给struct给了一个别名叫做Node,之后在使用时就只需要用Node这个类型名即可,与int、float等内置类型名用法完全一致。

但在函数指针这里用法有一点不一样,是这么用的

typedef void (*funcptr)();

这里定义了一种函数指针类型,叫做funcptr,它的返回值是void,参数列表为空。

(*(void(*)())0)();这条语句就等价于(*(funcptr)0)();

将0这个地址通过强制类型转换为(funcptr)0,意思是0这个地址的位置存放了一个类型为funcptr的函数,对其解除引用*(funcptr)0就是0这个地址所存放的内容,即函数本身,然后后面跟参数列表,就是调用它。这就是这条看上去很复杂的语句的实际作用。

当然我们现在的系统一般不能对0地址解除引用,这里是一种硬件直接调用位于0地址位置的函数的案例。

2.2运算符优先级的问题

以前我没有太多想过去记忆运算符的优先级,总想着用括号来避免歧义,但实际上有时候括号过多也妨碍理解,要成为高手C程序员对于优先级应该有一个概念才是。

这章节里作者提出了C语言运算符优先级的排列是有一定规律的,也有的有历史原因遗留,并不是凭空有这样的优先级顺序的,本身肯定是有一定的好处。

优先级最高的不是真正意义上的运算符,包括了数组下标[],函数调用操作符(),结构体成员选择符->和.,它们是自左向右结合的。

因此a.b.c是(a.b).c而不是a.(b.c),二维数组calendar[12][31]是一个12元素的数组,每个元素是一个31元素的数组。

接下来是单目运算符,包括符号运算符-,强制类型转换运算符(type),自增运算符++,自减运算符--,取值(解除引用)运算符*,取地址运算符&,逻辑非运算符!,按位取反运算符~,长度运算符sizeof,结合方向为从右往左。

因为函数调用优先于单目运算符,所以如果p是个函数指针,要调用它必须为(*p)(),而不是*p(),因为后者是*(p())。由于单目运算符是从右往左,所以*p++是*(p++)而不是(*p)++。

然后就是双目运算符,双目运算符的优先级顺序依次是:算术运算符(*、/、%;+、-)、移位运算符(<<、>>)、关系运算符(<、<=、>、>=;==、!=)、逻辑运算符(按位与&;异或^;或|;逻辑与&&;或||),都是从左往右结合的。

然后是三目运算符(?:)是从右往左结合的

之后是赋值运算符,包括=以及各种+=,-=,*=,/=,%=,<<=,>>=,&=,^=,|=从右往左结合的

最后是逗号,从左往右结合。

不过这里还是有个问题,如果赋值运算优先级这么低的话a=i++应该是i++先,然后a=i,实际上是a=i然后i++。所以涉及++还是要更加谨慎一些。比较重要的是双目运算符的顺序,也是容易出错的地方。

5.2文件写入顺序的问题

对同一个文件不可以交错进行fread和fwrite,中间必须间隔一个fseek操作更新文件的状态才可以进行下一个fread或者fwrite。书中列举了一个例子:

FILE *fp;
struct record rec;
...
while(fread((char*)&rec, sizeof(rec), 1, fp) == 1)
{
/*对rec进行某些操作*/
if (/*rec需要被重新写入*/)
{
fseek(fp, -(long)sizeof(rec), SEEK_CUR);
fwrite((char*)&rec, sizeof(rec), 1, fp);
fseek(fp, 0L, SEEK_CUR);//这一行是必要的
}
}

这段程序注意到了很多地方,比如fseek函数的第二个参数必须是long类型的,而sizeof的返回值是unsigned类型,所以如果要取其负值(为了将文件位置往前偏移到文件开始),必须要进行类型转换为有符号long。

比如fwrite和fread的第一个参数是char*类型的,也进行了强制类型转换,把结构体的地址转换成char*类型。

第二个fseek其实没有移动文件位置,但是也是必要的,因为fwrite之后不能直接跟循环条件里面那个fread

5.3缓冲输出与内存分配

#include <stdio.h>
int main()
{
int c;
char buf[BUFSIZ];//BUFSIZ宏位于stdio.h中,定义了缓冲区的大小
setbuf(stdout, buf); while ((c = getchar()) != EOF)
{
putchar(c);
} return 0;
}

这个程序是错误的,因为指定了缓冲区为buf,然后buf在清空是位于main结束以后,进行清理工作时由操作系统进行。

但那之前,在main结束的时候buf已经被回收了,所以不存在。

解决方式有两种,一种是声明buf为static,那么退出main的时候不会被从栈上回收掉。也可以把它的声明放在main外面。

另一种是动态分配,如下

char *malloc();
setbuf(stdout, malloc(BUFSIZ));

这里如果malloc失败会返回一个NULL,就是无缓冲区的情况。

这里中文版漏翻译了一点,就是这种动态方式是永远不会free掉这块分配出来的内存的。原文如下

Another possibility is to allocate the buffer dynamically and never free it.

5.4使用errno检查错误

使用errno时,应确定库函数的返回是错误时再检查errno,因为库函数可能调用其他库函数,这一过程中会修改errno

这样errno不一定代表这个库函数是否执行错误。因为库函数成功时,并不一定会清零errno也不一定会设置errno

5.5关于signal库函数

需要引用signal.h头文件,其中对signal函数的定义是这样的void (*signal(int sig, void (*func)(int)))(int)

要处理一个特定的信号,可以这么调用signal函数

signal(signal type, handler function);

signal type是定义在头文件里的一些常量宏,标识要捕捉的信号类型,比如SIGINT

handler function则是当我们指定的事件发生时,要调用的函数

安全考虑handler function不能使用一些复杂的函数,因为很可能在执行一半的时候被中断

比如malloc函数执行一半时,触发了我们要求的信号,此时里面用于判断内存是否可以用的结构体可能还没有更新完,此时如果在handler function里再次调用malloc函数,就可能崩溃。

而如果只是设置一个标志就退出,期待主函数能够发现这个标志并进行处理也不总是安全,当一个算术运算错误(比如除零溢出)触发了信号后,有的机器在执行完handler function后会再次执行算术运算,于是又一次触发相同的信号。

所以唯一的方式就是打印一条错误信息,然后exit

附录:可变参数

有时候会用到这样的封装

#define ERR_MSG(fmt, ...)\
fprintf(stderr, "%s: %s: %d: errmsg: " fmt "\n", __FILE__, __func__, __LINE__, ##__VA_ARGS__)

用法就像这样

ERR_MSG("string %s, int %d", name, age);

打印的结果会在前面追加一些内容,说明这一行打印信息是位于哪个文件,哪个函数,哪一行的信息,方便调试时定位错误。

_FILE_, _func_, __LINE__以及__VA_ARGS__都是预定义的宏。

宏展开时,会把fmt展开为格式描述字符串,然后后面三个点...表示跟着可变数量的参数,直到右括号为止。展开的时候它将替换__VA_ARGS__的位置,而##是宏连字符,用它的原因是前面有个逗号,如果是空调用时,不用连字符,展开之后逗号后面紧跟了右括号,编译会报错。