一、文件概述
1.1、文件基本概念
- 存储在外存储器(如,硬盘)上用
文件名标识
数据的集合,它可以永久地存储数据 - 一个文件有唯一的文件标识,包括文件路径、主文件名、文件后缀(扩展名)
1.2、文本文件和二进制文件
-
计算机的存储在
物理上
是二进制的,所以物理上所有的磁盘文件本质上都是一样的:以 字节为单位 进行顺序存储 -
从用户或者操作系统使用的角度(
逻辑上
)把文件分为:- 文本文件:基于 字符编码 的文件,常见编码有
ASCII、UNICODE、UTF-8
等,可用编辑器直接打开(可以正常解析) ,假设文件中有S 个 ASCII 字符,那么此文件的大小为S/1024/1024 MB
- 二进制文件:基于 值编码 的文件,采用 mp4、gif、exe 等特殊编码格式,文本编辑器并不认识这些编码格式,只能按照字符编码格式胡乱解析
- 总的来说,不同类型的文件有不同的编码格式,必须使用对应的程序(软件)才能正确解析,否则就是一堆乱码,或者无法使用
- 文本文件:基于 字符编码 的文件,常见编码有
-
举个例子:把整数 65535 写入到文件
- 以文本形式写入:文件大小就是
5
个字节,因为 65535 会被当成5 个 ASCII 字符
,每个 ASCII 字符对应的 ASCII 编码是一个 8 位的无符号整数 - 以二进制形式(
16位无符号整型
)写入:文件大小就是16/8=2
个字节 - 以二进制形式(
32位无符号整型
)写入:文件大小就是32/8=4
个字节 - 以二进制形式(
64位无符号整型
)写入,文件大小就是64/8=8
个字节
- 以文本形式写入:文件大小就是
1.3、文件指针
- 在 C 语言中用一个 结构体指针变量(使用
FILE*
来定义) 指向一个文件,这个指针称为文件指针,通过该指针对文件的 打开、读写、关闭 等进行操作 FILE *文件指针标识符; eg:FILE *fp1, *fp2, *fp3;
,定义三个文件指针fp1
、fp2
和fp3
,再通过fopen
函数使得他们能够分别指向各自的 文件缓冲区,程序就可以利用他们来访问文件了
typedef struct {
short level; // 缓冲区"满"或者"空"的程度
unsigned flags; // 文件状态标志
char fd; // 文件描述符
unsigned char hold; // 如无缓冲区不读取字符
short bsize; // 缓冲区的大小
unsigned char *buffer; // 数据缓冲区的位置
unsigned ar; // 指针,当前的指向
unsigned istemp; // 临时文件,指示器
short token; // 用于有效性的检查
} FILE;
二、C 文件基本操作
2.1、文件的打开与关闭(fopen/fclose
)
文件保存使用注意事项:
wb
:写入的数据将以二进制形式写入文件,保留所有字节的原始值,这种模式适用于处理非文本数据,例如图像、音频或视频文件
wt
:写入的数据将按照文本格式进行处理,此外还会进行字符编码的处理,确保写入的数据以正确的编码方式保存,这种模式适用于处理文本数据
#include <stdio.h>
int main(void) {
FILE *fp = fopen("/text/back.txt", "r");
if (fp == NULL) // 返回空,说明打开失败
{
perror("Open Error Reason is"); // 标准出错打印函数,能打印调用库函数出错原因
return -1;
}
fclose(fp); // 关闭文件
return 0;
}
2.2、按照格式化读写文件(fscanf/fprintf
)
fscanf()
和fprintf()
函数与前面使用的scanf()
和printf()
功能相似,都是格式化读写函数,两者的区别在于fscanf()
和fprintf()
的读写对象不是键盘和显示器,而是磁盘文件fscanf()
会从 file stream 中按照指定的格式不断的读取,一次读取一个匹配字符串(会自动添加字符串结束符\0
),直到没有匹配的字符串返回-1
(匹配到返回大于0
的整数)
// 按照指定格式读从文件中不断读取图片路径并写将处理后的字符串按照指定格式写入文件
#include <stdio.h>
#include "string.h"
typedef struct {
int xmin;
int ymin;
int xmax;
int ymax;
} ZRECT;
static void WriteResult2Txt(char *SrcName, ZRECT result) {
char DstFileName[512] = {0};
char FileName[512] = {0};
strncpy(DstFileName, SrcName, strlen(SrcName) - 4); // 去掉图片后缀名 .bmp
sprintf(FileName, "%s.txt", DstFileName); // 填充格式化字符串到 FileName 中
FILE *fp = fopen(FileName, "wt");
// 按照指定格式写入文件
fprintf(fp, "pos = %d, %d, %d, %d\n", result.xmin, result.ymin, result.xmax, result.ymax);
fclose(fp);
}
int main(int argc, char *argv[]) {
char filepath[] = "../all.set";
char filename[512] = {0};
FILE *fp = fopen(filepath, "rt");
if (fp == NULL) // 返回空,说明打开失败
{
perror("Open Error Reason is"); // 标准出错打印函数,能打印调用库函数出错原因
return -1;
}
int i = 0;
ZRECT result = {0};
// 按照指定格式从文件中不断读取图片路径,会自动添加字符串结束符,所以不需要 memset(filename, 0, 512);
while (fscanf(fp, "%s\n", filename) > 0) {
i++;
result.xmin += i;
result.ymin += i;
result.xmax += i;
result.ymax += i;
printf("filename=%s\n", filename);
WriteResult2Txt(filename, result);
}
fclose(fp);
return 0;
}
// imglist.txt 示例,将结果写到每个文件中 img/test_00001.txt
img/test_00001.bmp
img/test_00002.bmp
img/test_00003.bmp
2.3、按照块读写文件(fread/fwrite
)
fgets()
有局限性,每次最多只能从文件中读取一行内容,因为fgets()
遇到换行符就结束读取。如果希望读取多行内容,需要使用fread()
函数;相应地写入函数为fwrite()
- 按块读取示例
#include <stdio.h>
typedef struct _TEACHER {
char name[8];
int age;
} Teacher;
void test() {
FILE *fp_write = fopen("/text/back.txt", "wb");
if (fp_write == NULL) {
perror("fopen:");
return;
}
Teacher teachers[4] = {{"Obama", 33},
{"John", 28},
{"Edward", 45},
{"Smith", 35}
};
for (int i = 0; i < 4; i++) {
fwrite(&teachers[i], sizeof(Teacher), 1, fp_write);
};
fclose(fp_write);
FILE *fp_read = fopen("/text/back.txt", "rb");
if (fp_read == NULL) {
perror("fopen:");
return;
}
Teacher temps[4];
fread(&temps, sizeof(Teacher), 4, fp_read);
for (int i = 0; i < 4; i++) {
printf("Name:%s Age:%d\n", temps[i].name, temps[i].age);
}
fclose(fp_read);
}
int main() {
test();
return 0;
}
// 输出如下:
Name:Obama Age:33
Name:John Age:28
Name:Edward Age:45
Name:Smith Age:35
- 读取模型,分配内存,保存内存示例
#include<iostream>
#include <sys/stat.h>
// 获取文件大小(Bytes)
uint64_t get_file_size(const char *file_name) {
struct stat stat_buf;
if (stat(file_name, &stat_buf)) {
perror(file_name);
return -1;
} else {
return stat_buf.st_size;
}
}
// 获取文件大小并分配内存(用完记得释放)
uint8_t *alloc_mem_for_file(const char *file_name, uint64_t *size) {
uint8_t *virt = NULL;
FILE *in = NULL;
uint64_t file_size = 0;
do {
file_size = get_file_size(file_name);
if (!file_size) {
break;
}
virt = (uint8_t *) malloc(file_size);
if (!virt) {
printf("Can not alloc memory size: %u\n", file_size);
break;
}
in = fopen(file_name, "rb");
if (in == NULL) {
perror(file_name);
break;
}
if (fread(virt, file_size, 1, in) != 1) {
perror(file_name);
if (virt) {
free(virt);
virt = NULL;
}
break;
}
} while (0);
if (in) {
fclose(in);
in = NULL;
}
if (size && virt) {
*size = file_size;
}
return virt;
}
// 将指针指向的内存保存到相应的文件中
int32_t save_ptr2bin(void *pVirt, uint64_t u64Size, char *filename) {
FILE *pFile = fopen(filename, "wb");
if (pFile == NULL) return -1;
fwrite(pVirt, u64Size, 1, pFile);
fclose(pFile);
return 0;
}
int main() {
uint64_t u64size = 0;
char model_path[1024] = "./model/det.bin";
uint8_t *pu8ModelBuf = alloc_mem_for_file(model_path, &u64size);
printf("u64 size is %d", u64size);
char save_path[1024] = "./model/mem.bin";
save_ptr2bin(pu8ModelBuf, u64size, save_path);
if (pu8ModelBuf) {
free(pu8ModelBuf);
pu8ModelBuf = NULL;
}
return 0;
}
2.4、按照字符读写文件(fgetc/fputc/feof
)
#include <stdio.h>
#include <string.h>
int main(void) {
char chw;
char buf[] = "hello";
FILE *fpw = fopen("/text/back.txt", "w");
// 按照字符来写文件
for (int i = 0; i < strlen(buf); i++) {
chw = fputc(buf[i], fpw); // 利用写文件指针 fpw 写入字符 buf[i]
printf("ch = %c\n", chw);
}
fclose(fpw);
char chr;
FILE *fpr = fopen("/text/back.txt", "r");
// 按照字符来读文件
while (!feof(fpr)) // 文件没有结束,则执行循环
{
chr = fgetc(fpr); // 利用读文件指针 fpr 来读取字符
printf("ch = %c\n", chr);
}
fclose(fpr);
return 0;
}
2.5、按照行读写文件(fgets/fputs)
- 读取到的字符串会在末尾自动添加
'\0'
,n 个字符也包括'\0'
。也就是说,实际只读取到了n-1
个字符,如果希望读取100
个字符,n
的值应该为101
- 若在读取到
n-1
个字符之前如果出现了换行,或者读到了文件末尾,则读取结束。这就意味着,不管n
的值多大,fgets()
最多只能读取一行数据,不能跨行;将 n 的值设置地足够大,每次就可以读取到一行数据
#include <stdio.h>
#include <string.h>
int main(void) {
char *buf[] = {"123456\n", "bbbbbbbbbb\n", "ccccccccccc\n"};
FILE *fpw = fopen("/text/back.txt", "w");
// 按照行来写文件
for (int i = 0; i < 3; i++) {
fputs(buf[i], fpw); // 利用写文件指针 fpw 写入字符串 buf[i]
}
fclose(fpw);
char buf1[100] = {0}; // 指定保存到的字符数组,其长度应超过所有行中的最大长度
FILE *fpr = fopen("/text/back.txt", "r");
// 按照行来读文件
while (!feof(fpr)) // 文件没有结束,则执行循环
{
char *p = fgets(buf1, sizeof(buf1), fpr); // p 为成功读取到的字符串
if (p != NULL) {
printf("buf1 = %s", buf1);
printf("buf1 = %s", p);
}
}
fclose(fpr); // 关闭文件
return 0;
}
2.6、文件的随机读取(fseek/ftell/rewind)
2.7、删除文件、重命名文件
2.8、文件缓冲区
三、C++ 文件基本操作
3.1、C++ 文件类(文件流类)及用法详解
- C++ 标准库中提供了 3 个类用于实现文件操作,它们统称为文件流类,这 3 个类分别为:
ifstream
:专用于从文件中读取数据ofstream
:专用于向文件中写入数据fstream
:既可用于从文件中读取数据,又可用于向文件中写入数据; 这 3 个文件流类都位于<fstream>
头文件中,因此在使用它们之前,程序中应先引入此头文件
- 这 3 个文件流类的继承关系,如图 1 所示
fstream
类常用成员方法如下表所示:
3.2、C++ 文件的打开与关闭
- 打开文件可以通过以下两种方式进行:
- 调用流对象的 open 成员函数打开文件:
open(const std::string& __s, ios_base::openmode __mode = ios_base::in)
:第一个参数是字符串类的引用,第二个参数是打开模式标记 - 定义文件流对象时,通过构造函数打开文件:
basic_ifstream(const std::string& __s, ios_base::openmode __mode = ios_base::in)
,第一个参数是字符串类的引用,第二个参数是打开模式标记
- 调用流对象的 open 成员函数打开文件:
- 判断文件打开是否成功,可以看“
对象名
”这个表达式的值是否为true
,如果为 true,则表示文件打开成功 - 文件的打开模式标记代表了文件的使用方式,这些标记可以单独使用,也可以组合使用。表 1 列出了各种模式标记单独使用时的作用,以及常见的两种模式标记组合的作用
- C++ 中使用 open() 打开的文件,在读写操作执行完毕后,应及时调用
close()
方法关闭文件,或者对文件执行写操作后及时调用flush()
方法刷新输出流缓冲区
3.3、C++ 文件的读和写
-
C++ 标准库中,提供了 2 套读写文件的方法组合,分别是:
- 使用
>>
和<<
读写文件:适用于以文本形式读写文件 - 使用
read()
和write()
成员方法读写文件:适用于以二进制形式读写文件
- 使用
-
C++
>>
和<<
读写文本文件示例:
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
int main(int argc, char *argv[]) {
// 1、设置文本打开路径及文本保存路径
std::string FilePath = "../test_imgs.txt";
std::string SavePath = "../imgs/out_txts/det_out.txt";
// 调用流对象的 open 成员函数打开文件
// std::ifstream ImgLists;
// ImgLists.open(FilePath, std::ios::in);
// 2、定义文件流对象时,通过构造函数打开文件(读和写)
std::ifstream ImgLists(FilePath, std::ios::in);
std::ofstream DetOuts(SavePath, std::ios::out);
if (ImgLists && DetOuts) {
std::cout << "File Open Success!" << "\n";
} else {
ImgLists.close(); // 关闭已打开的文件
DetOuts.close();
}
// 3、循环读取文件将图片路径保存到临时变量中,遇到空格、回车键则返回 true,然后接着读取,直到文件尾返回 false
std::string TmpImgPath;
std::vector<std::string> ImgPaths; // 定义保存图片路径的容器,用于后续的写入实验
while (ImgLists >> TmpImgPath) { // 可以像用 cin 那样用 ifstream 对象
DetOuts << TmpImgPath << "\n"; // 可以像 cout 那样使用 ofstream 对象
ImgPaths.push_back(TmpImgPath); // 将临时变量写入容器中
}
// 4、关闭已打开的文件
ImgLists.close();
DetOuts.close();
// 5、使用循环读取容器中的内容
for (auto &it: ImgPaths) {
std::cout << it << "\n";
}
}
- C++ read()和write()读写二进制文件:
write(const _CharT* __s, streamsize __n)
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
int main(int argc, char *argv[]) {
// 1、设置文本打开路径及文本保存路径
std::string FilePath = "../test_imgs.txt";
std::string SavePath = "../imgs/out_txts/det_out.bin";
// 调用流对象的 open 成员函数打开文件
// std::ifstream ImgLists;
// ImgLists.open(FilePath, std::ios::in);
// 2、定义文件流对象时,通过构造函数打开文件(读和写)
std::ifstream ImgLists(FilePath, std::ios::in);
std::ofstream DetOuts(SavePath, std::ios::out | std::ios::binary);
if (ImgLists && DetOuts) {
std::cout << "File Open Success!" << "\n";
} else {
ImgLists.close(); // 关闭已打开的文件
DetOuts.close();
}
// 3、循环读取文件将图片路径保存到临时变量中,遇到空格、回车键则返回 true,然后接着读取,直到文件尾返回 false
std::string TmpImgPath;
while (ImgLists >> TmpImgPath) { // 可以像用 cin 那样用 ifstream 对象
DetOuts.write((char *) &TmpImgPath, TmpImgPath.size());
}
// 4、关闭已打开的文件
ImgLists.close();
DetOuts.close();
//6、以二进制读方式打开
std::ifstream inFile(SavePath, std::ios::in | std::ios::binary);
}
四、多文件编程
4.1、如何引用其它文件的代码(函数、变量等)
- 第一种方式:
include
,引用源文件通过包含被引用源文件对应的头文件来实现- 头文件里一般包含以下内容:
#include "head.h"
- 公用的函数、变量、宏及结构体的定义、自定义数据类型一般放在头文件中
- 嵌入包含其他头文件
- 头文件(
.h
)与源文件(.cpp
)的分离:调用者不关心函数的实现,另外一个好处是可以保护版权,我们在发布相关模块之前,可以将它们都编译成目标文件,或者打包成静态库,只要向用户提供头文件,用户就可以将这些模块链接到自己的程序中
- 头文件里一般包含以下内容:
// 函数声明写在头文件 Object.h 里,在主函数中包含相应头文件
// 不一定要把所有函数定义都拿出来,也可以只拿一部分出来
class Object
{
public:
int x;
void Test(); // 成员函数的声明
};
// 函数实现写在源文件 Object.cpp 里(源文件中包含相应头文件)
void Object::Test() // 成员函数写在外边,加上类名限定(Object::)
{
printf("x is %d \n", x); // 仍然可以省略this->
}
// 防止头文件被重复包含方法1
#ifndef _SOMEFILE_H
#define _SOMEFILE_H
// 需要声明的变量、函数
// 宏定义
// 结构体
#endif
// 防止头文件被重复包含方法2
#pragma once
- 第二种方式:
extern
,引用源文件通过extern
声明被引用源文件中的函数或变量等- extern 的作用: 告诉编译器,在某个
cpp
文件中,存在这么一个函数/全局变量,但全局变量的声明语句是不能加初始值的 - extern 存在的意义:使程序模块化,可以使得多人协作变得更简单,不同的人编辑不同的
cpp
文件(main 函数中可以调用在其它cpp
文件中定义的全局函数和全局变量) - 下面演示一个多文件编程,在下面的代码段中我们创建了两个源文件
main.c
和module.c
:module.c
是整个程序的一个模块,我们在其中定义了一个全局变量和一个函数main.c
是程序的主模块(主文件),它使用到了module.c
中的变量和函数
- extern 的作用: 告诉编译器,在某个
// module.c 源码
#include <stdio.h>
int m = 100;
void func(){
printf("Multiple file programming!\n");
}
// main.c 源码
#include <stdio.h>
extern void func();
extern int m;
int n = 200;
int main(){
func();
printf("m = %d, n = %d\n", m, n);
return 0;
}
// 输出结果如下
Multiple file programming!
m = 100, n = 200
4.2、多文件编程中命名空间的使用
C++
引入命名空间是为了避免合作开发项目时产生命名冲突,当进行多文件编程时,命名空间常位于.h
头文件中,源文件中实现时注意要加上命名空间前缀
// student_li.h
#ifndef _STUDENT_LI_H
#define _STUDENT_LI_H
namespace Li { //小李的变量定义
class Student {
public:
void display();
};
}
#endif
// student_li.cpp
#include "student_li.h"
#include <iostream>
void Li::Student::display() {
std::cout << "Li::display" << std::endl;
}
// student_han.h
#ifndef _STUDENT_HAN_H
#define _STUDENT_HAN_H
namespace Han { //小韩的变量定义
class Student {
public:
void display();
};
}
#endif
//student_han.cpp
#include "student_han.h"
#include <iostream>
void Han::Student::display() {
std::cout << "han::display" << std::endl;
}
//main.cpp
#include <iostream>
#include "student_han.h"
#include "student_li.h"
int main() {
Li::Student stu1;
stu1.display();
Han::Student stu2;
stu2.display();
return 0;
}
4.3、C++ 和 C 混合编译
- 一个项目中
.cpp
调用的头文件中含有需要gcc
编译的部分,那么需要使用extern “C”{}
让这段代码按照 C 语言的方式进行编译、链接
- 使用
extern “C”{}
的主要原因是:- C++ 对函数进行了重载,在编译生成的汇编码中会对函数的名字进行一些处理,加入比如函数的参数类型、个数、顺序等,而在 C 中,只是简单的函数名字而已,不会加入其它的信息
- 若在 C++ 中调用一个使用 C 语言编写的函数,C++ 会根据
C++名称修饰方式
来查找并链接这个函数,那么就会发生链接错误
// 1、C 和 C++ 中对同一个函数经过编译后生成的函数名是不相同的 C 函数: void MyFunc(){}, 被编译成函数: MyFunc C++ 函数: void MyFunc(){}, 被编译成函数: _Z6Myfuncv // C++ 中调用 MyFunc 函数,在链接阶段会去找 _Z6Myfuncv, // 结果是没有找到的,因为这个 MyFunc 函数是 C 语言编写的,编译生成的符号是 MyFunc
- 代码实现示例:
// 1、在 cpp 相应的 .h 头文件中包含如下代码
#ifdef __cplusplus // __cplusplus 是 g++ 编译器中的自定义宏,用于说明正在使用 g++ 编译
extern "C" {
#endif
// 一段声明代码,使用 gcc 来编译
#ifdef __cplusplus
}
#endif
// 2、直接在 cpp 使用 extern "C" 包含 C 头文件
extern "C" {
// 一段声明代码,使用 gcc 来编译
}