19、C语言输入输出操作详解

C语言输入输出操作详解

1. 标准流与重定向

在C语言编程中,标准输入(stdin)、标准输出(stdout)和标准错误输出(stderr)是非常重要的概念。标准输入和标准输出流在不与交互式设备关联时是全缓冲的,而标准错误流则是非全缓冲的,这样能确保错误信息尽快显示。
例如,将文件 tempfile 的内容重定向到 wc 命令的标准输入,可以统计文件中的换行符、单词和字节数:

$ wc < tempfile

这里, wc 命令会输出 tempfile 文件的换行符(1)、单词(7)和字节(34)的数量。

另外,通过POSIX管道可以将一个程序的输出流重定向为另一个程序的输入流。例如:

$ echo "Hello Robert" | sed "s/Hello/Hi/" | sed "s/Robert/robot/"
Hi robot

在这个例子中, echo 命令的输出作为 sed 命令的输入,经过两次替换操作后,最终输出 Hi robot

2. 流的方向

每个流都有一个方向,用于指示流中包含的是窄字符还是宽字符。在流与外部文件关联但尚未进行任何操作时,流是无方向的。一旦对无方向的流应用了宽字符I/O函数,该流就会变成宽方向流;同理,应用字节I/O函数后,流会变成字节方向流。
需要注意的是,不要在同一个文件中混合使用窄字符数据、宽字符数据和二进制数据,并且可以使用 fwide 函数或关闭并重新打开文件来重置流的方向。在程序启动时,三个预定义流(stderr、stdin和stdout)都是无方向的。

3. 文本流和二进制流

C标准支持文本流和二进制流。
- 文本流 :是由字符组成的有序序列,按行组织,每行由零个或多个字符加上一个换行符序列组成。在类Unix系统中,通常用换行符 \n 表示行结束;而在大多数Windows程序中,使用回车符 \r 后跟换行符 \n 。不同系统的换行符约定可能会导致文本文件在不同系统之间传输时显示或解析错误,但现在这种情况越来越少见。
- 二进制流 :是任意二进制数据的有序序列。在相同的实现下,从二进制流中读取的数据与之前写入该流的数据相同。在非POSIX系统中,流的末尾可能会附加实现定义数量的空字节。
一般来说,二进制流比文本流更强大、更可预测,但如果要读写普通文本文件并与其他面向文本的程序协作,使用文本流是最简单的方法。

4. 打开和创建文件

在C语言中,可以使用 fopen 和POSIX的 open 函数来打开或创建文件。

4.1 fopen函数

fopen 函数用于打开指定名称的文件,并将其与一个流关联起来:

FILE *fopen(
  const char * restrict filename,
  const char * restrict mode
);

mode 参数指定了文件的打开模式,常见的模式如下表所示:
| 模式字符串 | 描述 |
| — | — |
| r | 打开现有的文本文件进行读取 |
| w | 将文件截断为零长度或创建文本文件进行写入 |
| a | 追加、打开或创建文本文件,在文件末尾写入 |
| rb | 打开现有的二进制文件进行读取 |
| wb | 将文件截断为零长度或创建二进制文件进行写入 |
| ab | 追加、打开或创建二进制文件,在文件末尾写入 |
| r+ | 打开现有的文本文件进行读写 |
| w+ | 将文件截断为零长度或创建文本文件进行读写 |
| a+ | 追加、打开或创建文本文件进行更新,在当前文件末尾写入 |
| r+b 或 rb+ | 打开现有的二进制文件进行读写 |
| w+b 或 wb+ | 将文件截断为零长度或创建二进制文件进行读写 |
| a+b 或 ab+ | 追加、打开或创建二进制文件进行更新,在当前文件末尾写入 |

需要注意的是,以读取模式打开文件时,如果文件不存在或无法读取,操作会失败;以追加模式打开文件时,后续的写入操作会在文件的当前末尾进行,并且在多线程环境下,只要文件也以追加模式打开,写入数据时对文件末尾的递增操作是原子的。

