开始之前...

来源:互联网 发布:服务器租用网站源码 编辑:程序博客网 时间:2024/05/02 09:18

摘要

首先会更新一些书中用到的文件,如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 4096void 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.oCC=gccSRCS=ourhdr.c 1-1.cOBJS=$(SRCS:.c=.o)EXEC=1-1start:$(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); `

从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:

  1. 首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变量是指向参数的指针.
  2. 然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第 一个可变参数的前一个参数,是一个固定的参数.
  3. 然后用va_arg返回可变的参数,并赋值给整数j. va_arg的第二个参数是你要返回的参数的类型,这里是int型.
  4. 最后用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_argint型的返回值:
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 Unixva_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 *,上面有介绍,就是一个指针,然后这个函数通过格式,调节指针,往字符数组里面写数据.这一堆函数用法非常一致.

0 0
原创粉丝点击