开始之前...

16 篇文章 7 订阅
6 篇文章 1 订阅

摘要

首先会更新一些书中用到的文件,如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); `

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

  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
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值