C11标准还增加了独占模式,用于读写二进制和文本文件,具体模式如下表:
| 模式字符串 | 描述 |
| — | — |
| wx | 创建独占的文本文件进行写入 |
| wbx | 创建独占的二进制文件进行写入 |
| w+x | 创建独占的文本文件进行读写 |
| w+bx 或 wb+x | 创建独占的二进制文件进行读写 |

以独占模式打开文件时,如果文件已经存在或无法创建,操作会失败。

此外,不要复制 FILE 对象,否则可能会导致未定义行为。例如以下代码:

#include <stdio.h>
#include <stdlib.h>
int main() {
  FILE my_stdout = *stdout;
  if (fputs("Hello, World!\n", &my_stdout) == EOF) {
    return EXIT_FAILURE;
  }
  return EXIT_SUCCESS;
}

这段代码通常会崩溃,因为它使用了 stdout 的按值副本。

4.2 open函数

在POSIX系统中, open 函数用于建立文件与文件描述符之间的连接:

int open(const char *path, int oflag, ...);

文件描述符是一个非负整数,用于引用表示文件的结构(称为打开文件描述)。 open 函数返回的文件描述符是未使用的最低编号的文件描述符,并且对于调用进程是唯一的。
oflag 参数设置了文件的访问模式和状态标志,具体如下:
- 文件访问模式
- O_EXEC :仅用于执行(非目录文件)
- O_RDONLY :仅用于读取
- O_RDWR :用于读写
- O_SEARCH :仅用于搜索目录
- O_WRONLY :仅用于写入
- 文件状态标志
- O_APPEND :在每次写入前将文件偏移量设置到文件末尾
- O_TRUNC :将文件长度截断为0
- O_CREAT :创建文件
- O_EXCL :如果同时设置了 O_CREAT 且文件已存在,则打开失败

以下是一个使用 open 函数以只写模式打开文件的示例:

#include <err.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
  int fd;
  int flags = O_WRONLY | O_CREAT | O_TRUNC;
  mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
  const char *pathname = "/tmp/file";
  if ((fd = open(pathname, flags, mode)) == -1) {
    err(EXIT_FAILURE, "Can't open %s", pathname);
  }
  // --snip--
}

在这个示例中, open 函数接受多个参数,包括文件路径名、 oflag 和文件权限模式。如果文件成功打开, open 函数返回一个非负整数表示文件描述符;否则返回 -1 并设置 errno 以指示错误。

除了 open 函数,POSIX还提供了其他与文件描述符相关的有用函数,如 fileno 函数用于获取与现有文件指针关联的文件描述符, fdopen 函数用于从现有文件描述符创建新的流文件指针。通过文件描述符可以访问POSIX文件系统的一些特性,如目录操作、文件权限设置和文件锁定等。

5. 关闭文件

打开文件会占用系统资源,如果不断打开文件而不关闭,最终可能会耗尽进程可用的文件描述符或句柄,导致无法再打开更多文件。因此,在使用完文件后,及时关闭文件非常重要。

5.1 fclose函数

fclose 函数用于关闭文件:

int fclose(FILE *stream);

该函数会将流中未写入的缓冲数据写入文件,并丢弃未读取的缓冲数据。不过, fclose 函数可能会失败,例如磁盘已满时写入剩余缓冲输出可能会返回错误。即使知道缓冲区为空,在使用网络文件系统(NFS)协议时关闭文件也可能会出错。
为了确保代码的健壮性,应该检查 fclose 函数的返回值。如果检测到错误,函数会返回 EOF

if (fclose(fp) == EOF) {
  err(EXIT_FAILURE, "Failed to close file\n");
}

需要注意的是,在程序写入过的任何缓冲流上,应该显式调用 fflush fclose ,而不是依赖 exit 或从 main 函数返回时自动刷新,以便进行错误检查。

5.2 close函数

在POSIX系统中,可以使用 close 函数释放指定的文件描述符:

int close(int fd);

如果在关闭文件时发生I/O错误, close 函数可能会返回 -1 并设置 errno 以指示失败原因。如果返回错误,文件描述符的状态将不确定,不能再对其进行读写操作或再次关闭。
一般情况下,使用 fopen 打开的文件使用 fclose 关闭,使用 open 打开的文件使用 close 关闭(除非将文件描述符传递给了 fdopen ,此时必须使用 fclose 关闭)。

