阅读:0

C语言格式化输入

当从一个格式化数据源中读取数据时,C 语言提供了 scanf()函数系列。与 printf()函数一样,scanf()函数需要一个格式化字符串作为其参数,以控制 I/O 格式与程序内部数据之间的转换。本文介绍在 scanf()和 printf()函数中使用格式化字符串和转换修饰符的差异。

scanf()函数系列

各种 scanf()函数处理输入源字符的方式都是相同的。不同的是这些函数所针对的数据源种类,以及它们接收参数的方式。下面的 scanf()函数针对字节导向流:
int scanf(const char*restrict format,...);
从标准输入流 stdin 中读取数据。
int fscanf(FILE*restrict fp,const char*restrict format,...);
从 fp 所引用的输入流中读取数据。
int sscanf(const char*restrict src,const char*restrict format,...);
从 src 指向的 char 数组中读取数据。

省略号表示还有更多的可选参数。可选参数是指向变量的指针,scanf()函数将转换结果存储在这些变量中。

类似于 printf()函数,scanf()函数系列也包含变体版本。变体版本将一个指针作为参数,指向一个参数列表,而不是在函数调用时直接接收数量可变的参数。

这些变体版本的函数名称以字母 v 开头,表示“variable argument list”(可变参数列表):例如,vscanf()、vfscanf()和 vsscanf()。如果想使用支持可变参数列表的函数,除了头文件 stdio.h 以外,还必须包含头文件 stdarg.h。

这些函数都具有相应的针对宽字符导向流的版本。宽字符函数名称中包含 wscanf 而不是 scanf,例如 wscanf()和 vfwscanf()。

C11 标准为这些函数都提供了一个新的“安全”的版本。这些对应的新函数均以后缀 _s(如 fscanf_s())。新函数测试在读入一个字符串到数组之前,是否超出了数组边界。

格式化字符串

scanf()函数的格式化字符串包含普通字符和转换说明,转换说明定义了如何解释以及转换读入的字符序列。scanf()函数所使用的大多数转换修饰符都与 printf()函数所定义的一样。然而,scanf()函数的转换说明没有标记和精度选项。针对 scanf()函数转换说明的通用语法如下所示:

%[*][字段宽度][长度修饰符]修饰符


对于格式化字符串中的每个转换说明,从输入源读入的字符的数量与转换方式都会与转换修饰符一致。结果会存储在对应指针参数所指向的对象中。如下例所示:
int age = 0;
char name[64] = "";
printf( "Please enter your first name and your age:n" );
scanf( "%s%d", name, &age );

假设用户在提示符下输入如下内容:
Bob 27n

调用 scanf()函数,会将字符串 Bob 写进 char 数组 name 中,然后将 27 写进 int 变量 age 中。

所有的转换说明,除了具有修饰符 c 的情况以外,都会忽略前面的空白字符(whitespace character)。在上例中,用户可以在第一个词 Bob 前,或者在 Bob 与 27 之间,放置任意多个空格、制表符或换行符,这些操作均不影响结果。

针对给定的转换说明,当 scanf()读到任何空白字符时,或者任何无法以该转换说明解释的字符时,读取序列字符的操作将会终止。无法被解释的字符会被放回到输入流中,下一个转换说明将从该字符开始。在前述例子中,假设用户输入如下:
Bob 27yearsn

在读取到字符 y 时,它不可能是十进制数值的一部分,针对转换说明 %d,scanf()会停止读取对应的字符。在调用该函数后,字符 yearsn 会继续留在输入流的缓冲区中。

如果在忽略所有空白符之后,scanf()还是找不到符合当前转换说明的字符,则生成错误,scanf()函数终止处理输入。下面将介绍如何捕获这类错误。

通常,在调用 scanf()函数时,格式化字符串只包含转换说明。如果不是,那么格式化字符串中除转换说明与空白符以外的其他所有字符,必须与输入源对应位置的字符完全一致。否则 scanf()函数就会终止处理,并将不匹配的字符放回到输入流中。

格式化字符串中所出现的一个或多个连续空白符,必须符合输入流中连续空格的数量。换句话说,对于格式化字符串中出现的所有空白符,scanf()会读取并略过数据源中的所有空白字符,直到读入第一个非空白符。在理解这一点后,请判断下面的 scanf()调用方式有什么问题。
scanf( "%s%dn", name, &age );    // 有什么问题

假设用户输入下面这一行字符:
Bob 27n

本例中,scanf()在读入换行符后不会返回,而是继续读取更多输入,直到出现非空白字符出现。

有时候,需要读取并略过符合给定转换说明的字符序列,不存储结果。可以在转换说明中采用 %* 来达到前述效果。对于具有星号的转换说明,不要包括对应的指针参数。

scanf()函数的返回值是成功存储数据项的数量。如果一切执行顺利,返回值就是转换说明的数量(但不计包含星号的转换说明)。如果发生读取错误或在转换数据项前就到达了输入源尾部,则 scanf()函数会返回值 EOF。如下例所示:
if ( scanf( "%s%d", name, &age ) < 2 )
  fprintf( stderr, "Bad input.n" );
