C语言, C++ IO 总结. 一篇文章帮你透析缓冲区存在的意义, C, C++ IO的常见用法

简介: C语言, C++ IO 总结. 一篇文章帮你透析缓冲区存在的意义, C, C++ IO的常见用法

一. IO缓冲区存在的意义    (屏蔽低级IO)

缓冲区一般是行设计....   也就是缓冲区大小一般就叫做一行, 一般就是1024字节 char buff[1024]

有了缓冲区, 我们就可以按照缓冲区大小进行读写操作, 基于行, 缓冲区 进一步向上封装成了C语言的  scanf 和 printf 这样的 可以跨平台使用的函数

缓冲区好处 :


可以屏蔽掉低级I/O的实现  (  低级IO就是基于操作系统内核实现的系统调用), 可以减少系统调用次数 (用户空间向内核空间切换次数) 提高效率   方便写出可移植的程序。

有了行的概念, 可以实现行读取, 解析整个缓冲区中的内容返回一个行

二. C标准IO + C++标准IO  刨析及其解决字符串输入空格结束问题刨析   (以及常用IO函数刨析)

FILE 的本质就是一个句柄  :   句柄思想, 使用简单的一个FILE* 就可以操作背后对应的文件, 做个比喻就是有点像是使用 遥控器 控制一个机器人, 通过对一个把手开关的简单操作, 将对应操作落实到一个比较复杂的东西上去,   操作 FILE就是操作文件 。。。 底层落实到 内核上的files_struct结构体上面去了


fflush :    将缓冲区中的内容实时刷新写入到文件中去.     (文件不一定是 stdout 其他也可)

其实可以简单做一个小小的测试....    如下使用sleep + printf()  +  fflush(stdout)  测试 fflush功能      stdin  + stdout    + stderr 是在一开始就打开的三个文件流...  操作系统给我们打开的            在  Linux 环境下测试才可获取结果:   windows环境下好像不行应该是做了优化

[tangyujie@VM - 4 - 9 - centos ~]$ cat test.c
#include <stdio.h>
int main() {
  printf("haha");
  //fflush(stdout);
  while (1);
  return 0;
}

希望读者自行测试,  上述存在 fflush 和 屏蔽fflush 效果是完全不一样的, 屏蔽掉发现显示器上没有输出, 说明  缓冲区中的数据没有刷新到显示器上 (没有写入标准输出设备文件中去)  解除屏蔽则可以瞬间刷新


然后接下来针对scanf提出一个小小要求 : 很多时候初学C语言的时候都在烦恼的问题. 想要输入一个完成的以行结束的字符串, 可以经常遇到  空格  就被截断的尴尬情况????? 咋办

此处给出格式化输入的办法:    专门应对 C语言想要输入字符串 但是不想被中间空格截断情况

#include <stdio.h>
int main() {
  char buff[256] = { 0 };
  scanf("%[^\n]%*c", buff);
  printf("buff: %s\n", buff);
  return 0;
}

C++ 中 为了充分体现面向对象编程的思想, 其中的  输入输出都是定义的类 然后重载 operator <<运算符 和  operator >> 运算符来实现的.  


而且将  ios 类叫做流类的基类:          为了实现这种流动,C++定义了I/O标准类库,这些每个类都称为流/流类,用以完成某方面的功能.....


C++ 将其形象的比作数据流, 使得  C++中对于数据的输入输出操作更加直观  。。。。  


C++标准库提供了4个全局流对象cin、cout、cerr、clog,使用cout进行标准输出,即数据从内存流向控制台(显示器)。使用cin进行标准输入即数据通过键盘输入到程序中,同时C++标准库还提供了cerr用来进行标 准错误的输出,以及clog进行日志的输出,从上图可以看出,cout、cerr、clog是ostream类的三个不同的 对象,因此这三个对象现在基本没有区别,只是应用场景不同。 在使用时候必须要包含文件并引入std标准命名空间。


注意点  :


空格和回车都可以作为数据之间的分格符   :  如何解决空格不能输入问题????


cin和cout可以直接输入和输出内置类型数据, 但是自定义类型必须自己重载operator << 和 operator >>才可以为什么???


 使用  getline 全局函数 解决 cin  空格结束问题

int main() {
  string s;
  //cin >> s;       //空格就结束了, 达不到输入一行的效果
  getline(cin, s);    //成功读取一行,以回车键结束
  cout << s << endl;
  return 0;
}

内置类型, 标准内库已经帮助我们做好了重载了, 所以自定义类型可以直接进行输入

最后再解决一个C++ 中循环输入的疑惑问题, 为何C++可以支持 while (cin >> 对象 ) {操作;}    这样的奇葩操作???   cin  的返回值 就是 istream流对象


这个操作的意思是什么???  意思就是说 返回值 istream 对象可以做判断, 对象如何做判断???


其实是因为 C++在  ios 类中重载了  operator bool    

三. C 文件IO (从常用函数刨析到具体案例实现)

  • 先是文本文件读写操作函数分析, 以及实际案例实操
  • 上述注意点,   以及所有文件读取操作的时候一定注意的就是 是不是文件末尾我们只有读取之后才知道, 所以一般为了避免对于文件末尾的多余操作, 很多时候会先读取后操作, 还有很多是在循环外面先读取一次文件, 其实都是这个道理

 代码写起来, 学以致用

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;
}
  • 定位函数 常用图解刨析    (定的什么位, 文件指针所指位)

活学活用, 上述 其实 rewind 完全是鸡肋了, 因为  fseek 完全可以代替其功能, 位异设置为0 直接起点文件指针设置为  SEEK_SET  可以达到一致效果:    如下代码端可以验证:

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++ 文件IO (从常见方法刨析到 案例实现)

