【C标准库】详解<stdio.h>标准输入输出库

文章目录


stdio.h 提供了C风格标准输入输出函数的声明、定义和相关宏定义。

API Reference Doc

常用定义

  • FILE,每个 FILE 对象代表一个 C I/O 流,保有控制 C I/O 流所需的全部信息。

    cpp 复制代码
    typedef struct _iobuf
    { 
    	void* _Placeholder;
    } FILE;
  • fpos_t 完整非数组对象类型,足以唯一指定文件中的位置,包含其多字节剖析状态。

    cpp 复制代码
    typedef __int64 fpos_t;
  • size_t

    cpp 复制代码
    //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打开文件

cpp 复制代码
FILE* fopen( const char* filename, const char* mode );

打开 filename 所指示的文件并返回与该文件关联的流。用 mode 确定文件访问模式。

  • filename - 要关联文件流到的文件名
  • mode - 确定文件访问模式的字符串
  • 返回值 - 若成功,则返回指向控制打开的文件流的对象的指针,并清除文件尾和错误位。流为完全缓冲,除非 filename 指代交互设备。错误时,返回空指针。
文件访问模式字符串 含义 解释 若文件已存在的行动 若文件不存在的行动
"r" 为读取打开文件 从起始读取 打开失败
"w" 为写入创建文件 销毁内容 创建新文件
"a" 追加 追加到文件 写入到末尾 创建新文件
"r+" 扩展读 为读取/写入打开文件 从起始读取 错误
"w+" 扩展写 为读取/写入创建文件 销毁内容 创建新文件
"a+" 扩展追加 为读取/写入打开文件 写入到末尾 创建新文件

文件访问模式标志 "b" 能可选地指定,以用二进制模式打开文件

在追加文件访问模式上,写入数据到文件尾,忽略文件位置指示器的当前位置,即无法用fseek指定文件位置。


fclose------关闭文件

cpp 复制代码
int fclose( FILE* stream );

关闭给定的文件流。无论操作是否成功,流都不再关联到文件。

  • 返回值:成功时为 ​0 ,否则为 EOF (-1)。

freopen------以不同名称打开既存流

cpp 复制代码
FILE* freopen( const char* filename, const char* mode, FILE* stream );
  1. 试图关闭与 stream 关联的文件,忽略任何错误。
  2. filename 非空,则试图用 mode 打开 filename 所指定的文件,如同用 fopen ,然后将该文件与 stream 所指向的文件流关联。
  3. filename 为空指针,则函数试图重打开已与 stream 关联的文件。

fflush------将输出流与实际文件同步

cpp 复制代码
int fflush( FILE* stream );
  • 对于输出流(和最近操作为输出的更新流),将来自 stream 缓冲区的未写入数据写入关联的输出设备。
  • 对于输入流(和最近操作为输入的更新流),行为未定义。
  • 若 stream 为空指针,则冲入所有打开的输出流,包含在库包内操作,或其他情况下程序不能直接访问的流。

setbuf------为文件流设置缓冲区

cpp 复制代码
void setbuf( std::FILE* stream, char* buffer );

setvbuf------为文件流设置缓冲区与其大小

cpp 复制代码
int setvbuf( std::FILE* stream, char* buffer, int mode, std::size_t size );

mode 缓冲模式

  • _IOFBF 全缓冲:当缓冲区为空时,从流读入数据。或者当缓冲区满时,向流写入数据。
  • _IOLBF 行缓冲:每次从流中读入一行数据或向流中写入一行数据。
  • _IONBF 无缓冲:直接从流中读入数据或直接向流中写入数据,缓冲设置无效。

2. 直接输入/输出

fread从文件读取

cpp 复制代码
size_t fread( void* buffer, size_t size, size_t count, FILE* stream );

从给定输入流 stream 读取至多 count 个对象到数组 buffer 中。流的文件位置指示器前进读取的字符数。

等同于对每个对象调用 sizestd::fgetc ,并按顺序存储结果到转译为 unsigned char 数组的 buffer 中的相继位置。

  • buffer - 指向要读取的数组中首个对象的指针
  • size - 每个对象的字节大小
  • count - 要读取的对象数
  • stream - 读取来源的输入文件流
  • 返回值
    • 成功读取的对象数,若出现错误或文件尾条件,则可能小于 count
    • sizecount 为零,则 fread 返回零且不进行其他动作。
cpp 复制代码
 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);

读取原始二进制数据(如图像、音频)

cpp 复制代码
#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++ 环境)

cpp 复制代码
#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写入文件

