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__的位置,而##是宏连字符,用它的原因是前面有个逗号,如果是空调用时,不用连字符,展开之后逗号后面紧跟了右括号,编译会报错。

C陷阱与缺陷读书笔记的更多相关文章

  1. C陷阱和缺陷学习笔记

    这段时间把<C陷阱和缺陷>看了,没时间自己写总结.就转一下别人的学习笔记吧http://bbs.chinaunix.net/thread-749888-1-1.html Chapter 1 ...

  2. C的陷阱和缺陷研读笔记01

    词法分析: 编译器将程序分解成符号的方法是 从左到右一个一个字符的读入,如果该字符可能组成一个符号,再读入下一个字符 而c语言里的符号 / * =只有一个字符长, 是单字符的, /* == 一些事双字 ...

  3. 软件测试价值提升之路- 第三章"拦截缺陷 "读书笔记

    作为一个测试团队,基本的职责是:测试产品,发现缺陷,报告结果,使每个版本的测试水准稳步提升.这些价值是作为一个测试所必须具备的,发挥这些价值能够让测试获得研发团队的基本信任.这类价值分为3部分: 1) ...

  4. C陷阱与缺陷学习笔记

    导读 程序是由符号(token)序列所组成的,将程序分解成符号的过程,成为"词法分析". 符号构成更大的单元--语句和声明,语法细节最终决定了语义. 词法陷阱 符号(token)指 ...

  5. C的陷阱和缺陷研读笔记02

    宏: 宏不是函数 展开会产生庞大的表达式 #define MIN(A,B) ((A) <= (B) ? (A) : (B))MIN(*p++, b)会产生宏的副作用 剖析: 这个面试题主要考查面 ...

  6. 《c陷阱与缺陷》笔记--注意边界值

    如果要自己实现一个获取绝对值的函数,应该都没有问题,我这边也自己写了一个: void myabs(int i){ if(i>=0){ printf("%d\n",i); }e ...

  7. 《c陷阱与缺陷》笔记--移位运算

    #include <stdio.h> int main(void){ int a = 2; a >> 32; a >> -1; a << 32; a & ...

  8. 读书笔记--C陷阱与缺陷(七)

    第七章 1.null指针并不指向任何对象,所以只用于赋值和比较运算,其他使用目的都是非法的. 误用null指针的后果是未定义的,根据编译器各异. 有的编译器对内存位置0只读,有的可读写. 书中给出了一 ...

  9. 读书笔记--C陷阱与缺陷(一)

    要参与C语言项目,于是作者只好重拾C语言(之前都是C++,还是C++方便). 看到大家都推荐看看  C陷阱与缺陷(C traps and pitfalls),于是好奇的开始了这本书的读书之旅. 决定将 ...

随机推荐

  1. javascript对数据处理

    数组去重 法一: // 遍历数组,建立新数组,利用indexOf判断是否存在于新数组中,不存在则push到新数组,最后返回新数组 function unique(ar) { var ret = []; ...

  2. Ubuntu一般软件安装后的路径

    Ubuntu一般安装的软件查找路径: computer/usr/local/

  3. yum更换国内源 yum下载rpm包 源码包安装

    7.6 yum更换国内源 7.7 yum下载rpm包 7.8/7.9 源码包安装 yum更换国内源 cd  /etc/yum.repo.d/ 删除源 rm -f   dvd.repo rm -f  C ...

  4. DIV 自定义滚动条样式

    当内容超出容器时,容器会出现滚动条,其自带的滚动条有时无法满足我们审美要求,那么我们可以通过css伪类来实现对滚动条的自定义. 首先我们要了解滚动条.滚动条从外观来看是由两部分组成:1,可以滑动的部分 ...

  5. Thinkphp5 runtime路径设置data

    路径设置 index.php // runtime文件路径define('RUNTIME_PATH', __DIR__ . '/data/runtime/');

  6. level1 - unit 1 - 句子结构

    preface 学习英语做为一种爱好,就是希望有一天能够和老外流畅沟通,目前来看,日常沟通还是没有问题的和我的外教. 知识日积月累,现在就把以前学习的(从初中到现在)知识总结下.每天一更,更到单元总结 ...

  7. lua中实现倒计时

    今天在开发的时候,涉及到了使用倒计时来显示. 首先自己的思路是: 1.设计显示的Lable. 2.对传入的时间进行处理,转成字符串00:00:00.通过调用回调函数来控制一秒刷新一次. 转换算法: h ...

  8. Redis性能测试Redis-benchmark

    Redis-benchmark是官方自带的Redis性能测试工具 测试Redis在你的系统及你的配置下的读写性能 redis-benchmark可以模拟N个机器,同时发送M个请求 redis-benc ...

  9. Nginx 反向代理的正确配置

    server { listen 80; server_name 127.0.0.1; #charset koi8-r; #access_log logs/host.access.log main; l ...

  10. springboot+elasticsearch配置实现

    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/20 ...