本文篇幅很长,分为两部分,前面讲解 scanf 的基础用法,后面讲解 scanf 的高级用法。
如果你是 C 语言初学者,可以选择只学习 scanf 基础用法,先初步掌握 scanf 的使用,等后续有经历再学习 scanf 的高级用法。
scanf基础用法
程序是人机交互的媒介,有输出必然也有输入,前面我们讲解了如何将数据输出到显示器上,这里我们开始讲解如何从键盘输入数据。
在 C语言中,有多个函数可以从键盘获得用户输入:
函数 | 说明 | 演示 |
---|---|---|
scanf() | 格式化输入函数,可以从键盘读取多种类型的数据。和 printf() 类似,也支持以% 开头的格式说明符。 | int age = 0; scanf("%d", &age); |
getchar() getche() getch() | 这三个函数都用于输入单个字符,但它们之间是有区别的,后续我们会讲解。 | char c; c = getchar(); c = getche(); c = getch(); |
gets() | 获取一行数据,并作为字符串处理。 | char name[30]; gets(name); |
scanf() 是最灵活、最复杂、最常用的输入函数,但它不能完全取代其他函数,大家都要有所了解。
scanf 是 scan format 的缩写,意思是格式化扫描,也就是从键盘获得用户输入,和 printf 的功能正好相反。
我们先来看一个例子:
/**
* 系统学习C语言 https://xiecoding.cn/c/
**/
#include <stdio.h>
int main()
{
int a = 0, b = 0, c = 0, d = 0;
scanf("%d", &a); //输入整数并赋值给变量a
scanf("%d", &b); //输入整数并赋值给变量b
printf("a+b=%d\n", a+b); //计算a+b的值并输出
scanf("%d %d", &c, &d); //输入两个整数并分别赋值给c、d
printf("c*d=%d\n", c*d); //计算c*d的值并输出
return 0;
}
运行结果:
12↙
60↙
a+b=72
10 23↙
c*d=230
↙
表示按下回车键。
从键盘输入12,按下回车键,scanf() 就会读取输入数据并赋值给变量 a;本次输入结束,接着执行下一个 scanf() 函数,再从键盘输入 60,按下回车键,就会将 60 赋值给变量 b,都是同样的道理。
第 8 行代码中,scanf() 有两个以空格分隔的%d
,后面还跟着两个变量,这要求我们一次性输入两个整数,并分别赋值给 c 和 d。注意"%d %d"
之间是有空格的,所以输入数据时也要有空格。对于 scanf(),输入数据的格式要和格式字符串的格式保持一致。
其实 scanf() 和 printf() 非常相似,只是功能相反罢了:
scanf("%d %d", &a, &b); //获取用户输入的两个整数,分别赋值给变量 a 和 b
printf("%d %d", a, b); //将变量 a 和 b 的值在显示器上输出
它们都有格式格式字符串,都有变量列表。不同的是,scanf() 的变量前面有时候会带一个&
符号。&
称为取地址符,也就是获取变量在内存中的地址。
在《数据在内存中的存储形式》一节中讲到,数据是以二进制的形式保存在内存中的,字节(Byte)是最小的可操作单位。为了便于管理,我们给每个字节分配了一个编号,使用该字节时,只要知道编号就可以,就像每个学生都有学号,老师会随机抽取学号来让学生回答问题。字节的编号是有顺序的,从 0 开始,接下来是 1、2、3……
下图是 4G 内存中每个字节的编号(以十六进制表示):
这个编号,就叫做地址(Address)。int a;
会在内存中分配四个字节的空间,我们将第一个字节的地址称为变量 a 的地址,也就是 &a
的值。对于前面讲到的整数、浮点数、字符,都要使用 & 获取它们的地址,scanf() 会根据地址把读取到的数据写入内存。
我们不妨将变量的地址输出看一下:
/**
* 系统学习C语言 https://xiecoding.cn/c/
**/
#include <stdio.h>
int main()
{
int a='F';
int b=12;
int c=452;
printf("&a=%p, &b=%p, &c=%p\n", &a, &b, &c);
return 0;
}
输出结果:
&a=0x18ff48, &b=0x18ff44, &c=0x18ff40
格式说明符%p
表示以十六进制的形式(带小写的前缀)输出数据的地址。如果写作%P
,那么十六进制的前缀也将变成大写形式。
图:a、b、c 的内存地址(虚拟地址)
再来看一个 scanf 的例子:
/**
* 系统学习C语言 https://xiecoding.cn/c/
**/
#include <stdio.h>
int main()
{
int a, b, c;
scanf("%d %d", &a, &b);
printf("a+b=%d\n", a+b);
scanf("%d %d", &a, &b);
printf("a+b=%d\n", a+b);
scanf("%d, %d, %d", &a, &b, &c);
printf("a+b+c=%d\n", a+b+c);
scanf("%d is bigger than %d", &a, &b);
printf("a-b=%d\n", a-b);
return 0;
}
运行结果:
10 20↙
a+b=30
100 200↙
a+b=300
56,45,78↙
a+b+c=179
25 is bigger than 11↙
a-b=14
第一个 scanf() 的格式字符串为"%d %d"
,中间有一个空格,而我们却输入了10 20
,中间有多个空格。第二个 scanf() 的格式字符串为"%d %d"
,中间有多个空格,而我们却输入了100 200
,中间只有一个空格。这说明 scanf() 对输入数据之间的空格的处理比较宽松,并不要求空格数严格对应,多几个少几个无所谓,只要有空格就行。
第三个 scanf() 的格式字符串为"%d, %d, %d"
,中间以逗号分隔,所以输入的整数也要以逗号分隔。
第四个 scanf() 要求整数之间以is bigger than
分隔。
用户每次按下回车键,程序就会认为完成了一次输入操作,scanf() 开始读取用户输入的内容,并根据格式字符串从中提取有效数据,只要用户输入的内容和格式字符串匹配,就能够正确提取。
本质上讲,用户输入的内容都是字符串,scanf() 完成的是从字符串中提取有效数据的过程。
scanf连续输入
在本文第一段示例代码中,我们一个一个地输入变量 a、b、c、d 的值,每输入一个值就按一次回车键。现在我们改变输入方式,将四个变量的值一次性输入,如下所示:
12 60 10 23↙
a+b=72
c*d=230
可以发现,两个 scanf() 都能正确读取。合情合理的猜测是,第一个 scanf() 读取完毕后没有抛弃多余的值,而是将它们保存在了某个地方,下次接着使用。
如果我们多输入一个整数,会怎样呢?
12 60 10 23 99↙
a+b=72
c*d=230
这次我们多输入了一个 99,发现 scanf() 仍然能够正确读取,只是 99 没用罢了。
如果我们少输入一个整数,又会怎样呢?
12 60 10↙
a+b=72
23↙
c*d=230
输入三个整数后,前两个 scanf() 把前两个整数给读取了,剩下一个整数 10,而第三个 scanf() 要求输入两个整数,一个单独的 10 并不能满足要求,所以我们还得继续输入,凑够两个整数以后,第三个 scanf() 才能读取完毕。
从本质上讲,我们从键盘输入的数据并没有直接交给 scanf(),而是放入了缓冲区中,直到我们按下回车键,scanf() 才到缓冲区中读取数据。如果缓冲区中的数据符合 scanf() 的要求,那么就读取结束;如果不符合要求,那么就继续等待用户输入,或者干脆读取失败。
注意,如果缓冲区中的数据不符合 scanf() 的要求,要么继续等待用户输入,要么就干脆读取失败,上面我们演示了“继续等待用户输入”的情形,下面我们对代码稍作修改,演示“读取失败”的情形。
/**
* 系统学习C语言 https://xiecoding.cn/c/
**/
#include <stdio.h>
int main()
{
int a = 1, b = 2, c = 3, d = 4; //修改处:给变量赋予不同的初始值
scanf("%d", &a);
scanf("%d", &b);
printf("a=%d, b=%d\n", a, b);
scanf("%d %d", &c, &d);
printf("c=%d, d=%d\n", c, d);
return 0;
}
运行结果:
12 60 a10↙
a=12, b=60
c=3, d=4
前两个整数被正确读取后,剩下了 a10,而第三个 scanf() 要求输入两个十进制的整数,a10 无论如何也不符合要求,所以只能读取失败。输出结果也证明了这一点,c 和 d 的值并没有被改变。
这说明 scanf() 不会跳过不符合要求的数据,遇到不符合要求的数据会读取失败,而不是再继续等待用户输入。
总而言之,正是由于缓冲区的存在,才使得我们能够多输入一些数据,或者一次性输入所有数据,这可以认为是缓冲区的一点优势。然而,缓冲区也带来了一定的负面影响,甚至会导致很奇怪的行为,请看下面的代码:
/**
* 系统学习C语言 https://xiecoding.cn/c/
**/
#include <stdio.h>
int main()
{
int a = 1, b = 2;
scanf("a=%d", &a);
scanf("b=%d", &b);
printf("a=%d, b=%d\n", a, b);
return 0;
}
输入示例:
a=99↙
a=99, b=2
输入a=99
,按下回车键,程序竟然运行结束了,只有第一个 scanf() 成功读取了数据,第二个 scanf() 仿佛没有执行一样,根本没有给用户任何机会去输入数据。
如果我们换一种输入方式呢?
a=99b=200↙
a=99, b=200
这样 a 和 b 都能够正确读取了。注意,a=99b=200
中间是没有任何空格的。
肯定有好奇的小伙伴又问了,如果a=99b=200
两个数据之间有空格又会怎么样呢?我们不妨亲试一下:
a=99 b=200↙
a=99, b=2
你看,第二个 scanf() 又读取失败了!在前面的例子中,输入的两份数据之前都是有空格的呀,为什么这里不能带空格呢,真是匪夷所思。好吧,这个其实还是跟缓冲区有关系。
要想破解 scanf() 输入的问题,一定要学习缓冲区,它能使你对输入输出的认识上升到一个更高的层次,以后不管遇到什么疑难杂症,都能迎刃而解。可以说,输入输出的“命门”就在于缓冲区。
scanf输入其它数据
除了输入整数,scanf() 还可以输入单个字符、字符串、小数等,请看下面的演示:
/**
* 系统学习C语言 https://xiecoding.cn/c/
**/
#include <stdio.h>
int main()
{
char letter;
int year;
char url[30];
float price;
scanf("%c", &letter);
scanf("%d", &year);
scanf("%s", url); //可以加&也可以不加&
scanf("%f", &price);
printf("26个英文字母的最后一个是 %c。\n", letter);
printf("本教程从%d年开始创作,首次发布地址是 %s,预计销售价格是%g元。\n", year, url, price);
return 0;
}
运行示例:
a↙
2025↙
https://xiecoding.cn/↙
49.9↙
26个英文字母的最后一个是 a。
本教程从2025年开始创作,首次发布地址是 https://xiecoding.cn/,预计销售价格是49.9元。
scanf() 和 printf() 虽然功能相反,但是格式说明符是一样的,单个字符、整数、小数、字符串对应的格式说明符分别是 %c、%d、%f、%s。
对读取字符串的说明
C语言的字符串有两种定义形式,它们分别是:
char str1[] = "C language is great";
char *str2 = "C语言很伟大";
这两种形式其实是有区别的,第一种形式的字符串所在的内存既有读取权限又有写入权限,第二种形式的字符串所在的内存只有读取权限,没有写入权限。printf()、puts() 等字符串输出函数只要求字符串有读取权限,而 scanf()、gets() 等字符串输入函数要求字符串有写入权限,所以,第一种形式的字符串既可以用于输出函数又可以用于输入函数,而第二种形式的字符串只能用于输出函数。
另外,对于第一种形式的字符串,在[ ]
里面要指明字符串的最大长度,如果不指明,也可以根据=
后面的字符串来自动推算,此处,就是根据"C language is great"
的长度来推算的。但是在前一个例子中,开始我们只是定义了一个字符串,并没有立即给它赋值,所以没法自动推算,只能手动指明最大长度,这也就是为什么一定要写作char url[30]
,而不能写作char url[]
的原因。
最后需要注意的一点是,scanf() 读取字符串时以空格为分隔,遇到空格就认为当前字符串结束了,所以无法读取含有空格的字符串,请看下面的例子:
/**
* 系统学习C语言 https://xiecoding.cn/c/
**/
#include <stdio.h>
int main()
{
char author[30], lang[30], url[30];
scanf("%s %s", author, lang);
printf("author:%s \nlang: %s\n", author, lang);
scanf("%s", url);
printf("url: %s\n", url);
return 0;
}
运行结果为:
xiecoding xiecoding.cn↙
author:xiecoding
lang: xiecoding.cn
https://xiecoding.cn https://baidu.com/↙
url: https://xiecoding.cn
对于第一个 scanf(),它将空格前边的字符串赋值给 author,将空格后边的字符串赋值给 lang;很显然,第一个字符串遇到空格就结束了,第二个字符串到了本行的末尾结束了。
或许第二个 scanf() 更能说明问题,我们输入了两个网址,但是 scanf() 只读取了一个,就是因为这两个网址以空格为分隔,scanf() 遇到空格就认为字符串结束了,不再继续读取了。
文章最后,我们汇总一下本节使用到的格式说明符:
格式说明符 | 说明 |
---|---|
%c | 读取单个字符 |
%d | 读取一个整数 |
%f | 读取一个小数 |
%s | 读取一个字符串(遇到空白符结束) |
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 | 匹配的字符 | 参数类型 |
---|---|---|
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,并且不区分大小写。 注意:这里不强调字符的顺序,只要出现在 - 来表示一个范围内的字符,例如 %[a-z]、%[0-9] 等。连字符左边的字符对应一个 ASCII 码,连字符右边的字符也对应一个 ASCII 码,位于这两个 ASCII 码范围以内的字符就是要读取的字符。 注意:连字符左边的 ASCII 码要小于右边的,如果反过来,那么它的行为是未定义的。 常用的连字符举例:
你也可以将它们合并起来,例如:
| char * |
[^characters] | 不允许读取的字符合集。出现在[ ] 中的字符不会被读取。 | char * |
n | 不读取任何字符,只计算截止到目前读取的字符的个数,并将它存储到对应参数指定的位置。 | int * |
% | % 后面再跟一个 %,表示读取一个 %,类似于 % 的转义形式。 | char * |
*(星号)
* 表示将读取到的字符丢弃,或者忽略,也即不进行存储。因为没有任何字符需要存储,所以它没有对应的参数。
width
width 表示允许读取的最大字符个数。超过 width 的字符即使符合要求,也不会被读取。
length
length 是 specifier 的子说明符,用来修改对应参数的数据类型,它只能是 hh、h、l、ll、j、z、t、L 其中之一。
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 * |
scanf() 用法举例
为了方便读者理解,这里给出几个有代表性的例子。
简单的综合示例
/**
* 系统学习C语言 https://xiecoding.cn/c/
**/
#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 指定读取长度
/**
* 系统学习C语言 https://xiecoding.cn/c/
**/
#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.123
https://xiecoding.cn
n=20, f=100.1, str=https://xiecoding.cn
为了避免受到缓冲区中遗留数据的影响,每次读取结束我们都使用 scanf("%*[^\n]"); scanf("%*c");
来清空缓冲区。
限制读取数据的长度在实际开发中非常有用,最典型的一个例子就是读取字符串:我们为字符串分配的内存是有限的,用户输入的字符串过长就存放不了了,就会冲刷掉其它的数据,从而导致程序出错甚至崩溃;如果被黑客发现了这个漏洞,就可以构造栈溢出攻击,改变程序的执行流程,甚至执行自己的恶意代码,这对服务器来说简直是灭顶之灾。
匹配特定的字符
%s 说明符会匹配除空白符以外的所有字符,它有两个缺点:
- %s 不能读取指定字符,比如只想读取小写字母,或者十进制数字等,%s 就无能为力;
- %s 读取到的字符串中不能包含空白符,有些情况会比较尴尬,例如,无法将多个单词存放到一个字符串中,因为单词之间就是以空格为分隔的,%s 遇到空格就读取结束了。
使用 %[xxx] 就可以解决以上问题,请看下面的例子:
/**
* 系统学习C语言 https://xiecoding.cn/c/
**/
#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:
/**
* 系统学习C语言 https://xiecoding.cn/c/
**/
#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