scanf在c语言中的作用(超级详细,完全最新)

本文篇幅很长,分为两部分,前面讲解 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 的用法及说明
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
lengthd iu o xf e g ac s [] [^]pn
默认(不指明length)int *unsigned int *float *char *void **int *
hhsigned char *unsigned char *signed char *
hshort int *unsigned short int *short int *
llong int *unsigned long int *double *wchar_t *long int *
lllong long int *unsigned long long int *long long int *
jintmax_t *uintmax_t *intmax_t *
zsize_t *size_t *size_t *
tptrdiff_t *ptrdiff_t *ptrdiff_t *
Llong 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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值