可变参函数(my_printf可变参函数的实现)

可变参函数:
其参数列表的参数类型与个数可变,采用ANSI标准形式时,参数个数可变的函数的原型声明是:

type funcname(type para1, type para2, ...)
//至少需要一个普通的形式参数,后面的省略号不表示省略,而是函数原型的一部分,为参数占位符,type是函数返回值和形式参数的类型

可变参函数的实现:
1、采用ANSI标准形式

2、可变参数列表:
通过定义在stdarg.h头文件(标准库的一部分)的来实现。
1)一个类型: va_list

 typedef char * va_list; 
 //用来保存宏va_start、va_arg和va_end所需信息的一种类型;
 //为了访问变长参数列表中的参数,必须声明 。

2)三个宏:仅用来确定可变参数列表中每个参数的内存地址
(1)va_start

#define  va_start   _crt_va_start
#define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define _ADDRESSOF(v)   ( &reinterpret_cast<const char &>(v) )
//相当于 &v
#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

//访问变长参数列表中的参数之前使用的宏;
//初始化用va_list声明的对象,初始化结果供宏va_arg和 va_end使用

(2)va_arg

#define va_arg _crt_va_arg
#define _crt_va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

//展开成一个表达式的宏;
//该表达式具有变长参数列表中下一个参数的值和类型,每次调用va_arg都会修改 

(3)va_end

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

//展开成一个表达式的宏;
//该表达式具有变长参数列表中下一个参数的值和类型。每次调用va_arg都会修改

3)可变参数在编译器中的处理:

(1)首先把va_list被定义成char*(我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址),有的机器上va_list是被定义成void*。

(2)定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统,为了得到最后一个固定参数的实际内存大小(可直接用sizeof运算符来代替)。

(3)va_start的定义为 &v+_INTSIZEOF(v) ,这里&v是最后一个固定参数的起始地址,再加上其实际占用大小后,目的为了得到第一个可变参数的起始内存地址;
当运行va_start(ap, v)后,此时ap指向第一个可变参数在的内存地址。

(4)va_arg由运行va_start(ap, v)后,取得第一个可变参数的地址,ap访问该地址根据指定的参数类型取得本参数的值,并且把指针调到下一个参数的起始地址。
①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值;
②计算出本参数的实际大小后将指针调到本参数的结尾,即下一个参数的首地址,便于处理下一个参数。

(5)va_end 的定义相当于((void*)0),使ap不再 指向堆栈,而是置NULL。

3、重要因数:
①函数栈的生长方向:向下生长,栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处,从栈底向栈顶看过去,地址是从高地址走向低地址的,因为称它为向下生长
(栈由编译器自动管理,其中存放的是函数参数及局部变量)

②参数的入栈顺序:自右向左,并且连续存储
参数进栈内存模型如下:
内存地址 内容
高内存地址处 最后一个可变参数 –栈顶出
……
第N个可变参数 –va_arg(arg_ptr,int),调用第N个可变参数的地址
……
第一个可变参数 –va_start(arg_ptr, start),得第一个可变参数地址
最后一个固定参数 –start的起始地址
……
低内存地址处 第一个固定参数
注意:最后一个固定变量不能声明为寄存器类型变量,函数类型变量或数组类型变量

③CPU的对齐方式

④内存地址的表达方式

4、举例说明:printf函数

//原型:printf(const char * _Format, ...);

int main()
{
    char a = 'a';
    int b = 2;
    float c = 3.1;
    char *str = "abc";
    printf("%c %d %f %s\n", a, b, c, str);
    getchar();
    return 0;
}

//反汇编
    char a = 'a';
00E113EE  mov         byte ptr [a],61h  
    int b = 2;
00E113F2  mov         dword ptr [b],2  
    float c = 3.1;
00E113F9  movss       xmm0,dword ptr ds:[0E1586Ch]  
00E11401  movss       dword ptr [c],xmm0  
    char *str = "abc";
00E11406  mov         dword ptr [str],0E15858h  
    printf("%c %d %f %s\n", a, b, c, str);
00E1140D  mov         esi,esp  
00E1140F  mov         eax,dword ptr [str]  
00E11412  push        eax  
00E11413  cvtss2sd    xmm0,dword ptr [c]  
00E11418  sub         esp,8  
00E1141B  movsd       mmword ptr [esp],xmm0  
00E11420  mov         ecx,dword ptr [b]  
00E11423  push        ecx  
00E11424  movsx       edx,byte ptr [a]  
00E11428  push        edx  
00E11429  push        0E1585Ch  
00E1142E  call        dword ptr ds:[0E192C0h]  
00E11434  add         esp,18h  
00E11437  cmp         esi,esp  
00E11439  call        __RTC_CheckEsp (0E11136h)  

这里写图片描述

C 语言中可变长参数的处理心得

#include<stdio.h>
#include<stdarg.h>
#include<string.h>

//va_start(ap, v)后,此时ap指向第一个可变参数在的内存地址
//va_arg(ap,t), ap访问该地址根据指定的参数类型取得本参数的值,并且把指针调到下一个参数的起始地址
//va_end(ap), ap不再 指向堆栈,而是置NULL

//打印%d
void print_int(const int num)
{
    if(num)
    {
        print_int(num / 10);    
        putchar((char)(num % 10 + '0'));
    }//itoa逆序输出(先递归,后输出实现)
    else
    {
        return;
    }
}
//打印%c
void print_char(const char ch)
{
    putchar(ch);
}
//打印%s
void print_str(const char *str)
{
    while(*str != '\0')
    {
        putchar(*str);
        str++;
    }
}
//打印%f
void print_float(const float f)
{
    if(f)
    {
        int temp1 = (int)f;
        int temp2 = (int)(1000000 * (f - temp1));

        print_int(temp1);
        putchar('.');
        print_int(temp2);
    }
    else
    {
        return;
    }
}

void my_printf(const char *format, ...)
{
    va_list ap;
    va_start(ap, format);//将ap指向第一个实际参数的地址 
    while(*format)//遍历第一个固定参数字符串内容 
    {
        if(*format != '%')
        {
            putchar(*format);
            format++;
        }
        else
        {
            format++;
            switch(*format)//选择判断输出格式
            {
            case 'd':
                {
                    int buffer = va_arg(ap, int);//得到此时指向的实际参数地址,然后ap再指向下一个实际参数
                    print_int(buffer);
                    format++;
                    break;
                }
            case 'c':
                {
                    char buffer = va_arg(ap, char);
                    print_char(buffer);
                    format++;
                    break;
                }
            case 's':
                {
                    char *buffer = va_arg(ap, char *);
                    print_str(buffer);
                    format++;
                    break;
                }
            case 'f':
                {
                    float buffer = va_arg(ap, double);
                    print_float(buffer);
                    format++;
                    break;
                }
            default:
                {
                    print_char(*format);
                    format++;
                }//原样输出语句
            }
        }

    }
    va_end(ap);
}

int main()
{
    int num = 123;
    char a = 'a';
    char *str = "hello";
    float f = 12.125;
    my_printf("%d %c %s %f\n", num, a, str, f);
    getchar();
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_39191122/article/details/79900720