摘要
首先会更新一些书中用到的文件,如ourhdr.h
,以及ourhdr.c
文件,然后会简要地介绍一下里面用到的函数.
书中用到的一些文件
首先是要用到头文件:
/*ourhdr.h*/
/*一下子写不了那么多,后面更新的时候慢慢来补*/
#ifndef __ourhdr_h
#define __ourhdr_h
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define MAXLINE 4096
void err_msg(const char *, ...);
void err_sys(const char *, ...);
void err_quit(const char *, ...);
void err_ret(const char *, ...);
void err_dump(const char *, ...);
#endif//__ourhdr_h
然后是实现文件:
#include <errno.h>
#include <stdarg.h>
#include "ourhdr.h"
char *pname = NULL; //调用者通过argv[0]来设置这个东西
static void err_doit(int, const char *, va_list);
/*和系统相关的非致命错误,输出信息,返回*/
void err_ret(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
err_doit(1, fmt, ap);
va_end(ap);
return;
}
static void err_doit(int errnoflag, const char *fmt, va_list ap)
{
int errno_save;
char buf[MAXLINE];
errno_save = errno; //将错误码存储起来
vsprintf(buf, fmt, ap);//将流写入到buf里面
if (errnoflag)
sprintf(buf + strlen(buf), ": %s", strerror(errno_save));
strcat(buf, "\n"); //在buf后面加一个\n
fflush(stdout); //刷新/更新标准输出
fputs(buf, stderr);//将buf中的数据写入到标准出错中去
fflush(NULL); //刷新所有的stdio输出流
return;
}
/*和系统调用无关的致命错误,输出信息,并终止*/
void err_quit(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
err_doit(0, fmt, ap);
va_end(ap);
exit(1); //退出整个程序
}
/*关于系统调用的致命错误,输出信息,并且退出*/
void err_sys(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
err_doit(1, fmt, ap);
va_end(ap);
exit(1);
}
/*关于系统调用的致命错误,输出信息,倾倒core文件,退出*/
void err_dump(const char *fmt, ...)
{
va_list ap;
err_doit(1, fmt, ap);
va_end(ap);
abort(); //异常终止,倾倒core文件
exit(1); //程序应该运行不到这里
}
/*关于系统调用的非致命的错误,输出信息,返回*/
void err_msg(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
err_doit(0, fmt, ap);
va_end(ap);
exit(1);
}
不管怎么样,先拿出来跑一跑,我们以这本书的1-1代码为例:
/*1-1.c*/
#include <sys/types.h>
#include <dirent.h>
#include "ourhdr.h"
int main(int argc, char * argv[])
{
DIR *dp;
struct dirent *dirp;
if (argc != 2)
err_quit("a single argument(the directory name) is required");
if ((dp = opendir(argv[1])) == NULL)
err_sys("can't open %s", argv[1]);
while((dirp = readdir(dp)) != NULL)
printf("%s\n", dirp->d_name);
closedir(dp);
exit(0);
}
我这里给出一个makefile文件,方便于编译:
.SUFFXES:.c.o
CC=gcc
SRCS=ourhdr.c 1-1.c
OBJS=$(SRCS:.c=.o)
EXEC=1-1
start:$(OBJS)
$(CC) -o $(EXEC) $(OBJS)
.c.o:
$(CC) -Wall -g -o $@ -c $<
clean:
rm -f $(OBJS)
rm -f core*
在我的机器上跑的很正常:
一些常用函数的说明
关于va_start
,va_arg
,va_end
.
下面直接转载自http://jazka.blog.51cto.com/809003/232331/,因为实在写得已经很好了,我没必要重复造轮子,不过我会添加一些自己的说明,以帮助理解.
下面的解说如果看不懂的话,强烈推荐先补一下 csapp .
(一)写一个简单的可变参数的C函数
下面我们来探讨如何写一个简单的可变参数的C函数.写可变参数的
C函数要在程序中用到以下这些宏:
#include <stdarg.h>
void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
void va_copy(va_list dest, va_list src);
va在这里是variable-argument(可变参数)的意思. 这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件.
下面我们写一个简单的可变参数的函数,改函数至少有一个整数参数,第二个参数也是整数,是可选的.函数只是打印这两个参数的值.
void simple_va_fun(int i, ...)
{
va_list arg_ptr;
int j=0;
va_start(arg_ptr, i);
j=va_arg(arg_ptr, int);
va_end(arg_ptr);
printf("%d %d\n", i, j);
return;
}
我们可以在我们的头文件中这样声明我们的函数:
extern void simple_va_fun(int i, ...);
我们在程序中可以这样调用:
simple_va_fun(100); `
simple_va_fun(100,200); `
从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:
- 首先在函数里定义一个
va_list
型的变量,这里是arg_ptr
,这个变量是指向参数的指针. - 然后用
va_start
宏初始化变量arg_ptr
,这个宏的第二个参数是第 一个可变参数的前一个参数,是一个固定的参数. - 然后用va_arg返回可变的参数,并赋值给整数
j
.va_arg
的第二个参数是你要返回的参数的类型,这里是int型. - 最后用
va_end
宏结束可变参数的获取.然后你就可以在函数里使 用第二个参数了.如果函数有多个可变参数的,依次调用va_arg
获取各个参数.
如果我们用下面三种方法调用的话,都是合法的,但结果却不一样:
simple_va_fun(100);
结果是:100 -123456789(会变的值)
simple_va_fun(100,200);
结果是:100 200
simple_va_fun(100,200,300);
结果是:100 200
我们看到第一种调用有错误,第二种调用正确,第三种调用尽管结果正确,但和我们函数最初的设计有冲突.下面一节我们探讨出现这些结果
的原因和可变参数在编译器中是如何处理的.
(二)可变参数在编译器中的处理
我们知道va_start
,va_arg
,va_end
是在stdarg.h
中被定义成宏的, 由于
硬件平台的不同
编译器的不同
所以定义的宏也有所不同,下面以VC++
中stdarg.h
里x86平台的宏定义摘录如下:
typedef char * va_list;
#define _INTSIZEOF(n) \
((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t) \
( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
[笔者注]之所以va_start宏的第二个参数是前一个参数,我们可以从vc6的宏定义中看出一些端倪,va_start实际上是通过前一个参数来确定可变参数的首地址,va_arg之所以要传入type,是因为va_arg中的ap要通过type来移动指针,所以你查man手册,它告诉你,如果type不对的话会出现未知的错误,这下你应该懂了吧,所谓的未知的错误由指针的移动的步长引起,比如说本来是一个int,你传入一个char类型,指针只会移动1,很明显,你的读取就会有问题.
定义_INTSIZEOF(n)
主要是为了某些需要内存的对齐的系统.C语言的函 数是从右向左压入堆栈的,图(1)是函数的参数在堆栈中的分布位置.我 们看到va_list
被定义成char*
,有一些平台或操作系统定义为void*
.再 看va_start
的定义,定义为&v+_INTSIZEOF(v)
,而&v
是固定参数在堆栈的地址,所以我们运行va_start(ap, v)
以后,ap
指向第一个可变参数在堆栈的地址,如图:
高地址|-----------------------------|
|函数返回地址 |
|-----------------------------|
|....... |
|-----------------------------|
|第n个参数(第一个可变参数) |
|-----------------------------|<--va_start后ap指向
|第n-1个参数(最后一个固定参数)|
低地址|-----------------------------|<-- &v
图( 1 )
然后,我们用va_arg()
取得类型t的可变参数值,以上例为int型为例,我 们看一下va_arg
取int
型的返回值:
j= ( *(int*)((ap += _INTSIZEOF(int))-_INTSIZEOF(int)) );
首先ap+=sizeof(int)
,已经指向下一个参数的地址了.然后返回 ap-sizeof(int)
的int*
指针,这正是第一个可变参数在堆栈里的地址 (图2).然后用*
取得这个地址的内容(参数值)赋给j
.
高地址|-----------------------------|
|函数返回地址 |
|-----------------------------|
|....... |
|-----------------------------|<--va_arg后ap指向
|第n个参数(第一个可变参数) |
|-----------------------------|<--va_start后ap指向
|第n-1个参数(最后一个固定参数)|
低地址|-----------------------------|<-- &v
图( 2 )
最后要说的是va_end
宏的意思,x86平台定义为ap=(char*)0;
,使ap
不再指向堆栈,而是跟NULL一样.有些直接定为((void*)0)
,这样编译器不会为va_end
产生代码,例如gcc linux的x86平台就是这样定义的.
在这里大家要注意一个问题:由于参数的地址用于va_start
宏,所以参数不能声明为寄存器变量或作为函数或数组类型.
关于va_start
, va_arg
, va_end
的描述就是这些了,我们要注意的 是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.
(三)可变参数在编程中要注意的问题
因为va_start
, va_arg
, va_end
等定义成宏,所以它显得很愚蠢, 可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能地识别不同参数的个数和类型.
有人会问:那么printf
中不是实现了智能识别参数吗?那是因为函数
printf
是从固定参数format
字符串来分析出参数的类型,再调用va_arg
的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通 过在自己的程序里作判断来实现的.
另外有一个问题,因为编译器对可变参数的函数的原型检查不够严
格,对编程查错不利.如果simple_va_fun()
改为:
void simple_va_fun(int i, ...)
{
va_list arg_ptr;
char *s=NULL;
va_start(arg_ptr, i);
s=va_arg(arg_ptr, char*);
va_end(arg_ptr);
printf("%d %s\n", i, s);
return;
}
可变参数为char*
型,当我们忘记用两个参数来调用该函数时,就会出现 core dump(Unix)
或者页面非法的错误(window平台).但也有可能不出 错,但错误却是难以发现,不利于我们写出高质量的程序.
以下提一下va系列宏的兼容性.
System V Unix
把va_start
定义为只有一个参数的宏:
va_start(va_list arg_ptr);
而ANSI C则定义为:
va_start(va_list arg_ptr, prev_param);
如果我们要用system V
的定义,应该用vararg.h
头文件中所定义的 宏,ANSI C的宏跟system V的宏是不兼容的,我们一般都用ANSI C,所以 用ANSI C的定义就够了,也便于程序的移植.
关于vsprintf
函数
好吧,这个其实也不难,直接用man手册一查就出来了.出来一堆的函数:
#include <stdio.h>
int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);
#include <stdarg.h>
int vprintf(const char *format, va_list ap);
int vfprintf(FILE *stream, const char *format, va_list ap);
int vsprintf(char *str, const char *format, va_list ap);
int vsnprintf(char *str, size_t size, const char *format, va_list ap);
我不会逐字翻译man手册里的东西,你只需要知道vsprintf
这个函数将字符数组当做写入的对象,这也是为什么第一个参数是char *
,然后是输入的格式char * format
,如"%s %d"
之类的东西,然后是一个va_list
,简化一点是char *
,上面有介绍,就是一个指针,然后这个函数通过格式,调节指针,往字符数组里面写数据.这一堆函数用法非常一致.