cpp 复制代码
size_t fwrite(const void *buffer, size_t size, size_t count, FILE *stream);
  • buffer - 指向源数据内存缓冲区的指针。
  • size - 每个数据对象的大小(以字节为单位)。
  • count - 要写入的对象数量。
  • stream - 目标输出文件流指针。

写入原始二进制数据

cpp 复制代码
#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++ 环境)

cpp 复制代码
#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);
}

写入结构体数组

cpp 复制代码
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------从文件流获取字符

cpp 复制代码
int fgetc(FILE *stream);
int getc(FILE *stream);
  • 成功时返回读取的字符(提升为 int 类型)
cpp 复制代码
#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------从文件流获取字符串

cpp 复制代码
char *fgets(char *str, int n, FILE *stream);
  • str:存储读取内容的字符数组(缓冲区)。
  • n:最多读取 n-1 个字符(保留一个给 \0)。
  • stream:输入流文件指针。
cpp 复制代码
#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------写字符到文件流

cpp 复制代码
int fputc(int ch, FILE *stream);
int putc(int ch, FILE *stream);
  • ch:要写入的字符(以 int 形式传递)。
cpp 复制代码
#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------写字符串到文件流

cpp 复制代码
int fputs(const char *str, FILE *stream);
  • str:以 \0 结尾的字符串。
  • stream:目标输出流。
cpp 复制代码
#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 读取字符

cpp 复制代码
int getchar(void);  // 等价于: fgetc(stdin)
cpp 复制代码
int ch;
while ((ch = getchar()) != EOF && ch != '\n') {
    // 处理字符
}

3.6 gets------从 stdin 读取字符串

(C++11 中弃用)

(C++14 中移除)

❌ 严重安全问题:不检查缓冲区边界,极易导致缓冲区溢出。

建议使用 fgets(str, size, stdin) 替代。

3.7 putchar------写字符到 stdout

cpp 复制代码
int putchar(int ch);  //等价于: fputc(ch, stdout)
cpp 复制代码
for (int i = 0; i < 10; ++i) {
    putchar('*');
}
putchar('\n');

3.8 puts------写字符串到 stdout

cpp 复制代码
int puts(const char *str);	//自动在末尾添加换行符 \n
cpp 复制代码
puts("Hello World");  // 等价于 printf("Hello World\n");

3.9 ungetc------把字符放回文件流

cpp 复制代码
int ungetc(int ch, FILE *stream);
  • ch:要"推回"的字符。
  • stream:对应的文件流。
  • 成功返回 ch
  • 失败返回 EOF
cpp 复制代码
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)。
cpp 复制代码
// 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 更安全,推荐使用。

cpp 复制代码
// 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------打印有格式输出到缓冲区(使用可变实参列表)
cpp 复制代码
#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);
cpp 复制代码
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------返回当前文件位置指示器

cpp 复制代码
long ftell(FILE *stream);

成功:返回从文件开头到当前位置的字节数(long 类型)。

失败:返回 -1L

用途: 记录当前位置,用于后续恢复。

cpp 复制代码
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------移动文件位置指示器到文件中的指定位置

cpp 复制代码
int fseek(FILE *stream, long offset, int origin);
  • stream:文件指针。
  • offset:偏移量(字节数)。
  • origin:起始位置,可选值:
    • SEEK_SET:文件开头(偏移从0开始)。
    • SEEK_CUR:当前位置。
    • SEEK_END:文件末尾。
cpp 复制代码
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);

在文本模式下,fseekoffset 必须是 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------获取文件位置

cpp 复制代码
int fgetpos(FILE *stream, fpos_t *pos);
  • stream:文件指针。
  • pos:指向 fpos_t 类型对象的指针,用于存储位置信息。
  • 成功返回 0。
    失败返回非零值。
cpp 复制代码
FILE *fp = fopen("largefile.dat", "r");
fpos_t position;

fgetpos(fp, &position);  // 保存当前位置

// 做一些操作...
fseek(fp, 1024, SEEK_CUR);

// 恢复到之前位置
fsetpos(fp, &position);

fclose(fp);

fsetpos------设置文件位置

cpp 复制代码
int fsetpos(FILE *stream, const fpos_t *pos);

rewind------重置文件位置到开头

cpp 复制代码
void rewind(FILE *stream);

等价于

cpp 复制代码
fseek(stream, 0L, SEEK_SET);

额外作用:清除文件结束标志(EOF)和错误标志。

cpp 复制代码
FILE *fp = fopen("data.txt", "r");
// ... 读取到文件末尾

