编程技能:格式化打印02,vsprintf

专栏导航

本节文章分别属于《Win32 学习笔记》和《MFC 学习笔记》两个专栏,故划分为两个专栏导航。读者可以自行选择前往哪个专栏。

(一)WIn32 专栏导航

上一篇:编程技能:格式化打印01,vsprintf 函数族简介

回到目录

下一篇:编程技能:格式化打印03,printf

(二)MFC 专栏导航

上一篇:编程技能:格式化打印01,vsprintf 函数族简介

回到目录

下一篇:编程技能:格式化打印03,printf

本节前言

本节,我们来讲解格式化打印的基础函数,vsprintf 。

一.    vsprintf 基本介绍

首先呢,我们来看一看 vsprintf 函数的声明。

extern int vsprintf(char *buf, const char *fmt, va_list args);

以上声明,是我从 Linux 0.12 内核中提取出来的。有可能,你学习过 vsprintf 函数,并且你所学过的声明形式,和我这里所列出的不同。不过,大体上,我这里所列出的声明形式,它是可用的。

在上面的声明中,有一个东西,大家可能有疑问。那就是【va_list】这个东西。

大家请看如下的一段代码。

typedef char * va_list;

看懂了没?va_list,它其实就是 char* 类型的别名而已。

第一个参数,我们先不去管它。先来看第二个参数,fmt 。

fmt,它是 format 的缩写,它是格式字符串的意思。

什么是格式字符串

int i, j, k;
i = 1;
j = 6;
k = 11;

printf("i = %d, j = %d, k = %d", i, j, k);

printf 与 vsprintf 的许多概念是相似的。所以呢,在上面的代码块里面,我用 printf 函数来举例子了。

在上面的代码块中,【"i = %d, j = %d, k = %d"】这一部分,便是格式字符串

也就是说,在 printf 函数中,包含有【%d】,【%c】,【%f】等等的格式控制符的字符串,被称作格式字符串

实际上,printf 的函数实现代码里面,它会调用 vsprintf 函数。在调用 vsprintf 函数的时候,传递给形参 fmt 的,便是格式字符串,是包含有【%d】,【%c】,【%f】等等的格式控制符的字符串。当然了,传递的是字符指针。字符串,字符指针,它俩的关系,大家应该是懂得的吧。

我们接着来看 vsprintf 的第三个参数。

va_list 右边的形参变量名为 args,它是参数列表中,第一个参数的栈指针

那么,什么是参数列表呢?

在上面的代码片段中,【i, j, k】就是参数列表

也就是说,在 printf 函数中,格式字符串的后面的部分,为参数列表。在本节里面,我所说的参数列表,大家将其当作是一个专有词汇吧。因为,正常来讲,上面的代码块的参数列表,既包括【i, j, k】的部分,也包括【"i = %d, j = %d, k = %d"】这一部分。

不过,当前,你暂且先将【参数列表】这一概念,看作是一个特殊的,专有的概念吧。

printf 的函数实现代码里面,它会调用 vsprintf 函数。而在调用 vsprintf 函数的过程中,传递给形参 args 的,便是格式字符串后面的第一个参数的栈指针

也就是说,printf 在调用 vsprintf 函数的时候,传递给形参 args 的,便是参数列表中的第一个参数的栈指针

栈指针又是什么?

A 函数调用 B 函数的时候,会将 B 函数所需要的参数,也就是传递给 B 函数的实参,压入栈中。完成了参数入栈的工作以后,CPU 才会前往执行 B 函数的代码。那么,A 函数所传递的某一个参数在栈中的位置,就是这个实参的栈指针

注意,这个栈指针,不是在 printf 函数内部调用 vsprintf 函数时产生,而是我们编程人员在某处代码位置,调用 printf 函数的时候产生的。我们调用 printf 函数时,产生了参数列表的第一个参数的栈指针,然后 printf 函数内部的代码在调用 vsprintf 函数的时候,将这个栈指针传递给了 vsprintf 函数的 args 形参。

