
对于 printf 函数,相信你们并不陌生。之所以称它为格式化输出变量,关键就是该数组可以按用户选定的格式,把指定的数据显示至显示器屏幕上。该变量原型的通常格式如下:
int printf(const char * format, ... );
很显然,与其它库函数不同的是,printf 函数是一个“可变参数变量”(即方程参数的个数是可变的)。确切地说,是其输出参数的个数是可变的,且每一个输出参数的输出格式都有对应的格式说明符与之对应,从格式串的左端第 1 个格式说明符对应第 1 个输出参数,第 2 个格式说明符对应第 2 个输出参数,第 3 个格式说明符对应第 3 个输出参数,以此类推。其中,格式说明符的通常方式如下(方括号 [] 中的项为能选项):
%[flags][width][.prec][length] type_char
/*用英文标识如下:*/
%[标志符][宽度][精度][长度]类型符
1) 类型符(type_char)它用以表示输出数据的类别,如表 1 所示。
表 1 常见的类别符以及表明符号种类说明例子结果
输出字符“%”本身
pnntf("%%");
d、i
以整型输出
printf("%i,%d",100,100);
100,100
unsigned int
以无符号整型输出
printf( "%u,%u",100u,100);
100,100
unsigned int
以八进制无符号整S输出
printf( "%o”,100);
unsigned int

以十六进制小写输出
printf("%x",11);
unsigned int
以十六制大写输出
printf("%X",11);
除表 1 所示的类别符之外,还有一个比较特殊与独特的类别符“%n”,当在格式化字符串中遇到“%n”时,在“%n”之前输出的字节个数会储存到下一个参数里。例如,下面的样例代码演示了怎样获得在两个格式化的数字之间空间的偏量:
int main(void)
{
int pos=0;
int x = 123;
int y = 456;
printf("%d%n%d\n", x, &pos, y);
printf("pos=%d\n", pos);
return 0;
}很显然,上面代码中的 pos 将输出 3,即“123”的长度,运行结果为:
123456
pos=3
这里应该非常注意,“%n”返回的是必须被输出的字符数量,而不是实际输出的字符数量。当把一个字符串格式化输出到一个定长缓冲区内时,输出字符串可能被截短。不考量截短的妨碍,“%n”格式表示即使不被截短的偏量值(输出字符数量)。看以下的实例代码:
int main(void)
{
char buf[20];
int pos=0;
int x = 0;
snprintf(buf, sizeof(buf), "%.100d%n", x, &pos);
printf("pos=%d\n", pos);
return 0;
}很显然,上面的代码会输出 100,而不是 20。
由此可见,相对于“%d”“%x”“%s”等,“%n”的明显不同之处就是“%n”会改变变量的值,这也就是格式化字符串攻击的爆破点,如以下的样例代码所示:
char daddr[16];
int main(void)
{
char buf[100];
int x=1;
memset(daddr,'/0',16);
printf("前X: %d/%#x (%p)\n", x, x, &x);
strncpy(daddr,"AAAAAAA%n",9);
snprintf(buf,sizeof(buf),daddr);
buf[sizeof(buf) - 1] = 0;
printf("后X: %d/%#x (%p)\n",x, x, &x);
return 0;
}在里面的代码中,x 将被从 1 修改成 7,其运行结果为:
前X: 1/0x1 (0061FEA8)
后X: 7/0x7 (0061FEA8)
之所以会出现这样的结果,是因为程序在读取 snprintf 函数之前,首先读取了 printf 函数,而此时 printf 函数的 &x 参数在 main 函数的堆栈内存中留下了 &x 的存储残像。当调用 snprintf 时,系统本身只帮 snprintf 准备了 3 个参数,但是鉴于格式化字符串攻击原因,使得 snprinf 认为应当有 4 个参数传给它,这样 snprintf 就私自把 &x 的内存残像作为第 4 个参数读走,而 snprintf 所谓的第 4 个参数对应的就是“%n”,于是 snprintf 就成功更改了函数 x 的值。这也就是最常用的使用 Linux 函数调用时的存储残像来实现格式化字符串攻击的方式之一,所以在使用的之后一定要注意。2) 标志符(flags)它用于要求输出格式,如表 2 所示。
表 2 标志符以及表明符号说明
(空白)
右对齐,左边填充 0 和空格
(空格)
输出值为正时加上空格,为负时加上负号
输出结果为左对齐(默认为右对齐),边填空格(如果存在表格最终一行介绍的0,那么将忽略0)

