目录
一. IO缓冲区存在的意义 (屏蔽低级IO)
二. C标准IO + C++标准IO 刨析及其解决字符串输入空格结束问题刨析 (以及常用IO函数刨析)
三. C 文件IO (从常用函数刨析到具体案例实现)
四. C++ 文件IO (从常见方法刨析到 案例实现)
五. 从C语言的 fprintf 进化到 sstream 实现序列化和反序列化
六. 总结本章
缓冲区好处 :
[tangyujie@VM - 4 - 9 - centos ~]$ cat test.c #include <stdio.h> int main() { printf("haha"); //fflush(stdout); while (1); return 0; }
此处给出格式化输入的办法: 专门应对 C语言想要输入字符串 但是不想被中间空格截断情况
#include <stdio.h> int main() { char buff[256] = { 0 }; scanf("%[^\n]%*c", buff); printf("buff: %s\n", buff); return 0; }
注意点 :
使用 getline 全局函数 解决 cin 空格结束问题
int main() { string s; //cin >> s; //空格就结束了, 达不到输入一行的效果 getline(cin, s); //成功读取一行,以回车键结束 cout << s << endl; return 0; }
内置类型, 标准内库已经帮助我们做好了重载了, 所以自定义类型可以直接进行输入
最后再解决一个C++ 中循环输入的疑惑问题, 为何C++可以支持 while (cin >> 对象 ) {操作;} 这样的奇葩操作??? cin 的返回值 就是 istream流对象
代码写起来, 学以致用
int main() { FILE* fp; if ((fp = fopen("./test.txt", "r+")) == NULL) { perror("Error open file"); exit(EXIT_FAILURE); } char ch; //一定先读取之后才判断是否是结尾 /* 或者这样写 ch = fgetc(); while (ch != EOF) { 操作; ch = fgetc(fp); } */ //又或者是这样写 /* ch = fgetc(fp); while (!feof(fp)) { fputc(ch, stdout);//操作 ch = fgetc(fp); } */ /* while ((ch = fgetc(fp)) != EOF) { fputc(ch, stdout); //操作 } */ fclose(fp); //最后一定关闭文件, 养成好习惯 return 0; }
继续文本文件操作: 接下来介绍格式化输入输出函数....
测试使用:
#include <stdio.h> #include <time.h> #include <stdlib.h> int main() { srand((unsigned int)time(NULL)); FILE* fp; if ((fp = fopen("./test.txt", "r+")) == NULL) { perror("Error open file"); exit(EXIT_FAILURE); } int a, b; for (int i = 0; i < 3; ++i) { fprintf(fp, "%d\t%d\n", a = rand() % 1024, b = rand() % 1024); printf("a: %d, b: %d\n", a, b); } /* //先写入后读取 int aa, bb; for (int i = 0; i < 3; ++i) { fscanf(fp, "%d\t%d\n", &aa, &bb); printf("aa: %d, bb: %d\n", aa, bb); } */ fclose(fp); return 0; }
学以致用, 常规用法: 一般来说二进制内存块文件读写操作都是针对数组 这种连续内存比较合适, 不论什么数组, 或者是结构体也是可以的...... 核心关键 : 内存连续 多个内存小块(一个内存大块)
typedef struct student { int age; char name[20]; }STU; int main() { //针对结构体数组其实用二进制块读写操作简直不要太爽 (小缺陷: 乱码) STU s[3] = { { 18, "张三" }, { 20, "李四" }, { 24, "王五" } }; FILE* fp; if ((fp = fopen("myfile.bin", "rb+")) == NULL) { perror("Error open file"); exit(EXIT_FAILURE); } //fwrite(s, sizeof(STU), 3, fp); /* //先写入之后屏蔽写入放开读取测试 int n = fread(s, sizeof(STU), 3, fp);//从里面读取 //n : 成功读取的小块数目 for (int i = 0; i < n; ++i) { printf("name: %s, age: %d\n", s[i].name, s[i].age); } */ fclose(fp); return 0; }
int main() { FILE* fp; if ((fp = fopen("test.txt", "r+")) == NULL) { perror("Error open file"); exit(EXIT_FAILURE); } //首先是演示常用功能1:fseek + ftell 可获取文件大小 { fseek(fp, 0, SEEK_END);//定位文件末尾 int file_size = ftell(fp);//获取文件开头到当前位置字节数 printf("file_size: %d\n", file_size); } //演示功能 2: fseek 如何达到和rewind一致效果 { //rewind(fp); fseek(fp, 0, SEEK_SET); printf("cur position is %d\n", ftell(fp)); } return 0; }
基本面向对象式的实现文件读写操作, 核心 C++ 相对于C 函数式 -----> 过度到 面向对象实现文件IO操作.. (fstream 基类 : 既可以完成文件读也可以完成文件写操作, ifstream 仅文件读操作, ofstream, 仅文件写操作, C++文件读写操作 都是基于上面三个类的实例化对象完成的......)
图解刨析一下上述类的构造函数:
C++文件操作流程
文本读写操作
代码测试:
int main() { { ifstream ifs("test.txt"); //mode默认 ios::in if (!ifs) { cerr << "destruct ifs error" << endl; exit(EXIT_FAILURE); } char ch; /*方式1: while ((ch = ifs.get()) != EOF) { cout << ch; } */ /*方式2: while (ifs.get(ch)) { cout << ch; } */ //方式3: while (ifs) { cout << (char)ifs.get(); } } ifs.close(); //也可以不写, 析构中会清理 return 0; }
实例测试:
int main() { //文件写 默认是 ios::out, 不存在会create a file ofstream ofs("writefile.txt"); char ch; //提示一个 \n回车换行键 作为结束 cout << "Type some text (type a \\n to finish):" << endl; do { ch = cin.get(); ofs.put(ch); } while (ch != '\n'); return 0; }
二进制文件读写操作 + << >> 文件读写操作: ( 使用 << 和 >> 替代了 C 中的格式化文件读写操作 ) << 和 >> 插入提取运算符 重载 代替了 C 语言中的格式化输入输出方式
//基础案例: int main() { //先写点东西进去, 然后才能够读取 /* ofstream ofs("test.txt", ios::out | ios::binary); ofs.write("Hello write\n", strlen("Hello write\n")); ofs.close(); */ ifstream ifs("test.txt", ios::in | ios::binary); if (!ifs) { cerr << "Error open file" << endl; exit(EXIT_FAILURE); } //第一步先获取文件大小, 开缓冲区..... ifs.seekg(0, ifs.end); int length = ifs.tellg(); ifs.seekg(0, ifs.beg); char *buff = new char[length]; ifs.read(buff, length); //此处体现的C++ 判断读取的方式是直接 判断 ifs(对于bool的重载) //C语言则是通过返回值判断 if (ifs) { cout << "Read all bytes success and read " << length << " bytes" << endl; buff[length] = 0; cout << buff << endl; } else { cerr << "Read error" << endl; exit(EXIT_FAILURE); } ifs.close(); //有始有终, 虽然最后ifstream的析构中会处理 return 0; }
流式用法, 替换C语言的格式化, 简易测试一下:
int main() { int age = 10; char name[20] = "张三"; //先向文件中流入数据 /* ofstream ofs("test.txt"); ofs << age << "\t" << name; //直接进行数据流入 ofs.close(); //好习惯 //(类比 cout << 向标准输出设备文件显示器上输出) */ ifstream ifs("test.txt"); if (!ifs) { cerr << "Error open file" << endl; exit(EXIT_FAILURE); } ifs >> age >> name; //数据流出操作 //(类比 cin >> 从标准输入设备文件键盘上读取数据存储到内存) cout << "age: " << age << ", name: " << name << endl; ifs.close(); //好习惯 return 0; }
来一个总的类 测试案例 (这个是比较常用的, ip + port管理类):
char* <-----> int C语言版本函数刨析 :
struct ServerInfo { char _ip[20]; int _port; }; struct ConfigManager { public: ConfigManager(const char* filename) :_filename(filename) {} void ReadBin(ServerInfo& info) { ifstream ifs(_filename); ifs.read(char*)&info, sizeof(info)); } void WriteBin(const ServerInfo& info) { ofstream ofs(_filename); ofs.write((char*)&info, sizeof(info)); } void WriteTxt(const ServerInfo& info) { /*ofstream ofs(_filename); ofs.write(info._ip, strlen(info._ip)); ofs.put('\n'); string portstr = to_string(info._port); ofs.write(portstr.c_str(), portstr.size());*/ // C++流多提供的,其他的c一样都可以实现 ofstream ofs(_filename); ofs << info._ip <<"\n"<< info._port; } void ReadTxt(ServerInfo& info) { //ifstream ifs(_filename); //ifs.getline(info._ip, 20); //char portbuff[20]; //ifs.getline(portbuff, 20); //info._port = stoi(portbuff); // C++流多提供的,其他的c一样都可以实现 ifstream ifs(_filename); ifs >> info._ip >> info._port; } private: string _filename; }; //int main() //{ // ServerInfo rinfo; // ServerInfo winfo = {"192.0.0.1", 8000}; // // 读写 -- 二进制 -- 读写简单、高效快捷。 缺点:除了字符和字符串,内存中写到文件,是乱码 // /*ConfigManager cfbin("config.bin"); // cfbin.WriteBin(winfo);*/ // //ConfigManager cfbin("config.bin"); // //cfbin.ReadBin(rinfo); // // // 读写 -- 文本 // //ConfigManager cftxt("config.txt"); // //cftxt.WriteTxt(winfo); // // ConfigManager cftxt("config.txt"); // cftxt.ReadTxt(rinfo); // // // return 0; //}
文件定位操作
测试代码:
int main() { ifstream ifs("test.txt"); if (!ifs) { cerr << "Destruct ifstream obj error" << endl; exit(EXIT_FAILURE); } int length; ifs.seekg(0, ifs.end); length = ifs.tellg(); ifs.seekg(0, ifs.beg); //回到开头了 cout << "file_size: " << length << endl; ifs.close(); while (1); return 0; } int main() { ifstream ifs("test.txt"); if (!ifs) { cerr << "Destruct ifstream obj error" << endl; exit(EXIT_FAILURE); } int length; ifs.seekg(0, ifs.end); length = ifs.tellg(); ifs.seekg(0, ifs.beg); //回到开头了 cout << "file_size: " << length << endl; ifs.close(); return 0; }
代码测试:
//案例1 int main() { int a = 10; char s[5] = "abc"; char buff[20] = { 0 }; sprintf(buff, "%d %s", a, s); //序列化 a = 100; s[0] = 'c'; sscanf(buff, "%d %s", &a, s); //反序列化还原 cout << a << " " << s << endl; while (1); return 0; } //案例2: /* int main () { char sentence []="Rudolph is 12 years old"; char str [20]; int i; sscanf (sentence,"%s %*s %d",str,&i); printf ("%s -> %d\n",str,i); return 0; } */
既然是流所以还是支持流式的数据流入和流出的..... 测试代码如下:
int main() { stringstream ss; int age = 20; char name[20] = "张三"; ss << age <<"\t"<< name;//先流入到 ss 中去 age = 100; strcpy(name, "12345"); //先改一下 这样后面才知道是不是流出了数据的 ss >> age >> name; cout << "age: " << age << " " << "name: " << name << endl; return 0; }
从一道实例题目上手: 序列化和反序列化二叉树
剑指 Offer II 048. 序列化与反序列化二叉树
class Codec { public: // Encodes a tree to a single string. string serialize(TreeNode* root) { string ans = ""; if (root == NULL) return ans; //直接返回就是了 stringstream in_out; in_out << root->val; //流入根部 in_out >> ans; //每次将ss流入到ans中 /* 1.后面只要右孩子 就需要加上 () 2.如果有右孩子加上 , 3. 先 ( + 左孩子序列 + , + 右孩子序列 + ) */ if (root->left || root->right) { ans += "("; //只要有一个孩子加上( } ans += serialize(root->left); // + 左孩子 if (root->right) ans += ","; //存在右孩子 + , ans += serialize(root->right); if (root->left || root->right) { ans +=")"; } return ans; } int toi(const string& s, int& i) { int val = 0; bool flag = 0; //标记负数 if (s[i] == '-') { flag = 1; i += 1; //跳过负号 } for (; s[i] >= '0' && s[i] <= '9'; ++i) { val = val * 10 + (s[i] - '0'); } if (flag) val *= -1; return val; } // Decodes your encoded data to tree. TreeNode* deserialize(string data) { //广义表转二叉树 TreeNode* root = NULL, *pTemp = NULL; //保存 根部 + 临时节点 stack<TreeNode*> st; bool flag = 0; //标记是否存在右孩子 for (int i = 0; i < data.size(); ++i) { switch(data[i]) { case '(' : { st.push(pTemp);// ( : 节点入栈 flag = 0; // 重置 flag } break; case ',' : { flag = 1;//说明存在右孩子 } break; case ')' : { root = st.top();//记录可能的根部 st.pop(); //) pop节点 } break; default : { //说明是一个实际节点 int val = toi(data, i); //提取数据 pTemp = new TreeNode(val); if (!st.empty() && flag == 0) { st.top()->left = pTemp; //栈顶节点的左孩子 } else if (!st.empty() && flag == 1) { st.top()->right = pTemp;//栈顶节点的右孩子 } i -= 1; //(需要体会) i -= 1(因为在 toi中 i走到哪里去了?) } break; } } if (root == NULL && pTemp) root = pTemp; //仅有一个孩子 return root; } };
- 首先我们从为何需要缓冲区 入手解析 C 和 C++ 这些对系统调用进一步封装实现的 更高层的IO方法的好处 (减少系统调用次数) 提高效率 便利写出可移植性更好的代码
- 然后解析 C 语言 IO 和 C++ IO 的 基本 函数 和 C++流式IO的引入, C++流式IO 核心关键在于operator << 和 operator >> 运算符的重载实现. 以及 C 语言 利用 %[^\n]%*c 实现输入字符串仅仅以回车结束, getline(cin, s); 实现 C++的字符串输入仅回车结束
- C语言常见的文件IO 文本 + 二进制 + 格式化 + 定位
- C++ 常见的文件IO 文本 + 二进制 + 流式(代替格式化) + 定位
- sscanf + sprintf 的字符串序列化 + 反序列化 实现 + 整形转字符串 + 字符串拼接 转换为 C++中的 利用 stringstream实现的 序列化和反序列化
上述仅个人的学习C和 C++的一个笔记小结, 很多是个人看法和用法, 如有不足或者错误之处欢迎大家改正, 祝看本文的人一切顺利,共同进步 (内容简单, 但书写不易, 如果觉得有帮助, 希望点赞支持, 三连缘分)