神奇的scanf


作为标准输入输出函数组中的一个重要的输入的函数,scanf/sscanf/vscanf函数和printf/sprintf/vsprintf有个重要的区别:如果格式参数和后面的参数不匹配,printf系列函数可能会导致打印出的格式或者数据不是自己期望的 ,而scanf系列函数如果格式参数和后面的参数不匹配,可能导致有待输入的参数附近的内存发生变化,甚至导致程序崩溃。


以下面的函数为例:

8 #include<stdio.h>

9 #include<string.h>

10

11 int main(int argc, char * argv)

12 {

13    char names[6];

14    char c='D';

15    short cnt=0x1234;

16

17    strcpy(names, "Hello");

18

19    printf("Original Values:\n");

20    printf("cnt = %d, c = %c, names = %s\n", cnt, c, names);

21

22    printf("Please Input your settings:\n");

23    printf("C = ");

24    scanf("%c", &c);

25    getchar();

26    printf("Names = ");

27    scanf("%s", names);

28    getchar();

29    printf("Cnt = ");

30    scanf("%ld", &cnt);

31    getchar();

32

33    printf("Latest Values:\n");

34    printf("cnt = %d, c = %c, names = %s\n", cnt, c, names);

35

36    return 0;

37 }              


程序期望从标准输入中接受字符C,字符串names和short类型counter,然后把接受到的输入打印出来。但如果把这样的程序进行充分的测试就会发现,当输入的cnt为负数或者输入的cnt值比较大,发现最后打印出来的部分值跟输入的值大相径庭,例如下面的情况:

[root@localhost CStudy]# ./scan

Original Values:

cnt = 4660, c = D, names = Hello

Please Input your settings:

C = h

Names = Leifeng

Cnt = -2

Latest Values:

cnt = -2, c = , names = Leifeng

根本原因是scanf系列函数会根据格式字符串中指定的格式往某个指针所对应的内存区域里写入数据,而非根据指针所指向的数据类型写入。这一点常常给一些刚接触C语言的程序员带来麻烦和困惑,甚至某些工作多年的程序员也经常忽视。为此,需要对scanf系列函数的接口要求和格式参数需要清晰的认识。


1. scanf系列函数的接口要求

scanf函数的格式参数指定了往内存写入数据的类型,包括字节长度、有无符号、整型还是浮点。如果将要往内存写入的数据长度超过对应指针指向的数据类型的长度,就会覆盖那个指针指向数据的相邻内存,具体影响哪些数据,取决于编译器的堆栈类型(满递减、满递增、空递减、空递增)、硬件环境(大尾端和小尾端)。比如在上面的例子中,就是因为格式字符串 ”ld“超过了对应数据short cnt的长度,因此局部变量char c 被覆盖。 因此,为了避免scanf函数不正确的调用可能覆盖有用的数据和代码,要求格式参数后面的指针参数所指向的数据类型必须严格符合接受输入的数据的类型。


2.scanf系列函数的格式字符串

总体说来,scanf函数的格式字符串和printf系列的格式字符串差不多,下面列出了常用的格式字符串及其含义:

h: half, 用于d,i,o,u,x,X,n的前面,表示short, 例如%hd,表示short int

hh: half half, 用于d,i,o,u,x,X,n的前面char,表示unsigned char或者signed char

j:类似h,但是只用来修饰intmax_t,uintmax_t

l: long,用来修饰d,i,o,u,x,X,表示就按照long int或者unsigned long int往下一个指针指向的内存写数据。如果它是用来修饰e,f,g,那么将会按照double而非float格式读入输入的数据

L: long long, 用来修饰d,i,o,u,x,X,表示就按照long long int或者unsigned long long int往下一个指针指向的内存写数据。如果它是用来修饰e,f,g,那么将会按照long double而非float/double格式读入输入的数据

t:表示ptridiff_t类型,C99引入

z:表示size_t类型,C99引入

d:表示有符号十进制整型,int

i: 如果输入以0x、0X开头,输入的数据按照16进制读入,如果以0开头,按照8进制读入;其他的类型都按照10进制读入

o:无符号8进制

u:无符号10进制

x/X:无符号16进制

f/e/g/E/a:有符号浮点

p:指针类型

s:空间足够的字符串,会往输入的末尾自动加上'\0'

c:一段字符序列,默认长度为1

[:指定非空字符串序列中字符的范围

m:字符串,scanf系列函数会根据输入的数据长短自动申请空间,调用者自己注意释放


下面给出了一个充分利用格式字符m和[ ]的例子:

char *p;

int n;


errno = 0;

n = scanf("%m[A-Z]", &p);

if (n == 1) {

printf("read: %s\n", p);

free(p);

} else if (errno != 0) {

perror("scanf");

} else {

fprintf(stderr, "No matching characters\n");

}


参考资料:scanf man手册