C语言scanf的用法(非常详细和全面)

 
C语言 scanf() 函数的用法是比较复杂和灵活的,我们在《C语言scanf()从键盘读取数据》一节仅仅演示了它的基本用法,这一节我们将会介绍它的完整用法以及高级用法。

scanf() 的用法

scanf 是 scan format 的缩写,意思是格式化扫描,也就是从键盘获得用户输入,和 printf 的功能正好相反。

严格来说,scanf() 是从标准输入文件中读取数据,而这个标准输入文件通常都是“黑底白字”的控制台,或者说是键盘。

scanf() 的标准用法(原型)为:

int scanf ( const char * format, argument... );

format 为格式字符串,由格式说明符和普通字符构成。其中:
  • 格式说明符以%开头,比如 %d、%s、%c 等,表示要读取什么样的数据;
  • 普通字符按照原样输入,比如英文、数字、逗号、空格等。

argument 为参数列表,或者变量列表,多个参数以,分隔。每个参数都是一个指针,用来指明将数据存储在哪里。参数的个数和类型,要与格式说明符一一对应。

注意:argument 指向的位置必须已被分配内存,并且允许写入。

C语言 scanf() 会根据 format 中的格式说明符来读取数据,并将读取到的数据放到 argument 指定的位置。

int 表示 scanf() 的返回值类型,也即处理结果的数据类型:
  • 如果读取成功,scanf() 将返回成功匹配并赋值的个数;
  • 如果读取失败,或者达到文件末尾,或者遇到输入结束的条件,则返回 EOF。

EOF 是在 stdio.h 中定义的,它的值在不同的平台或者不同的编译器中可能不同,但通常都是 -1。

format 中的格式说明符

format 中的格式说明符比较复杂,它的标准写法如下:

%[*][width][length]specifier

末尾的 specifier 不能省略,其它由[ ]包围的部分可以省略。

specifier

specifier 是格式字符,它最重要,指明了要读取的数据的类型和形式。

specifier 的用法及说明
specifier 匹配的字符 参数类型
i 整数,前面可以带正号+和负号-

默认为十进制,带上前缀0表示八进制,带上前缀0x表示十六进制。
int *
d
u
十进制整数,d 表示有符号整数,u 表示无符号整数。 int *
unsigned int *
o 八进制整数(无符号)。 unsigned int *
x 十六进制整数(无符号),可以带有0x或者0X前缀。 unsigned int *
f, e, g 浮点数/小数,前面可以带正号+和负号-,接受普通形式(比如 3.1415)以及科学计数法(比如 5.23e4)。 float *
a
c 单个字符。如果指定的宽度 width 不是 1,那么 scanf() 会读取 width 个字符,并将它们连续存储到参数所指定的位置(末尾不追加任何字符)。 char *
s 字符串,不包含空白符(空格、换行、制表符等)。读取连续的字符,直到遇见第一个空白符就结束读取。读取结束后,scanf() 会自动在末尾追加空字符\0,用以表示字符串的结束。 char *
p 指针/地址。在不同的平台和不同的编译器中,指针的格式可能有所区别,但它始终和在 printf() 中使用%p输出的格式相同。 void **
[characters] 允许读取的字符集合。只有出现在[ ]中的字符会被读取,遇到第一个不符合的字符就结束读取,比如[abcABC]表示读取字母 abc,并且不区分大小写。

注意:这里不强调字符的顺序,只要出现在[ ]中的字符,不管先后,都能匹配成功。

为了简化字符集合的写法,scanf() 支持使用连字符-来表示一个范围内的字符,例如 %[a-z]、%[0-9] 等。

连字符左边的字符对应一个 ASCII 码,连字符右边的字符也对应一个 ASCII 码,位于这两个 ASCII 码范围以内的字符就是要读取的字符。

注意:连字符左边的 ASCII 码要小于右边的,如果反过来,那么它的行为是未定义的。

常用的连字符举例:
  • %[a-z]表示读取 abc...xyz 范围内的字符,也即小写字母;
  • %[A-Z]表示读取 ABC...XYZ 范围内的字符,也即大写字母;
  • %[0-9]表示读取 012...789 范围内的字符,也即十进制数字。

你也可以将它们合并起来,例如:
  • %[a-zA-Z]表示读取大写字母和小写字母,也即所有英文字母;
  • %[a-z-A-Z0-9]表示读取所有的英文字母和十进制数字;
  • %[0-9a-f]表示读取十六进制数字。
char *
[^characters] 不允许读取的字符合集。出现在[ ]中的字符不会被读取。 char *
n 不读取任何字符,只计算截止到目前读取的字符的个数,并将它存储到对应参数指定的位置。 int *
% % 后面再跟一个 %,表示读取一个 %,类似于 % 的转义形式。 char *

