24、输入输出

厨子大约 14 分钟C语言基础程序程序厨

输入与输出(I/O)操作是程序与外界交互的基本方式。C语言提供了一系列函数来处理不同类型的输入与输出需求,使得程序能够读取用户输入的数据,以及将处理结果输出到屏幕或文件中。

标准输入输出函数

  1. printf()函数

printf()函数是C语言中最常用的输出函数之一,用于向标准输出设备(通常是屏幕)输出格式化的字符串。

其原型为:

int printf(const char *format, ...);
  • format:一个字符串,包含了普通字符和格式说明符。格式说明符用于指定如何格式化后续的参数。
  • ...:可变参数列表,表示可以传递任意数量的参数,这些参数将根据format中的格式说明符进行格式化输出。

示例代码:

#include <stdio.h>

int main() {
    int num = 10;
    float pi = 3.14159;
    char ch = 'A';
    char str[] = "Hello, World!";

    printf("整数:%d\n", num);
    printf("浮点数:%.2f\n", pi); // 输出两位小数
    printf("字符:%c\n", ch);
    printf("字符串:%s\n", str);

    return 0;
}

输出:

整数:10
浮点数:3.14
字符:A
字符串:Hello, World!
  1. scanf()函数

scanf()函数用于从标准输入设备(通常是键盘)读取格式化的输入。其原型为:

int scanf(const char *format, ...);
  • format:一个字符串,包含了格式说明符,用于指定如何解析输入的数据。
  • ...:可变参数列表,表示接收输入数据的变量。

示例代码:

#include <stdio.h>

int main() {
    int num;
    float pi;
    char ch;
    char str[100];

    printf("请输入一个整数:");
    scanf("%d", &num);
    printf("请输入一个浮点数:");
    scanf("%f", &pi);
    printf("请输入一个字符:");
    scanf(" %c", &ch); // 注意前面的空格,用于消耗可能存在的换行符
    printf("请输入一个字符串:");
    scanf("%99s", str); // %99s限制输入长度,防止缓冲区溢出

    printf("你输入的整数是:%d\n", num);
    printf("你输入的浮点数是:%.2f\n", pi);
    printf("你输入的字符是:%c\n", ch);
    printf("你输入的字符串是:%s\n", str);

    return 0;
}

注意:在使用scanf()读取字符时,如果之前输入了其他类型的数据并按下了回车键,回车符会被留在输入缓冲区中,导致scanf("%c", &ch);直接读取到回车符。为了解决这个问题,可以在%c前加一个空格,即scanf(" %c", &ch);,这样空格会消耗掉缓冲区中的空白字符(包括空格、制表符和换行符)。

字符输入输出函数

  1. getchar()和putchar()函数

getchar()函数用于从标准输入读取一个字符,putchar()函数用于将一个字符输出到标准输出。它们的原型分别为:

int getchar(void);
int putchar(int c);
  • getchar()不需要参数,返回读取到的字符(以整数形式表示)。
  • putchar()接受一个整数参数(通常是一个字符的ASCII码),并将其输出为字符。

示例代码:

#include <stdio.h>

int main() {
    char ch;

    printf("请输入一个字符:");
    ch = getchar();
    putchar(ch);
    putchar('\n'); // 输出换行符

    return 0;
}
  1. gets()和puts()函数(不推荐使用)

gets()函数用于从标准输入读取一行字符串,直到遇到换行符或文件结束符(EOF)。puts()函数用于将一个字符串输出到标准输出,并在末尾自动添加一个换行符。它们的原型分别为:

char *gets(char *s);
int puts(const char *s);
  • gets()函数由于不检查缓冲区大小,容易导致缓冲区溢出,因此不推荐使用。建议使用fgets()代替。
  • puts()函数接受一个字符串参数,并将其输出到标准输出,同时在末尾添加换行符。

注意:由于gets()函数的安全性问题,现代C编程中通常使用fgets()来替代它。fgets()允许指定缓冲区的大小,从而避免了缓冲区溢出的风险。

文件输入输出函数

C语言提供了丰富的文件输入输出函数,使程序能够读取和写入文件。

主要有fopen()fclose()fread()fwrite()fprintf()fscanf()等。

