【C标准库】详解<stdio.h>标准输入输出库
文章目录
- 常用定义
- 1. 文件访问函数
- fopen打开文件
- 2. 直接输入/输出
- fread从文件读取
- fwrite写入文件
- 3. 无格式输入/输出
- 4. 有格式输入/输出
- scanf / fscanf / sscanf —— 格式化输入
- printf / fprintf / sprintf / snprintf —— 格式化输出
- 使用va_list可变实参列表输入/输出
- 5. 文件寻位
- ftell——返回当前文件位置指示器
- fseek——移动文件位置指示器到文件中的指定位置
- fgetpos——获取文件位置
- fsetpos——设置文件位置
- rewind——重置文件位置到开头
- 6. 文件上的操作
- remove——删除文件
- rename——重命名或移动文件
- 7. 错误处理
stdio.h
提供了C风格标准输入输出函数的声明、定义和相关宏定义。
API Reference Doc
常用定义
FILE
,每个FILE
对象代表一个C
I/O
流,保有控制C
I/O
流所需的全部信息。typedef struct _iobuf { void* _Placeholder; } FILE;
fpos_t
完整非数组对象类型,足以唯一指定文件中的位置,包含其多字节剖析状态。typedef __int64 fpos_t;
size_t
//64位系统下 typedef unsigned __int64 size_t;//32位系统下 typedef unsigned int size_t;
std::size_t
可以存放下理论上可能存在的对象的最大大小,该对象可以是任何类型,包括数组。
大小无法以std::size_t
表示的类型是非良构的。 (C++14 起) 在许多平台上(使用分段寻址的系统除外),std::size_t
可以存放下任何非成员的指针,此时可以视作其与std::uintptr_t
同义。
std::size_t
通常被用于数组索引和循环计数。使用其它类型来进行数组索引操作的程序可能会在某些情况下出错,例如在 64 位系统中使用unsigned int 进行索引时,如果索引号超过 UINT_MAX 或者依赖于 32 位取模运算的话,程序就会出错。在对诸如 std::string、std::vector 等 C++ 容器进行索引操作时,正确的类型是该容器的成员 typedef size_type,而该类型通常被定义为与 std::size_t 相同。
1. 文件访问函数
fopen打开文件
FILE* fopen( const char* filename, const char* mode );
打开 filename
所指示的文件并返回与该文件关联的流。用 mode
确定文件访问模式。
filename
- 要关联文件流到的文件名mode
- 确定文件访问模式的字符串- 返回值 - 若成功,则返回指向控制打开的文件流的对象的指针,并清除文件尾和错误位。流为完全缓冲,除非 filename 指代交互设备。错误时,返回空指针。
文件访问模式字符串 | 含义 | 解释 | 若文件已存在的行动 | 若文件不存在的行动 |
---|---|---|---|---|
"r" | 读 | 为读取打开文件 | 从起始读取 | 打开失败 |
"w" | 写 | 为写入创建文件 | 销毁内容 | 创建新文件 |
"a" | 追加 | 追加到文件 | 写入到末尾 | 创建新文件 |
"r+" | 扩展读 | 为读取/写入打开文件 | 从起始读取 | 错误 |
"w+" | 扩展写 | 为读取/写入创建文件 | 销毁内容 | 创建新文件 |
"a+" | 扩展追加 | 为读取/写入打开文件 | 写入到末尾 | 创建新文件 |
文件访问模式标志 "b"
能可选地指定,以用二进制模式打开文件。
在追加文件访问模式上,写入数据到文件尾,忽略文件位置指示器的当前位置,即无法用
fseek
指定文件位置。
fclose——关闭文件
int fclose( FILE* stream );
关闭给定的文件流。无论操作是否成功,流都不再关联到文件。
- 返回值:成功时为
0
,否则为EOF
(-1
)。
freopen——以不同名称打开既存流
FILE* freopen( const char* filename, const char* mode, FILE* stream );
- 试图关闭与
stream
关联的文件,忽略任何错误。 - 若
filename
非空,则试图用mode
打开filename
所指定的文件,如同用fopen
,然后将该文件与stream
所指向的文件流关联。 - 若
filename
为空指针,则函数试图重打开已与stream
关联的文件。
fflush——将输出流与实际文件同步
int fflush( FILE* stream );
- 对于输出流(和最近操作为输出的更新流),将来自 stream 缓冲区的未写入数据写入关联的输出设备。
- 对于输入流(和最近操作为输入的更新流),行为未定义。
- 若 stream 为空指针,则冲入所有打开的输出流,包含在库包内操作,或其他情况下程序不能直接访问的流。
setbuf——为文件流设置缓冲区
void setbuf( std::FILE* stream, char* buffer );
setvbuf——为文件流设置缓冲区与其大小
int setvbuf( std::FILE* stream, char* buffer, int mode, std::size_t size );
mode 缓冲模式
_IOFBF
全缓冲:当缓冲区为空时,从流读入数据。或者当缓冲区满时,向流写入数据。_IOLBF
行缓冲:每次从流中读入一行数据或向流中写入一行数据。_IONBF
无缓冲:直接从流中读入数据或直接向流中写入数据,缓冲设置无效。
2. 直接输入/输出
fread从文件读取
size_t fread( void* buffer, size_t size, size_t count, FILE* stream );
从给定输入流 stream
读取至多 count
个对象到数组 buffer
中。流的文件位置指示器前进读取的字符数。
等同于对每个对象调用
size
次std::fgetc
,并按顺序存储结果到转译为unsigned char
数组的buffer
中的相继位置。
buffer
- 指向要读取的数组中首个对象的指针size
- 每个对象的字节大小count
- 要读取的对象数stream
- 读取来源的输入文件流- 返回值
- 成功读取的对象数,若出现错误或文件尾条件,则可能小于
count
。 - 若
size
或count
为零,则fread
返回零且不进行其他动作。
- 成功读取的对象数,若出现错误或文件尾条件,则可能小于
FILE* pFile = fopen("test.txt", "rb");unsigned short pRawData = new unsigned short[width*height];fread(pRawData, sizeof(unsigned short), width*height, pFile);//std::vector<char> buf(4); // char 可平凡复制//fread(&buf[0], sizeof buf[0], buf.size(), f);
读取原始二进制数据(如图像、音频)
#include <stdio.h>
#include <stdlib.h>int main() {FILE *pFile = fopen("raw_data.bin", "rb"); // 二进制模式if (!pFile) {perror("无法打开文件");return 1;}int width = 640, height = 480;size_t total_elements = width * height;// 分配内存存储 unsigned short 数据unsigned short *pRawData = (unsigned short*)malloc(total_elements * sizeof(unsigned short));if (!pRawData) {fclose(pFile);return 1;}// 读取数据size_t elements_read = fread(pRawData, sizeof(unsigned short), total_elements, pFile);printf("请求读取 %zu 个元素,实际读取 %zu 个。\n", total_elements, elements_read);// **重要:检查是否读取完整**if (elements_read != total_elements) {if (feof(pFile)) {printf("警告:文件提前结束,数据可能不完整!\n");} else if (ferror(pFile)) {printf("错误:读取文件时发生 I/O 错误!\n");}}// ... 使用 pRawData 中的数据 ...free(pRawData);fclose(pFile);return 0;
}
读取到 std::vector
(C++ 环境)
#include <vector>
#include <cstdio>// C++ 示例
FILE* f = fopen("data.bin", "rb");
if (f) {std::vector<char> buf(1024); // 预分配 1024 字节的缓冲区size_t bytes_read = fread(buf.data(), sizeof(char), buf.size(), f);// 或者使用 &buf[0] (C++11 前)// size_t bytes_read = fread(&buf[0], sizeof(char), buf.size(), f);buf.resize(bytes_read); // 调整 vector 大小到实际读取的字节数fclose(f);
}
fwrite写入文件
size_t fwrite(const void *buffer, size_t size, size_t count, FILE *stream);
buffer
- 指向源数据内存缓冲区的指针。size
- 每个数据对象的大小(以字节为单位)。count
- 要写入的对象数量。stream
- 目标输出文件流指针。
写入原始二进制数据
#include <stdio.h>int main() {FILE *pFile = fopen("output.bin", "wb"); // 二进制写入模式if (!pFile) {perror("无法创建文件");return 1;}// 准备一些数据unsigned short data[] = {100, 200, 300, 400, 500};size_t num_elements = sizeof(data) / sizeof(data[0]);// 写入数据size_t elements_written = fwrite(data, sizeof(unsigned short), num_elements, pFile);printf("请求写入 %zu 个元素,实际写入 %zu 个。\n", num_elements, elements_written);// **重要:检查写入是否成功**if (elements_written != num_elements) {printf("错误:写入文件时发生错误或磁盘空间不足!\n");// 可能需要调用 ferror(pFile) 进一步诊断}fclose(pFile);return 0;
}
写入 std::vector
内容 (C++ 环境)
#include <vector>
#include <cstdio>// C++ 示例
std::vector<double> values = {3.14, 2.71, 1.41, 0.57};
FILE* f = fopen("doubles.bin", "wb");
if (f) {size_t written = fwrite(values.data(), sizeof(double), values.size(), f);if (written != values.size()) {fprintf(stderr, "写入失败!\n");}fclose(f);
}
写入结构体数组
typedef struct {int id;char name[50];float score;
} Student;Student students[] = {{1, "Alice", 95.5},{2, "Bob", 87.0},{3, "Charlie", 92.3}
};FILE *fp = fopen("students.dat", "wb");
if (fp) {size_t count = fwrite(students, sizeof(Student), 3, fp);if (count != 3) {printf("写入学生记录失败!\n");}fclose(fp);
}
3. 无格式输入/输出
窄字符
宽字符加w后缀
3.1 fgetc
/getc
——从文件流获取字符
int fgetc(FILE *stream);
int getc(FILE *stream);
- 成功时返回读取的字符(提升为 int 类型)
#include <stdio.h>int main() {FILE *fp = fopen("test.txt", "r");if (!fp) return 1;int ch;while ((ch = fgetc(fp)) != EOF) {putchar(ch); // 输出到 stdout}fclose(fp);return 0;
}
3.2 fgets
——从文件流获取字符串
char *fgets(char *str, int n, FILE *stream);
str
:存储读取内容的字符数组(缓冲区)。n
:最多读取 n-1 个字符(保留一个给 \0)。stream
:输入流文件指针。
#include <stdio.h>int main() {char buffer[100];printf("请输入一行文本:");if (fgets(buffer, sizeof(buffer), stdin)) {printf("你输入的是:%s", buffer); // 包含 \n}return 0;
}
3.3 fputc
/putc
——写字符到文件流
int fputc(int ch, FILE *stream);
int putc(int ch, FILE *stream);
ch
:要写入的字符(以 int 形式传递)。
#include <stdio.h>int main() {FILE *fp = fopen("output.txt", "w");if (!fp) return 1;for (char c = 'A'; c <= 'Z'; ++c) {fputc(c, fp);}fclose(fp);return 0;
}
3.4 fputs
——写字符串到文件流
int fputs(const char *str, FILE *stream);
str
:以\0
结尾的字符串。stream
:目标输出流。
#include <stdio.h>int main() {FILE *fp = fopen("log.txt", "a");if (!fp) return 1;fputs("程序启动\n", fp);fputs("日志记录...\n", fp);fclose(fp);return 0;
}
3.5 getchar
——从 stdin
读取字符
int getchar(void); // 等价于: fgetc(stdin)
int ch;
while ((ch = getchar()) != EOF && ch != '\n') {// 处理字符
}
3.6 gets——从 stdin 读取字符串
(C++11 中弃用)
(C++14 中移除)
❌ 严重安全问题:不检查缓冲区边界,极易导致缓冲区溢出。
建议使用 fgets(str, size, stdin) 替代。
3.7 putchar
——写字符到 stdout
int putchar(int ch); //等价于: fputc(ch, stdout)
for (int i = 0; i < 10; ++i) {putchar('*');
}
putchar('\n');
3.8 puts
——写字符串到 stdout
int puts(const char *str); //自动在末尾添加换行符 \n
puts("Hello World"); // 等价于 printf("Hello World\n");
3.9 ungetc
——把字符放回文件流
int ungetc(int ch, FILE *stream);
ch
:要“推回”的字符。stream
:对应的文件流。- 成功返回
ch
。 - 失败返回
EOF
。
int ch = getchar();
if (ch >= '0' && ch <= '9') {ungetc(ch, stdin); // 推回,后续用 scanf 读数字int num;scanf("%d", &num);
}
4. 有格式输入/输出
窄/多字节字符
scanf / fscanf / sscanf —— 格式化输入
函数 | 原型 | 作用 |
---|---|---|
scanf | int scanf(const char *format, ...); | 从 stdin 读取 |
fscanf | int fscanf(FILE *stream, const char *format, ...); | 从文件流读取 |
sscanf | int sscanf(const char *str, const char *format, ...); | 从字符串缓冲区读取 |
format
:格式控制字符串(如%d
,%s
,%f
等)。...
:可变参数列表,传入变量地址(如&x
)。
// scanf
int age;
printf("输入年龄:");
scanf("%d", &age);// fscanf
FILE *fp = fopen("data.txt", "r");
float price;
fscanf(fp, "%f", &price);
fclose(fp);// sscanf
char line[] = "张三 25 89.5";
char name[20];
int age;
float score;
sscanf(line, "%s %d %f", name, &age, &score);
printf / fprintf / sprintf / snprintf —— 格式化输出
函数 | 原型 | 作用 |
---|---|---|
printf | int printf(const char *format, ...); | 输出到 stdout |
fprintf | int fprintf(FILE *stream, const char *format, ...); | 输出到文件流 |
sprintf | int sprintf(char *str, const char *format, ...); | 输出到字符串缓冲区 |
snprintf | int snprintf(char *str, size_t size, const char *format, ...); | 安全版本,限制写入长度 |
sprintf
不检查缓冲区大小,有溢出风险。
snprintf
更安全,推荐使用。
// printf
printf("姓名:%s,年龄:%d\n", name, age);// fprintf
FILE *log = fopen("error.log", "w");
fprintf(log, "错误代码:%d\n", errno);
fclose(log);// sprintf (谨慎使用)
char buffer[50];
sprintf(buffer, "结果=%d", result); // 若结果很大,可能溢出!// snprintf (推荐)
snprintf(buffer, sizeof(buffer), "编号:%04d", id); // 安全格式化
使用va_list可变实参列表输入/输出
这些函数用于实现自定义的格式化函数(如日志函数)。
vscanf
——从stdin读取有格式输入(使用可变实参列表)vfscanf
——从文件流读取有格式输入(使用可变实参列表)vsscanf
——从缓冲区读取有格式输入(使用可变实参列表)vprintf
——打印有格式输出到stdout(使用可变实参列表)vfprintf
——打印有格式输出到文件流(使用可变实参列表)vsprintf
——打印有格式输出到缓冲区(使用可变实参列表)vsnprintf
——打印有格式输出到缓冲区(使用可变实参列表)
#include <stdio.h>
#include <stdarg.h>void my_log(const char *format, ...) {va_list args;va_start(args, format);fprintf(stderr, "[LOG] ");vfprintf(stderr, format, args);fprintf(stderr, "\n");va_end(args);
}// 使用
my_log("用户 %s 登录失败,尝试次数:%d", username, count);
void Log( const char* pFormat, ... )
{char iBuffer[IBUFFER_LEN];va_list args;va_start(args, pFormat);_vsnprintf_s(iBuffer, IBUFFER_LEN, pFormat, args);va_end(args);mEventQueue.push_back(std::string(iBuffer));
}
5. 文件寻位
ftell——返回当前文件位置指示器
long ftell(FILE *stream);
成功:返回从文件开头到当前位置的字节数(long 类型)。
失败:返回 -1L
用途: 记录当前位置,用于后续恢复。
FILE *fp = fopen("data.bin", "rb");
if (!fp) return -1;// 读取一些数据
fseek(fp, 100, SEEK_SET);
long pos = ftell(fp); // pos == 100
printf("当前位置:%ld\n", pos);fclose(fp);
fseek——移动文件位置指示器到文件中的指定位置
int fseek(FILE *stream, long offset, int origin);
stream
:文件指针。offset
:偏移量(字节数)。origin
:起始位置,可选值:SEEK_SET
:文件开头(偏移从0开始)。SEEK_CUR
:当前位置。SEEK_END
:文件末尾。
FILE *fp = fopen("example.txt", "r");// 移动到文件第50个字节
fseek(fp, 50, SEEK_SET);// 向前移动10字节
fseek(fp, 10, SEEK_CUR);// 移动到倒数第5个字节
fseek(fp, -5, SEEK_END);fclose(fp);
在文本模式下,
fseek
的offset
必须是ftell
返回的值或0
。
在二进制模式下,可以使用任意偏移量。
文件大小超过4GB大小会超出
long
类型的上限
特性 | fseek | _fseeki64 |
---|---|---|
标准性 | ISO C 标准函数 | Microsoft 扩展(非标准) |
偏移量类型 | long (通常 32 位) | __int64 (64 位) |
最大文件支持 | ~2GB(有符号)或 4GB(无符号) | ~9EB (Exabytes, 2^63 字节) |
可移植性 | 高(所有 C 编译器) | 低(主要在 Windows/MSVC 环境) |
头文件 | <stdio.h> | <stdio.h> |
fgetpos——获取文件位置
int fgetpos(FILE *stream, fpos_t *pos);
stream
:文件指针。pos
:指向fpos_t
类型对象的指针,用于存储位置信息。- 成功返回 0。
失败返回非零值。
FILE *fp = fopen("largefile.dat", "r");
fpos_t position;fgetpos(fp, &position); // 保存当前位置// 做一些操作...
fseek(fp, 1024, SEEK_CUR);// 恢复到之前位置
fsetpos(fp, &position);fclose(fp);
fsetpos——设置文件位置
int fsetpos(FILE *stream, const fpos_t *pos);
rewind——重置文件位置到开头
void rewind(FILE *stream);
等价于
fseek(stream, 0L, SEEK_SET);
额外作用:清除文件结束标志(EOF)和错误标志。
FILE *fp = fopen("data.txt", "r");
// ... 读取到文件末尾rewind(fp); // 回到开头,可重新读取
6. 文件上的操作
remove——删除文件
int remove(const char *filename);
filename
:要删除的文件路径。
if (remove("temp.log") == 0) {printf("文件删除成功\n");
} else {perror("删除失败");
}
rename——重命名或移动文件
int rename(const char *old_filename, const char *new_filename);
old_filename
:原文件名。new_filename
:新文件名(可包含路径,实现移动)。
// 重命名
rename("old.txt", "new.txt");// 移动并重命名
rename("data.txt", "/backup/archived_data.txt");
如果目标文件已存在,行为未定义(通常会失败或覆盖,取决于系统)。
tmpfile
——创建并打开一个临时、自动移除的文件
FILE *tmpfile(void);
-
成功返回指向临时文件的
FILE*
指针。
失败返回NULL
。 -
文件以 “w+b” 模式自动打开(二进制读写)。
-
程序正常结束或文件关闭时自动删除。
-
不需要手动调用
remove
。
FILE *tmp = tmpfile();
if (tmp) {fprintf(tmp, "临时数据:%d\n", 123);rewind(tmp);// 使用临时文件...fclose(tmp); // 自动删除
}
tmpnam
——返回一个唯一独有的文件名
char *tmpnam(char *str);
str
:存储文件名的缓冲区(至少L_tmpnam
字节)。- 若为
NULL
,函数返回静态缓冲区的指针(不安全,不推荐多线程)。
- 若为
- 成功返回指向文件名字符串的指针。
失败返回 NULL。
char temp_name[L_tmpnam];
if (tmpnam(temp_name)) {printf("临时文件名:%s\n", temp_name);FILE *fp = fopen(temp_name, "w");if (fp) {fprintf(fp, "临时内容\n");fclose(fp);remove(temp_name); // 手动清理}
}
7. 错误处理
clearerr
——清除错误和EOF标志
void clearerr(FILE *stream);
FILE *fp = fopen("data.txt", "r");
// ... 读取到EOFif (feof(fp)) {printf("到达文件末尾\n");clearerr(fp); // 清除EOF标志// 可以尝试重新定位或进行其他操作
}
feof
——检查文件结束标志
int feof(FILE *stream);
如果文件结束标志被设置,返回非零值。
否则返回 0。
feof
只在尝试读取并发现已到文件末尾后才返回真。不能用于循环条件判断的首选方法。
// 错误!可能多处理一次
while (!feof(fp)) {fscanf(fp, "%d", &x);// 如果读取失败(如格式错误),会无限循环
}// 正确用法
int x;
while (fscanf(fp, "%d", &x) == 1) {// 正确处理
}
ferror
——检查文件错误
int ferror(FILE *stream);
- 如果流的错误标志被设置,返回非零值。
否则返回 0。 - 用途: 区分是文件结束还是I/O错误。
int c;
while ((c = fgetc(fp)) != EOF) {putchar(c);
}if (feof(fp)) {printf("\n正常到达文件末尾。\n");
} else if (ferror(fp)) {printf("\n读取过程中发生错误!\n");
}
perror
——打印错误信息
void perror(const char *s);
s
:用户自定义的错误前缀(如 “读取文件”)。
作用: 将s
和当前errno
对应的错误消息输出到stderr
。
FILE *fp = fopen("nonexistent.txt", "r");
if (!fp) {perror("打开文件失败"); // 输出类似:打开文件失败: No such file or directory
}