*(星号)

* 表示将读取到的字符丢弃,或者忽略,也即不进行存储。因为没有任何字符需要存储,所以它没有对应的参数。

width

width 表示允许读取的最大字符个数。超过 width 的字符即使符合要求,也不会被读取。

length

length 是 specifier 的子说明符,用来修改对应参数的数据类型,它只能是 hh、h、l、ll、j、z、t、L 其中之一。

length 的用法及说明
  specifier
length d i u o x f e g a c s [] [^] p n
默认(不指明length) int * unsigned int * float * char * void ** int *
hh signed char * unsigned char *       signed char *
h short int * unsigned short int *       short int *
l long int * unsigned long int * double * wchar_t *   long int *
ll long long int * unsigned long long int *       long long int *
j intmax_t * uintmax_t *       intmax_t *
z size_t * size_t *       size_t *
t ptrdiff_t * ptrdiff_t *       ptrdiff_t *
L     long double *      

上面淡黄色背景的行,为 C99 标准引入的说明符或者子说明符。

scanf() 用法举例

为了方便读者理解,这里给出几个有代表性的例子。

简单的综合示例

#include <stdio.h>
int main()
{
    char str[31];
    int i;

    printf("Enter your name: ");
    scanf("%30s", str);
    printf("Enter your age: ");
    scanf("%d", &i);
    printf("Hello %s, you are %d years old.\n", str, i);
    printf("Enter a hexadecimal number: ");
    scanf("%x", &i);
    printf("You have entered %#x(%d).\n", i, i);

    return 0;
}
输入示例:

Enter your name: Tom↙
Enter your age: 18↙
Hello Tom, you are 18 years old.
Enter a hexadecimal number: 5e↙
You have entered 0x5e(94).

使用 width 指定读取长度

#include <stdio.h>

int main(){
    int n;
    float f;
    char str[23];

    scanf("%2d", &n);
    scanf("%*[^\n]"); scanf("%*c");  //清空缓冲区
    scanf("%5f", &f);
    scanf("%*[^\n]"); scanf("%*c");  //清空缓冲区
    scanf("%22s", str);
    printf("n=%d, f=%g, str=%s\n", n, f, str);

    return 0;
}
输入示例 ①:

20↙
100.5↙
http://c.biancheng.net↙
n=20, f=100.5, str=http://c.biancheng.net

输入示例 ②:

8920↙
10.2579↙
http://data.biancheng.net↙
n=89, f=10.25, str=http://data.biancheng.


为了避免受到缓冲区中遗留数据的影响,每次读取结束我们都使用scanf("%*[^\n]"); scanf("%*c");来清空缓冲区。关于 scanf() 和缓冲区的话题,我们将在后面几节中详细讲解:
限制读取数据的长度在实际开发中非常有用,最典型的一个例子就是读取字符串:我们为字符串分配的内存是有限的,用户输入的字符串过长就存放不了了,就会冲刷掉其它的数据,从而导致程序出错甚至崩溃;如果被黑客发现了这个漏洞,就可以构造栈溢出攻击,改变程序的执行流程,甚至执行自己的恶意代码,这对服务器来说简直是灭顶之灾。

匹配特定的字符

%s 说明符会匹配除空白符以外的所有字符,它有两个缺点:
  • %s 不能读取指定字符,比如只想读取小写字母,或者十进制数字等,%s 就无能为力;
  • %s 读取到的字符串中不能包含空白符,有些情况会比较尴尬,例如,无法将多个单词存放到一个字符串中,因为单词之间就是以空格为分隔的,%s 遇到空格就读取结束了。

使用 %[xxx] 就可以解决以上问题,请看下面的例子:
#include <stdio.h>
int main() {
    char str1[30];
    char str2[30];

    scanf("%[abcd]", str1);  //只读取abcd字母
    scanf("%*[^\n]"); scanf("%*c");  //清空缓冲区
    scanf("%[a-zA-Z]", str2);  //只读取小写和大写的英文字母
    printf("str1: %s\nstr2: %s", str1, str2);

    return 0;
}
输入示例:

baccdaxyz↙
abcXYZ123↙
str1: baccda
str2: abcXYZ


再比如,读取一行不能包含十进制数字的字符串,并且长度不能超过 30:
#include <stdio.h>
int main() {
    char str[31];
    scanf("%30[^0-9\n]", str);
    printf("str: %s", str);

    return 0;
}
输入示例:

I have been programming for 8 years now↙
str: I have been programming for