6. 读写字符和行

C标准定义了许多用于读写特定字符或行的函数。大多数字节流函数都有对应的宽字符版本,不过为了减少编程错误和安全漏洞,建议尽可能使用UTF - 8字符编码,避免使用宽字符函数变体。

以下是一些常用的字节流I/O函数:
| 窄字符函数 | 宽字符函数 | 描述 |
| — | — | — |
| fgetc | fgetwc | 从流中读取一个字符 |
| getc | getwc | 从流中读取一个字符 |
| getchar | getwchar | 从标准输入读取一个字符 |
| fgets | fgetws | 从流中读取一行 |
| fputc | fputwc | 将一个字符写入流 |
| putc | putwc | 将一个字符写入流 |
| fputs | fputws | 将一个字符串写入流 |
| putchar | putwchar | 将一个字符写入标准输出 |
| puts | N/A | 将一个字符串写入标准输出并换行 |
| ungetc | ungetwc | 将一个字符返回到流中 |
| scanf | wscanf | 从标准输入读取格式化字符输入 |
| fscanf | fwscanf | 从流中读取格式化字符输入 |
| sscanf | swscanf | 从缓冲区读取格式化字符输入 |
| printf | wprintf | 将格式化字符输出到标准输出 |
| fprintf | fwprintf | 将格式化字符输出到流 |
| sprintf | swprintf | 将格式化字符输出到缓冲区 |
| snprintf | N/A | 与 sprintf 类似,但有截断功能 |

下面详细介绍一些常用的字节流函数:
- fputc函数 :将字符 c 转换为 unsigned char 类型并写入流:

int fputc(int c, FILE *stream);

如果发生写入错误,返回 EOF ;否则返回写入的字符。
- putc函数 :与 fputc 类似,但大多数库将其实现为宏:

int putc(int c, FILE *stream);

如果 putc 是宏实现,可能会多次计算其流参数。因此,使用 fputc 通常更安全。
- putchar函数 :相当于 putc 函数,只是默认使用 stdout 作为流参数:

int putchar(int c);
  • fputs函数 :将字符串 s 写入流 stream ,不写入字符串的空字符和换行符:
int fputs(const char * restrict s, FILE * restrict stream);

如果发生写入错误,返回 EOF ;否则返回非负值。例如:

fputs("I ", stdout);
fputs("am ", stdout);
fputs("Groot\n", stdout);

这段代码会输出 I am Groot 并换行。
- puts函数 :将字符串 s 写入 stdout 并换行:

int puts(const char *s);

这是打印简单消息最方便的函数,例如:

puts("This is a message.");
  • fgetc函数 :从流中读取下一个字符,将其作为 unsigned char 读取并转换为 int 类型返回:
int fgetc(FILE *stream);

如果遇到文件结束或读取错误,返回 EOF
- getc函数 :与 fgetc 等效,但如果实现为宏,可能会多次计算其流参数。因此,通常建议使用 fgetc
- getchar函数 :相当于 getc 函数,默认使用 stdout 作为流参数。
- fgets函数 :从流中读取最多 n - 1 个字符到字符数组 s 中:

char *fgets(char * restrict s, int n, FILE * restrict stream);

需要注意的是, gets 函数由于存在安全隐患,已在C99中被弃用,并在C11中被移除,应避免使用。如果需要从标准输入读取字符串,建议使用 fgets 函数。

综上所述,在C语言的文件I/O操作中,正确使用打开、关闭、读写等函数,以及处理可能出现的错误,对于编写健壮的程序至关重要。通过合理运用这些函数和技巧,可以高效地完成文件的读写操作,同时避免潜在的安全问题和资源泄漏。

C语言输入输出操作详解

7. 格式化输入输出

在C语言中,格式化输入输出函数可以方便地处理不同类型的数据,将数据按照指定的格式进行输入或输出。常见的格式化输入输出函数有 scanf 系列和 printf 系列。

7.1 格式化输出函数
  • printf函数 :用于将格式化的数据输出到标准输出( stdout )。
int printf(const char *format, ...);

例如:

int num = 10;
printf("The number is %d\n", num);

