利用可变参数模拟Printf()函数实现一个my_print()函数和调用可变参数注意的陷阱!
入栈规则
可变参数函数的实现与函数调用的栈帧结构是密切相关的。所以在我们实现可变参数之前,先得搞清楚 栈是怎样传参的。
正常情况下,C的函数参数入栈遵照__stdcall规则, 它是从右到左的,即函数中的参数入栈是从右到左的。
例如:
void test(char a, int b,double c,char * d){
printf("a:%#p\nb:%#p\nc:%#p\nd:%#p",&a,&b,&c,d);
}
int main(){
char ch;
test('a',,,&ch);
return ;
}

从各个形参变量的地址可以看出它们地址大小确实是从右到左依次减小的,说明它们是从右到左压栈的,
实现原理
对于固定参数列表的函数,每个参数的名称、类型都是直接可见的,他们的地址也都是可以直接得到的,比如:通过&a就可以得到a的地址,并通过函数原型声明了解到a是char类型的。
对于变长参数的函数,怎么办呢?其实想想函数传参的过程,无论"..."中有多少个参数、每个参数是什么类型的,它们都和固定参数的传参过程是一样的,简单来讲都是栈操作,同时C标准的说明中,是支持变长参数的函数在原型声明中的,但须至少有一个最左固定参数,嘿嘿~ 这样,我们不就可以得到其中固定参数的地址了吗?
知道了某函数帧的栈上的一个固定参数的位置,所以我们完全可以自己通过栈操作,推导出其他变长参数的位置,进而实现可变参数函数。(这个“固定的参数”一般就是可变参数函数里在第一个位置的参数,通过它就可以开始找到后面各种类型、个数不定的参数了)
(上述说了一下实现原理,知道的大佬就请忽略咯~)
实现步骤
我们常用的可变参数列表有这几个:
1.va_list
源码:typedef char * va_list;
va_list为char*类型重定义,所以va_list为一个指向char类型的指针(va_list p就等同于 char *p)
2.va_start(ap,v)
源码:.#define va_start _crt_va_start
.#define _crt_va_start(ap,v) (ap=(va_list)_ADDRESSOF(v)+ _INTSIZEOF(V))
把v的地址强转为va_list类型即char* ,把其移动_INTSIZEOF(V)个字节后的地址赋值给ap,其实就是让ap跳过第一个参数,指向"..."里的第一个可变参数。
(这里这个_ADDRESSOF(v)是一个宏,对变量v取地址的意思;这个INSIZEOF(v)也是宏,是对变量v向上取4的倍数,
也就是说如果v占字节大小在1~4个字节范围内,就取4,v所占字节大小在5~8字节之间就取8,以此类推...
至于这里,_ADDRESSOF(v),_INTSIZEOF(V)这两个宏怎么实现,后面有小弟我一点浅浅的见解~ )
3.va_arg(ap,t)
源码:.#define va_arg _crt_va_arg
.#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
它的作用就是将ap移动_INTSIZEOF(t)个字节后,然后再取出ap未移动前所指向位置对应的数据。
下面假使t为一个int型变量,如下图分析

