控制台和文件I/O

这一章涵盖了控制台(键盘/显示器)和文件输入输出(I/O)。你已经见过一个控制台I/O函数——printf(),还有其他一些类似的函数。C语言有两种独立的文件I/O方式,一种基于类似控制台I/O的库函数,另一种使用“系统调用”。这些话题将在下文详细讨论。


控制台I/O

控制台I/O通常指的是与计算机的键盘和显示器进行通信。然而,在大多数现代操作系统中,键盘和显示器只是默认的输入和输出设备,用户可以轻松地将输入重定向到文件或其他程序,输出重定向到例如串口I/O端口:

type infile > myprog > com

程序本身,“myprog”,并不会知道输入输出的区别。程序使用控制台I/O来读取它的“标准输入(stdin)”,这可能是键盘输入、文件内容,或其他程序的输出,并打印到它的“标准输出(stdout)”,这可能是显示器、打印机、另一个程序或文件。程序本身并不关心或知道具体的输入输出设备。


控制台I/O函数

控制台I/O要求声明:

#include <stdio.h>

常用的函数包括:

  • printf(): 将格式化的字符串打印到标准输出。
  • scanf(): 从标准输入读取格式化的数据。
  • putchar(): 将单个字符打印到标准输出。
  • getchar(): 从标准输入读取单个字符。
  • puts(): 打印字符串到标准输出。
  • gets(): 从标准输入读取一行。

在Windows系统的编译器中,还可以使用一组替代的控制台I/O函数。此时需要声明:

#include <conio.h>

三个最常用的Windows控制台I/O函数是:

  • getch(): 从键盘获取字符(不需要按回车键)。
  • getche(): 从键盘获取字符并回显。
  • kbhit(): 检查是否有键被按下。

printf()函数

如前所述,printf()函数可以打印包含格式化数据的字符串:

printf("This is a test!\n");

它也可以包括变量的内容:

printf("Value1:  %d   Value2:  %f\n", intval, floatval);

printf()使用的格式代码包括:

  • %d: 十进制整数
  • %ld: 长十进制整数
  • %c: 字符
  • %s: 字符串
  • %e: 浮点数的指数形式
  • %f: 浮点数的十进制形式
  • %g: 根据情况使用%e%f,选择较短的表示
  • %u: 无符号十进制整数
  • %o: 无符号八进制整数
  • %x: 无符号十六进制整数

使用错误的格式代码可能会导致奇怪的输出。通过修改格式代码可以获得更多控制,例如,可以指定最小字段宽度:

%10d

这表示字段宽度至少为10个字符。如果字段宽度过小,printf()会自动使用更宽的字段。添加负号后:

%-10d

会使文本左对齐。

可以指定数字精度:

%6.3f

这表示在宽度为6个字符的字段中保留3位小数。

还可以指定字符串的精度,表示最多打印字符数,例如:

/* prtint.c */
#include <stdio.h>

int main(void)
{
  printf("<%d>\n", 336);
  printf("<%2d>\n", 336);
  printf("<%10d>\n", 336);
  printf("<%-10d>\n", 336);
  return 0;
}

这将输出:

<336>
<336>
<       336>
<336       >

类似地:

/* prtfloat.c */
#include <stdio.h>

int main(void)
{
  printf("<%f>\n", 1234.56);
  printf("<%e>\n", 1234.56);
  printf("<%4.2f>\n", 1234.56);
  printf("<%3.1f>\n", 1234.56);
  printf("<%10.3f>\n", 1234.56);
  printf("<%10.3e>\n", 1234.56);
  return 0;
}

这将输出:

<1234.560000>
<1.234560e+03>
<1234.56>
<1234.6>
<  1234.560>
< 1.234e+03>

再比如:

/* prtstr.c */
#include <stdio.h>

int main(void)
{
  printf("<%2s>\n", "Barney must die!");
  printf("<%22s>\n", "Barney must die!");
  printf("<%22.5s>\n", "Barney must die!");
  printf("<%-22.5s>\n", "Barney must die!");
  return 0;
}

输出为:

<Barney must die!>
<      Barney must die!>
<                 Barne>
<Barne                 >

printf()字符串中的特殊字符

在第2章中列出的特殊字符也可以嵌入在printf字符串中:

  • '\a': 警告(蜂鸣)字符
  • '\b': 退格
  • '\f': 换页
  • '\n': 换行
  • '\r': 回车
  • '\t': 水平制表符
  • '\v': 垂直制表符
  • '\\': 反斜杠
  • '\?': 问号
  • '\'': 单引号
  • '\"': 双引号
  • '%%': 百分号
  • '\0NN': 八进制字符编码
  • '\xNN': 十六进制字符编码
  • '\0': 空字符

scanf()函数

scanf()函数通过类似于printf()的语法读取格式化的数据,不过它需要指针作为参数,因为它需要返回值。例如:

/* cscanf.c */
#include <stdio.h>

int main(void)
{
  int val;
  char name[256];

  printf("Enter your age and name.\n");
  scanf("%d %s", &val, name);
  printf("Your name is: %s -- and your age is: %d\n", name, val);
  return 0;
}

