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

被折叠的 条评论
为什么被折叠?



