IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> 【C语言库函数】可变参数 va_start、va_arg、va_end、va_list、stdarg.h 库详解 -> 正文阅读

[C++知识库]【C语言库函数】可变参数 va_start、va_arg、va_end、va_list、stdarg.h 库详解

可变参数详解

printf( )是我们在编程中避不开的函数,之前我们研究了printf( )的实现原理,初步了解了printf( )函数在库中是借用putchar( )来进行实现输出的,但是还有一个问题我们上一篇文章没有解决,那就是printf( )中的可变参数是怎么实现的呢,如何去使用可变参数完成我们自己的输出函数呢?

让我们再来看一眼printf( )的长相,在stdio.h文件中他是这样声明的

int printf(const char *format, ...)

printf( )中分为两种参数,一种是const char *format代表的固定参数,一种是...代表的就是可变参数

/* *****************************************************
* Name: my_vprintf
* fuction: Implement formatting string function
* Input: const char *fmt --  A pointer to a formatted string
         va_list ap -- Parameters of the pointer
* Output: None
* Return: None
* ****************************************************** */
static void my_vprintf(const char *fmt, va_list ap)
{
    char lead = ' ';
    int maxwidth = 0;

    for (; *fmt != '\0'; fmt++)
    {
        if (*fmt != '%')
        { //顺序查找判断,遇到%就推出,否则继续循环输出
            outc(*fmt);
            continue;
        }

        fmt++;
        if (*fmt == '0')
        { //遇到‘0’说明前导码是0
            lead = '0';
            fmt++;
        }

        while (*fmt >= '0' && *fmt <= '9')
        { //紧接着的数字是长度,算出指定长度
            maxwidth *= 10;
            maxwidth += (*fmt - '0');
            fmt++;
        }

        switch (*fmt)
        { //判断格式输出
        case 'd':
            out_num(va_arg(ap, int), 10, lead, maxwidth);
            break;
        case 'o':
            out_num(va_arg(ap, unsigned int), 8, lead, maxwidth);
            break;
        case 'u':
            out_num(va_arg(ap, unsigned int), 10, lead, maxwidth);
            break;
        case 'x':
            out_num(va_arg(ap, unsigned int), 16, lead, maxwidth);
            break;
        case 'c':
            outc(va_arg(ap, int));
            break;
        case 's':
            outs(va_arg(ap, char *));
            break;

        default:
            outc(*fmt);
            break;
        }
    }
}
/* *****************************************************
* Name: printf
* fuction: None
* Input: const char *fmt --  A pointer to a formatted string
         ...  -- Variable number of arguments
* Output: None
* Return: None
* ****************************************************** */

void printf(const char *fmt, ...)
{
    va_list ap; /* 获取输入的参数指针 */
    va_start(ap, fmt); /* 获取 */
    my_vprintf(fmt, ap);
    va_end(ap);
}

可变参数与 stdarg.h 库

首先我们来看看stdarg.h库中有那些内容

库变量

下面是头文件 stdarg.h 中定义的变量类型:

序号变量 & 描述
1va_list 这是一个适用于 va_start()、va_arg()va_end() 这三个宏存储信息的类型。

库宏

下面是头文件 stdarg.h 中定义的宏:

序号宏 & 描述
1void va_start(va_list ap, last_arg) 这个宏初始化 ap 变量,它与 va_argva_end 宏是一起使用的。last_arg 是最后一个传递给函数的已知的固定参数,即省略号之前的参数。
2**type va_arg(va_list ap, type)**这个宏检索函数参数列表中类型为 type 的下一个参数。
3**void va_end(va_list ap)**这个宏允许使用了 va_start 宏的带有可变参数的函数返回。如果在从函数返回之前没有调用 va_end,则结果为未定义。

注意:以上的所有操作,只能从头到尾顺序访问后面的可变参数,可以暂停,但不能反向读取

库实现

/* VC++ 6.0 */
#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)                                           // 将指针置为无效

如果将其转换成函数则是

void va_start(va_list ap, xxx v) /* 其中的 xxx 为任意类型变量 */
{
    ap = (va_list)&v + sizeof(v);
}
xxx va_arg(va_list ap, xxx t) /* 其中的 xxx 为任意类型变量 */
{
    // (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))  
    ap += sizeof(t);
    return *((t *)(ap - sizeof(t)))
   	
}
void va_end(va_list ap) 
{
    ap = (va_list)0; //强制转换
}

可变参数的存储与实现

以下文字引用自博客 可变参数函数详解

C调用约定下可使用va_list系列变参宏实现变参函数,此处va意为variable-argument(可变参数)。

典型用法如下:

#include <stdarg.h>

int VarArgFunc(int dwFixedArg, ...){ //以固定参数的地址为起点依次确定各变参的内存起始地址

    va_list pArgs = NULL;  //定义va_list类型的指针pArgs,用于存储参数地址
    va_start(pArgs, dwFixedArg); //初始化pArgs指针,使其指向第一个可变参数。该宏第二个参数是变参列表的前一个参数,即最后一个固定参数
    int dwVarArg = va_arg(pArgs, int); //该宏返回变参列表中的当前变参值并使pArgs指向列表中的下个变参。该宏第二个参数是要返回的当前变参类型
    //若函数有多个可变参数,则依次调用va_arg宏获取各个变参
    va_end(pArgs);  //将指针pArgs置为无效,结束变参的获取
    /* Code Block using variable arguments */

}
//可在头文件中声明函数为extern int VarArgFunc(int dwFixedArg, ...);,调用时用VarArgFunc(FixedArg, VarArg);

