至此我们学了很多很多东西了,最早各种程序设计语言的出现都是为了计算服务的,像是过去使用的DOS、UNIX,他们都没有图形界面,一来是机能限制,二来是用户的交互在没有那么重要。
当然,后来随着家庭计算机的普及,也有越来越多软件开始考虑与用户的交互,这一篇我们就要介绍一下在没有图形界面的情况下如何利用IO设计更好的用户交互。
I/O是Input/Output(输入/输出)的缩写,标准I/O是从用户键盘输入,输出到用户的屏幕上。没错,stdio.h这个头文件就是standard input & output的缩写,在stdio.h中有很多标准IO函数,在其中还定义了stdin(标准输入), stdout(标准输出)和stderr(标准错误),我们之后会和stdin和stdout打交道,stderr可能会简单提一下。
我们之前好像都是用的这个办法输入和输出字符:
#include
int main()
{
char a = 0;
scanf("%c", &a);
printf("%c", a);
return 0;
}
即用scanf()和printf()这一对函数来完成,其实还有更简单、更单纯的用于单字符输入输出的函数:getchar()和putchar()
#include
int main()
{
char a = getchar();
putchar(a);
return 0;
}
相当简单的两个函数哈,我们来看看他们的原型:
getchar()函数无参数,返回一个从stdin中读取的字符,putchar则会将传入的字符输出到stdout中,若输出失败则返回EOF,否则返回输出字符转换为unsigned int的值。当然,一般不怎么用putchar()的返回值。
你可能会想,我都有scanf()和printf()了,这俩函数有啥用呢?举个简单的例子,你在Dev-C++中运行一个程序到最后会有这样一个提示:
在你按下任意键之后,这个黑框框才会退出,这对于想要看运行结果的你来说是非常方便的,我们都知道,C语言程序完成编译之后会产生一个可执行程序,在Windows下一般是一个.exe文件,假设你写了下面这一段代码:
#include
int main()
{
for (int i = 0; i < 10; i++) {
printf("%d ", i);
}
printf("\n");
return 0;
}
你可以试试编译一下这段源代码,然后找到同目录下的.exe文件执行一下,然后就会发现:这个黑框框一闪而过,假设这个时候你回到IDE去运行,结果还是正常的,这就说明其实程序本身是没有问题的,那为什么会这样呢?
问题就出在结束的时候,因为没有任何语句阻断main函数执行完毕,一旦main函数return 0了,这个程序的运行就结束了,窗口就会自动退出,所以我们需要一些不影响程序正常运行,但是又可以帮助我们暂停程序执行的语句,这时候getchar()就起作用了,我们试试看:
#include
int main()
{
for (int i = 0; i < 10; i++) {
printf("%d ", i);
}
printf("\n");
getchar();
return 0;
}
很好,程序没有直接退出,这时候如果我们按一下回车,就会退出了。getchar()在这里一直等待从stdin中获取一个字符,一旦获取到了,它的任务也就完成了,程序也就结束了。这是我们构建一个对用户友好的界面的第一步,首先至少要让用户在结束前能看到运行结果是吧?
你现在知道这个了,不过好像还有点小问题,假设你的程序在程序最后的getchar()之前有用户输入scanf()或getchar(),程序好像还是会一闪而过:
#include
int main()
{
char a = 0;
a = getchar();
putchar(a);
getchar();
return 0;
}
这又是为什么呢?我先不说,你先在getchar()的后面再加一个getchar():
#include
int main()
{
char a = 0;
a = getchar();
putchar(a);
getchar();
getchar();
return 0;
}
成功了,这是为什么呢?还记得我们说getchar()是从stdin中取字符吗?stdin和stdout是一种流(stream),你可以把他们看成两条河,stdin(标准输入流)这条河上游是用户的键盘输入,下游是各种从stdin中取东西的函数,当getchar()发现这条河里啥玩意儿没有的时候,它就会要求你在上游给它输入点什么东西进来。
比如你输入了字符a,显然你只打一个a是没用的,这个a会一直停在那个地方,只有你按一下回车,程序才会继续下去,你在键盘上做的一切操作都不是白做的,回车是’\n’,因此这样一下你就往stdin这条河里丢了两个字符,一个是’a’,一个是’\n’,假设下游只有一个getchar(),那么按顺序来,它会把’a’取走,所以这段代码中我们输入a之后,变量a的值就是字符a了,这没问题,不过这条河里还有一个’\n’,之前代码末尾只有一个getchar()的时候,它会把河里的’\n’给取过来,所以最后的getchar()也直接完成了工作了,程序就退出了,现在你明白了吗?为了解决这个问题,我们多用了一个getchar(),把stdin里的’\n’也给捞走了,最后河里什么都不剩了,你就得输入点什么才能让程序退出了。
我们使用getchar()防止程序退出的先决条件是:stdin(标准输入流)中没有字符,必须要用户完成一次输入操作。
所以我们会发现,无论是输入还是输出,我们都不是直接和程序完成数据交互的,输入通过stdin完成,输出通过stdout完成。
printf()可以说是我们最熟悉的函数之一了。不过其实我们之前只是很简单地应用了printf()完成了输出,其实printf()还有很多其他的使用方法。
我们先来复习一下各种格式化占位符:
关键字 | 中文名 | 基本数据类型 | 格式占位符 |
---|---|---|---|
short | 短整型 | 整型 | %d |
int | 整型 | 整型 | %d |
long | 长整型 | 整型 | %ld |
long long | 超长整型 | 整型 | %lld |
unsigned int | 无符号整型 | 整型 | %u |
unsigned long | 无符号长整型 | 整型 | %lu |
unsigned long long | 无符号超长整型 | 整型 | %llu |
float | 单精度浮点型 | 浮点型 | %f |
double | 双精度浮点型 | 浮点型 | %lf |
char | 字符型 | 字符型 | %c |
这些格式化占位符主要是帮助我们把一些基本数据类型变为字符串以便输出,每种数据都有自己的特性,例如整数可以有八进制、十六进制表示,浮点数打印有位数差距、可以选择是否使用科学计数法等等,这样的需求C语言当然是可以实现的,我们来介绍一下:
格式占位符的一般格式为:
标志符 | 含义 | 长度指示符 | 含义 |
---|---|---|---|
- | 左对齐 | hh | 单字节 |
+ | 在前方补+/-号 | h | 以short类型输出 |
(空格) | 正数留空(正数前保留一个符号的位置) | l | 以long类型输出 |
最小宽度/.精度 | 含义 | ll | 以long long类型输出 |
number(最小宽度) | 最小字符数(字符不足时补空格) | L | 以long double类型输出 |
.number(精度) | 小数的位数 |
type | 用于 | type | 用于 |
---|---|---|---|
i/d | int | a/A | 十六进制浮点数 |
u | unsigned | e/E | 科学计数法 |
o | 八进制 | f/F | float |
x | 十六进制 | X | 十六进制大写字母 |
上面的表格可以自己写一写试试看,这是一些比较常用的输出格式占位符,还有一个比较特殊的标志符是 #,在o/O,x/X之前使用#表示在输出的八进制/十六进制数字前加上0或0x标记:
#include
int main()
{
printf("%o\n", 330);
printf("%#o\n", 330);
printf("%x\n", 330);
printf("%X\n", 330);
printf("%#x\n", 330);
printf("%#X\n", 330);
return 0;
}
当#后面跟着0和最小宽度数字时,可以表示用0填充直到达到最小宽度:
#include
int main()
{
printf("%#07d\n", 330);
return 0;
}
这么一来,我们就可以设计一个相对整齐的界面了:
#include
#include
#include
int main()
{
time_t timep;
struct tm *p;
time (&timep);
p = gmtime(&timep);
double discount = 7.5, origin = 199, after = origin * discount/10;
printf("Thanks for purchase!!\n");
printf("Here is your script!\n\n");
printf("+-----------------\n");
printf("+ Time:\t%d/%d/%d\n", 1900+p->tm_year, 1+p->tm_mon, p->tm_mday);
printf("+ Origin:%+9.2lf\n", origin);
printf("+ Discount:%7.1lf\n", discount);
printf("+ Cost:%+11.2lf\n", after);
printf("+-----------------\n");
printf("Looking forward to your next visit.\n");
return 0;
}
以上是一张购物收据的输出样例,可能不太好看哈,不过还是比较整齐的,你可以依靠我们刚刚说的格式化输出来构造一个更好看的输出格式。
我相信假设你有做一些C语言的练习总会遇到这样的输出要求:
输出的数字间以空格分割,最后一个数字后没有空格
一般来说大家都会选择在第一个或者最后一个单独打印作为一个特例进行特别处理,不过依托于printf中这个""字符串字面量的特殊性,我们可以写出这样的代码来:
#include
int main()
{
int a[10] = {4123,22,1,131,332,12};
for (int i = 0; i < 10; i++) {
printf(" %d"+!i, a[i]);
}
printf("\n");
return 0;
}
WTF,这个+!i是在干什么?这是一个很好玩的技巧:首先我们知道字符串对应的是字符数组,对数组名进行加减操作其实就是对指针进行加减操作。
i从0到9,!i仅在i=0时会等于1,这时候假设" 4123"的地址是0x00123456,那么一次+1的操作就会让地址跳转为0x00123457,对于字符串的读取我们只需要到\0截止即可,初始位置并不重要,+1之后正好可以忽略掉一个字符,也就是第一个元素打印的时候前面的空格,然后之后的元素因为都是非0的数字,取逻辑非之后值都为0,所以空格就被保留下来了。
以上这个技巧对于分隔符是一个字符的情况都是有效的,如果是多个字符你可能就要开动一下自己的脑子想想有没有类似的办法了。
putchar()是把一个字符放进stdout里面,这我们已经知道了,那printf()呢?它当然也是了,不过因为printf()支持字符串的输出,所以printf()是一次性将一大串内容在合适的时机一起加入stdout,之后输出到用户的屏幕上。
刚刚我们介绍了printf()的不同用法,那scanf()有吗?当然有啊,不过我们先来看看scanf()函数的原型:
int scanf(const char* restrict format,...);
你发现,scanf()这个函数居然还有返回值,的确是这样的,有的时候如果你在OJ上做题,使用了scanf函数的话可能还会收到这样一条警告信息:
warning: ignoring return value of ‘scanf’, declared with attribute warn_unused_result [-Wunused-result]
scanf("%d",&D);
^~~~~~~~~~~~~~
这个警告的意思是你忽略了scanf()函数的返回值,当然,一般来说我们是用不到scanf()函数的返回值的,scanf()函数的返回值与前面的格式化字符串有关,scanf()函数读入了多少个变量,scanf()就会返回多少,而当读取到了"文件末尾"的时候,scanf()会返回EOF(End Of File,一个stdio.h定义的宏,值为-1).
关于scanf()和EOF,我们一般在不指定输入规模的时候更容易用到,比如这个例子:
先输入一个数字a,再求之后输入的一串数字中,这个数字a出现的次数
很简单的题,不过你看这就觉得奇怪,这道题,没有说明会有多少个数字啊!这我就不能用for循环了,如果用while循环的话,什么时候才能停止呢?这时候就要用到EOF了,一般这种题在数据输入结束的时候会再输入一个EOF作为停止标记,那我们只需要这么做就好了:
#include
int main()
{
int cnt = 0;
int a = 0, input = 0;
scanf("%d", &a);
while (scanf("%d", &input) != EOF) {
if (input == a) cnt++;
}
printf("%d\n", cnt);
return 0;
}
很好的程序,你运行之后发现怎么样都结束不了,当然不是程序有问题,当你的数字输入结束之后,你需要输入一个EOF,在Windows下,同时按Ctrl+Z即可,在Linux环境下则需要按下Ctrl+D,然后再敲回车,你就发现程序成功终止了。
那么接下来我们就如同printf()一样,介绍一下scanf()函数的格式化字符串吧。它的基本形式是:
标志符 | 含义 | 类型 | 用于 |
---|---|---|---|
* | 跳过字符 | d | int |
数字 | 最大字符数 | i | 整数(可能为十六/八进制) |
hh | char | u | unsigned int |
h | short | o/x | 八进制/十六进制 |
l | long, double | a,e,f,g | float |
ll | long long | c | char |
L | long double | s | 无空白字符的字符串 |
[…] | 允许的一系列字符 | ||
p | 指针 |
其余的你可以自己尝试,我想提一提[…],这个[…]事实上是一种正则表达式,当然更深入的我就不提了,我说说简单的,这个中括号可以表示一个集合,当我们往里输入什么东西的时候,scanf会匹配满足这个集合的所有内容,直到匹配到不符合的为止,这很抽象,我们举个例子:
#include
int main()
{
int cnt = 0;
char str[1000];
scanf("%[a-zA-Z]", str);
printf("%s\n", str);
return 0;
}
不错,按照我们想的来了,输入了一大串字符,中间有两个数字,scanf一读到3就停止读入了,还有一个符号^,代表否定,假设我们写[^a-zA-Z],那就是读到a-z和A-Z之间的字符的时候就停止读入,这在一定条件下可以说是相当好用的。
有的时候我们的程序可能要定期读入一定量的规律字符串,一系列的数据通过逗号分隔开,我们要把每个数据都从中抽取出来,全部读取再for循环遍历拆解是可以的,但是很麻烦,不如我们在输入的时候就直接赋值到各个变量,例如:
//$GPRMC,004319.00,A,3016.98468,N,12006.39211,E,0.047,,130909,,,D*79
scanf("%*[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,],%[^,]",
sTime,sAV,sLati,&sNW,sLong,&sEW,sSpeed,sAngle,sDate);
上述输入的是一串GPS信息1,我们可以把这些信息精准的赋值给特定的变量,这可就方便多了。
whitespace(空白字符)包含" “(空格),”\t"(制表符),“\n”(换行符),当scanf()接受到空白字符的时候,对于一个变量的接收就终止了,而我们现在所有的输入输出函数只要与屏幕和键盘交互,那就一定要和stdin或stdout打交道,所以scanf()这个函数也一样是从stdin中获取字符,这时候就有一个问题:假设输入的是一串字符,而且字符之间以空格分割:
// c d a e f
#include
int main()
{
char a[5];
for (int i = 0; i < 5; i++) {
scanf("%c", &a[i]);
}
for (int i = 0; i < 5; i++) {
printf("%c ", a[i]);
}
return 0;
}
这个输出结果跟我们的输入还真是完全不一致呢!产生这个结果的原因就是:scanf()接受字符的情况下,接收到了一个空白字符即停止,但下一次接收的时候stdin中还存在空白字符,scanf()就会把这个字符再收入进来,这么下去最终就会有两个字符没有被传入。而在收集数字类型的时候就不会有这个问题——收集数字类型时如果stdin中存在空白字符,stdin会被刷新,空白字符会消失。
因为我们如果要输入一连串字符,就一定要注意:要么把字符连在一起输入,要么每次输入字符后用一个getchar()捞走空白字符:
#include
int main()
{
char a[5];
for (int i = 0; i < 5; i++) {
scanf("%c", &a[i]);
getchar();
}
for (int i = 0; i < 5; i++) {
printf("%c ", a[i]);
}
printf("\n");
return 0;
}
这就是我们希望的样子了。还有一个关于字符串的事情:我们都知道字符串可以直接用%s接收,那假设说我们要输入这样一个句子:This is a sentence.会怎么样呢?
#include
int main()
{
char str[100];
scanf("%s", str);
printf("%s\n", str);
return 0;
}
str只成功收入了一个This,这又是为什么呢?就如我们之前所说的一样,scanf()函数在接受到了空白字符之后就会停止接受,所以这种带有空白字符的句子,用scanf()是没有办法接受的,那有没有什么办法可以解决呢?有的!
gets函数可以解决scanf()在输入字符串的过程中可能遇到空格的问题,例如:
#include
int main()
{
char sentence[100];
gets(sentence);
printf("%s\n", sentence);
return 0;
}
非常好,就算有空格也还是读取成功了,gets()函数的截止条件是接收到一个换行符’\n’,这样我们就可以把一整行的内容全部接收进来了,不过有一个问题:gets()函数是有危险的,比如上面这个程序,我们输入一个很长的句子试试看:
//Haha I'm trying to input a long sentence to test the gets function. My mother always say, life was like a box of chocolate, you're nerver know what you're going to get. I believe this sentence is long enough to raise some exceptions.
我们把一个字符数非常多的句子输入了进来,虽然后面它还是成功打印了出来,不过到最后return了3221225477,那肯定是出异常了。
我相信你也可以看得出来:sentence这个字符串最大只能接受99个字符,但是我们输入的句子远远长于99个字符,gets()函数的问题就在这里:gets()函数完全不检查输入内容和目标字符数组的长度,这样就可能引发越界访问等等问题,所以我们一般有fgets()与gets_s()两个备选的函数用于取代gets()。
fgets()函数的原型如下:
char* fgets(char* str, int n, FILE* stream);
三个参数:str是要存入的字符串,n是最大接收的字符数(一般设为str数组的长度),stream是一个文件指针,文件指针我们之后会在文件篇细讲,在此我们先引入一点基本的内容:
当然,其实现在可以不用了解那么多,你只需要知道:C语言把从键盘读取(stdin)的流和向屏幕输出内容(stdout)或错误(stderr)的流都当做文件来处理,一系列对文件使用的函数一样可以对标准输入输出流使用。
那么我们回到fgets()函数,第三个参数FILE* stream是一个文件指针,既然是一个输入的函数,那我们应该传入stdin以让fgets()从stdin读取字符,不过毕竟是文件指针,之后我们在学习文件的时候,fgets()函数也可以对于文件使用。说了这么多,我们来写个代码试着用一用看:
#include
int main()
{
char a[100];
fgets(a, 100, stdin);
printf("%s", a);
return 0;
}
你看,结果非常正常,在合适的位置截断了输入,最终程序也return了0。不过这个函数挺麻烦的,还得传一个stdin进来。另一个叫做gets_s()的函数就不需要传入这个stdin,因为它是gets_safety,是gets()函数的安全版本(MSVC可喜欢_s了),它的原型如下:
char* gets_s(char* buffer, size_t sizeInCharacters);
这个函数的确是少了个stdin,但它是C11标准引入的可选支持内容,我的gcc甚至过不了编译,所以我换Visual Studio完成下面代码的运行:
#include
int main()
{
char str[100];
gets_s(str, 100);
// sample1: This is a sentence.
// sample2: Haha I'm trying to input a long sentence to test the gets function. My mother always say, life was like a box of chocolate, you're nerver know what you're going to get. I believe this sentence is long enough to raise some exceptions.
printf("%s\n", str);
return 0;
}
这回直接给我弹Debug断言错误,事实上gets_s这个函数有一个不太好的特性:如果读取到最大数量都没有读到换行符,首先会把目标数组的首字符设置为’\0’,读取并丢弃随后的输入,然后返回空指针,之后还有可能会中止或结束程序。 这可不太好,读入内容超过了最大大小,直接把程序给我中止掉了,而且很多编译器可能都不支持,所以一般是不用这个函数的。
所以我们一般在输入字符串这件事情上有两个选择:gets()与fgets(),一般我们这么选择:gets()函数用一些确定输入规模的情况,例如你在oj上做题,oj确认输入的数据都是成一定规模而且最大长度受限的,你可以开一个比较大的数组,然后直接存入。 fgets()就用于一些与用户交互的程序上,因为不能保证用户输入是可信的,这个时候就要以安全为主。
puts()函数是与gets()函数对应的输出函数,puts()的用法很简单,往里传入一个字符串str就行了:
#include
int main()
{
char* str = "Haha I'm trying to input a long sentence to test the gets function. My mother always say, life was like a box of chocolate, you're nerver know what you're going to get. I believe this sentence is long enough to raise some exceptions.";
puts(str);
return 0;
}
轻轻松松,是吧?puts()函数有一个比较好的特性,你可能也发现了,我们这个字符串的末尾是没有换行符的,但是用puts()函数在最后会补一个换行符。
刚说了fgets()是对文件操作的,那么对应的还有一个对文件操作的fputs()函数,与fgets()一样,我们这次要向其中传入一个stdout作为目标文件,这个函数不会在输出末尾补充换行符:
#include
int main()
{
char* str = "Haha I'm trying to input a long sentence to test the gets function. My mother always say, life was like a box of chocolate, you're nerver know what you're going to get. I believe this sentence is long enough to raise some exceptions.";
fputs(str, stdout);
return 0;
}
就像这样,puts()与fputs()还是比较简单的。
fprintf()和fscanf()两个函数和printf()、scanf()长得很像,相差只有一个f,事实上这两个函数也是对文件进行操作的函数,不过因为我们的stdin和stdout也是"文件",所以我们也可以用这两个函数完成输出与输入的操作,他们的原型如下:
int fprintf(FILE* stream, const char* format [,argument...]);
int fscanf(FILE* stream, char* format [,argument...]);
后面的格式字符串与printf()和scanf()一样,只要传入stdin或stdout即可,在此我就不细讲了,在文件篇中我们再来讲一讲。
在python中,我们可以通过格式化的方式构造一个字符串出来,比如:
a = 123
b = 456
c = 123.4556789
s = f"a = {a}, b = {b}, c = {c:.7f}"
print(s)
在C中我们也可以利用sprintf()函数完成对字符串的构造,比如下面这样:
#include
int main()
{
char str[1000];
int a = 123, b = 456;
double x = 123.4556612;
sprintf(str, "a = %d, b = %d, x = %.7lf\n", a, b, x);
printf("%s", str);
return 0;
}
很生动形象,把一串字符串输入进一个字符数组中形成一个新字符串。
函数原型 | 作用 | 函数原型 | 作用 |
---|---|---|---|
int isalnum(int c) | 判断传入字符是否为字母和数字 | int isalpha(int c) | 判断传入字符是否为字母 |
int iscntrl(int c) | 判断传入字符是否为控制字符 | int isdigit(int c) | 判断传入字符是否为十进制数字 |
int isgraph(int c) | 判断传入字符是否有图形表示 | int islower(int c) | 判断传入字符是否为小写字母 |
int isprint(int c) | 判断传入字符是否为可打印字符 | int ispunct(int c) | 判断传入字符是否为标点符号 |
int isspace(int c) | 判断传入字符是否为空白字符 | int isupper(int c) | 判断传入字符是否为大写字母 |
int isxdigit(int c) | 判断传入字符是否为十六进制数字 | - | - |
int tolower(int c) | 把大写字母转换为小写字母 | int toupper(int c) | 把小写字母转换为大写字母 |
有这样的需求:一个漂亮的选单界面,用户输入了选项后要检测输入的内容是否正确,还是挺简单的吧?
#include
#include
int main()
{
int choice = 0;
char target[1024];
printf("Here are all choices: \n");
printf("+----------------------\n");
for (int i = 1; i < 7; i++) {
printf("| choice %d : Content %d\n", i, i);
}
printf("+----------------------\n");
printf("Please input your choice: ");
int check = scanf("%d", &choice);
if (check != EOF && check == 1) {
if (choice < 7 && choice >= 1) {
printf("...\n");
}
else {
printf("%d is a wrong choice!\n", choice);
}
}
else {
printf("Input Error! Try again later!\n");
}
return 0;
}
依旧是很简单的界面,在你学完了这一节的内容之后,你可以自己去尝试构建一个更好看的界面。
这一章是相对比较短的一章,因为很多函数和文件联系紧密,我会在文件篇具体介绍,不过这次介绍的printf和scanf的各种输入格式需要重点掌握一下,它对于你之后更加熟练地完成格式化输入是非常有利的。
下一章我们会介绍可以构造自己的数据类型的结构体与联合。
ECNU Online Judge 1168. GPS数据处理 ↩︎