else
{ /* ...测试存储的值... */ }

字段宽度

字段宽度是十进制整型正数,它指定了对于给定的转换说明,scanf()所读取字符的最大数量。对于字符串输入来说,字段宽度可以防止缓冲区出现溢出情况:
char city[32];
printf( "Your city: ");
if ( scanf( "%31s", city ) < 1 )     // 不要读入超过31个字符
  fprintf( stderr, "Error reading from standard input. n" );
else
/* ... */

printf()会输出超过指定字段宽度的字符,但 scanf()不同于 printf(),转换修饰符 s 不会读入超过指定字段宽度的字符到缓冲区。

读取字符和字符串

转换说明 %c 和 %1c 都会从输入流中读取一个字符,包括空白符。通过指定字段宽度,可以读取数量等于字段宽度的字符,包括空白符,只要没有遇到输入流的结束。当采用这种方式读取多个字符时,对应的指针参数必须指向一个空间足够大的 char 数组,以存储下所有读到的字符。

使用转换修饰符 c 的 scanf()函数,不会在读入字符序列的尾部加上字符串终止符。例如:
scanf( "%*5c" );
该 scanf()调用会读取并丢弃输入源紧接着的 5 个字符。

转换说明 %s 总是读取恰好一个词,遇到空白符时结束读取。如果想读取整行文本,可以使用函数 fgets()。

下面的示例逐词地读取文本文件的内容。假设文件指针 fp 关联了一个文本文件,并且该文件已打开,以用于读取:
char word[128];
while ( fscanf( fp, "%127s", word ) == 1 )
{
   /* ...处理读到的词... */
}

除了转换修饰符 s 以外,也可以使用“扫描集”(scanset)修饰符来读取字符串,它由方括号所包含的一串无序字符组成([scanset])。scanf()函数接着读取所有字符,然后将它们存储为一个字符串(带有字符串终止符),直到遇到不匹配扫描集中任一字符时才停止。例如:
char strNumber[32];
scanf( "%[0123456789]", strNumber );

如果用户输入 345X67,那么 scanf()会把 3450 字符串存储到数组 strNumber 中。字符 X 以及后续字符则仍然留在输入缓冲区中。

逆向使用转换扫描集(也就是说,除扫描集中的字符外,其他都符合),做法是在扫描集的左括号后面加上一个插入号(^)。下面的 scanf()调用读取所有字符(包括空白符),直到句子结束的标点符号,然后再读入标点符号本身:
char ch, sentence[512];
scanf( "%511[^.!?]%c", sentence, &ch );

下面的 scanf()调用读取并丢弃所有字符,一直到当前行结束:
scanf( "%*[^n]%*c" );

读取整数

类似 printf()函数,scanf()函数为整数提供了下面的转换修饰符:d、i、u、o、x 和 X。它们允许读入十进制、八进位与十六进制表示法,并转换为 int 或 unsigned int 变量。如下例所示:
// 读入一个非负的十进制整数
unsigned int value = 0;
if ( scanf( "%u", &value ) < 1 )
  fprintf( stderr, "Unable to read an integer.n" );
else
  /* ... */

对于 scanf()函数内的修饰符 i,读入数字的基数(进制)并非预先定义好的。基数是由读入的数字字符序列的前缀符号所决定的,这些符号的表示方式与 C 源代码中整数常量相同。

如果字符序列不是以 0 开始,那么它会被解释为十进制数字。如果以 0 开始,并且第二个字符不是 x 或 X,那么该序列会被解释为八进位数字。如果以 0x 或 0X 开始,则以十六进制数字读入。

如果想把所读取的整数赋值给一个 short、char、long 或 long long 变量(或者它们所对应的无符号类型),必须在转换修饰符之前插入一个长度修饰符:h 表示 short,hh 表示 char,l 表示 long,ll 表示 long long。在下面的示例中,FILE 指针 fp 指向一个打开用于读取的文件:
unsigned long position = 0;
if (fscanf( fp, "%lX", &position) < 1 )  // 读取一个十六进制整数
  /* ... 处理错误:无法读入数字... */

读取浮点数

当处理浮点数时,scanf()函数使用与 printf()相同的转换修饰符:f、e、E、g 和 G。而且,C99 新增了修饰符 a 和 A。所有这些修饰符以同样的方式解释读取的字符序列。可以被解释成浮点数的字符序列,与 C 语言中的有效浮点常量是一样的。scanf()也可以转换整数,并将它们存储在浮点变量中。

所有这些修饰符将数字转换成 float 类型浮点值。如果想将它们转换并存储成 double 或 long double,必须插入一个长度修饰符:double 使用 l(小写L),long double 则使用 L。如下例所示:
float x = 0.0F;
double xx = 0.0;
// 读取两个浮点数:将一个转换为float,另一个转换为double
if ( scanf( "%f %lf", &x, &xx ) < 2 )
  /* ... */

如果该 scanf()调用接收到的输入序列是 12.37n,那么会将 12.3 存储在到 float 变量 x 中,而 7.0 存储到 double 变量 xx 中。