深入解析可变参数函数,以及可变参数常用到的宏函数解释

本文主要解释大家经常遇到的可变参数函数的一些问题

函数的参数传递使用栈的方法进行传值,所以第一个参数就在栈底,但是可变参数函数的困难就在于,不知道栈顶在哪,此时如果不做处理的话,程序就会很危险,这个需要开发者在函数中进行处理,例如常见的printf函数,就在第一个参数中指定了函数参数的个数,用%做参数标示。

下面举个可变参数示例:

#include <stdarg.h>  //使用va_list ,va_start等宏必须要包含的头文件
void arg_test(int i, char c ,...)
{
    int j;
    va_list arg_ptr;         //定义可变参数指针
    va_start(arg_ptr, c);    //设置可变参数指针指向...后的第一个参数地址
//固定参数
    printf("&i = %p  i=%d  \n", &i, i); //打印参数i的值以及在堆栈中的地址  
    printf("&c = %p  c=%c  \n", &c, c); //打印参数c的值以及在堆栈中的地址  
//非固定参数
    printf(" ----------下面是可变参数  ----------\n");
    printf("arg_ptr = %p  ", arg_ptr);  //打印第一个可变参数地址
    j = va_arg(arg_ptr, int);           //获取此时arg_ptr的值,并把arg_ptr后移到下一个参数地址
    printf("j=%d  \n", j);              //打印参数j
    //判断最后一个参数是否为-1,-1就结束
    while (j != -1)
    {
        printf("arg_ptr = %p  ", arg_ptr);  //打印新的可变参数地址
        j = va_arg(arg_ptr, int);           //获取此时arg_ptr的值,并把arg_ptr后移到下一个参数地址
        printf("j=%d  \n", j);              //打印新的参数值
    }

}

int main(int argc,char *argv[])
{
    int int_size = _INTSIZEOF(int);
    printf("int_size=%d\n", int_size);
    arg_test(1, 'c', 2,3,4,5,6,7,-1);
    return 0;
}

说明:
int int_size = _INTSIZEOF(int);得到int类型所占字节数
va_start(arg_ptr, i); 得到第一个可变参数地址
根据定义(va_list)&v得到起始参数的地址, 再加上_INTSIZEOF(v) ,就是其实参数下一个参数的地址,即函数void arg_test(int i, …)中参数 i 后的第一个可变参数地址.
j=va_arg(arg_ptr, int); 得到arg_ptr指向的可变参数的值,并且arg_ptr指针上移一个_INTSIZEOF(int),即指向下一个可变参数的地址.
va_end(arg_ptr);置空arg_ptr,即arg_ptr=(void *)0;

运算结果为:
这里写图片描述

关于可变参数的宏,再解释一次,在VS中,声明如下:

     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 ) 

网上有个帖子介绍这几个宏说的不错,如下:
1、首先把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的
2、定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.这个宏的目的是为了得到最后一个固定参数的实际内存大小。在我的机器上直接用sizeof运算符来代替,对程序的运行结构也没有影响。(后文将看到我自己的实现)。
3、va_start的定义为 &v+_INTSIZEOF(v) ,这里&v是最后一个固定参数的起始地址,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在的内存地址,有了这个地址,以后的事情就简单了。
这里要知道两个事情:
⑴在intel+windows的机器上,函数栈的方向是向下的,栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处。
(2)在VC等绝大多数C编译器中,默认情况下,参数进栈的顺序是由右向左的,因此,参数进栈以后的内存模型如下图所示:最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。
|————————–|

最后一个可变参数->高内存地址处
第N个可变参数->va_arg(arg_ptr,int)后arg_ptr所指的地方,
即第N个可变参数的地址。
第一个可变参数->va_start(arg_ptr,start)后arg_ptr所指的地方
即第一个可变参数的地址
最后一个固定参数-> start的起始地址
……………..
-> 低内存地址处

(4) va_arg():有了va_start的良好基础,我们取得了第一个可变参数的地址,在va_arg()里的任务就是根据指定的参数类型取得本参数的值,并且把指针调到下一个参数的起始地址。
因此,现在再来看va_arg()的实现就应该心中有数了:
#define va_arg(ap,t) ( (t )((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
这个宏做了两个事情,
①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值
②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。
(5)va_end宏的解释:x86平台定义为ap=(char*)0;使ap不再 指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的. 在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型. 关于va_start, va_arg, va_end的描述就是这些了,我们要注意的 是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.

如果你对上面说的完全理解了,那么其实那些宏你也可以不用,如果你只是简单使用,那么会使用宏就足够了。
如下:

void Li_prinf(char * str, ...)
{
    char * start = str;         //定义字符指针
    //char obj;
    //int leth;

    char* arg_ptr;                  //定义可变参数指针
    arg_ptr = (char *)&str + 4;     //设置可变指针指向为第一个可变参数 ,
                                    //!!!注意这里需要取地址,因为参数进入堆栈的是指针,而不是具体的值!!!
                                    // 这里为什么+4 因为参数指针大小就是4个字节,除非操作系统不同
    while (*start)
    {
        if (*start == '%')
        {
            switch (*(++start))
            {
            case 'c':
                //std::cout << va_arg(arg_ptr, char) << std::endl;
                //obj = (*(char*)((arg_ptr += ((sizeof(char) + sizeof(int) - 1) & ~(sizeof(int) - 1))) - ((sizeof(char) + sizeof(int) - 1) & ~(sizeof(int) - 1))));
                //obj = (*(char*)((arg_ptr += 4) - 4));
                //使用C++打印数值比较方面,当然用putchar也可以,只不过面对int、浮点值、字符串等需要特殊处理,有兴趣的自己完善
                std::cout << (*(char*)((arg_ptr += 4) - 4)) << std::endl;
                start++;
                break;
            case 'd':
                std::cout << (*(int*)((arg_ptr += 4) - 4)) << std::endl;
                start++;
                break;
            case 's':
                std::cout << (*(char**)((arg_ptr += 4) - 4)) << std::endl;
                start++;
                break;
            default:
                break;
            }
        }
        else
        {
            putchar(*start);
            start++;
        }
    }
    //设置指针指向0
    arg_ptr = 0;
}

这里就重写了printf函数,有兴趣的可以自己完善,很久没写博客了,如果写的有误,望各位大神告知

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值