首页 > 学院 > 开发设计 > 正文

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

2019-11-14 09:02:35
字体:
来源:转载
供稿:网友

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

函数的参数传递使用栈的方法进行传值,所以第一个参数就在栈底,但是可变参数函数的困难就在于,不知道栈顶在哪,此时如果不做处理的话,程序就会很危险,这个需要开发者在函数中进行处理,例如常见的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函数,有兴趣的可以自己完善,很久没写博客了,如果写的有误,望各位大神告知


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表