4.va_end(ap)
源码:.#define va_end _crt_va_end
.#define _crt_va_end(ap) ( ap = (va_list)0 )
将0强转为va_list类型,并赋值给ap,使其置空
于是这样就可以开始实现my_print函数
#include<stdio.h>
#include<assert.h>
#include<stdarg.h>
void putInt(int n){
if(n>){
putInt(n/);
}
putchar(n%+ '');
}
int My_print(const char *formt, ...)
{
assert(formt);
va_list arg;//定义arg
va_start(arg, formt);//初始化arg 即跳过传进来的第一个参数 ,这里相当于跳过"output:>%s %c %c %d"这个字符串
const char *start=formt;
while (*start!= '\0')
{
if(*start =='%'){
start++;
switch(*start)
{
case 'd':
putInt(va_arg(arg, int));
break;
case 'c':
putchar(va_arg(arg, int)); //char类型提升,用int类型。
break;
case 's':
{ /*puts(va_arg(arg, char*));*/ //字符串可以直接用puts()函数输出
char *ch = va_arg(arg, char*);//定义一个指针变量接收获取的字符,用putchar()一个一个输出
while (*ch)
{
putchar(*ch);
ch++;
}
}
break;
case 'f':{
float a=(float)va_arg(arg,double); //float类型提升,所以用double (小陷阱)
printf("%f",a); //BUG 要模拟浮点型比较复杂,这里耍个小聪明~
}
break;
default :
break;
}
}
else {
putchar(*start) ;
}
start++;
}
va_end(arg); //必须有这一步,结束栈操作
return ;
} int main()
{
char str[]="Beat box!";
My_print("Output:>%f %c%c %d %s",3.14,'t','p',,str) ;
return ;
}
结果:
注意陷阱
从上面的例子中,不难注意到了这样一个问题,这里借助查询的资料来说明:
我们用va_arg(ap,type)取出一个参数的时候,
type绝对不能为以下类型:
——char、signed char、unsigned char
——short、unsigned short
——signed short、short int、signed short int、unsigned short int
——float
在没有函数原型的情况下,char与short类型都将被默认转换为int类型,float类型将被转换为double类型。
——《C语言程序设计》第2版 2.7 类型转换 p36
va_arg宏的第2个参数不能被指定为char、short或者float类型。
因为char和short类型的参数会被转换为int类型,而float类型的参数会被转换为double类型 ……
例如,这样写肯定是不对的:
c = va_arg(ap,char);
因为我们无法传递一个char类型参数,如果传递了,它将会被自动转化为int类型。上面的式子应该写成:
c = va_arg(ap,int);
——《c陷阱与缺陷》
这就是在实现可变参数时,常常需要注意的小问题
至于前面所说的那两个宏
_ADDRESSOF(V)
源码:#define _ADDRESSOF(v) ( &(v) )
其实就是对变量v取地址的意思。
_INTSIZEOF(v)
源码:#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) 按式子可以先把这个宏改写成:(sizeof(n) + 4 - 1)& (-3) 然后传入变量 n,
n如占2个字节,就成了 5&(111...100)=4;
如占3个字节,就成了6&(111...100)=4;
如占5个字节,就成了8&(111...100)=8;
。。。
结果始终是4的倍数,如此便不难发现上面所述的规律了。
如有错误,希望指出!
欢迎来扰~~
利用可变参数模拟Printf()函数实现一个my_print()函数和调用可变参数注意的陷阱!的更多相关文章
- 可变参数模拟printf()函数实现一个my_print()函数以及调用可变参数需注意的陷阱
入栈规则 可变参数函数的实现与函数调用的栈帧结构是密切相关的.所以在我们实现可变参数之前,先得搞清楚 栈是怎样传参的. 正常情况下,C的函数参数入栈遵照__stdcall规则, 它是从右到左的,即函数 ...
- 一个 Map 函数、一个 Reduce 函数和一个 main 函数
MapReduce 最简单的 MapReduce应用程序至少包含 3 个部分:一个 Map 函数.一个 Reduce 函数和一个 main 函数.main 函数将作业控制和文件输入/输出结合起来.在这 ...
- [c++][语言语法]函数模板和模板函数 及参数类型的运行时判断
参考:http://blog.csdn.net/beyondhaven/article/details/4204345 参考:http://blog.csdn.net/joeblackzqq/arti ...
- jquery回调函数的一个案例
1.引言 今天在学习<jQuery基础教程>在学习编写插件的时候,书中说利用回调函数来当参数,会极大的提高程序的灵活性.对回调函数很陌生.研究了一下给的示例程序.感觉对回调函数有了基本的了 ...
- python一个用例,多组参数,多个结果
在某种情况下,需要用不同的参数组合测试同样的行为,你希望从test case的执行结果上知道在测试什么,而不是单单得到一个大的 test case:此时如果仅仅写一个test case并用内嵌循环来进 ...
- const参数,const返回值与const函数
在C++程序中,经常用const 来限制对一个对象的操作,例如,将一个变量定义为const 的: const int n=3; 则这个变量的值不能被修改,即不能对变量赋值. const 这个关键字 ...
- Scala 基础(十一):Scala 函数式编程(三)高级(一)偏函数、作为参数的函数、匿名函数、高阶函数
1 偏函数 1)在对符合某个条件,而不是所有情况进行逻辑操作时,使用偏函数是一个不错的选择 2)将包在大括号内的一组case语句封装为函数,我们称之为偏函数,它只对会作用于指定类型的参数或指定范围值的 ...
- Callback函数详解(我感觉,回掉函数的本质是函数指针,在业务做循环处理的时候,调用一下通知外部)
2010年的最后一天了,转载一篇自己认为还不错的文章与大家分享.希望对大家有所帮助. 一,回调函数 我们经常在C++设计时通过使用回调函数可以使有些应用(如定时器事件回调处理.用回调函数记录某操作进度 ...
- 深入浅出剖析C语言函数指针与回调函数(一)【转】
本文转载自:http://blog.csdn.net/morixinguan/article/details/65494239 关于静态库和动态库的使用和制作方法. http://blog.csdn. ...
随机推荐
- PE格式第六讲,导出表
PE格式第六讲,导出表 请注意,下方字数比较多,其实结构挺简单,但是你如果把博客内容弄明白了,对你受益匪浅,千万不要看到字数多就懵了,其实字数多代表它重要.特别是第五步, 各种表中之间的关系. 作者: ...
- 深入浅出 Spring
前言:笔记中提供了大量的代码示例,需要说明的是,大部分代码示例都是以图片的形式展示的,所有的图片都是来自本人所敲代码的截图,不足之处,请大家指正~ 第一部分:环境搭建及 IOC 容器 一.Spring ...
- Ubuntu下使用网易云音乐
Ubuntu15真心各种崩溃啊 最后决定还是换成ubuntu14.04LTS了 在win.android平台上网易云音乐好用到爆 ubuntu下没有网易云音乐的客户端怎么能行 https://gith ...
- IO基础内容(File)
JavaIO基础内容 IO技术概述 Output 把内存中的数据存储到持久化设备上这个动作称为输出(写)Output操作 Input 把持久设备上的数据读取到内存中的这个动作称为输入(读)Input操 ...
- [译]ASP.NET Core 2.0 带初始参数的中间件
问题 如何在ASP.NET Core 2.0向中间件传入初始参数? 答案 在一个空项目中,创建一个POCO(Plain Old CLR Object)来保存中间件所需的参数: public class ...
- 【深度学习系列】PaddlePaddle之手写数字识别
上周在搜索关于深度学习分布式运行方式的资料时,无意间搜到了paddlepaddle,发现这个框架的分布式训练方案做的还挺不错的,想跟大家分享一下.不过呢,这块内容太复杂了,所以就简单的介绍一下padd ...
- Ajax中与服务器的通信【发送请求与处理响应】
一.发送请求 Ajax中通过XMLHttpRequest对象发送异步方式的后台请求时.通常有两种方式的请求,一种是GET请求,另一种是POST请求.发送请求一般要经过4个步骤分别是: (1)初始化XM ...
- N厂劳力士黑水鬼V7出了1年,如今依旧被追捧,供不应求
今天和大家一起来谈谈,风靡复刻界的潜航者,国人眼中的一劳永逸,何为一劳永逸,即(用这个腕表能省很多事)真的有这么牛?其实不然只要是机械腕表都会有或多或少的问题,一劳永逸更多的是指腕表的质量给力,所谓潜 ...
- 2.动手实操Apache ZooKeeper
Tips 做一个终身学习的人! 日拱一卒,功不唐捐. 在本节中,我们将讲解如何下载并安装Apache ZooKeeper,以便我们可以直接开始使用ZooKeeper. 本部分旨在通过提供详细的安装和使 ...
- switchhost -- 切换host的工具
https://github.com/oldj/SwitchHosts/downloads 下载链接: 1,290 downloads SwitchHosts! _v0.2.2.1790.dmg - ...