fopen()和fclose()函数

fopen()函数用于打开文件,fclose()函数用于关闭文件。它们的原型分别为:

FILE *fopen(const char *filename, const char *mode);
int fclose(FILE *stream);
  • fopen()函数接受文件名和打开模式作为参数,返回一个FILE指针用于后续的文件操作。如果打开失败,返回NULL
  • fclose()函数接受一个FILE指针作为参数,用于关闭打开的文件。如果关闭成功,返回0;否则返回EOF。

打开模式包括:

  • "r":只读模式,打开已存在的文件。
  • "w":只写模式,打开文件用于写入。如果文件不存在,则创建新文件;如果文件已存在,则清空文件内容。
  • "a":追加模式,打开文件用于在文件末尾追加数据。如果文件不存在,则创建新文件。
  • "r+":读写模式,打开已存在的文件用于读写。
  • "w+":读写模式,打开文件用于读写。如果文件不存在,则创建新文件;如果文件已存在,则清空文件内容。
  • "a+":读写模式,打开文件用于在文件末尾追加数据,同时允许读取文件内容。

fread()和fwrite()函数

fread()fwrite()函数分别用于从文件和向文件读写二进制数据。它们的原型分别为:

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
  • fread()函数从stream指向的文件中读取nmemb个元素,每个元素大小为size字节,并将读取的数据存储到ptr指向的内存中。返回成功读取的元素个数。
  • fwrite()函数将ptr指向的内存中的nmemb个元素写入到stream指向的文件中,每个元素大小为size字节。返回成功写入的元素个数。

下面是一段分段读取的示例代码:

#include <stdio.h>
#include <stdlib.h>

#define BUFFER_SIZE 10  // 每次读取的字节数

int main() {
    FILE *file;
    char buffer[BUFFER_SIZE];
    size_t bytesRead;

    // 打开文件用于读取
    file = fopen("example.txt", "rb");
    if (file == NULL) {
        perror("打开文件失败");
        return 1;
    }

    // 分段读取文件内容
    while ((bytesRead = fread(buffer, sizeof(char), BUFFER_SIZE, file)) > 0) {
        // 输出读取到的数据
        fwrite(buffer, sizeof(char), bytesRead, stdout);  // 将数据写到标准输出
    }

    // 检查是否发生读取错误
    if (ferror(file)) {
        perror("读取文件时发生错误");
    }

    // 关闭文件
    fclose(file);

    return 0;
}

代码说明:

  • 定义缓冲区大小:
    • 定义了一个宏 BUFFER_SIZE,表示每次读取的字节数。在这个示例中,我们设置为 10 字节。
  • 打开文件:
    • 使用 fopen() 函数以二进制读取模式("rb")打开文件 example.txt。如果文件打开失败,输出错误信息并返回。
  • 分段读取文件内容:
    • 使用 fread() 函数在循环中分段读取文件内容。每次读取 BUFFER_SIZE 字节的数据到 buffer 中。
    • fread() 返回实际读取的字节数,存储在 bytesRead 中。
  • 输出读取到的数据:
    • 使用 fwrite() 函数将读取到的数据写到标准输出(控制台)。这里我们直接将 buffer 中的数据写到 stdout,这样可以看到文件的内容。
  • 检查读取错误:
    • 在循环结束后,使用 ferror() 函数检查是否发生读取错误。如果有错误,输出错误信息。
  • 关闭文件:
    • 使用 fclose() 函数关闭文件。

fprintf()和fscanf()函数

fprintf()fscanf()函数分别用于向文件和从文件读写格式化数据。它们的原型与printf()scanf()类似,只是多了一个FILE指针参数用于指定文件。

示例代码:

#include <stdio.h>