具体来讲,我们在某一个代码位置上,假定我们将其称作主调位置。在主调位置上调用 printf 函数的时候,假定,我们的调用代码为:【printf("a = %d, b = %f, str01 = %s", a, b, str01);】。为了调用 printf ,主调位置所在的函数,就需要将格式字符串"a = %d, b = %f, str01 = %s"】的指针和参数列表a, b, str01】都压入栈中,做好了参数入栈的工作,CPU 才会从主调位置所在的主调函数跳转到 printf 函数,前往执行 printf 函数的代码。那么,在参数压栈的过程中,参数列表a, b, str01】入栈了,由此,参数列表中的第一个参数【a】的栈指针也就产生了。在 printf 函数中,它会把参数列表中的第一个参数的栈指针给提取出来,并作为 vsprintf 的第三个形参 —— args 形参的实参,来调用 vsprintf 函数。至于说,printf 函数是如何将参数列表中的第一个参数的栈指针给提取出来的,本节,我们暂时不讲。等以后去讲 printf 的时候,我们会去讲解。

我不知道,这个栈指针的概念,你能否理解。能理解最好了。如果不理解,那就把它当作一个特殊的,专有的概念,就好了。此时,如果你实在不理解栈指针的概念的话,暂时你也不必深究。因为,想要彻底理解它的含义,你需要具备汇编语言知识。

当然了,我还是希望,在我所讲的范围内,你能够形成对于栈指针这一概念的模糊的认识。一个模糊的,大致的认识,就足以了。

某些个知识点,想要理解它,一个可以采用的方法就是,多读几遍,借助于冥想,沉思,沉淀,让自己形成对它的印象。这个印象,可能会是一个比较生硬的印象。在学习的时候,也许,我们需要对某些个不易理解的东西,采取这种冥思与沉淀的方式,让自己暂且接受此概念,先形成一个生硬的印象,以后,逐渐地来消化它。

对于知识点,如果当时能够 完全理解,那是最好了。可是,对于某些个拗口,不易理解的知识点,可能就真的需要这种冥思沉淀的方式了。

我们接着往下讲。

在上面的代码块中,参数列表为【i, j, k】,而参数列表中的第一个,为【i】。所以呢,vsprintf 函数中的 args,它是参数 i 的栈指针。注意,参数 i 的栈指针,不等于参数 i 的指针。参数 i 的栈指针参数 i 的指针,这是两个不同的概念。

参数 i 的指针,这是我们在进行变量声明时,在执行【int i, j, k;】代码的时候产生的。而参数 i 的栈指针,则是在调用代码【printf("i = %d, j = %d, k = %d", i, j, k);】被执行的时候,在参数入栈的过程中产生的。参数 i 的指针与参数 i 的栈指针,产生的原因不同,自然地,它俩也不是同一个东西。

注意,【参数 i 的栈指针不等于参数 i 的指针】这一点,你能够理解则最好。不理解的话,你也暂时不要深究。想要彻底地理解,同样需要你具备汇编语言知识。

这么说,似乎有点乱。麻烦大家多看几遍吧,希望你能够理解我所说的意思。

大体上,我就当作,大家已经理解了 fmt 与 args 的含义了,理解了格式字符串参数列表的含义。

在你已经理解了 fmt 与 args 的含义的基础上,我们来讲 vsprintf 的功能。

vsprintf 函数的总体功能是对 fmt 所指向的格式字符串进行格式化转换。其大致的执行逻辑为,对 fmt 所指向的格式字符串进行扫描。对于普通字符与转义字符,直接复制到 buf 里面;而对于【%d】,【%c】,【%f】等等的格式控制符,则是根据格式要求,依次转换为参数列表中的各个参数的值,并将转换结果保存到 buf 里面。vsprintf 还会在最后添加一个 '\0' 结束符。最终,完成了格式化转换工作的字符串,全部保存在了字符指针 buf 所指向的位置里面。返回值为转换后的字符串的有效长度。这个有效长度,是指不包含 NUL 结束符的字符串长度。在整个的格式化转换过程中,fmt 所指向的格式字符串始终是不变的,变化的,只是 buf 中的内容。

说完了vsprintf 的格式化转换的大致思路与返回值,我们再来说它的第一个参数,buf 。

printf 函数在执行的时候,会调用 vsprintf 函数。调用 vsprintf 的时候,也将一个长度很大的字符数组的指针传递给了 vsprintf 的 buf 形参。printf 将这个字符数组的指针传递给 vsprintf 的 buf 形参,vsprintf 则是将格式字符串的格式化转换结果存放在 buf 所指向的位置。等到 vsprintf 返回以后,printf 当初传递给 vsprintf 的字符数组指针的位置,就保存了格式化转换后的字符串,并且 printf 函数里面,可以通过这个字符数组指针,来访问这个字符串。

所以,vsprintf 中的第一个形参,buf,是用来保存和传递格式字符串的格式化转换的结果的。