在数字前增加符号“+”(正号)或“-”(负号)
类塑符是o、x、X吋,增加前缀0、0x、0X;类型符是e、E、f、F、g、G时,一定要使用小数点;类型符是g、G时,尾部的 0 保留
参数的后面用0填充,直到占满指定列宽为止(如果同时存在“-”,将被“-”覆盖,导致 0 被忽略
3) 宽度(width)它用于控制显示数值的长度,如表 3 所示。
表 3 宽度以及表明符号说 明
至少输出 n 个字符(n 是一个正整数)。如果输出超过 n 个字节,则用括号填满余下的位置(如果标识符为“-”,则在左侧填,否则在左端填)
至少输出 n 个字符(n 是一个正整数)。如果输出值超过 n 个字符,则在右边填满 0
输出字符个数由下一个输出参数选定(其需要为一个整形量)
4) 精度(.prec)它用于控制显示数值的误差。如果输出的是数字,则表示小数的位数;如果输出的是字符,则表示输出字符的个数;若实际位数小于所定义的误差数,则截去超过的个别。如表 4 所示。
表 4 精度以及表明符号说明
系统默认精度
对于 d、i、o、u、x、X等整形类型符,采用平台默认精度;对于f、F、e、E等浮点类型符,不输出小数部分
1) 对于d、i、o、u、x、X类别符,至少输出 n 位数字,且:2) 对于f、F、e、E类型符,输出结果保留 n 位小数。如果小数部分大于 n 位,则对其四舍五入
3) 对于 g 和 G 类型符,最多输出 n 位有效数字
4) 对于 s 类型符,如果对应的输出串的厚度不少于 n 个字符,则将其原样输出,否则输出其头 n 个宁符
输出精度由下一个输出参数选定(其需要为一个整型量)
5) 长度(length)它用于控制显示数值的长度,如表 5 所示。
表 5 长度以及表明符号说明
与d、i一起使用,表示一个signed char 类型的值;与o、u、x、X—起使用,表示一个unsigned char类型的值;与 n 一起使用,表示相应的变元是指向 signed char 型函数的指针(c99 )
与d、i、o、u、x、X 或 n 一起使用,表示一个short int 或 unsigned short int 类型的值
与d、i、o、u、x、X 或 n 一起使用,表示一个 long int 或者 unsigned long int 类型的值
与d、i、o、u、x、X 或 n —起使用,表示相应的变元是 long long int 或 unsigned long long int 类型的值(c99 )

与 d、i、o、u、x、X 或 n —起使用,表示匹配的变元是 intmax_t 或 uintmax_t 类型,这些类型在“stdint. h”中声明(c99 )
与 d、i、o、u、x、X 或 n —起使用,表示匹配的变元是指向 size_t 类型对象的指针,该类别在“stddef. h”中声明(c99 )
与d、i、o、u、x、X 或 n —起使用,表示匹配的变元是指向 ptrdiff_t 类型对象的指针,该类别在“stddef. h”中声明(c99 )
和a、A、e、E、f、F、g、G—起使用,表示一个long double类型的值
最后,在使用 printf 函数时还需要留意,尽量不要在 printf 语句中改变输出函数的值,因为可能会造成输出结果的不确定性。如以下的样例代码所示:
int k=8;
printf("%d,%d\n",k,++k);对于前面的代码,表面上看起来输出的结果需要是“8,9”。但实际状况只是那么,在读取printf函数时,其参数是从右到左进行处理的,即将先进行 ++k 运算,所以最终的结果是“9,9”。由此可见,千万不要在 printf 语句中强行改变输出函数的值,如果确实必须改变,可以根据以下的样例代码方式来处理:
printf("%d\n",k);
printf("%d\n",++k);这样处理过后printf d,其结果就是我们所必须的“8,9”了。
除此之外,每一个输出参数的输出格式都需要有对应的格式说明符与之一一对应,并且种类应该匹配。若两者不能否一一对应匹配,则不能够正确输出,而且编译时也许不会报错。同时,若格式说明符个数超过输出项个数,则多余的输出项将不予输出;若格式说明符个数大于输出项个数,则可能会输出一些毫无意义的数字乱码。
scanf 函数相对于 printf 函数,scanf 函数就简单得多。scanf 函数的功能与 printf 函数正好相反,执行格式化输入功能。即 scanf 函数从格式串的更左端开始,每遇到一个字符便将其与下一个输入字符进行“匹配”,如果两者匹配(相同)则再次,否则结束对前面输入的处理。而每见到一个格式说明符,便按该格式说明符所叙述的格式对其后的输入值进行转化,然后将其存于与其对应的输入地址中。以此类推,直到格式串结束为止。该变量原型的通常格式如下:
int scanf (const char *format, ...);从函数原型可以看出printf d,同 printf 函数相同,scanf 函数只是一个“可变参数变量”。同时,scanf 函数的第一个参数 format 也需要是一个格式化串。除此格式化串之外,scanf 函数还可以有若干个输入地址,且对于每一个输入地址,在格式串中都需要有一个格式说明符与之一一对应。即从格式串的左端第 1 个格式说明符对应第 1 个输入地址,第 2 个格式说明符对应第 2 个输入地址,第 3 个格式说明符对应第 3 个输入地址,以此类推。
也就是说,除第 1 个格式化串参数之外,其他参数的个数是可变的,且每一个输入地址应该指向一个合法的存储空间,以便可正确地接受相应的输入值。每个输入值的转化格式都由格式说明符决定。格式说明符的通常方式如下(方括号 [] 中的项为能选项):
%[*][width][length] type_char
/*用英文标识如下:*/
%[*][宽度][长度]类型符
在使用 scanf 函数的之后,需要非常注意的就是缓冲区问题。对 scanf 函数来说,估计最容易出错、最令人捉摸不透的难题需要是缓冲区问题了。
下面先来看一段示例代码:
int main(void)
{
char c[5];
int i=0;
printf("输入数据(hello):\n");
for(i = 0;i < 5; ++i)
{
scanf("%c", &c[i]);
}
printf("输出数据:\n");
printf("%s\n", c);
return 0;
}对于前面这段实例代码,我们期望在“c[5]”字符数组中能够传输“hello”字符串,并在最终输出至屏幕上。从表面上看,这段程序没有任何疑问,但实际状况只是这么。当我们依次输入“h(回车)”“e(回车)”,然后再输入“l”时,问题出现了。此时,程序虽然中止输入操作,而且会打印出字符数组 c 中的内容,其运行结果为:
输入数据(hello):
h
e
l
输出数据:

h
e
l
很显然,字符数组“c[5]”是完全无法存储“hello”字符串的,但为什么输入到“l”就结束了呢?
其实原因很简单,在我们输入“h”和第一个回车后,“h”和这个回车符“\n”都保留在缓冲区中。第 1 个 scanf 读取了“h”,但是输入缓冲区里面还留有一个“\n”,于是第 2 个 scanf 读取这个“\n”,然后输入“e”和第 2 个回车符“\n”。同理,第 3 个 scanf 读取了“e”,第 4 个 scanf 读取了第 2 个回车符“\n”,第 5 个 scanf读取了“l”。因此,程序并没有提前结束,而是完整地循环了5次scanf语句,只不过有两次scanf都写入到回车符“\n”而已。
由此可见,在使用 scanf 函数时,如果不迅速刷新输入缓冲区,有时会出现莫名其妙的出错。对于这类问题,其实解决方法有许多,比如可以使用“fflush(stdin);”语句来刷新输入缓冲区。但不得不说明的是,fflush 函数在可移植性上并不是很好。当然,也可以借助自己编写代码来解决,如以下的样例代码所示:
#include <stdio.h>
void flush()
{
char c;
while ((c=getchar()) != '\n'&&c!=EOF);
}
int main(void)
{
char c[5];
int i=0;
printf("输入数据(hello):\n");
for(i = 0; i < 5; ++i)
{
scanf("%c", &c[i]);
flush();
}
printf("输出数据:\n");
printf("%s\n", c);
return 0;
}这样,就从根本上缓解了输入缓冲区问题,其运行结果为:
输入数据(hello):
h
e
l
l
o
输出数据:
hello
除此之外,还需要切记 scanf 中的空白符(这里所指的空白符包括括号、制表符、换行符、回车符和换页符)带来的问题,如以下的代码所示:
int main(void)
{
int a=0;
printf("输入数据:\n");
/*请注意,这里多了一个回车符\n*/
scanf("%d\n",&a);
printf("输出数据:\n",a);
printf("%d\n",a);
return 0;
}在里面的代码中,因为在“scanf("%d\n",&a);”语句中多加了一个回车符“\n”,导致的结果就是要输入两个数,程序能够正常结束,而不是我们所希望的一个数。运行结果为:
输入数据:
22
11
输出数据:
22
原因就是在用空白符结尾时,scanf 会跳过空白符去读下一个字符,所以需要再输入一个数。因此在编写程序时一定要多留意这类手误造成的出错。
本文来自电脑杂谈,转载请注明本文网址:
https://www.pc-fly.com/a/ruanjian/article-136056-1.html
干燥环境下能生蛆
各种复杂添加剂
迅速崛起