当前位置:网站首页>附录A printf、varargs与stdarg A.2 使用varargs.h来实现可变参数列表
附录A printf、varargs与stdarg A.2 使用varargs.h来实现可变参数列表
2022-08-01 21:00:00 【weixin_客子光阴】
A.2 使用varargs.h来实现可变参数列表
在编写C程序的过程中,随着程序规模的增大,程序员经常感到有必要进行系统化的错误处理。
error("%d is out of bounds", x);
就与下式等效
fprintf(stderr, "error: %d is out of bounds\n", x);
exit(1);
只有一个小细节“梗”住了我们:error函数的参数数目与类型在不同的调用间并非一成不变,而是像
printf函数那样可能随调用的不同而变动。一个典型的解决之道是把error函数写成像下面这样,可惜这种做法并不正确:
void error(a, b, c, d, e, f, g, h, i, j, k);
{
fprintf(stderr, "error:");
fprintf(stderr, a, b, c, d, e, f, g, h, i, j, k);
fprintf(stderr, "\n");
exit(1);
}
因为函数a到k并没有声明,所以它们默认为int类型。当然,error函数至少包括了一个非int类型的参数(即格式字符串)。因此,这个程序能够正常工作就依赖于是否可以用一组整型参数来复制任意类型的数值。
printf函数的第一个参数必须是一个字符串,我们可以通过检查这个字符串来得到其他参数的数目与类型(当然,假定printf函数的调用是正确的)。
为了便于printf函数的实现,这样一种机制就应该拥有以下特性。
*只需要知道函数的第一个参数的类型,就可以对其进行存取。
*一旦第n个参数被成功地存取,第n+1个参数就可以在仅知道类型的情况下进行存取。
*按这种方式存取一个参数所需的时间不应太多。
需要特别注意的是,逆向存取参数,或者随机存取参数,或者以任何非从头到尾的顺序方式来存取参数,都是不必要的。进一步来说,检测参数列表是否结束通常既不必要,也不可能。
大多数C语言实现都是通过一组总称为varargs的宏定义来达到上述目的。这些宏的确切定义虽然与特定的C语言实现有关,但是只要我们在程序中运用得当,还是能够在相当多的机器上使用可变参数列表。
任何一个程序,只要用到varargs中的宏,就应该像下面这样:
#include<varargs.h>
varargs.h头文件中定义了宏名va_list、va_dcl、va_start、va_end以及va_arg。va_alist一般由编程人员来定义。
任何一个C语言实现中,对于可变参数列表的第n个参数,在已知其类型的情况下要对其进行存取,还需要一些额外的信息。这些信息是通过已经可以存取的第1个参数到第n-1个参数而间接得到的,可以把它看做一个指向参数列表内部的指针。当然,在某些机器上具体的实现可能要复杂得多。
这些信息存储在一个类型为va_list的对象中。因此,当声明了一个名称为ap的类型为va_list的对象后,我们只需要给定ap与第一个参数的类型,就可以确定第一个参数的值。
通过va_list存取一个参数之后,va_list将被更新,指向参数列表中的下一个参数。
因为一个va_list中包括了存取全部参数的所有必要信息,所以函数f可以为它的参数创建一个va_list,然后把它传递给另一个函数g。这样,函数g就能够访问函数f的参数。
在许多C语言实现中,printf函数族中的3个函数(printf、fprintf和sprintf),它们都调用了一个公共的子函数。而对这个子函数来说,获取它的调用函数的参数就很重要。
被调用是带有可变参数列表的函数,必须在函数定义的首部使用va_alist和va_dcl宏,如下所示:
#include<varargs.h>
void error(va_alist) va_dcl
宏va_alist将扩展成特定C实现所要求的参数列表,这样函数就能够处理变长参数;而宏va_dcl将扩展为与参数列表对应的声明,必要时还包括一个作为语句结束标志的分号。
error函数必须创建一个va_list变量,把变量名传递给宏va_start来初始化该变量。这样做之后,就可以逐个读取error函数的参数列表中的参数了。当程序不再用到参数列表中的参数时,我们必须以va_list变量名为参数来调用宏va_end,表示不再需要用到va_list变量了。
error函数于是进一步扩展为:
#include <varargs.h>
void error(va_alist) va_dcl
{
va_list ap;
va_start(ap); //初始化ap变量,或许会分配动态内存
//这里是使用ap的程序部分
va_end(ap); //释放动态内存或者什么也不做
//这里是不使用ap的其他程序部分
}
务必记住,在使用完va_list变量后一定要调用宏va_end。某些版本的va_start宏为了方便对va_list进行遍历,会给参数列表动态分配内存。这样一种C实现很可能利用va_end宏来释放此前动态分配的内存,如果忘记,则会发生“内存泄漏”。
#include <varargs.h>
void error(va_alist) int va_alist;
{
char *ap
ap = (char *)&va_alist;
//这里是使用ap的程序部分
va_end(ap);
//这里是不使用ap的其他程序部分
}
宏va_arg用于对一个参数进行存取。它的两个参数分别为va_list变量名和希望存取的数据类型。va_list宏将取得这个参数,并更新va_list变量,使其指向下一个参数。
#include <varargs.h>
void error(va_alist) va_dcl
{
va_list ap;
char *format;
va_start(ap);
format = va_arg(ap, char *); //用于对一个参数进行存取
fprintf(stderr, "error: ");
//(do something magic) //某些实现方式暂时未知的工作
va_end(ap);
fprintf(stderr, "\n") ;
exit(1);
}
#include<varargs.h>
void error(va_alist) int va_alist; {
char *ap;
char *format;
ap = (char *)&va_alist;
format = ((char **)(ap += sizeof(char*)))[-1];
fprintf(stderr, "error: ");
//(do something magic) //某些实现方式暂时未知的工作
va_end(ap);
fprintf(stderr, "\n") ;
exit(1);
}
暂时受阻了:没有办法让printf函数接受一个va_list变量作为参数。
幸运的是,ANSI C标准要求,而且很多C语言实现也提供了分别称为vprintf、vfprintf和vsprintf的函数。这些函数与对应的printf函数族中的函数在行为方式上完全相同,只不过用va_list替换了格式字符串后的参数序列。这些函数之所以能够存在,理由有两个:其一,va_list变量可以作为参数传递;其二,va_arg宏可以独立实现在一个函数中,并不强制要求与va_start宏(该宏的作用是初始化va_list变量)成对使用。
error函数的最终版本如下所示:
#include <stdio.h>
#include <varargs.h>
void error(va_alist) va_dcl
{
va_list ap;
char *format;
format = va_arg(ap, char *);
fprintf(stderr, "error: ");
vfprintf(stderr, format, ap);
va_end(ap);
fprintf(stderr, "\n");
exit(1);
}
void error(va_alist) int va_alist; {
char *ap;
char *format;
format = ((char **)(ap += sizeof(char*)))[-1];
fprintf(stderr, "error: ");
vfprintf(stderr, format, ap);
va_end(ap);
fprintf(stderr, "\n");
exit(1);
}
保存vprintf函数的结果,我们需要把这个结果返回给printf函数的调用方。
#include <varargs.h>
int printf(va_alist) va_dcl {
va_list ap;
char *format;
int n;
va_start(ap);
format = va_arg(ap, char *);
n = vprintf(format, ap);
va_end(ap);
return n;
}
int printf(va_alist) int va_alist {
char *ap;
char *format;
int n;
ap = (char *)&va_alist;
format = ((char **)(ap += sizeof(char *)))[-1];
n = vprintf(format, ap);
va_end(ap);
return n;
}
A.2.1 实现varargs.h
#include <varargs.h>
typedef char *va_list;
#define va_dcl int va_alist;
#define va_start(list) list = (char *)&va_alist;
#define va_end(list)
#define va_arg(list, mode) \
((mode *)(list += sizeof(mode)))[-1]
这个版本中,va_alist甚至不是一个宏:
#include <varargs.h>
void error(va_alist) va_dcl
将扩展为:
typedef char *va_list;
void error(va_alist) int va_alist;
这个例子实际上隐含了如下假定:底层的C语言要求函数参数在内存中连续存储,这样我们只需知道当前参数的地址,就能依次访问参数列表中的其他参数。
va_arg:它必须返回一个由va_list所指向的恰当类型的数值,同时递增va_list,使它指向参数列表中的下一个参数(即递增的大小等于与va_arg宏所返回的数值具有相同类型的对象的长度)。因为类型转换的结果不能作为赋值运算的目标(译注:即只能先赋值再进行类型转换,而不能先类型转换再赋值)
这里有一个“陷阱”需要避免:va_arg宏的第二个参数不能被指定为char、short或float类型。因为char和short类型的参数会被转换为int类型,而float类型的参数被转换为double类型。如果错误地指定了,将会在程序中引起麻烦。
c = va_arg(ap, char); //wrong
会被转换为int类型。
应该写成:
c = va_arg(ap, int);
cp = va_arg(ap, char *); //right
当作为参数时,指针并不会被转换,只有char、short和float类型的数值才会被转换。
我们应该注意到,不存在任何内建的方式来得知给定的参数数目。使用varargs系列宏的每个程序,都有责任通过确立某种约定或惯例来标志参数列表的结束。
边栏推荐
- The configuration manual for the secondary development of the XE training system of the missing moment document system
- iptables的使用简单测试
- MySQL 中出现的字符编码错误 Incorrect string value: ‘\x\x\x\x‘ for column ‘x‘
- 线程池处理异常的方法
- 这些 hook 更优雅的管理你的状态
- 1374. 生成每种字符都是奇数个的字符串 : 简单构造模拟题
- Common pits in the Go language
- [Multi-task model] Progressive Layered Extraction: A Novel Multi-Task Learning Model for Personalized (RecSys'20)
- Application of Acrel-5010 online monitoring system for key energy consumption unit energy consumption in Hunan Sanli Group
- Goroutine Leaks - The Forgotten Sender
猜你喜欢
KDD2022 | Self-Supervised Hypergraph Transformer Recommendation System
R语言 数据的关系探索
【Social Media Marketing】How to know if your WhatsApp is blocked?
技能大赛训练:A部分加固题目
微服务负载均衡器Ribbon
人工智能可信安全与评测
Pytorch框架学习记录8——最大池化的使用
Nacos 配置中心
[Energy Conservation Institute] Comparative analysis of smart small busbar and column head cabinet solutions in data room
数字孪生北京故宫,元宇宙推进旅游业进程
随机推荐
微信小程序云开发|个人博客小程序
STAHL触摸屏维修一体机显示屏ET-316-TX-TFT常见故障
The configuration manual for the secondary development of the XE training system of the missing moment document system
通俗解释:什么是临床预测模型
2022年秋招,软件测试开发最全面试攻略,吃透16个技术栈
用户身份标识与账号体系实践
AQS原理和介绍
任务调度线程池-应用定时任务
记录第一次给开源项目提 PR
通过这两个 hook 回顾 Set/Map 基础知识
kubernetes各名词缩写
KDD2022 | Self-Supervised Hypergraph Transformer Recommendation System
Which websites are commonly used for patent searches?
Go Atomic
4.1 配置Mysql与注册登录模块
漏洞分析丨HEVD-0x6.UninitializedStackVariable[win7x86]
Pytorch框架学习记录12——完整的模型训练套路
响应式织梦模板清洁服务类网站
【微信小程序】【AR】threejs-miniprogram 安装(76/100)
【Social Media Marketing】How to know if your WhatsApp is blocked?