scanf()中,name不需要加&,因为字符串的名称本身就是一个指针。输入字段由空白(空格、制表符或换行符)分隔,不过可以使用如%10d这样的格式说明符来定义特定的字段宽度。

scanf()的格式代码与printf()相同,唯一的不同是:

  • 没有%g格式代码
  • %f%e格式代码工作方式相同
  • 有一个%h格式代码用于读取短整型(short)整数

如果格式代码中包含字符,scanf()会读取这些字符并丢弃它们。例如,如果将上面的示例修改为:

scanf("%d,%s", &val, name);

那么,scanf()会假定两个输入值由逗号分隔,并在遇到逗号时将其丢弃。

如果格式代码前面加上星号(*),则数据会被读取并丢弃。例如,如果修改为:

scanf("%d%*c%s", &val, name);

那么,如果两个字段由冒号(:)分隔,scanf()会读取并丢弃冒号字符。

scanf()的输入被终止时,它会返回EOF(一个整数),EOF在stdio.h中被定义。


putchar()getchar()函数

putchar()getchar()函数用于单个字符的输入输出。例如,下面的程序从标准输入一个一个地接受字符:

/* inout.c */
#include <stdio.h>

int main(void)
{
  unsigned int ch;

  while ((ch = getchar()) != EOF)
  {
    putchar(ch);
  }
  return 0;
}

getchar()函数返回一个整数,并且也会在遇到EOF时终止。注意,C语言允许程序在同一个表达式中获取值并进行测试,这是处理循环时特别有用的功能。

关于单字符I/O的一个警告:如果程序从键盘读取字符,大多数操作系统不会在用户按下“回车”键之前将字符发送到程序,这意味着无法通过这种方式执行单字符键盘输入输出。

上面的简单程序是一个字符模式文本“过滤器”的核心程序,过滤器是一种可以在标准输入和标准输出之间进行转换的程序。这样的过滤器可以作为构建更复杂应用程序的元素:

type file.txt > filter1 | filter2 > outfile.txt

以下过滤器将输入中每个单词的第一个字符大写。该程序作为一个“状态机”工作,使用一个可以设置为不同值(或“状态”)的变量来控制其工作模式。它有两个状态:SEEK(查找状态),在此状态下它寻找第一个字符;REPLACE(替换状态),在此状态下它寻找一个单词的结束。

SEEK状态下,它扫描空白符(空格、制表符或换行符),并回显字符。如果找到可打印字符,它会将其转换为大写,并进入REPLACE状态。在REPLACE状态下,它将字符转换为小写,直到遇到空白符,然后返回SEEK状态。

程序使用了tolower()toupper()函数来进行大小写转换,这两个函数将在下一章中详细讨论。

/* caps.c */
#include <stdio.h>
#include <ctype.h>

#define SEEK 0
#define REPLACE 1

int main(void)
{
  int ch, state = SEEK;
  while ((ch = getchar()) != EOF)
  {
    switch (state)
    {
      case REPLACE:
        switch (ch)
        {
          case ' ':
          case '\t':
          case '\n':
            state = SEEK;
            break;
          default:
            ch = tolower(ch);
            break;
        }
        break;
      case SEEK:
        switch (ch)
        {
          case ' ':
          case '\t':
          case '\n':
            break;
          default:
            ch = toupper(ch);
            state = REPLACE;
            break;
        }
    }
    putchar(ch);
  }
  return 0;
}

puts()函数

puts()函数类似于简化版的printf(),它没有格式化代码。它打印一个字符串,并自动以换行符结束:

puts("Hello world!");

fgets()函数

fgets()函数特别有用:它读取一行以换行符结束的文本。与scanf()相比,它对输入的要求宽松得多:

/* cgets.c */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main(void)
{
  char word[256], *guess = "blue\n";
  int i, n = 0;

  puts("Guess a color (use lower case please):");
  while (fgets(word, 256, stdin) != NULL)
  {
    if (strcmp(word, guess) == 0)
    {
      puts("You win!");
      break;
    }
    else
    {
      puts("No, try again.");
    }
  }
  return 0;
}

这个程序使用了strcmp函数,它执行字符串比较,如果匹配则返回0。strcmp函数将在下一章中详细讨论。

这些函数可以用于实现处理文本行而非单字符的过滤器。下面是这样一个过滤器的核心程序:

/* lfilter.c */
#include <stdio.h>

int main(void)
{
  char b[256];
  while ((fgets(b, 256, stdin)) != NULL)
  {
    puts(b);
  }
  return 0;
}

fgets()函数在输入终止或出现错误时会返回NULLNULLstdio.h中定义。


Windows下的控制台I/O函数

基于Windows的控制台I/O函数getch()getche()的操作方式与getchar()类似,只是getche()会自动回显字符。

kbhit()函数与其他不同,它仅表示是否有键被按下。如果有键被按下,它返回非零值;如果没有键被按下,它返回零。这允许程序轮询键盘输入,而不是在键盘输入时阻塞,等待某些操作。正如前面所提到的,这些函数需要conio.h头文件,而不是stdio.h头文件。

最后修改: 2025年01月27日 星期一 23:38