对于 buf,大家就先理解到这里好了。

至于说,printf 函数里面的长度很大的字符数组是怎么回事,这个,大家先不必深究。等到讲 printf 函数的时候,我们再去讨论这个问题。

二.    vsprintf 跟踪举例

我在这里给出如下的代码片段。

char str01[] = "Windows Program";
int score = 100;

printf("I hope that, you can study %s well, and get %d score.\n",
    str01, score);

在上述代码块里面,我们使用了 printf 函数。printf 函数会在内部调用 vsprintf 函数。调用 vsprintf 函数的时候,会将一个长度很大的字符数组的指针传递给 vsprintf 的 buf 形参,将格式字符串【"I hope that, you can study %s well, and get %d score.\n"】的指针传递给 vsprintf 的 fmt 参数,将 str01 的栈指针传递给 vsprintf 函数的 args 形参。 

接下来,vsprintf 会开始执行格式化转换工作。

格式字符串【"I hope that, you can study %s well, and get %d score.\n"】里面,包含有两个格式控制符,分别为 %s 和 %d 。为此,我们分成六个阶段,来描述 vsprintf 的格式化转换工作。

第一阶段,在 %s 格式控制符左边,是一些普通字符。这些字符,会被原样复制,追加到 buf 里面。完成此阶段的任务以后,字符指针 buf 中的字符串为【"I hope that, you can study "】。

第二阶段,是处理 %s 格式控制符。%s 格式控制符所对应的参数列表中的参数,为 str01 。str01 字符指针所指向的字符串内容为【"Windows Program"】。vsprintf 对 %s 格式控制符的处理方式为,将 %s 所对应的参数,在本例中为 str01,将这个参数字符指针所对应的字符串【"Windows Program"】,追加到 buf 中。而原格式字符串中的 %s 则不会被追加到 buf 中。完成此工作以后,字符指针 buf 中的字符串为【"I hope that, you can study Windows Program"】。

第三阶段,是处理 %s 格式控制符右边与 %d 格式控制符左边的东西。这一段内容,为普通字符。对于普通字符,vsprintf 会将其原样复制,追加到 buf 中。完成此工作以后,字符指针 buf 中的字符串为【"I hope that, you can study Windows Program well, and get "】。

第四阶段,是处理 %d 格式控制符。%d 格式控制符所对应的参数列表中的参数,为 score 变量,其值为 100 。vsprintf 对 %d 格式控制符的处理方式为,将 %d 所对应的参数,本例中为 score,将它的数值化为字符串,也就是将 int 值 100 转化为字符串 "100"。然后呢,将这个转化之后的数值字符串 "100" 追加到 buf 中。而原格式字符串中的 %d 则不会被追加到 buf 中。完成此工作以后,字符指针 buf 中的字符串为【"I hope that, you can study Windows Program well, and get 100"】。

第五阶段,是处理 %d 格式控制符右边的部分。这一部分,是普通字符与转义字符 \n 。对于普通字符与转义字符,vsprintf 的处理方式为,直接将其复制,追加到 buf 中。完成这一工作以后,字符指针 buf 中的字符串为【"I hope that, you can study Windows Program well, and get 100 score.\n"】。

至此,原格式字符串中的内容,都已转换完毕。然后呢,vsprintf 会进入格式化转换工作的最后阶段。

第六阶段,在 buf 的最后,添加 '\0',也就是添加 NUL 结束符。

这样一来,格式化转换工作的所有步骤就全都完成了。完成了这一任务以后,vsprintf 会计算全部转换完成之后的字符串的不含 NUL 结束符的有效长度,并将这一有效长度值予以返回。也就是,将转换后的字符串【"I hope that, you can study Windows Program well, and get 100 score.\n" 的有效长度数值作为返回值,予以返回。

这样一来,转换案例代码我就算是大体上讲完了。

我在上面的案例代码中,谈到了 %s,%d 两种格式控制符。其它的格式控制符我没有讲。我简单地说一说其它的格式控制符的转换方法。

对于 %X 或 %x,vsprintf 会将其对应参数的值,转换为十六进制数,并将这个十六进制整数,转换为字符串,然后将这个转换后的数值十六进制数值字符串追加到 buf 中。注意,转换后的数值字符串里面,不含【0x】或【0X】字样。如果你想要让转换后的字符串中包含有【0x】或者【0X】字样,你得自己写。比如,你可以这样子写,【printf("小花的卡里有 0x%X 万元存款", 160)】,格式化转换之后的结果为【"小花的卡里有 0xA0 万元存款】。注意,%x 或 %X 格式控制符本身不会被追加到 buf 中。