变参宏根据堆栈生长方向和参数入栈特点,从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址。

变参宏的定义和实现因操作系统、硬件平台及编译器而异(但原理相似)。

下面这四小段引用自 C语言中函数参数入栈的顺序

C程序栈底为高地址,栈顶为低地址,函数参数入栈顺序的确是从右至左的。可到底为什么呢?查了一直些文献得知,参数入栈顺序是和具体编译器实现相关的。比如,Pascal语言中参数就是从左到右入栈的,有些语言中还可以通过修饰符进行指定,如Visual C++.即然两种方式都可以,为什么C语言要选择从右至左呢?

进一步发现,Pascal语言不支持可变长参数,而C语言支持这种特色,正是这个原因使得C语言函数参数入栈顺序为从右至左。具体原因为:C方式参数入栈顺序(从右至左)的好处就是可以动态变化参数个数。通过栈堆分析可知,自左向右的入栈方式,最前面的参数被压在栈底。除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数。这样就变成了左边参数的个数不确定,正好和动态参数个数的方向相反。

因此,C语言函数参数采用自右向左的入栈顺序,主要原因是为了支持可变长参数形式。换句话说,如果不支持这个特色,C语言完全和Pascal一样,采用自左向右的参数入栈方式。

System V Unix在varargs.h头文件中定义va_start宏为va_start(va_list arg_ptr),而ANSI C则在stdarg.h头文件中定义va_start宏为va_start(va_list arg_ptr, prev_param)

两种宏并不兼容,为便于程序移植通常采用ANSI C定义。

gcc编译器使用内置宏间接实现变参宏,如#define va_start(v,l) __builtin_va_start(v,l)。因为gcc编译器需要考虑跨平台处理,而其实现因平台而异。例如x86-64或PowerPC处理器下,参数不全都通过堆栈传递,变参宏的实现相比x86处理器更为复杂。

x86平台VC6.0编译器中,stdarg.h头文件内变参宏定义如下:

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, type)   ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) )
#define va_end(ap)       ( ap = (va_list)0 )

各宏的含义如下:

  1. _INTSIZEOF宏考虑到某些系统需要内存地址对齐。从宏名看应按照sizeof(int)即堆栈粒度对齐,即参数在内存中的地址均为sizeof(int)=4的倍数。例如,若在1≤sizeof(n)≤4,则_INTSIZEOF(n)=4;若5≤sizeof(n)≤8,则_INTSIZEOF(n)=8。为便于理解,简化该宏为

#define _INTSIZEOF(n) ((sizeof(n) + x) & ~(x))x = sizeof(int) - 1 = 3 = 0b’0000 0000 0000 0011~x = 0b’1111 1111 1111 1100

一个数与(~x)相与的结果是sizeof(int)的倍数,即_INTSIZEOF(n)将n圆整为sizeof(int)的倍数。

  1. va_start宏根据(va_list)&v得到第一个可变参数前的一个固定参数在堆栈中的内存地址,加上_INTSIZEOF(v)即v所占内存大小后,使ap指向固定参数后下个参数(第一个可变参数地址)。

    固定参数的地址用于va_start宏,因此不能声明为寄存器变量(地址无效)或作为数组类型(长度难定)。

  2. va_arg宏取得type类型的可变参数值。首先ap+=_INTSIZEOF(type),即ap跳过当前可变参数而指向下个变参的地址;然后ap-_INTSIZEOF(type)得到当前变参的内存地址,类型转换后返回当前变参值。

    va_arg宏的等效实现,如下将指针移动至下个变参,并返回左移的值[-1](数组下标表示偏移量),即当前变参值#define va_arg(ap,type) ((type *)((ap) += _INTSIZEOF(type)))

  3. va_end宏使ap不再指向有效的内存地址。该宏的某些实现定义为((void*)0),编译时不会为其产生代码,调用与否并无区别。但某些实现中va_end宏用于函数返回前完成一些必要的清理工作:如va_start宏可能以某种方式修改堆栈,导致返回操作无法完成,va_end宏可将有关修改复原;又如va_start宏可能对参数列表动态分配内存以便于遍历va_list,va_end宏可释放此前动态分配的内存。因此,从使用va_start宏的函数中退出之前,必须调用一次va_end宏。函数内可多次遍历可变参数,但每次必须以va_start宏开始,因为遍历后ap指针不再指向首个变参。

下图给出基于变参宏的可变参数在堆栈中的分布:

img

变参宏无法智能识别可变参数的数目和类型因此实现变参函数时需自行判断可变参数的数目和类型

前者可显式提供变参数目或设定遍历结束条件(如-1、’\0’或回车符等)。

后者可显式提供变参类型枚举值,或在固定参数中包含足够的类型信息(如printf函数通过分析format字符串即可确定各变参类型),甚至主调函数和被调函数可约定变参的类型组织等。

都看到这里了,如果有帮助,点个赞👍👍👍再走呗!
点个赞,代码没有Bug呦~

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-09-10 10:40:43  更:2021-09-10 10:42:54 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 20:44:46-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码