int main() {
    FILE *file;
    int num = 123;
    float pi = 3.14159;
    char str[] = "Hello, File!";

    // 打开文件用于写入
    file = fopen("example.txt", "w");
    if (file == NULL) {
        perror("打开文件失败");
        return 1;
    }

    // 向文件写入格式化数据
    fprintf(file, "整数:%d\n", num);
    fprintf(file, "浮点数:%.2f\n", pi);
    fprintf(file, "字符串:%s\n", str);

    // 关闭文件
    fclose(file);

    // 打开文件用于读取
    file = fopen("example.txt", "r");
    if (file == NULL) {
        perror("打开文件失败");
        return 1;
    }

    // 从文件读取格式化数据(这里只是演示,实际应用中需要更复杂的处理)
    int read_num;
    float read_pi;
    char read_str[100];

    fscanf(file, "整数:%d", &read_num);
    fscanf(file, "浮点数:%f", &read_pi);
    fscanf(file, "字符串:%s", read_str);

    // 输出读取到的数据
    printf("从文件读取的整数:%d\n", read_num);
    printf("从文件读取的浮点数:%.2f\n", read_pi);
    printf("从文件读取的字符串:%s\n", read_str);

    // 关闭文件
    fclose(file);

    return 0;
}

首先使用fopen()以写模式("w")打开一个名为example.txt的文件,然后使用fprintf()函数将格式化的数据写入文件。接着,我们关闭文件并重新以读模式("r")打开它,使用fscanf()函数读取文件中的数据,并使用printf()函数将读取到的数据输出到控制台。

文件定位函数

在文件操作中,有时需要定位到文件的特定位置进行读写。C语言提供了一系列文件定位函数,包括ftell()fseek()rewind()

ftell()函数

ftell()函数用于获取当前文件位置指针的位置,即相对于文件开头的字节数。原型为:

long ftell(FILE *stream);

如果成功,返回当前文件位置指针的位置;如果失败,返回-1L。

fseek()函数

fseek()函数用于设置文件位置指针的位置。原型为:

int fseek(FILE *stream, long offset, int whence);
  • stream:文件指针。
  • offset:相对于whence的偏移量,以字节为单位。
  • whence:起始位置,可以是以下值之一:
    • SEEK_SET:文件开头。
    • SEEK_CUR:当前文件位置。
    • SEEK_END:文件末尾。

如果成功,返回0;如果失败,返回非0值。

rewind()函数

rewind()函数用于将文件位置指针重新定位到文件开头。原型为:

void rewind(FILE *stream);

它相当于调用fseek(stream, 0L, SEEK_SET);

示例代码:

#include <stdio.h>

int main() {
    FILE *file;
    char ch;

    // 打开文件用于读写
    file = fopen("example.txt", "r+");
    if (file == NULL) {
        perror("打开文件失败");
        return 1;
    }

    // 读取并输出第一个字符
    ch = fgetc(file);
    printf("第一个字符:%c\n", ch);

    // 定位到文件开头
    rewind(file);

    // 再次读取并输出第一个字符(应该与上次相同)
    ch = fgetc(file);
    printf("重新定位后的第一个字符:%c\n", ch);

    // 定位到文件末尾前的一个字节(假设文件不为空)
    fseek(file, -1L, SEEK_END);

    // 读取并输出最后一个字符
    ch = fgetc(file);
    printf("最后一个字符:%c\n", ch);

    // 关闭文件
    fclose(file);

    return 0;
}

首先使用fgetc()函数读取并输出文件的第一个字符。然后,使用rewind()函数将文件位置指针重新定位到文件开头,并再次读取并输出第一个字符。最后,使用fseek()函数将文件位置指针定位到文件末尾前的一个字节,并读取并输出最后一个字符。

错误处理

在进行文件操作时,可能会遇到各种错误,如文件不存在、权限不足、磁盘空间不足等。为了处理这些错误,C语言提供了perror()ferror()函数。

perror()函数

perror()函数用于输出描述最近一次I/O操作错误的字符串。其原型为:

void perror(const char *s);
  • s:用户自定义的错误信息字符串,可以为NULL。如果为NULL,则只输出系统提供的错误信息字符串。

示例代码:

#include <stdio.h>

int main() {
    FILE *file;

    // 尝试打开一个不存在的文件
    file = fopen("nonexistent.txt", "r");
    if (file == NULL) {
        perror("打开文件失败");
    }

    return 0;
}

尝试打开一个不存在的文件,并使用perror()函数输出错误信息。

ferror()函数

ferror()函数用于检查文件流是否发生错误。其原型为:

int ferror(FILE *stream);

如果发生错误,返回非0值;否则返回0。