对于 %f,则将对应的浮点参数值转换为浮点数字符串,然后追加到 buf 中。注意,%f 格式控制符本身不会被追加到 buf 中。

对于 %c,直接将对应参数的 ASCII 码追加到 buf 中。在这里,并不是说将对应参数的 ASCII 码转换为数值字符串之后再追加到 buf,而是直接将 ASCII 码追加到 buf 中。所以呢,追加到 buf 中的是一个 ASCII 码,是一个占用 1 字节的带符号整数,而在显示的时候,它仍然会被显示为对应的字符。例如,字符 a 的 ASCII 码为 97,将它追加到 buf 的时候,添加的是 97 这个 ASCII 码整数,是一个占用 1 字节的带符号整数,而在显示的时候,仍然会将其显示为字符 a 。注意,%c 格式控制符本身不会被追加到 buf 中。

对于格式控制符的转换规则,我暂且简单地举出这样的几个例子。

其实,对于格式控制符的转换规则,它还有着更为复杂的内容。这种更为复杂的内容,它的具体的代码与执行过程,我不想在 Windows 编程专栏中细讲。如果以后,我去写 Linux 0.12 内核专栏教程的话,那么,我会在那里,给大家细讲各种格式控制符的转换。

在当前的阶段里,在你未学习过汇编语言,仅仅是在高级语言层面学习编程知识的阶段里,我仅仅是讲解一点简单的东西,让你达到大致理解,大概会用的程度。至于,进一步的内容,那就等你具备了汇编语言的基础,等你准备去学习 Linux 0.12 内核,或者流行的内核代码教程的时候,再来细研究 vsprintf 的格式转换规则吧。

我们接着往下讲。

三.    如果 printf 中不含有格式字符该怎么办

假定有如下代码。

printf("Windows Program Design");

这段代码里面,格式字符串中不含有【%d】,【%c】,【%f】等等的格式字符。这个时候,printf 函数中不含有参数列表,也就谈不上参数列表中的第一个参数了。

这种情况下,传给 vsprintf 的 args 参数的,是什么呢?

不用理会。

在这种情况里面,printf 当然会给 vsprintf 的 args 形参传递过去一个值,作为实参。但是呢,这个实参究竟是啥,我们不用管。

之所以不用去管,是因为,想要理解它,同样是需要汇编语言知识。很可能,此刻学习本专栏的同学,有很多是尚未学习过汇编语言的。而本专栏的一个假定,是假定大家尚未学习过汇编语言。

args 不用管,然而,传给 vsprintf 的 fmt 形参与 buf 形参,我们还得去理会的。

在上述代码块中,prijntf 函数传递给 vsprintf 的 buf 形参位置的实参,同样是一个长度很大的字符数组的指针。而传递给 fmt 形参位置的实参,是 "Windows Program Design" 。由于不含有格式字符,所以,此字符串无须转换,直接存入 buf 。返回值,为字符串 "Windows Program Design" 的有效长度。这个有效长度,不包含结尾的 NUL 字符。

稍微小结一下,当 printf 的格式字符串里面不包含格式字符的时候,printf 传递给 vsprintf 函数的 buf 形参位置的实参,是一个长度很大的字符数组的指针。而传递给 fmt 形参位置的实参,是格式字符串。传递给 args 形参位置的实参,我们不用管。vsprintf 对形参 fmt 所接收的格式字符串,无须转换,直接存入 buf 。至于返回值,则是原本的格式字符串的有效长度。此有效长度,不包含结尾的 NUL 字符。

结束语

本节知识,到这里就讲完了。

可能会有些不好理解。大家多看几遍吧。

这一块,我也觉得讲得比较费劲儿。希望你能够大致理解本节内容。

本节结束。

专栏导航

本节文章分别属于《Win32 学习笔记》和《MFC 学习笔记》两个专栏,故划分为两个专栏导航。读者可以自行选择前往哪个专栏。

(一)WIn32 专栏导航

上一篇:编程技能:格式化打印01,vsprintf 函数族简介

回到目录

下一篇:编程技能:格式化打印03,printf

(二)MFC 专栏导航

上一篇:编程技能:格式化打印01,vsprintf 函数族简介

回到目录

下一篇:编程技能:格式化打印03,printf

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值