基本面向对象式的实现文件读写操作,  核心 C++  相对于C   函数式 -----> 过度到 面向对象实现文件IO操作..     (fstream 基类 : 既可以完成文件读也可以完成文件写操作, ifstream 仅文件读操作, ofstream, 仅文件写操作,  C++文件读写操作 都是基于上面三个类的实例化对象完成的......)    

图解刨析一下上述类的构造函数:

C++文件操作流程


定义一个  fstream\ifstream\ofstream 对象,调用默认构造函数


调用open 成员函数 (打开文件, 建立 对象和磁盘文件的关联)


其实上述两个步骤可以合二为一(合成调用有参构造,在构造函数中传入文件参数)


调用成员函数进行文件读写操作  或者是  使用C++专属的  流式  操作, 其实就是对于 operator << 和  operator >> 运算符的重载, 利用 << 和  >> 运算符 进行直观的文件读写操作


( 其实底层的 << 和 >>  本质 还是在重载函数中   进行的调用成员函数处理方式,只不过不需要我们写了. 直接用<< 和 >> 非常便捷)  


文本读写操作

  • 第一个文本读get方法:    

代码测试:

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;
}

文本写put方法....

实例测试:

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 语言中的格式化输入输出方式


首先 文件打开方式需要重新设置, 需要加入  ios::binary 进去,  然后是介绍两个函数, 几乎和C用法差不多, 只不过变成了类的方法,  read() 和 write() 方法   (缺陷, 还是乱码)

//基础案例:
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管理类):

  • 两个(常用)函数刨析:string <-----> int   (C++的)
  • 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;
}

五. 从C语言的 fprintf 进化到 sstream  实现序列化和反序列化

  • 首先刨析一下  sprintf  +  sscanf
  • 代码测试:      
//案例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;
}
*/
  • C++ 中的 序列化  和  反序列化 (针对这个:专门设置了一个字符串流类)  string streams 字符串流

既然是流所以还是支持流式的数据流入和流出的.....     测试代码如下:

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;
    }
};

解决上述题目 首先需要运用序列化 利用 stringstream 将 所有的 int数据转换为 string 并且实现字符串的广义表形式表示


解码 就是广义表转二叉树的过程, 如下博客可以解决这个问题:https://blog.csdn.net/weixin_53695360/article/details/122895869

六. 总结本章

首先我们从为何需要缓冲区 入手解析 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实现的  序列化和反序列化



相关文章
|
25天前
|
安全 编译器 C语言
C++入门1——从C语言到C++的过渡
C++入门1——从C语言到C++的过渡
45 2
|
8天前
|
存储 编译器 C语言
如何在 C 语言中判断文件缓冲区是否需要刷新?
在C语言中,可以通过检查文件流的内部状态或使用`fflush`函数尝试刷新缓冲区来判断文件缓冲区是否需要刷新。通常,当缓冲区满、遇到换行符或显式调用`fflush`时,缓冲区会自动刷新。
|
8天前
|
存储 编译器 C语言
C语言:文件缓冲区刷新方式有几种
C语言中文件缓冲区的刷新方式主要包括三种:自动刷新(如遇到换行符或缓冲区满)、显式调用 fflush() 函数强制刷新、以及关闭文件时自动刷新。这些方法确保数据及时写入文件。
|
27天前
|
C语言 C++
C 语言的关键字 static 和 C++ 的关键字 static 有什么区别
在C语言中,`static`关键字主要用于变量声明,使得该变量的作用域被限制在其被声明的函数内部,且在整个程序运行期间保留其值。而在C++中,除了继承了C的特性外,`static`还可以用于类成员,使该成员被所有类实例共享,同时在类外进行初始化。这使得C++中的`static`具有更广泛的应用场景,不仅限于控制变量的作用域和生存期。
41 10
|
2月前
|
算法 机器人 C语言
ROS仿真支持C++和C语言
ROS仿真支持C++和C语言
53 1
|
25天前
|
C语言 C++
实现两个变量值的互换[C语言和C++的区别]
实现两个变量值的互换[C语言和C++的区别]
16 0
|
2月前
|
Linux C语言
C语言 文件IO (系统调用)
本文介绍了Linux系统调用中的文件I/O操作,包括文件描述符、`open`、`read`、`write`、`lseek`、`close`、`dup`、`dup2`等函数,以及如何获取文件属性信息(`stat`)、用户信息(`getpwuid`)和组信息(`getgrgid`)。此外还介绍了目录操作函数如`opendir`、`readdir`、`rewinddir`和`closedir`,并提供了相关示例代码。系统调用直接与内核交互,没有缓冲机制,效率相对较低,但实时性更高。
|
3月前
|
Java 数据处理
Java IO 接口(Input)究竟隐藏着怎样的神秘用法?快来一探究竟,解锁高效编程新境界!
【8月更文挑战第22天】Java的输入输出(IO)操作至关重要,它支持从多种来源读取数据,如文件、网络等。常用输入流包括`FileInputStream`,适用于按字节读取文件;结合`BufferedInputStream`可提升读取效率。此外,通过`Socket`和相关输入流,还能实现网络数据读取。合理选用这些流能有效支持程序的数据处理需求。
38 2
|
3月前
|
编译器 Linux C语言
【C++小知识】为什么C语言不支持函数重载,而C++支持
【C++小知识】为什么C语言不支持函数重载,而C++支持
|
3月前
|
存储 编译器 C语言
C++内存管理(区别C语言)深度对比
C++内存管理(区别C语言)深度对比
80 5