示例代码:

#include <stdio.h>

int main() {
    FILE *file;
    char ch;

    // 打开文件用于读取
    file = fopen("example.txt", "r");
    if (file == NULL) {
        perror("打开文件失败");
        return 1;
    }

    // 尝试读取文件中的一个字符(假设文件不为空)
    if (fgetc(file) == EOF) {
        if (ferror(file)) {
            perror("读取文件时发生错误");
        } else {
            printf("已到达文件末尾\n");
        }
    }

    // 关闭文件
    fclose(file);

    return 0;
}

尝试从文件中读取一个字符,并检查是否发生错误。如果发生错误,我们使用perror()函数输出错误信息。

扩展知识

缓冲区

C语言的标准I/O库使用了缓冲区来提高I/O操作的效率。缓冲区是一块内存区域,用于暂时存储输入或输出的数据。当程序执行输入或输出操作时,数据并不是立即从设备读取或写入设备,而是先存储到缓冲区中。当缓冲区满或程序显式地刷新缓冲区时,数据才会被实际地读取或写入设备。

C语言提供了三种类型的缓冲区:

  • 全缓冲:只有在缓冲区满或调用fflush()函数时,才进行实际的I/O操作。通常用于文件流。
  • 行缓冲:在遇到换行符或缓冲区满时,进行实际的I/O操作。通常用于终端(标准输入和标准输出)。
  • 无缓冲:不进行缓冲,直接进行实际的I/O操作。通常用于标准错误输出。

可以使用setvbuf()函数来设置缓冲区的类型和大小。

格式化字符串

printf()scanf()fprintf()fscanf()等函数中,格式化字符串用于指定如何格式化输出或解析输入的数据。格式化字符串可以包含普通字符和格式说明符。格式说明符以百分号(%)开头,后面跟着一个或多个字符,用于指定数据类型、宽度、精度等。

常见的格式说明符包括:

  • %d:输出十进制整数。
  • %f:输出浮点数。
  • %c:输出单个字符。
  • %s:输出字符串。
  • %x:输出十六进制整数。
  • %%:输出百分号本身。

此外,还可以在格式说明符之间添加修饰符,如宽度修饰符(如%5d表示输出宽度为5的十进制整数)、精度修饰符(如%.2f表示输出保留两位小数的浮点数)等。

文件结尾(EOF)

在文件操作中,EOF(End Of File)是一个特殊的标记,表示文件的结尾。

当读取文件时,如果到达文件结尾,fgetc()fgets()等函数会返回EOF。可以使用feof()函数来检查是否已到达文件结尾。

二进制文件与文本文件

C语言支持两种类型的文件:二进制文件和文本文件。二进制文件以二进制形式存储数据,不进行任何转换。文本文件则以文本形式存储数据,可能会根据系统的字符编码进行转换(如换行符的转换)。在打开文件时,可以通过指定打开模式("rb"、"wb"等)来选择以二进制模式或文本模式打开文件。

练习

  1. 编写一个程序,实现以下功能:

    1. 打开一个名为"example.txt"的文件,如果文件不存在则创建该文件。
    2. 向文件中写入字符串"Hello, World!\n"。
    3. 关闭文件。
    4. 重新打开该文件,以读取模式打开。
    5. 读取文件内容并打印到控制台。
    6. 再次关闭文件。
  2. 编写一个程序,完成以下任务:

    1. 定义一个结构体Student,包含姓名(字符数组)和年龄(整型)。
    2. 创建一个包含多个Student结构体的数组,并初始化。
    3. 将这些结构体数据写入到名为"students.dat"的二进制文件中。
    4. 从该文件中读取结构体数据,并打印到控制台。
  3. 编写一个程序,实现以下功能:

    1. 尝试打开一个名为"log.txt"的文件,以追加模式("a")打开。
    2. 向文件中追加当前日期和时间(可以使用标准库函数获取)。
    3. 如果文件打开失败,打印错误信息。
    4. 关闭文件。

进阶

  1. 封装一个方法函数,读取文件的所有内容到内存中,返回内存指针和文件内容的长度。
  2. 封装一个方法函数,输入参数为指针和长度和文件名,把内容写入到文件中,并flush