可变参数列表源码的剖析

这部分的知识讲起来比较复杂,这里通过一个求平均数的函数来讲解

使用可变参数,实现函数,求函数参数的平均值

#include <stdarg.h>

int ave (int n, ...)
{
    int i = 0;
    int sum = 0;

    va_list arg;//①

    va_start(arg,n);//②

    for (i = 0; i<n; i++)
    {
        sum += va_arg(arg,int);//③
    }
    va_end(arg); //④ 

    return sum / n;
}

上面就是一个求平均数的函数,不过这个函数比较特别,它接受的形参是可变的,它可以就任意个数字的平均数。

形参n代表求n个数的平均数,而后面的形参是 . . . 。这表示它可以接受的函数参数是可变的。

代码① va_ list arg; 我们在vs13点中 va_list,然后转到它的定义,可以看到typedef char * va _list;这句话的意思是类型重命名,把 char * 重命名为va _list 。所以代码①就相当于char * arg;定义一个字符型指针变量arg;

代码②va_ start(arg,n);同样这里我们也查看va _ start 的定义,看到的是

#define va_start _crt_va_start

这句代码里我们能得到信息只有 va _start 是定义的一个宏,那他宏内容是啥东东,别急我们接着看它宏内容的定义,F12一跳,我们看到

#define _ crt_va_start(ap,v) ( ap =(va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
我们把这里面不认识的换成认识的再看看
#define _ crt_va_start(ap,v) ( arg =(char *)_ADDRESSOF(n) + _INTSIZEOF(n) )

解读一下这句代码:把形参n的地址强制类型转换成char *类型,再加上 _INTSIZEOF(n),那问题又来了, _INTSIZEOF(n)又是啥东东?

_INTSIZEOF(n)整个做的事情就是将n的长度化为int长度的整数倍。

这句解释应该简洁易懂,n是我们定义的int形参,4字节大小,所以这里_INTSIZEOF(n)的值就是4。但现在我们还是没看懂代码②的作用是啥,这里插播一点函数栈帧的知识。

假设我们ave函数写好了,现在在main函数中使用,ave(3, 1, 2, 3);括号里面最左边的形参3 表示要进行三个数求平均值,这三个数是1,2,3。然后就要创建形参了。形参是创建在栈空间里,而栈空间的使用规则是:
先用高地址空间,再用低地址空间
传参的顺序是从右到左

先把最右边的参数3压进栈最高地址处,再是2、1、3。现在我们配合这个图再看这句代码
#define _ crt_va_start(ap,v) ( arg =(char *)_ADDRESSOF(n) + _INTSIZEOF(n) )
把n的地址转换成char *,再加上4,往下跳了4个字节。那么这个地址就是形参n下面一个地址,即数字1的地址。那这句话的意思是:
初始化arg,让其指向未知参数的第一个参数,就是让arg指向数字1
这里写图片描述

代码③sum += va_arg(arg,int);同样我们也查看va_arg的定义,这是个多重定义,我们就直接看最后结果

#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

老规矩把里面不认识的替换掉,代码②就变成了

sum += ( *(int *)((arg += 4) - 4) )

解读一下,我们看上面画的那张图,定义好的arg指向数字1,那+4,arg往下移4个字节,那就指向数字2,然后(arg-4)又往上移4字节,(arg-4)就代表数字1的地址,把这个地址强制转换成int * ,最后用 * 借用。
等号右边就是1。把1加到sum,循环一次后sum变成1了。

接下来注意了,这句代码的精华部分 arg += 4
注意看这句意思是让arg加上4,然后把这个和赋给arg,赋值完后,arg里存的已经是数字2的地址了,下次循环进来,就是让sum加上2。这样循环结束后sum里面存的就是(1+2+3)的和。

代码④va_end(arg);转到va_end定义:

#define _crt_va_end(ap) ( ap = (va_list)0 )

替换后:

代码④(arg = (char *)0
把数字0强制转换成char *类型,赋给arg,这句其实就是把指针arg指向空(arg = NULL)。

下面写一个printf函数,print(“s ccc d.\n”,”hello”,’b’,’i’,’t’,100);
函数原型: print(char *format, ...)

#include <stdio.h>
#include <windows.h>
#include <stdarg.h>
#include <assert.h>

void print(char *format, ...)
{
    char *ch = NULL;
    assert(format != NULL);  //断言
    va_list arg;   //创建一个字符型指针变量  char *arg;
    va_start(arg, format); //让arg指向未知参数部分的第一个参数  
    while (*format != '\0')
    {
        switch (*format)
        {
        case 's':             //输出字符串 "hello" 
            ch = va_arg(arg, char*); 
            while (*ch != '\0')
            {
                putchar(*ch++);
            }
            break;

        case 'c':  //输出三个字符 'b' 'a'  't'
            putchar(va_arg(arg, char));
            break;

        case 'd':  //输出整型数字 100
            printf("%d", va_arg(arg, int));
            break;

        default:  //其他的遇到啥就输出啥
            putchar(*format);
        }
        format++;
    }
    va_end(arg);
}
int main()
{
    print("s ccc d.\n", "hello", 'b', 'a', 't',100);
    system("pause");
    return 0;
}

这题与上面的栗子很相似,就是要注意字符串输出那里,要定义一个字符指针接受字符串首地址。

到此,可变参数列表源码的剖析就结束,写的比较啰嗦,欢迎各位大佬斧正。
PS : 想深究 #define _INTSIZEOF(n) 的戳下面红字。
#define _INTSIZEOF(n)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值