本节分析一下printf的机理,通过编制一个自己的myprintf打印函数,进一步加深对打印输出函数的理解,用好这个函数。
20.4.1 具有可变参数的函数
printf函数的原型声明如下:
int printf
(const char *format
,...
)
按照这个格式,声明如下的test函数。
int test
(const char *format
,...
)
在主函数中声明整型变量a和b,并把它们的地址&a和&b打印出来,参照printf函数的调用方式,写出如下对test函数的调用方法。
test
("%d
,%d\n"
,a
,b
);
由此可以写出如下主函数。
#include <stdio.h>
int test
(const char *format
,...
); //
声明test
函数
int main
()
{
int a=100
, b=-100
;
printf
("
变量a
和b
的地址:%p
,%p\n"
,&a
,&b
); //
输出变量a
和b
的地址
test
("\n"
,a
,b
); //
调用test
函数
return 0
;
}
在test函数中再次输出传递参数a和b的地址,这是函数test内的临时变量,所以它们的地址与主函数里的地址并不相同。声明指针p并用format初始化。因为format是字符常量指针,所以使用int强制转换。
为了简单。test函数内并不处理字符串,所以可以随便赋值,这里用一个换行符。根据对函数test的要求,编写如下实现程序。在程序里移动指针p,看看会带来什么结果。
int test
(const char *format
, int a
, int b
)
{
int *p
;
printf
("test
内变量a
和b
的地址:%p
,%p\n"
,&a
,&b
);
p=
(int*
)&format
; //
指向format
地址
printf
("format
:%p\n"
,p
); //
输出format
地址
p++
; //p
现在指向format + 1
的地址
printf
("%p
,%d\n"
,p
,*p
); //
输出当前p
的指向地址和地址里的内容
p++
; // p
现在指向format + 2
的地址
printf
("%p
,%d\n"
,p
,*p
); //
输出当前p
的指向地址和地址里的内容
return 0
;
}
程序运行结果如下:
变量a
和b
的地址:0012FF7C
,0012FF78
test
内变量a
和b
的地址:0012FF24
,0012FF28
format
:0012FF20
0012FF24
,100
0012FF28
,-100
传输给函数test的参数在函数里将作为临时变量被重新分配地址。format是test函数的第1个参数,被分配的地址是0012FF20,参数a为0012FF24,b为0012FF28。如果再有一个参数,将依次分配地址。这就是test内的参数地址分配规律。
因为分配给参数的地址是连续的,所以根据formart的地址就可以利用指针找到后面的参数了。在test函数里,正是利用指针依次打印出a和b的值。
为了演示变量a和b在test内分配的地址与format的关系,将它设计成只有两个参数的函数。下面将它设计为可变参数并能将一个整数按10进制和16进制打印出来。为了分析方便,添加测试用的打印信息。
处理10进制和16进制的字符串使用标准的“%d”和“%x”,它们将作为字符串常量传给test函数,在test函数内,将根据是“%d”还是“%x”借用printf函数输出。
【例20.16】设计可变参数程序的例子。
#include <stdio.h>
int test
(const char *format
,...
); //
声明可变参数test
函数
int main
()
{
int a=100
;
test
("%d%x%d
结束!\n"
,a
,a
,-200
);
return 0
;
}
int test
(const char *format
,...
)
{
int *p
;
char c
;
int value
;
p=
(int*
)&format
;
p++
; //
先做p++
,使p
指向字符串常量后面的第1
个参数
while
((c = *format++
)
!= '\0'
) //
循环到常量字符串结束标志
{
if
(c
!= '%'
) //
如果不是格式字符则直接输出
{
putchar
(c
);
continue
;
}
else //
处理格式字符
{
c = *format++
; //
取%
后面的字符
if
(c=='d'
)
{
value=*p++
; //
将参数值赋给value
,加1
指向下一个参数
printf
("10
进制:%d\n"
,value
); //
借用测试
}
if
(c=='x'
)
{
value=*p++
; //
将参数值赋给value
,加1
指向下一个参数
printf
("16
进制:%x\n"
,value
); //
借用测试
}
}
}
return 0
;
}
测试时有意使用"%d%x%d结束!\n"字符串,以便演示判断语句的正确性。程序中的注释已经很清楚,不再赘述,下面给出程序的运行结果。
10
进制:100
16
进制:64
10
进制:-200
结束!
20.4.2 设计简单的打印函数
test函数已经初具雏形,但它的输出是借用了printf函数。为了设计自己的myprint函数,现在不再借用printf函数,而是设计自己的函数完成打印。
【例20.17】设计实现printf简单功能的myprintf可变参数函数的例子。
设计自己的打印函数myprintf,实现最简单的“%d”和“%x”功能。函数原型如下:
int myprintf
(const char *format
,...
);
要把数值转换成倒序的字符串,再把字符串反序即得到正确的字符串。设计一个根据进制转换相应的字符串函数,最后一个参数为要转换的进制。其原型如下:
void itoa
(int
, char *
, int
);
在itoa函数里,先把数字按进制转换为数字字符串,这是一个与给定数字逆序的字符串,直接在程序里面设计一个宏SWAP,通过交换实现字符串反转,得到与给定数字相同的字符串供输出。
在调用itoa之前,还需要判断数字的正负,如果是负整数,需要变成正整数,待转换后再在它的前面输出负的符号位。
因为puts函数自动在尾部实现换行,这不符合输出要求(会多一个换行)。设计一个去掉换行的函数myputs。其原型如下:
void myputs
(char *buf
)
为了验证程序,除了正负整数,也需要打印0以及与格式字符一起的其他字符。曾经提到过,对于一个字符串s,“printf(s);”与“printf("%s",s);”是不等效的,通过这个演示,将能进一步证明这一点。
//
完整的程序
#include <stdio.h>
int myprintf
(const char *format
,...
); //
声明打印函数的函数原型
void myputs
(char *
); //
声明输出字符串函数的函数原型
void itoa
(int
, char *
, int
); //
声明数制转换函数的函数原型
int main
(
)
{
int a=100
;
char s="OK
!"
;
myprintf
("10
进制:%d\n16
进制:%x\n10
进制:%d
零%d\n"
,a
,a
,-100
,0
);
myprintf
(s
);
myprintf
("
原来如此!\n"
);
myprintf
("here
!%s\n"
,s
);
return 0
;
}
//puts
有换行符,必须去掉,设计myputs
替代它
void myputs
(char *buf
)
{
while
(*buf
)
putchar
(*buf++
);
return
;
}
//
数制转换函数内部使用宏定义SWAP
void itoa
(int num
, char *buf
, int base
)
{
char *hex= "0123456789ABCDEF"
;
int i=0
,j=0
;
do
{
int rest
;
rest = num % base
;
buf[i++]=hex[rest]
;
num/=base
;
}while
(num
!=0
);
buf[i]='\0'
;
printf
("\n
逆序:%s\n"
,buf
); //
验证信息
//
定义交换宏实现反转
#define SWAP
(a
,b
) do{a=
(a
)+
(b
); \
b=
(a
)-
(b
); \
a=
(a
)-
(b
); \
}while
(0
)
//
反转
for
(j=0
; j<i/2
; j++
)
{
SWAP
(buf[j]
,buf[i-1-j]
);
}
printf
("\n
正序:%s\n"
,buf
); //
验证信息
return
;
}
//
可变参数输出函数
int myprintf
(const char *format
,...
)
{
int *p
;
char c
;
char buf[32]
;
int value
;
p=
(int*
)&format
;
p++
;
while
((c = *format++
)
!= '\0'
)
{
if
(c
!= '%'
)
{
putchar
(c
); //
输出字符串中的非格式字符
continue
;
}
else
{
c = *format++
; //
取%
后面的字符
if
(c=='d'
) //
处理10
进制
{
value=*p++
;
if
(value<0
) //
处理负整数
{
value=-value
;
itoa
(value
,buf
,10
);
putchar
('-'
);
myputs
(buf
);
}
else //
处理正整数
{
itoa
(value
,buf
,10
);
myputs
(buf
);
}
}
if
(c=='x'
) //
将10
进制正整数按16
进制处理
{
value=*p++
;
itoa
(value
,buf
,16
);
myputs
(buf
);
}
}
}
return 0
;
}
程序输出结果如下:
10
进制:
逆序:001
正序:100
100
16
进制:
逆序:46
正序:64
64
10
进制:
逆序:001
正序:100
-100
零
逆序:0
正序:0
0
OK
!原来如此!
here
!
程序对0的处理正确。语句
myprintf
("
原来如此!\n"
);
是由“putchar(c);”语句输出。语句
myprintf
(s
);
中的字符串“OK”,也是由“putchar(c);”语句输出。因为没有设计“%s”的功能,所以语句
myprintf
("here
!%s\n"
,s
);
只是通过“putchar(c);”语句输出“here!”,而不输出s的内容。如果设计了“%s”的功能,则将s的内容作为字符串输出,如果字符串里有“%”号,它也不会处理,只会原样输出。对于printf函数而言,如果字符串不是自己预先设计的,而是程序运行的中间产物,都应尽可能地使用格式“%s”输出,以免发生错误。
【例20.18】为myprintf函数增加处理字符和字符串的功能。
增加“%c”和“%s”的功能也很容易,为了简洁,将调试信息去掉。下面是它的源程序。为了对照主程序的输出结果,将主程序放在最后,其他函数按先后顺序排列,所以就不需要先声明它们的函数原型了。
#include <stdio.h>
void myputs
(char *buf
)
{
while
(*buf
)
putchar
(*buf++
);
return
;
}
void itoa
(int num
, char *buf
, int base
)
{
char *hex= "0123456789ABCDEF"
;
int i=0
,j=0
;
do
{
int rest
;
rest = num % base
;
buf[i++]=hex[rest]
;
num/=base
;
}while
(num
!=0
);
buf[i]='\0'
;
//
定义交换宏
#define SWAP
(a
,b
) do{a=
(a
)+
(b
); \
b=
(a
)-
(b
); \
a=
(a
)-
(b
); \
}while
(0
)
//
反转
for
(j=0
; j<i/2
; j++
)
{
SWAP
(buf[j]
,buf[i-1-j]
);
}
return
;
}
int myprintf
(const char *format
,...
)
{
int *p
;
char c
;
char buf[32]
;
int value
;
p=
(int*
)&format
;
p++
;
while
((c = *format++
)
!= '\0'
)
{
if
(c
!= '%'
)
{
putchar
(c
);
continue
;
}
else
{
c = *format++
; //
取%
后面的字符
if
(c=='c'
)
{
value=*p++
;
putchar
(value
);
}
if
(c=='s'
)
{
value=*p++
;
myputs
((char*
)value
);
}
if
(c=='d'
)
{
value=*p++
;
if
(value<0
)
{
value=-value
;
itoa
(value
,buf
,10
);
putchar
('-'
);
myputs
(buf
);
}
else
{
itoa
(value
,buf
,10
);
myputs
(buf
);
}
}
if
(c=='x'
)
{
value=*p++
;
itoa
(value
,buf
,16
);
myputs
(buf
);
}
}
}
return 0
;
}
int main
()
{
char c1='H'
;
char c2="How are you
?"
;
myprintf
("%d
,%d
,%d
,%x
,%x\n"
,100
,0
,-100
,100
,0
); //1
验证%d
和%x
myprintf
("%c
,%s\n"
,c1
,c2
); //2
验证%c
和%s
myprintf
("%c
,%s\n"
,'H'
,"Fine
!"
); //3
带格式使用字符常量
myprintf
("How are you
?\n"
); //4
直接用字符串常量
myprintf
(c2
); //5
直接用字符串名字
myprintf
("%s\n"
,c2
); //6
标准格式
myprintf
("\n"
,c2
); //7
使用有误,只输出换行,不处理c2
myprintf
("How are%s"
,"you
?\n"
); //8
格式正确
return 0
;
}
主程序使用6条验证语句,注意它们执行路径的区别。第4条和第5条是在判别格式字符的时候直接一个字一个字地输出。第7条有误,但编译系统无法识别错误。第8条的参数是字符常量,经由“%s”的路径输出。显然,字符串作为整体输出时的速度会快些,字符串愈长,差别愈显著。比较下面的运行结果,仔细体会不同语句的区别。
100
,0
,-100
,64
,0
H
,How are you
?
H
,Fine
!
How are you
?
How are you
?How are you
?
How areyou
?
20.4.3 利用宏改进打印函数
标准库实现printf函数用到了va_开头的三个有参数宏va_start、va_arg和va_end。这些宏定义在头文件stdarg.h中。利用这些宏可以大大简化设计,为了看看它们的作用,设计一个不处理10进制,仅输出参考信息的myprintf函数。va_list用来声明一个供宏使用的指针类型的变量。
【例20.19】研究如何使用宏来简化设计的例子。
#include <stdio.h>
#include <stdarg.h>
int myprintf
(const char *format
,...
)
{
int *p
,i=101
;
va_list va_p
; //1
char c
;
char buf[32]={'\0'}
;
int value=0
;
p=
(int*
)&format
;
printf
("format
的地址=%x\n"
,(int
)p
); //
打印对照
p++
; //
先做p++
,使两者相等,后面程序也变化
printf
("p+1
后的变量%d
的地址=%x\n"
,i
,(int
)p
); //
打印对照
va_start
(va_p
,format
); //2
printf
("va_p=%x\n"
,(int
)va_p
); //
打印对照
while
((c = *format++
)
!= '\0'
)
{
if
(c
!= '%'
)
{
putchar
(c
);
continue
;
}
else
{
c = *format++
;
if
(c=='d'
)
{
printf
("
变量%d
的va_p=%x\n"
, i
,(int
)va_p
); //
打印对照
value=va_arg
(va_p
,int
);
printf
("
执行va_arg
(va_p
,int
)后的va_p=%x\n"
,
(int
)va_p
); //
打印对照
i++
;
printf
("
变量%d
的va_p=%x\n"
, i
,(int
)va_p
); //
打印对照
printf
("%d"
,value
);
}
}
}
printf
("
结束后的va_p=%x\n"
,
(int
)va_p
); //
打印对照
va_end
(va_p
);
printf
("
执行va_end
(va_p
)后的va_p=%x\n"
,
(int
)va_p
); //
打印对照
return 0
;
}
int main
()
{
myprintf
("%d\n%d\n%d\n"
,101
,102
,103
);
return 0
;
}
程序输出结果如下:
format
的地址=12ff24
p+1
后的变量101
的地址=12ff28
va_p=12ff28
变量101
的va_p=12ff28
执行va_arg
(va_p
,int
)后的va_p=12ff2c
变量102
的va_p=12ff2c
101
变量102
的va_p=12ff2c
执行va_arg
(va_p
,int
)后的va_p=12ff30
变量103
的va_p=12ff30
102
变量103
的va_p=12ff30
执行va_arg
(va_p
,int
)后的va_p=12ff34
变量104
的va_p=12ff34
103
结束后的va_p=12ff34
执行va_end
(va_p
)后的va_p=0
对照分析输出结果,执行语句
va_start
(va_p
,format
);
的作用首先是把format地址赋给va_p,然后执行加1,这时va_p就变成第1个变量101的地址。原来的程序要执行p++才能取得变量101的地址,这就可以不需要执行+1操作了。
执行value=va_arg(va_p,int)语句,将整数值赋给value的同时,也对va_p执行加1操作,使va_p指向下一个变量102的地址12ff2c,这就可以直接取得变量102的value值。原来利用指针p时,需要执行p+1操作。改用宏,宏内执行了这一操作,所以简化了指令。
程序循环结束后的va_p=12ff34(程序指示是变量104,其实是越界的地址),所以要求调用一个用于释放空间的宏va_end,执行va_end(va_p)后的va_p=0。
下面的例题是使用宏完成简单打印函数的完整程序,程序中还改用异或定义交换宏,异或运行快(加法要有进位操作),提高程序性能。
【例20.20】使用宏优化简单打印函数的例子。
#include <stdio.h>
#include <stdarg.h>
void myputs
(char *buf
)
{
while
(*buf
)
putchar
(*buf++
);
return
;
}
void itoa
(int num
, char *buf
, int base
)
{
char *hex= "0123456789ABCDEF"
;
int i=0
,j=0
;
do
{
int rest
;
rest = num % base
;
buf[i++]=hex[rest]
;
num/=base
;
}while
(num
!=0
);
buf[i]='\0'
;
//
使用异或定义交换宏,异或运行快(加法要有进位操作)
#define SWAP
(a
,b
) do{a=
(a
)^
(b
); \
b=
(a
)^
(b
); \
a=
(a
)^
(b
); \
}while
(0
)
//
反转
for
(j=0
; j<i/2
; j++
)
{
SWAP
(buf[j]
,buf[i-1-j]
);
}
return
;
}
int myprintf
(const char *format
,...
)
{
va_list ap
;
char c
;
char buf[32]
;
int value
;
va_start
(ap
,format
);
while
((c = *format++
)
!= '\0'
)
{
if
(c
!= '%'
)
{
putchar
(c
);
continue
;
}
else
{
c = *format++
; //
取%
后面的字符
if
(c=='c'
)
{
putchar
(va_arg
(ap
,char
));
}
if
(c=='s'
)
{
myputs
(va_arg
(ap
,char *
));
}
if
(c=='d'
)
{
value=va_arg
(ap
,int
);
if
(value<0
)
{
value=-value
;
itoa
(value
,buf
,10
);
putchar
('-'
);
myputs
(buf
);
}
else
{
itoa
(value
,buf
,10
);
myputs
(buf
);
}
}
if
(c=='x'
)
{
value=va_arg
(ap
,int
);
itoa
(value
,buf
,16
);
myputs
(buf
);
}
}
}
va_end
(ap
);
return 0
;
}
int main
()
{
char c1='H'
;
char c2="How are you
?"
;
myprintf
("%d
,%d
,%d
,%x
,%x\n"
,100
,0
,-100
,100
,0
); //1
验证%d
和%x
myprintf
("%c
,%s\n"
,c1
,c2
); //2
验证%c
和%s
myprintf
("%c
,%s\n"
,'H'
,"Fine
!"
); //3
带格式使用用字符常量
myprintf
("How are you
?\n"
); //4
直接用字符串常量
myprintf
(c2
); //5
直接用字符串名字
myprintf
("%s\n"
,c2
); //6
标准格式
myprintf
("\n"
,c2
); //7
使用有误,只输出换行,不处理c2
myprintf
("How are%s"
,"you
?\n"
); //8
格式正确
return 0
;
}
这是改写例20.18的程序,主程序一样,所以运行结果也相同。
注意程序中有一条语句
putchar
(va_arg
(ap
, char
));
是可以正确执行的,这是因为直接作为putchar的参数。其实,va_arg宏的第2个参数不能被指定为char、short或float类型。因为char和short类型的参数会被转换为int类型,而float类型会被转换成double类型。如果指定错误,将会引起麻烦。语句
c = va_arg
(ap
, char
);
肯定是不对的,因为无法传递一个char类型参数,如果传递了,它会被自动转换为int类型。应该将它写为如下语句:
c = va_arg
(ap
, int
);
如果cp是一个字符指针,而程序中又需要一个字符指针类型的参数,则下面的写法是正确的。
cp = va_arg
(ap
, char *
);
当作为参数时,指针并不会转换,只有char、short或float类型的数值才会被转换。
【例20.21】分析下面程序的输出结果。
#include <stdio.h>
#include <string.h>
int main
()
{
int i=0
,len=0
;
char str="Look
!"
;
len=strlen
(str
);
for
(i=0
; i<len
;i++
)
printf
("%s\n"
,str+i
);
}
【解答】“printf("%s\n",str+i);”语句不是把str作为首地址,而是str+i做地址。由自行设计myprintf函数中可以知道,str+i等效于&str[i]。它与下面程序的输出结果一样。
#include <stdio.h>
#include <string.h>
int main
()
{
int i=0
,len=0
;
char str="Look
!"
;
len=strlen
(str
);
for
(i=0
; i<len
;i++
)
printf
("%s\n"
,&str[i]
);
}
程序每循环一次,输出字符就从左边减少一个字符。输出结果如下:
Look
!
ook
!
ok
!
k
!
!