rewind(fp);  // 回到开头,可重新读取

6. 文件上的操作

remove------删除文件

cpp 复制代码
int remove(const char *filename);
  • filename:要删除的文件路径。
cpp 复制代码
if (remove("temp.log") == 0) {
    printf("文件删除成功\n");
} else {
    perror("删除失败");
}

rename------重命名或移动文件

cpp 复制代码
int rename(const char *old_filename, const char *new_filename);
  • old_filename:原文件名。
  • new_filename:新文件名(可包含路径,实现移动)。
cpp 复制代码
// 重命名
rename("old.txt", "new.txt");

// 移动并重命名
rename("data.txt", "/backup/archived_data.txt");

如果目标文件已存在,行为未定义(通常会失败或覆盖,取决于系统)。


tmpfile------创建并打开一个临时、自动移除的文件

cpp 复制代码
FILE *tmpfile(void);
  • 成功返回指向临时文件的 FILE* 指针。

    失败返回 NULL

  • 文件以 "w+b" 模式自动打开(二进制读写)。

  • 程序正常结束或文件关闭时自动删除。

  • 不需要手动调用 remove

cpp 复制代码
FILE *tmp = tmpfile();
if (tmp) {
    fprintf(tmp, "临时数据:%d\n", 123);
    rewind(tmp);
    // 使用临时文件...
    fclose(tmp);  // 自动删除
}

tmpnam------返回一个唯一独有的文件名

cpp 复制代码
char *tmpnam(char *str);
  • str:存储文件名的缓冲区(至少 L_tmpnam 字节)。
    • 若为 NULL,函数返回静态缓冲区的指针(不安全,不推荐多线程)。
  • 成功返回指向文件名字符串的指针。
    失败返回 NULL。
cpp 复制代码
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标志

cpp 复制代码
void clearerr(FILE *stream);
cpp 复制代码
FILE *fp = fopen("data.txt", "r");
// ... 读取到EOF

if (feof(fp)) {
    printf("到达文件末尾\n");
    clearerr(fp);  // 清除EOF标志
    // 可以尝试重新定位或进行其他操作
}

feof------检查文件结束标志

cpp 复制代码
int feof(FILE *stream);

如果文件结束标志被设置,返回非零值。

否则返回 0。

feof 只在尝试读取并发现已到文件末尾后才返回真。不能用于循环条件判断的首选方法。

cpp 复制代码
// 错误!可能多处理一次
while (!feof(fp)) {
    fscanf(fp, "%d", &x);
    // 如果读取失败(如格式错误),会无限循环
}

// 正确用法
int x;
while (fscanf(fp, "%d", &x) == 1) {
    // 正确处理
}

ferror------检查文件错误

cpp 复制代码
int ferror(FILE *stream);
  • 如果流的错误标志被设置,返回非零值。
    否则返回 0。
  • 用途: 区分是文件结束还是I/O错误。
cpp 复制代码
int c;
while ((c = fgetc(fp)) != EOF) {
    putchar(c);
}

if (feof(fp)) {
    printf("\n正常到达文件末尾。\n");
} else if (ferror(fp)) {
    printf("\n读取过程中发生错误!\n");
}

perror------打印错误信息

cpp 复制代码
void perror(const char *s);
  • s:用户自定义的错误前缀(如 "读取文件")。
    作用: 将 s 和当前 errno 对应的错误消息输出到 stderr
cpp 复制代码
FILE *fp = fopen("nonexistent.txt", "r");
if (!fp) {
    perror("打开文件失败"); 
    // 输出类似:打开文件失败: No such file or directory
}
相关推荐
小十一再加一4 分钟前
【C初阶】自定义类型--结构体
c语言
重启的码农3 小时前
llama.cpp 分布式推理介绍(7) 远程后端缓冲区 (RPC Buffer)
c++·人工智能·神经网络
Vect__3 小时前
链表漫游指南:C++ 指针操作的艺术与实践
数据结构·c++·链表
saltymilk5 小时前
C++ 使用分治减小模板递归深度
c++
悠哉清闲5 小时前
C ++代码学习笔记(一)
c++·笔记·学习
zhysunny6 小时前
Day22: Python涡轮增压计划:用C扩展榨干最后一丝性能!
c语言·网络·python
希望_睿智6 小时前
实战设计模式之解释器模式
c++·设计模式·架构
海鸥_8 小时前
C++中不加{}导致的BUG
c++·bug
努力努力再努力wz10 小时前
【c++进阶系列】:万字详解多态
java·linux·运维·开发语言·c++