一文讲清C/C++的文件IO的所有操作(详细总结)


文件是纯粹的字节流,即文件内的数据都是按照一个字节8位(取值范围:0-255)的单位进行存放,具体需要按照某种编码格式解析字节数据去映射到具体字符,与文件本身无关。例如:ASCII编码方式是将1个字节进行翻译成一个字符,UTF‑8编码方式是将1-4个字节进行翻译成一个字符,GBK编码方式使用1个字节翻译成一个英文字符,使用2个字节翻译成一个中文字符。

1. Linux系统调用的文件操作

通过Linux 系统调用进行文件操作,包括 文件创建、打开、读写、定位、关闭 等核心系统调用。

1.1 open文件打开/创建操作

int open(const char *pathname, int flags, mode_t mode);

open函数以指定方式flags和权限mode打开指定路径parhname的文件,具体参数意义:

  • 返回值:成功返回大于0的文件描述符,失败返回-1。

  • pathname:指定要打开的文件路径

  • flags:指定文件的打开方式,包括以下几种方式,可以使用|运算进行组合

    Flag 含义
    O_RDONLY 只读
    O_WRONLY 只写
    O_RDWR 读写
    O_CREAT 不存在则创建
    O_TRUNC 清空文件
    O_APPEND 追加写
    O_EXCL 文件必须不存在
  • mode:在文件创建时指定文件的权限,仅在flags具有O_CREAT标志时使用,用4位8进制数据表示权限,一般常用宏表示如下

    八进制 权限表示 宏组合(推荐写法) 含义 场景
    0600 rw-·---·--- `S_IRUSR S_IWUSR` 仅所有者可读写
    0644 rw-·r--·r-- `S_IRUSR S_IWUSR S_IRGRP
    0755 rwx·-rx·-rx `S_IRWXU S_IRGRP S_IXGRP
    0666 rw-·rw-·rw- `S_IRUSR S_IWUSR S_IRGRP

1.2 read文件读取操作

ssize_t read(int fd, void *buf, size_t count);

read函数用于通过文件描述符fd去读取一个打开文件的指定count个字节数据到缓冲区buf,由于Linux系统一切皆文件,因此通过该系统调用可以读取任意文件描述符的文件数据,对于默认阻塞的文件描述符,read系统调用在没有数据时会阻塞;对于非阻塞的文件描述符,read系统调用会直接返回-1,并设置errnoEAGAINread函数还可以用于网络套接字表示接收网络数据。

  • 返回值:>0表示成功读取的字节数,=0表示读到文件末尾EOF,对于网络套接字表示对端关闭连接,-1表示出错
  • fd:打开文件的文件描述符
  • buf:存储从文件读取的数据缓冲区
  • count:缓冲区buf大小

1.3 write文件写入操作

ssize_t write(int fd, const void *buf, size_t count);

write函数可以通过文件描述符fd向打开文件写入缓冲区buf的指定count个字节数据,不保证一次性全部写完,需要循环写入,对于阻塞的文件描述符,在文件写满时会直接阻塞,对于非阻塞的文件描述符,在文件写满时会直接返回-1并设置errnoEAGAINwrite函数也可以用于网络套接字表示发送网络数据。

  • 返回值:>=0表示成功写入的字节数,-1表示出错
  • fd:打开文件的文件描述符
  • buf:准备写入文件的数据缓冲区
  • count:准备发送的数据缓冲区的指定字节个数

1.4 lseek文件读写指针操作

off_t lseek(int fd, off_t offset, int whence);

lseek函数用于移动文件的读写指针位置

  • 返回值:>0表示新的偏移量,-1表示出错
  • fd:打开文件的文件描述符
  • offset:偏移量,>0表示在基准位置向后偏移,<0表示在基准位置向前偏移
  • whence:基准位置,包括以下常用宏:
    • SEEK_SET:表示文件开始位置
    • SEEK_CUR:表示文件当前位置
    • SEEK_END:表示文件末尾位置

1.5 close文件关闭操作

int close(int fd);

close函数用于关闭文件描述符fd,并释放内核资源,不可重复释放已经被释放的文件描述符fd,会造成未定义行为

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
int main()
{
	//creat()创建一个文件
	int fd1;
	char filename_1[] = "./build/creat_test.txt";
	fd1 = creat(filename_1, 0755);
	printf("creat_test.txt fd1=%d\n", fd1);
	close(fd1);

	//open()创建一个文件
	int fd2;
	fd2 = open("./build/open_test.txt", O_RDONLY | O_CREAT, 0751);
	printf("open_test.txt fd2=%d\n", fd2);
	close(fd2);

	//open()打开一个文件fd3,并且使用write()写入数据,再使用open()打开另一个文件fd4,并且使用read()读入一个文件fd3数据,使用write()保存在这个文件fd4中
	char buf1[50];
	printf("输入要写入文件的数据。\n");
	scanf("%s", buf1);
	int n1 = strlen(buf1); //使用n1记录输入字符串的长度,以确保write()系统调用第三个参数的正确使用
	int fd3;
	fd3 = open("./build/creat_test.txt", O_RDWR | O_APPEND | O_CREAT, 0766);
	printf("creat_test.txt fd3=%d\n", fd3);
	if ((write(fd3, buf1, n1)) == -1)
	{
		printf("写入数据失败,退出");
		exit(1);
	}
	//lseek()函数重新指定fd3文件的读写指针位置在文件开始,以避免写入fd3文件后读写指针位置在文件末尾,无法继续读入数据到缓冲区buf2
	lseek(fd3, 0, SEEK_SET); //移动fd3对应文件的读写指针到文件开始SEEK_SET偏移0个位置即文件开始处

	int fd4;
	fd4 = open("./build/open_test.txt", O_RDWR | O_TRUNC | O_CREAT, 0766);
	printf("open_test.txt fd4=%d\n", fd4);
	char buf2[1024]; //定义读写的缓冲区1024的整数倍
	int n2;
	//read()读到文件末尾时,读写指针指向文件末尾,读出的数据为0,以此作为循环结束
	//使用n2记录read()读出的文件数据个数,以便write()的第三个参数可以准确使用写入的数据个数
	while ((n2 = read(fd3, buf2, 1024)) > 0)
	{
		write(fd4, buf2, n2);
		printf("写入%d个数据\n", n2);
	}
	close(fd3);
	close(fd4);

	return 0;
}


2. C语言中的FILE文件操作

FILE*是 C 标准 I/O 的抽象句柄,内部包含一个内核里的文件描述符 fd。其原型如下:

typedef struct _IO_FILE FILE;

2.1 fopen文件打开操作

// 按照指定路径打开一个文件
FILE *fopen(const char *path, const char *mode);
  • 返回值:返回打开文件成功的FILE*指针,打开失败时返回NULL

  • path:指定要被打开的文件路径

  • mode指定文件的具体打开方式,包括以下几种方式及其组合方式:

    模式 含义 详细说明
    "r" 只读模式 文件必须存在,否则打开失败。指针定位到文件开头
    "w" 写入模式 若文件存在则清空原内容;若不存在则创建新文件指针定位到文件开头
    "a" 追加模式 若文件存在,指针定位到文件末尾(新数据追加到尾部);若不存在则创建新文件
    "b" 二进制模式 需要和”r“、”w“、"a"组合,默认为文本方式,使用该模式表示显示使用二进制方式读、写、追加
    "+" 增加权限 需要和”r“、”w“、"a"组合,在原来的读或写权限上增加写或读权限,使用该模式表示读写模式

    ⚠️ 重要注意事项

    • 文件指针位置:除追加模式("a", "a+", "ab+")外,其他模式初始指针均在文件开头。
    • 数据覆盖风险"w", "w+", "wb+"会立即清空文件内容,需谨慎使用。需要追加写时必须使用"a", "a+", "ab+"
    • 文本模式和二进制模式的跨平台差异
      • 在 Windows 中:"r""rb"行为不同。
        • 在文本模式下会转换换行符 \n\r\n,在向文件写入\n时,实质会写入\r\n,在从文件读取到\r\n时,只会读出\n
        • 在二进制模式下不会进行任何转换包括换行符\n\r\n的转换,原内容是什么,在读出和写入时就是什么内容
      • Linux/Unix 中:"b"被忽,Linux系统不区分文本与二进制文件并且也不会做任何转换,换行就是"\n",不需要转换

2.2 文件读/写操作

  • 字节读写操作

    // 通过文件指针从文件中读取出一个字节返回
    int fgetc(FILE *stream);
    // 通过文件指针向文件中写入一个字节
    int fputc(int c, FILE *stream);
    

    fgetc函数和fputc函数适用于需要逐字节方式读取或写入文件,常用于进行协议解析、需要按字节进行查找处理的情况

    • fgetc函数执行成功会返回读取的文件字节,执行失败会返回EOF(-1)。使用int类型可以方便的表示错误情况需要返回-1,在执行成功的情况下获取的文件字符的值范围为0-255,本质上只使用了低8位即1个字节。
    • fputc函数执行成功会返回写入字节的值范围为0-255,执行失败会返回-1
    // 循环从文件中读取一个字符,直到读到文件末尾返回EOF
    int c;
    while ((c = fgetc(fp)) != EOF)
        putchar(c);
    
  • 文本行读写操作

    // 从文件读出一行字节或最多size-1个字节到缓冲区buf中
    char *fgets(char *buf, int size, FILE *stream);
    // 向文件写入一个字符串str
    int fputs(const char *str, FILE *stream);
    

    fgets函数和fputs函数允许通过逐行方式读取或写入文件,注意这种方式不适于读取和写入二进制文件,因为以函数以\n标记一行的结束,对于二进制文件容易误判\n

    • fgets函数会从文件中读取1行字节数据存储在缓冲区buf中,并且会保留换行符\n,最多读取出缓冲区大小size-1个字节数据或读到文件末尾EOF,并在最后添加字符串结束符\0
      • 返回值**:失败或文件尾的EOF状态返回NULL,成功返回非空指针**
      • buf:表示存放从文件读取一行字节数据存放的缓冲区。
      • size:表示缓冲区buf大小,避免越界,最多只会读取1行中size-1个字节数据,以便最后可以添加\0结束符。
      • stream:表示要读取文件的文件指针。
    • fputs函数会向文件中写入一个字符串str,并且不会写入字符串结束符\0,也不会自动添加写入\n换行符
      • 返回值:成功返回大于或等于0的数,失败返回EOF(-1)
      • str:要写入文件的字符串str
      • stream:表示要写入文件的文件指针
    // 循环从文件读取一行字符串数据,直到文件尾
    char line[1024];
    while (fgets(line, sizeof(line), fp))
        printf("%s", line);
    
  • 二进制读写操作

    // 向文件写入nmemb个大小为size的元素的二进制数据
    size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
    // 从文件读取nmemb个大小为size的元素的二进制数据
    size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
    

    fread函数和fwrite函数适用于以二进制内存数据的方式从文件写入或读取到变量

    • 返回值:返回完成操作的元素个数,当返回值为nmemb时表示完全成功,否则表示部分成功、EOF或者出现错误
    • ptr:指定该变量的地址,必须要准备好空间
    • size:指定该变量的单个元素字节大小,一般使用sizeof函数进行计算
    • nmemb:指定该变量的相同元素个数,对于非数组类型一般为1,对于数组类型为该数组元素个数
    • stream:指定要被写入文件或从文件中读取的文件指针
    // 向文件已二进制内存数据读取方式
    struct Header h;
    fread(&h, sizeof(h), 1, fp);
    
    // 向文件以二进制内存数据写入方式
    struct Header head[10];
    fwrite(head, sizeof(head[0]), sizeof(head)/sizeof(head[0]), fp);
    
  • 格式化读/写操作

    // 按照指定格式化字符串format,将变量数据写入到文件中
    int fprintf(FILE *stream, const char *format, ...);
    // 按照指定的格式化字符串format,从文件读出数据写入到变量中保存
    int fscanf(FILE *stream, const char *format, ...);
    

    fprintf函数和fscanf函数分别用于按照指定的格式化字符串将变量数据写入或读取文件中,相对应的有按照指定格式化字符串写入或读取缓冲区的snprintf函数和sscanf函数,还有常见的从控制台进行格式输出和读取的printf函数和scanf函数。

    • stream:要写入或读取的文件指针
    • format:指定要写入或读取的格式化字符串
    • ...:不定参数方式,可以提供任意个和格式化字符串相匹配的,并需要被写入的变量数据

2.3 fseek文件读写指针操作

// 设置文件读写指针的字节位置
int fseek(FILE *stream, long offset, int whence);
// 获取文件读写指针的当前字节位置
long ftell(FILE *stream);

fseek函数用于设置文件当前读取或写入文件的读写指针字节位置

  • 返回值:返回0表示成功,返回非0表示失败
  • stream:文件指针
  • offset:偏移量,可以为负数表示向前偏移,可以为正数表示向后偏移
  • whence:基准位置,常用以下几个宏:
    • SEEK_SET文件起始位置
    • SEEK_CUR文件当前位置
    • SEEK_END文件末尾位置

ftell函数用于获取文件读写指针当前字节位置

  • 返回值:大于或等于0的返回值表示当前文件的字节位置,-1表示失败
  • stream:表示文件指针
// 通过文件读写指针获取文件大小的经典方式
fseek(fp, 0, SEEK_END);
long size = ftell(fp);

2.4 fclose文件关闭操作

  • fflush文件刷新操作

    // 刷新文件内部缓冲区数据并写入磁盘,但不关闭文件
    int fflush(FILE *stream);
    
    • 返回值:返回0表示成功,返回-1表示失败
    • stream:即需要刷新的文件指针
  • fclose文件关闭操作

    // 刷新文件内部缓冲区数据并写入磁盘,最后解除文件锁,然后关闭文件
    int fclose(FILE *stream);
    
    • 返回值:返回0表示成功,返回EOF(-1)表示失败
    • stream:已经通过fopen函数打开的并要进行关闭的文件指针

3. C++中的标准文件操作

​ C++ 标准文件操作的核心是 <fstream> 库,它建立在 RAII(资源获取即初始化) 理念之上,相比 C 语言的 FILE*,最大的优势是自动资源管理类型安全

C++ 将文件操作抽象为“流”(Stream),主要分为三类:

类名 用途 对应 C 语言
std::ifstream 输入文件流(读) fopen("r")
std::ofstream 输出文件流(写) fopen("w")
std::fstream 输入输出流(读写) fopen("r+")
  • 析构即关闭对象离开作用域时,析构函数会自动调用 close(),杜绝忘记 fclose导致的句柄泄漏。

3.1 打开/关闭文件

  • 在创建文件流对象时,可以提供文件路径以直接在构造函数中打开,在没有提供文件打开方式时,相应类型的文件流对象以相应的默认方式进行打开,也可以显示提供文件打开方以指定方式打开

    #include <fstream>
    #include <iostream>
    
    // 写文件
    std::ofstream ofs("output.txt"); // 默认为只写方式打开:std::ios::out
    if (!ofs.is_open()) {
        std::cerr << "Failed to open file for writing!" << std::endl;
    }
    
    // 读文件
    std::ifstream ifs("input.txt"); // 默认为只读方式打开:std::ios::in
    if (!ifs) { // 可直接用 if (!ifs) 判断
        std::cerr << "Failed to open file for reading!" << std::endl;
    }
    
  • 在创建文件流对象后,可以手动调用成员函数open以指定打开模式打开指定路径下的文件

    // 以允许读写并且二进制方式打开文件
    std::fstream fs;
    fs.open("data.bin", std::ios::in | std::ios::out | std::ios::binary);
    

    常用打开模式(Mode Flags):这些标志定义在 std::ios中,可以位或(|)组合使用

    标志 含义
    std::ios::in 读,默认的std::ifstream打开方式
    std::ios::out 写,默认的std::ofstream打开方式
    std::ios::app 追加(每次写都在末尾)
    std::ios::trunc 截断(清空原文件)
    std::ios::binary 二进制模式(强烈推荐用于数据的文件读写)
    std::ios::ate 打开后立即定位到末尾

    ⚠️ 警告如果不指定 appate,单纯用 out可能会清空文件

3.2 文件读写操作

  • 文本读写方式std::ifstream类似于std::cin重载>>运算符将数据从文件读到变量数据,std::ofstream类似于std::cout重载<<运算符将数据从变量写入文件中,std::fstream允许通过<<>>运算符进行同时读写。

    // 写文本
    std::ofstream ofs("log.txt");
    ofs << "IP: 192.168.1.1" << std::endl;
    ofs << "Port: " << 8080 << std::endl;
    
    // 读文本(按行)
    std::ifstream ifs("config.txt");
    std::string line;
    while (std::getline(ifs, line)) {
        std::cout << line << std::endl;
    }
    
  • 二进制读写方式:使用成员函数write将变量的二进制内存数据写入文件中,使用成员函数read将文件中的二进制内存数据写入变量中,不可以使用<<>>运算符实现二进制数据写入或读出文件,因为格式化字符方式可能会出现类型转换等无法保证原二进制数据一致。打开文件时需要显式使用二进制方式std::ios::binary打开文件,,否则 Windows 下会把 \n转义,导致数据损坏。注意成员函数**writeread也可以用于非二进制数据的文本数据的读写文件操作**,但一般使用<<>>重载运算符更方便。

struct Header {
    uint32_t magic;
    uint32_t length;
};

Header hdr;
hdr.magic = 0xDEADBEEF;
hdr.length = 1024;

// 写二进制
std::ofstream ofs("data.bin", std::ios::binary); // 必须显示使用二进制方式打开std::ios::binary
ofs.write(reinterpret_cast<char*>(&hdr), sizeof(Header));

// 读二进制
std::ifstream ifs("data.bin", std::ios::binary); // 必须显示使用二进制方式打开std::ios::binary
ifs.read(reinterpret_cast<char*>(&hdr), sizeof(Header));

3.3 文件读写指针

C语言的FILE*文件指针的读写指针位置相同。C++的文件流对象允许文件读写指针位置分离,但是这种分离是逻辑上的分离,在底层要求读写指针位置必须相同,因此在进行读写混合操作同一个文件流时,向文件写入数据后会自动更新写指针位置或从文件读出数据后会自动更新读指针位置,此时必须更新读指针位置和写指针位置相同,否则会出现未定义行为

fs << "HELLO"; 			// 向文件流对象fs写入数据
fs.seekg(fs.tellp());	// 同时更新当前读指针位置和写指针位置相同

操作读写指针的成员函数

函数 作用
seekg(off_type off, ios_base::seekdir dir) 设置指针位置 (Get)
seekp(off_type off, ios_base::seekdir dir) 设置指针位置 (Put)
tellg() 获取当前位置
tellp() 获取当前位置
  • off:表示偏移字节数,为负数表示向前偏移,为正数表示向后偏移。

  • dir:表示基准位置的宏,常见的表示文件基准位置的宏包括:

    枚举值 含义 等价 C 语言
    std::ios::beg 文件起始 SEEK_SET
    std::ios::cur 当前位置 SEEK_CUR
    std::ios::end 文件末尾 SEEK_END
std::fstream fs("data.bin", std::ios::in | std::ios::out | std::ios::binary);

// 移动到文件末尾
fs.seekg(0, std::ios::end);
std::streampos fileSize = fs.tellg();

// 回到开头
fs.seekg(0, std::ios::beg);

// 跳到第 100 个字节
fs.seekg(100, std::ios::beg);

3.4 状态检查与错误处理

C++ 流对象通过重载bool运算符,允许可以像布尔值一样被判断文件流是否正常出错。

函数 说明
is_open() 判断文件是否成功打开,一般在文件首先打开时使用
good() 判断文件流是否正常或下面3种出错一般直接使用文件流对象重载bool运算符,较少直接使用该函数
eof() 判断文件是否到达末尾,不可作为while循环判断文件是否结束标志,其只会在读到文件尾时在读一次出错时设置
fail() 判断文件是否出现逻辑错误(如格式转换失败),非致命错误,可使用clear函数恢复状态
bad() 判断文件是否出现致命错误(如磁盘损坏),致命错误,文件不可用
clear() 用于在文件操作出现非致命错误时,还想重新操作时必须先清除错误状态

标准读取循环(推荐写法)

// 默认以只读方式std::ios:in方式打开文件,以文本文件方式进行读取
std::ifstream ifs("data.txt");
if (!ifs.is_open()) return;

// 这里使用std::getline函数循环按行读取文件流数据
std::string line;
// 使用文件流对象重载bool运算符进行判断流是否正常或出错,不直接使用good成员函数,也不可以直接使用eof成员函数判断文件结束标志
while (std::getline(ifs, line)) {
    std::cout << line << std::endl;
}

if (ifs.eof()) {
    std::cout << "正常结束" << std::endl;
} else if (ifs.fail()) {
    std::cerr << "格式错误" << std::endl;
} else if (ifs.bad()) {
    std::cerr << "IO 错误" << std::endl;
}

更多推荐