在这个例子中, %d 是格式说明符,用于表示要输出一个整数。 printf 函数会将 num 的值按照整数格式输出。

  • fprintf函数 :与 printf 类似,但可以将格式化的数据输出到指定的流。
int fprintf(FILE *stream, const char *format, ...);

例如,将数据输出到文件:

#include <stdio.h>
int main() {
    FILE *fp = fopen("output.txt", "w");
    if (fp != NULL) {
        int num = 20;
        fprintf(fp, "The number in file is %d\n", num);
        fclose(fp);
    }
    return 0;
}
  • sprintf函数 :将格式化的数据输出到一个字符数组中。
int sprintf(char *str, const char *format, ...);

例如:

#include <stdio.h>
int main() {
    char buffer[50];
    int num = 30;
    sprintf(buffer, "The number in buffer is %d", num);
    printf("%s\n", buffer);
    return 0;
}
  • snprintf函数 :与 sprintf 类似,但可以指定输出的最大长度,避免缓冲区溢出。
int snprintf(char *str, size_t size, const char *format, ...);

例如:

#include <stdio.h>
int main() {
    char buffer[10];
    int num = 40;
    snprintf(buffer, sizeof(buffer), "The number is %d", num);
    printf("%s\n", buffer);
    return 0;
}
7.2 格式化输入函数
  • scanf函数 :从标准输入( stdin )读取格式化的数据。
int scanf(const char *format, ...);

例如:

#include <stdio.h>
int main() {
    int num;
    printf("Enter a number: ");
    scanf("%d", &num);
    printf("You entered %d\n", num);
    return 0;
}

在这个例子中, %d 表示要读取一个整数, &num 是变量 num 的地址, scanf 函数会将用户输入的整数存储到 num 中。

  • fscanf函数 :从指定的流中读取格式化的数据。
int fscanf(FILE *stream, const char *format, ...);

例如,从文件中读取数据:

#include <stdio.h>
int main() {
    FILE *fp = fopen("input.txt", "r");
    if (fp != NULL) {
        int num;
        fscanf(fp, "%d", &num);
        printf("Read number from file: %d\n", num);
        fclose(fp);
    }
    return 0;
}
  • sscanf函数 :从一个字符数组中读取格式化的数据。
int sscanf(const char *str, const char *format, ...);

例如:

#include <stdio.h>
int main() {
    char str[] = "50";
    int num;
    sscanf(str, "%d", &num);
    printf("Read number from string: %d\n", num);
    return 0;
}
8. 文件定位

在文件操作中,有时需要在文件的不同位置进行读写操作,这就涉及到文件定位。C语言提供了一些函数来实现文件定位。

8.1 fseek函数

fseek 函数用于设置文件位置指示器的位置。

int fseek(FILE *stream, long int offset, int whence);
  • offset :表示偏移量,可以是正数、负数或零。
  • whence :表示起始位置,有以下三个取值:
  • SEEK_SET :从文件开头开始偏移。
  • SEEK_CUR :从当前位置开始偏移。
  • SEEK_END :从文件末尾开始偏移。

例如,将文件位置指示器移动到文件开头:

#include <stdio.h>
int main() {
    FILE *fp = fopen("test.txt", "r");
    if (fp != NULL) {
        fseek(fp, 0, SEEK_SET);
        // 后续可以从文件开头开始读取数据
        fclose(fp);
    }
    return 0;
}
8.2 ftell函数

ftell 函数用于获取文件位置指示器的当前位置。

long int ftell(FILE *stream);

例如:

#include <stdio.h>
int main() {
    FILE *fp = fopen("test.txt", "r");
    if (fp != NULL) {
        long int pos = ftell(fp);
        printf("Current file position: %ld\n", pos);
        fclose(fp);
    }
    return 0;
}
8.3 rewind函数

rewind 函数用于将文件位置指示器设置到文件开头。

void rewind(FILE *stream);

例如:

#include <stdio.h>
int main() {
    FILE *fp = fopen("test.txt", "r");
    if (fp != NULL) {
        // 进行一些读写操作
        rewind(fp);
        // 此时可以从文件开头重新读取数据
        fclose(fp);
    }
    return 0;
}
9. 错误处理

