C中printf函数的实现原理

您所在的位置:网站首页 printf的函数头 C中printf函数的实现原理

C中printf函数的实现原理

2023-09-29 07:15| 来源: 网络整理| 查看: 265

一、printf函数的实现原理

在C/C++中,对函数参数的扫描是从后向前的。C/C++的函数参数是通过压入堆栈的方式来给函数传参数的(堆栈是一种先进后出的数据结构),最先压入的参数最后出来,在计算机的内存中,数据有2块,一块是堆,一块是栈(函数参数及局部变量在这里),而栈是从内存的高地址向低地址生长的,控制生长的就是堆栈指针了,最先压入的参数是在最上面,就是说在所有参数的最后面,最后压入的参数在最下面,结构上看起来是第一个,所以最后压入的参数总是能够被函数找到,因为它就在堆栈指针的上方。printf的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移量了,下面给出printf("%d,%d",a,b);(其中a、b都是int型的)的汇编代码

.section .data string out = "%d,%d" push b push a push $out call printf

你会看到,参数是最后的先压入栈中,最先的后压入栈中,参数控制的那个字符串常量是最后被压入的,所以这个常量总是能被找到的。

二、可变参数表函数的设计

标准库提供的一些参数的数目可以有变化的函数。例如我们很熟悉的printf,它需要有一个格式串,还应根据需要为它提供任意多个“其他参数”。这种函数被称作“具有变长度参数表的函数”,或简称为“变参数函数”。我们写程序中有时也可能需要定义这种函数。要定义这类函数,就必须使用标准头文件,使用该文件提供的一套机制,并需要按照规定的定义方式工作。本节介绍这个头文件提供的有关功能,它们的意义和使用,并用例子说明这类函数的定义方法。

C中变长实参头文件stdarg.h提供了一个数据类型va_list和三个宏(va_start、va_arg和va_end),用它们在被调用函数不知道参数个数和类型时对可变参数表进行测试,从而为访问可变参数提供了方便且有效的方法。va_list是一个char类型的指针,当被调用函数使用一个可变参数时,它声明一个类型为va_list的变量,该变量用来指向va_arg和va_end所需信息的位置。下面给出va_list在C中的源码:

typedef char * va_list;

void va_start(va_list ap,lastfix)是一个宏,它使va_list类型变量ap指向被传递给函数的可变参数表中的第一个参数,在第一次调用va_arg和va_end之前,必须首先调用该宏。va_start的第二个参数lastfix是传递给被调用函数的最后一个固定参数的标识符。va_start使ap只指向lastfix之外的可变参数表中的第一个参数,很明显它先得到第一个参数内存地址,然后又加上这个参数的内存大小,就是下个参数的内存地址了。下面给出va_start在C中的源码:

#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) //得到可变参数中第一个参数的首地址

type va_arg(va_list ap,type)也是一个宏,其使用有双重目的,第一个是返回ap所指对象的值,第二个是修改参数指针ap使其增加以指向表中下一个参数。va_arg的第二个参数提供了修改参数指针所必需的信息。在第一次使用va_arg时,它返回可变参数表中的第一个参数,后续的调用都返回表中的下一个参数,下面给出va_arg在C中的源码:

#define va_arg(ap,type) ( *(type *)((ap += _INTSIZEOF(type)) - _INTSIZEOF(type)) ) //将参数转换成需要的类型,并使ap指向下一个参数

在使用va_arg时,要注意第二个参数所用类型名应与传递到堆栈的参数的字节数对应,以保证能对不同类型的可变参数进行正确地寻址,比如实参依次为char型、char* 型、int型和float型时,在va_arg中它们的类型则应分别为int、char *、int和double.

void va_end(va_list ap)也是一个宏,该宏用于被调用函数完成正常返回,功能就是把指针ap赋值为0,使它不指向内存的变量。下面给出va_end在C中的源码:

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

va_end必须在va_arg读完所有参数后再调用,否则会产生意想不到的后果。特别地,当可变参数表函数在程序执行过程中不止一次被调用时,在函数体每次处理完可变参数表之后必须调用一次va_end,以保证正确地恢复栈。

