一 errno.h 背景知识
unix的每一个实现对错误的系统调用都采用一种简单的指示方法 用汇编语言实现 通常使用条件编码的仅为标志来测试 如果进位标志被清零 系统调用流成功 要求所有的结构都返回到机器寄存器或者程序的一个结构中 如果进位标志不是0 调用就出错 寄存器记录一个很小的正数来说明错误的性质
c中的错误处理
一个一般的系统调用 可以定义一个和所有的有效结果都不相同的错误返回值 在这点上 空指针就是实例 值-1可以在很多情况下被设定为错误返回值,但是c调用的函数不能做到是准确地报告发生了什么错误 所以需要从其他的地方来获得错误的细节
外部链接的数据对象 内核中存储一个叫errno的整形变量作为错误编码,或者返回-1 或者返回其他适当的无意义的值来说明一个错误 尽早访问errno 如果一个系统调用失败,原来的错误编码就会被新的错误编码覆盖掉
过劳的机制
啰嗦 太方便导致增加了系统调用的错误数量 每个可能引起麻烦的函数引入一个或多个错误编码 却容易引起混乱
数学错误的归类
a 当一个结果在数值上太大而不能作为指定类型的浮点值表示的时候就会发生向上溢出
b 当一个结果在数值上太小而不能作为指定类型的浮点值表示的时候会发生向下溢出
c 结果没有位置容纳它的类型指示的有效位的时候会发生有效值丢失
d 接受一个指定的参数值而产生的结果没有被定义的时候会发生域错误
不相同的系统调用可能产生相同的错误代码 数学函数也是如此 实际仅使用两个错误代码就足够表示所有的数学错误
a 当一个域错误发生时报告EDOM
b 当溢出发生时报告ERANGE
通过errno中存储EDOM和ERANGE来报告数学错误是c标准库一个较早的符合标准的做法 下面是库函数中其他几个需要设置errno的地方
清单:
a <math.h>中声明的很多函数都将<errno.h>中声明的宏EDOM和ERANGE的值存储在errno中
b <stdlib.h>中声明的一些函数把文本串转换为各类算数类型的值,这些函数的部分或者全部将ERANGE存储在errno中
c <stdio.h>中声明的函数 改变文件中下一次读写操作的位置 在errno中存储一个正值 在实现中定义此值 在errno.h中定义的宏名 该宏和那个值相对应 并非一个常用的宏名
d <signal.h>中声明的函数signal 也在errno中存储一个正值 非实现定义
二 c标准的内容
errno.h
头文件中定义了几个宏 和错误状态的报告有关
EDOM 和ERANGE
具有不同非零值的正数常量表达式 适合在#if预处理指令中使用 而宏errno 则展开为一个可以修改的int类型的左值 被某几个库函数设置为一个正的错误编号 无需说明errno是一个宏还是一个声明为具有外部链接的标识符
errno的值在程序启动时设置为0 但不会被任何库函数设置为0
注意 宏errno不一定是一个对象的标识符 可能展开为一个函数调用返回 可以修改为lvalue
一个程序如果使用errno进行错误检查 应该在一个库函数调用之前把它设置为0
三 <errno.h>的使用
一种方式是为特定的系统编写代码 需要学习它特定的错误编码集合
另一方式是编写可以移植的代码 避免任何附加的错误编码的假设 只能依靠c标准中指定的errno属性
#include <errno.h>
#include <math.h>
...
errno=0;
y=sqrt(x);
if(errno!=0)
printf("invalid x:%e\n",x);
四 <errno.h>的实现
c标准对错误的实现要求较少
/*errno.h standard header*/
#ifndef _ERRNO
#define _ERRNO
#define EDOM 1
#define ERANGE 2
extern int errno;
#endif
在某个库文件中 必须为数据对象添加一个定义:int errno=0;
另外需要在库函数内适当的位置把像EDOM和ERANGE 这样的值存储在errno中
数学异常
数学协处理器独立地工作 而且不停运转 需要记录错误编码 并且保留 协处理器用他们自己的状态码记录错误的发 生,主处理器必须把协处理器的状态码复制过来才能检查是否有错误发生 这样就阻止了流水线协处理器的全速运 转。
宏errno
标准没有要求errno是一个真实的数据对象 可以作为一个宏来定义 此宏可以被扩展为一个可以更改的左值
两个含义 首先 实现可能不想记录错误 可以等到有人急切地寻找errno的时候才记录最新的错误编码 数学协处理器可以大部分时间独立地工作
其次 errno可以移动 函数每次调用时都可以返回一个不同的地址
作为一个宏 errno任然时体制中令人讨厌的一部分 任何程序都可以包含下面的代码
y=sqrt(x);
if(errno=EDOM)
...
严重限制了实现对sqrt之类的函数的操作 因为任何库函数都可以修改errno
参数编码
errno.h代码 不简单 参数化的这种简单的形式一定要做些修改以适合每个管理库的操作系统
/*errno.h standard header*/
#ifndef _ERRNO
#define _ERRNO
#ifndef _YVALS
#include <yvals.h>
#endif
/*error codes*/
#define EDOM _EDOM
#define ERANGE _ERANGE
#define EFPOS _EFPOS
/*ADD YOURS HERE*/
#define _NERR _ERRMAX /*one more than last code*/
/*declarations*/
extern int errno;
#endif
使用<errno.h>的大部分代码都很在意一两个错误编码的值 维护麻烦
头文件 <yvals.h>
引入一个叫做“内部标准头文件”的东西 一些标准头文件包含头文件<yvals.h> 尖括号告诉编译器在其他标准头文件存储的地方找此头文件
errno.h以yvals.h中定义的其他宏为基础定义了自己的宏 分两步的过程是必要的 因为其他的头文件也包含yvals.h 只有当程序包含了errno.h 才能定义宏ERANGE
yvals.h的宏保护是在包含y.h的头文件中 而不是在y.h中的。此为小的优化 几个标准头文件包含这个头文件时 可能在一个翻译单元中被多次请求 一旦y.h变为翻译单元的一部分 宏保护就会跳过#include预处理指令 此头文件就不会被重复地读入
errno.c
/*errno storage*/
#include <errno.h>
#undef errno
int errno=0;
代码显示了定义errno数据对象的文件error.c #undef预处理指令只是将来<errno.h>改变的一个安全保证
头文件 yfuns.h
使用这个库 适用于一个给定的操作系统 仅仅有<yvals.h>是不够的 作用与上个头文件相似 但是不同
五 <errno.h>的测试
terrno.c 源代码省略