在文件I/O操作中,可能会出现各种错误,如文件打开失败、读写错误等。因此,正确处理这些错误非常重要。

9.1 errno变量

errno 是一个全局变量,用于记录最近一次系统调用或库函数调用的错误代码。在发生错误时,函数会设置 errno 的值,程序员可以根据这个值来判断错误的类型。
例如,在使用 open 函数打开文件时:

#include <stdio.h>
#include <fcntl.h>
#include <errno.h>
int main() {
    int fd = open("nonexistent_file.txt", O_RDONLY);
    if (fd == -1) {
        perror("Open file error");
        printf("errno value: %d\n", errno);
    }
    return 0;
}

在这个例子中, perror 函数会根据 errno 的值输出错误信息。

9.2 feof和ferror函数
  • feof函数 :用于检查文件是否到达末尾。
int feof(FILE *stream);

例如:

#include <stdio.h>
int main() {
    FILE *fp = fopen("test.txt", "r");
    if (fp != NULL) {
        int ch;
        while ((ch = fgetc(fp)) != EOF) {
            // 处理读取的字符
        }
        if (feof(fp)) {
            printf("Reached end of file\n");
        }
        fclose(fp);
    }
    return 0;
}
  • ferror函数 :用于检查文件流是否发生错误。
int ferror(FILE *stream);

例如:

#include <stdio.h>
int main() {
    FILE *fp = fopen("test.txt", "r");
    if (fp != NULL) {
        int ch = fgetc(fp);
        if (ferror(fp)) {
            perror("Read file error");
        }
        fclose(fp);
    }
    return 0;
}
10. 总结

本文详细介绍了C语言中的输入输出操作,包括标准流与重定向、流的方向、文本流和二进制流、文件的打开与关闭、读写字符和行、格式化输入输出、文件定位以及错误处理等方面。

以下是操作流程总结的mermaid流程图:

graph TD
    A[开始] --> B[选择文件操作类型]
    B --> |打开文件| C[fopen或open函数]
    B --> |关闭文件| D[fclose或close函数]
    B --> |读写操作| E[字符读写函数或格式化输入输出函数]
    B --> |文件定位| F[fseek、ftell或rewind函数]
    C --> G{是否成功}
    G --> |是| H[进行相应操作]
    G --> |否| I[错误处理]
    H --> J{是否完成操作}
    J --> |是| D
    J --> |否| H
    D --> K{是否成功}
    K --> |是| L[结束]
    K --> |否| I
    I --> M[输出错误信息]
    M --> L

通过合理运用这些函数和技巧,可以高效地完成文件的读写操作,同时避免潜在的安全问题和资源泄漏。在实际编程中,要注意错误处理,确保程序的健壮性。同时,根据不同的需求选择合适的流类型和文件打开模式,以达到最佳的效果。

分布式微服务企业级系统是一个基于Spring、SpringMVC、MyBatis和Dubbo等技术的分布式敏捷开发系统架构。该系统采用微服务架构和模块化设计,提供整套公共微服务模块,包括集中权限管理(支持单点登录)、内容管理、支付中心、用户管理(支持第三方登录)、微信平台、存储系统、配置中心、日志分析、任务和通知等功能。系统支持服务治理、监控和追踪,确保高可用性和可扩展性,适用于中小型企业的J2EE企业级开发解决方案。 该系统使用Java作为主要编程语言,结合Spring框架实现依赖注入和事务管理,SpringMVC处理Web请求,MyBatis进行数据持久化操作,Dubbo实现分布式服务调用。架构模式包括微服务架构、分布式系统架构和模块化架构,设计模式应用了单例模式、工厂模式和观察者模式,以提高代码复用性和系统稳定性。 应用场景广泛,可用于企业信息化管理、电子商务平台、社交应用开发等领域,帮助开发者快速构建高效、安全的分布式系统。本资源包含完整的源码和详细论文,适合计算机科学或软件工程专业的毕业设计参考,提供实践案例和技术文档,助力学生和开发者深入理解微服务架构和分布式系统实现。 【版权说明】源码来源于网络,遵循原项目开源协议。付费内容为本人原创论文,包含技术分析和实现思路。仅供学习交流使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值