一个变参数函数至少需要有一个普通参数,其普通参数可以具有任何类型。在函数定义中,这种函数的最后一个普通参数除了一般的用途之外,还有其他特殊用途。下面从一个例子开始说明有关的问题。

假设我们想定义一个函数sum,它可以用任意多个整数类型的表达式作为参数进行调用,希望sum能求出这些参数的和。这时我们应该将sum定义为一个只有一个普通参数,并具有变长度参数表的函数,这个函数的头部应该是(函数原型与此类似):

int sum(int n, ...)

我们实际上要求在函数调用时,从第一个参数n得到被求和的表达式个数,从其余参数得到被求和的表达式。在参数表最后连续写三个圆点符号,说明这个函数具有可变数目的参数。凡参数表具有这种形式(最后写三个圆点),就表示定义的是一个变参数函数。注意,这样的三个圆点只能放在参数表最后,在所有普通参数之后。

下面假设函数sum里所用的va_list类型的变量的名字是vap。在能够用vap访问实际参数之前,必须首先用宏va_start对这个变量进行初始化。宏va_start的类型特征可以大致描述为:

va_start(va_list vap, 最后一个普通参数)

在函数sum里对vap初始化的语句应当写为:

va_start(vap, n); 相当于 char *vap= (char *)&n + sizeof(int);

此时vap正好指向n后面的可变参数表中的第一个参数。

在完成这个初始化之后,我们就可以通过另一个宏va_arg访问函数调用的各个实际参数了。宏va_arg的类型特征可以大致地描述为:

类型 va_arg(va_list vap, 类型名)

在调用宏va_arg时必须提供有关实参的实际类型,这一类型也将成为这个宏调用的返回值类型。对va_arg的调用不仅返回了一个实际参数的值(“当前”实际参数的值),同时还完成了某种更新操作,使对这个宏va_arg的下次调用能得到下一个实际参数。对于我们的例子,其中对宏va_arg的一次调用应当写为:

v = va_arg(vap, int);

这里假定v是一个有定义的int类型变量。

在变参数函数的定义里,函数退出之前必须做一次结束动作。这个动作通过对局部的va_list变量调用宏va_end完成。这个宏的类型特征大致是:

void va_end(va_list vap); 三、栈中参数分布以及宏使用后的指针变化说明

在这里插入图片描述 下面是函数sum的完整定义,从中可以看到各有关部分的写法:

#include using namespace std; #include int sum(int n,...) { int i , sum = 0; va_list vap; va_start(vap , n); //指向可变参数表中的第一个参数 for(i = 0 ; i va_list args; int n; va_start(args, fmt);//初始化参数指针 n = vsprintf(sprint_buf, fmt, args);/*函数放回已经处理的字符串长度*/ va_end(args);//与va_start 配对出现,处理ap指针 if (console_ops.write) console_ops.write(sprint_buf, n);/*调用控制台的结构中的write函数,将sprintf_buf中的内容输出n个字节到设备*/ return n; } vs_printf函数的实现代码是: int vsprintf(char *buf, const char *fmt, va_list args) { int len; unsigned long long num; int i, base; char * str; const char *s;/*s所指向的内存单元不可改写,但是s可以改写*/ int flags; /* flags to number() */ int field_width; /* width of output field */ int precision; /* min. # of digits for integers; max number of chars for from string */ int qualifier; /* 'h', 'l', or 'L' for integer fields */ /* 'z' support added 23/7/1999 S.H. */ /* 'z' changed to 'Z' --davidm 1/25/99 */ for (str=buf ; *fmt ; ++fmt) { if (*fmt != '%') /*使指针指向格式控制符'%,以方便以后处理flags'*/ { *str++ = *fmt; continue; } /* process flags */ flags = 0; repeat: ++fmt; /* this also skips first '%'--跳过格式控制符'%' */ switch (*fmt) { case '-': flags |= LEFT; goto repeat;/*左对齐-left justify*/ case '+': flags |= PLUS; goto repeat;/*p plus with ’+‘*/ case ' ': flags |= SPACE; goto repeat;/*p with space*/ case '#': flags |= SPECIAL; goto repeat;/*根据其后的转义字符的不同而有不同含义*/ case '0': flags |= ZEROPAD; goto repeat;/*当有指定参数时,无数字的参数将补上0*/ } //#define ZEROPAD 1 /* pad with zero */ //#define SIGN 2 /* unsigned/signed long */ //#define PLUS 4 /* show plus */ //#define SPACE 8 /* space if plus */ //#define LEFT 16 /* left justified */ //#define SPECIAL 32 /* 0x */ //#define LARGE 64 /* use 'ABCDEF' instead of 'abcdef' */ /* get field width ----deal 域宽 取当前参数字段宽度域值,放入field_width 变量中。如果宽度域中是数值则直接取其为宽度值。 如果宽度域中是字符'*',表示下一个参数指定宽度。因此调用va_arg 取宽度值。若此时宽度值小于0,则该负数表示其带有标志域'-'标志(左靠齐),因此还需在标志变量中添入该标志,并将字段宽度值取为其绝对值。 */ field_width = -1; if ('0' field_width = -field_width; flags |= LEFT; } } /* get the precision-----即是处理.pre 有效位 */ precision = -1; if (*fmt == '.') { ++fmt; if ('0' qualifier = 'q'; fmt += 2; } else if (*fmt == 'h' || *fmt == 'l' || *fmt == 'L'|| *fmt == 'Z') { qualifier = *fmt; ++fmt; } /* default base */ base = 10; /*处理type部分*/ switch (*fmt) { case 'c': if (!(flags & LEFT))/*没有左对齐标志,那么填充field_width-1个空格*/ while (--field_width > 0) *str++ = ' '; *str++ = (unsigned char) va_arg(args, int); while (--field_width > 0)/*不是左对齐*/ *str++ = ' ';/*在参数后输出field_width-1个空格*/ continue; /*如果转换参数是s,则,表示对应的参数是字符串,首先取参数字符串的长度,如果超过了精度域值,则取精度域值为最大长度*/ case 's': s = va_arg(args, char *); if (!s) s = ""; len = strnlen(s, precision);/*字符串的长度,最大为precision*/ if (!(flags & LEFT)) while (len long * ip = va_arg(args, long *); *ip = (str - buf); } else if (qualifier == 'Z') { size_t * ip = va_arg(args, size_t *); *ip = (str - buf); } else { int * ip = va_arg(args, int *); *ip = (str - buf); } continue; //若格式转换符不是'%',则表示格式字符串有错,直接将一个'%'写入输出串中。 // 如果格式转换符的位置处还有字符,则也直接将该字符写入输出串中,并返回到继续处理 //格式字符串。 case '%': *str++ = '%'; continue; /* integer number formats - set up the flags and "break" */ case 'o': base = 8; break; case 'X': flags |= LARGE; case 'x': base = 16; break; // 如果格式转换字符是'd','i'或'u',则表示对应参数是整数,'d', 'i'代表符号整数,因此需要加上 // 带符号标志。'u'代表无符号整数 case 'd': case 'i': flags |= SIGN; case 'u': break; default: *str++ = '%'; if (*fmt) *str++ = *fmt; else --fmt; continue; } /*处理字符的修饰符,同时如果flags有符号位的话,将参数转变成有符号的数*/ if (qualifier == 'l') { num = va_arg(args, unsigned long); if (flags & SIGN) num = (signed long) num; } else if (qualifier == 'q') { num = va_arg(args, unsigned long long); if (flags & SIGN) num = (signed long long) num; } else if (qualifier == 'Z') { num = va_arg(args, size_t); } else if (qualifier == 'h') { num = (unsigned short) va_arg(args, int); if (flags & SIGN) num = (signed short) num; } else { num = va_arg(args, unsigned int); if (flags & SIGN) num = (signed int) num; } str = number(str, num, base, field_width, precision, flags); } *str = '/0';/*最后在转换好的字符串上加上NULL*/ return str-buf;/*返回转换好的字符串的长度值*/ }

原文链接:https://blog.csdn.net/hackbuteer1/article/details/7558979#



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3