【重走C++学习之路】27、C++IO流

目录

一、C语言的IO函数

二、C++IO流

2.1 标准IO流

2.2 文件IO流 

三、stringstream

结语


一、C语言的IO函数

C 语言的 IO 操作主要通过标准库函数(如stdio.h中的printfscanffopenfreadfwrite等)实现。这些函数基于文件描述符格式化字符串,主要有以下特点:

  • 格式化控制
int num;
scanf("%d", &num);  // 必须指定%d格式
printf("Number: %d\n", num);
  • 基于文件指针
FILE* file = fopen("data.txt", "r");
if (file != NULL) 
{
    int value;
    fscanf(file, "%d", &value);
    fclose(file);
}
  • 错误处理

通过返回值(如EOFNULL)或设置errno来指示错误,需要手动检查。

C语言借助了相应的缓冲区来进行输入与输出,对输入输出缓冲区的理解:

  • 可以屏蔽掉低级I/O的实现,低级I/O的实现依赖操作系统本身内核的实现,所以如果能够屏蔽这部分的差异,可以很容易写出可移植的程序。
  • 可以使用这部分的内容实现“行”读取的行为,对于计算机而言是没有“行”这个概念,有了这部分,就可以定义“行”的概念,然后解析缓冲区的内容,返回一个“行”。

二、C++IO流

C++ 的 IO 操作是通过“流”(Stream) 来实现的。流是一种抽象的概念,表示数据从源(输入流)到目标(输出流)的连续传输,有序连续、具有方向性,C++ 标准库提供了一套完整的 IO 类层次结构,主要包括:

  • 输入流(istream):用于从文件或键盘读取数据。
  • 输出流(ostream):用于向文件或屏幕输出数据。
  • 输入 / 输出流(iostream):支持双向操作。
  • 文件流(fstream):用于文件操作,包括ifstream(输入文件流)和ofstream(输出文件流)。
  • 字符串流(sstream):用于内存中的字符串操作,包括istringstreamostringstream

C++系统实现了一个庞大的类库,其中ios为基类,其他类都是直接或间接派生自ios类

2.1 标准IO流

C++标准库提供了4个全局流对象cin、cout、cerr、clog,使用cout进行标准输出,即数据从内存流向控制台(显示器)。使用cin进行标准输入即数据通过键盘输入到程序中,同时C++标准库还提供了cerr用来进行标准错误的输出,以及clog进行日志的输出。

特点:

  • 类型安全

C++ 流通过运算符重载自动处理数据类型,不用和 C 语言一样使用格式化字符串(如%d%s)。

int num;
std::cin >> num;  // 自动识别int类型
std::cout << "Number: " << num << std::endl;
  • 扩展性

可以通过重载<<>>运算符为自定义类型添加 IO 支持。

class Point 
{
public:
    int x, y;

    friend std::ostream& operator<<(std::ostream& os, const Point& p) 
    {
        os << "(" << p.x << ", " << p.y << ")";
        return os;
    }
};

      注意:

      • cin为缓冲流。键盘输入的数据保存在缓冲区中,当要提取时,是从缓冲区中拿。如果一次输入过多,会保存在缓冲区中,如果输入错了,必须在回车之前修改,如果回车键按下就无法挽回了。只有把输入缓冲区中的数据取完后,才要求输入新的数据。
      • 输入的数据类型必须与要提取的数据类型一致,否则出错。出错只是在流的状态字state中对应位置置为1,程序继续。
      • 空格和回车都可以作为数据之间的分格符,所以多个数据可以在一行输入,也可以分行输入。但如果是字符型和字符串,则空格(ASCII码为32)无法用cin输入,字符串中也不能有空格。回车符也无法读入。    
      • 由于标准库已经将所有内置类型的输入和输出全部重载了,cin和cout可以直接输入和输出内置类型数据。
      • 对于自定义类型,如果要支持cin和cout的标准输入输出,需要对<<和>>进行重载。
      • 在线OJ中的输入和输出:
      单个元素循环输入
      while(cin >> a)
      {
          // ...
      }
       
      多个元素循环输入
      while(cin >> a >> b >> c)
      {
          // ...
      }
      
      整行接收
      while(cin >> str)
      {
          // ...
      }
      • istream类型对象转换为逻辑条件判断值
      istream& operator>> (int& val);
      explicit operator bool() const;

      这就是为什么能支持循环输入的原因,operator>>调用完后会返回istream对象,这个对象又调用了operator bool,operator bool调用时如果接收流失败,或者有结束标志,则返回false。

      2.2 文件IO流 

      C++根据文件内容的数据格式分为二进制文件文本文件

      二进制文件:

      里面是任意的字符,二进制文件是把内存中的数据按其在内存中的存储形式原样输出到磁盘上存放,也就是说存放的是数据的原形式。

      文本文件:

      里面没有特殊字符,都是ascii靠后的文本字符,都能打印来。我们说文本文件是特殊的二进制文件,是因为文本文件实际上的解释格式已经确定了:ASCII或者unicode编码。文本文件是把数据的终端形式的二进制数据输出到磁盘上存放,也就是说存放的是数据的终端形式。

      从存储方式来说,文件在磁盘上的存储方式都是二进制形式,所以,文本文件其实也应该算二进制文件。先从他们的区别来说,虽然都是二进制文件,但是二进制代表的意思不一样。 二进制读写是将内存里面的数据直接读写入文本中,而文本则是将数据先转换成了字符串,再写入到文本中。

      采用文件流对象操作文件的一般步骤:

      1. 定义一个文件流对象
      2. 使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系
      3. 使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写
      4. 关闭文件

      • ifstream ifile(只输入用)
      // 读取文本文件
      std::ifstream inFile("data.txt");
      if (inFile) 
      {
          std::string line;
          while (std::getline(inFile, line)) 
          {
              std::cout << line << std::endl;
          }
      }
      • ofstream ofile(只输出用)
      // 写入文本文件
      std::ofstream outFile("data.txt");
      if (outFile) 
      {
          outFile << "Name: John Doe" << std::endl;
          outFile << "Age: 30" << std::endl;
          outFile << "Score: 95.5" << std::endl;
      }
      • fstream iofile(既输入又输出用)

      在文件形式的读写中 ,怎么用cout、cin就怎么用 ifstream 对象和 ofstream 对象(cout和cin从某种意义上来说 ,是一种特殊的文件流对象 )。

      完整示例:

      #include <fstream>
      #include <iostream>
      #include <string>
      #include <vector>
      #include <stdexcept>
      
      // 学生结构体,用于二进制文件示例
      struct Student 
      {
          char name[50];
          int age;
          float score;
      };
      
      int main() 
      {
          try {
              /******************** 文本文件操作示例 ********************/
              std::cout << "=== 文本文件操作 ===" << std::endl;
              
              // 1. 创建并写入文本文件
              std::ofstream outFile("text_example.txt");
              if (!outFile) 
              {
                  throw std::runtime_error("无法创建文件!");
              }
              
              outFile << "这是一个文本文件示例。\n";
              outFile << "姓名: 张三\n";
              outFile << "年龄: 25\n";
              outFile << "成绩: 92.5\n";
              outFile.close();
              
              // 2. 读取文本文件 - 逐行读取
              std::ifstream inFile("text_example.txt");
              if (!inFile) 
              {
                  throw std::runtime_error("无法打开文件!");
              }
              
              std::string line;
              std::vector<std::string> lines;
              while (std::getline(inFile, line)) 
              {
                  lines.push_back(line);
              }
              inFile.close();
              
              // 3. 显示读取的内容
              std::cout << "读取的文本内容:" << std::endl;
              for (const auto& l : lines) 
              {
                  std::cout << l << std::endl;
              }
              
              // 4. 追加内容到文本文件
              std::ofstream appendFile("text_example.txt", std::ios::app);
              if (!appendFile) 
              {
                  throw std::runtime_error("无法打开文件进行追加!");
              }
              
              appendFile << "附加信息: 优秀学生\n";
              appendFile.close();
              
              
              /******************** 二进制文件操作示例 ********************/
              std::cout << "\n=== 二进制文件操作 ===" << std::endl;
              
              // 5. 创建并写入二进制文件
              Student s1 = {"李四", 22, 88.5f};
              Student s2 = {"王五", 23, 95.0f};
              
              std::ofstream binOutFile("binary_example.dat", std::ios::binary);
              if (!binOutFile) 
              {
                  throw std::runtime_error("无法创建二进制文件!");
              }
              
              binOutFile.write(reinterpret_cast<const char*>(&s1), sizeof(Student));
              binOutFile.write(reinterpret_cast<const char*>(&s2), sizeof(Student));
              binOutFile.close();
              
              // 6. 读取二进制文件
              std::ifstream binInFile("binary_example.dat", std::ios::binary);
              if (!binInFile) 
              {
                  throw std::runtime_error("无法打开二进制文件!");
              }
              
              // 读取第一个学生
              Student readStudent;
              binInFile.read(reinterpret_cast<char*>(&readStudent), sizeof(Student));
              
              // 显示第一个学生信息
              std::cout << "\n读取的第一个学生信息:" << std::endl;
              std::cout << "姓名: " << readStudent.name << std::endl;
              std::cout << "年龄: " << readStudent.age << std::endl;
              std::cout << "成绩: " << readStudent.score << std::endl;
              
              // 7. 使用seekg定位到第二个学生
              binInFile.seekg(sizeof(Student), std::ios::beg);
              binInFile.read(reinterpret_cast<char*>(&readStudent), sizeof(Student));
              
              // 显示第二个学生信息
              std::cout << "\n读取的第二个学生信息:" << std::endl;
              std::cout << "姓名: " << readStudent.name << std::endl;
              std::cout << "年龄: " << readStudent.age << std::endl;
              std::cout << "成绩: " << readStudent.score << std::endl;
              
              binInFile.close();
              
              
              /******************** 文件状态检查示例 ********************/
              std::cout << "\n=== 文件状态检查 ===" << std::endl;
              
              std::ifstream testFile("nonexistent_file.txt");
              if (!testFile) 
              {
                  std::cout << "文件打开失败!" << std::endl;
                  std::cout << "failbit: " << testFile.fail() << std::endl;
                  std::cout << "badbit: " << testFile.bad() << std::endl;
                  std::cout << "eofbit: " << testFile.eof() << std::endl;
              }
              
              std::cout << "\n所有操作完成!" << std::endl;
              return 0;
              
          } catch (const std::exception& e) 
          {
              std::cerr << "错误: " << e.what() << std::endl;
              return 1;
          }
      }

      三、stringstream

      C++ 的stringstream类允许将字符串作为流进行处理,就像操作文件或控制台一样,这种功能在字符串解析、格式化输出和数据转换等场景中特别有用。

      基本概念

      stringstream是一个模板类,定义在<sstream>头文件中,主要有三种类型:

      1. istringstream:用于从字符串读取数据(输入流)。
      2. ostringstream:用于向字符串写入数据(输出流)。
      3. stringstream:支持读写操作(双向流)。

      这些类继承自iostream,因此可以使用与文件流和标准 IO 流相同的接口。

      主要用途

      1. 字符串解析(将字符串分割为多个值)

      #include <sstream>
      #include <iostream>
      #include <string>
      
      int main() 
      {
          std::string input = "John 25 92.5";
          std::istringstream iss(input);
          
          std::string name;
          int age;
          double score;
          
          // 从字符串流中提取数据
          iss >> name >> age >> score;
          
          std::cout << "Name: " << name << std::endl;
          std::cout << "Age: " << age << std::endl;
          std::cout << "Score: " << score << std::endl;
          
          return 0;
      }

      2. 数据类型转换(如数字转字符串)

      #include <sstream>
      #include <iostream>
      #include <string>
      
      int main() 
      {
          int number = 123;
          std::ostringstream oss;
          
          // 将数字写入字符串流
          oss << number;
          
          // 转换为字符串
          std::string str = oss.str();
          
          std::cout << "Number as string: " << str << std::endl;
          
          return 0;
      }

      3. 字符串拼接(替代+操作符)

      #include <sstream>
      #include <iostream>
      #include <string>
      
      int main() 
      {
          std::string name = "Alice";
          int age = 30;
          double height = 1.75;
          
          std::ostringstream oss;
          oss << "Name: " << name << ", Age: " << age 
              << ", Height: " << height << "m";
          
          std::string result = oss.str();
          std::cout << result << std::endl;
          
          return 0;
      }

      4. 格式化输出(控制精度、填充等)

      #include <sstream>
      #include <iostream>
      #include <iomanip>
      
      int main() 
      {
          double pi = 3.14159265358979323846;
          
          std::ostringstream oss;
          oss << std::fixed << std::setprecision(4) << pi;
          
          std::string formatted = oss.str();
          std::cout << "Formatted PI: " << formatted << std::endl;
          
          return 0;
      }

      注意:

      1. stringstream实际是在其底层维护了一个string类型的对象用来保存结果。
      2. 多次数据类型转化时,一定要用clear()来清空,才能正确转化,但clear()不会将stringstream底层的string对象清空。
      3. 可以使用s. str("")方法将底层string对象设置为""空字符串。
      4. 可以使用s.str()将让stringstream返回其底层的string对象。
      5. stringstream使用string类对象代替字符数组,可以避免缓冲区溢出的危险,而且其会对参数类型进行推演,不需要格式化控制,也不会出现格式化失败的风险,因此使用更方便,更安全。

      结语

      可以看到所谓的IO流就是对一些底层对象进行封装,使得编程更加容易,特别是在学习了Linux系统调用的接口后,更是如此。

      至此重走C++学习之路就告一段落了,总共输出了27篇文章,内容还是比较丰富、全面的。最开始写这一系列文章的目的是回顾、复习C++知识,等到写的时候才发现很多东西都是在重新学习一遍,并没有在过来人的视角提出些东西,可谓是惭愧。

      下一个系列将打算回顾数据结构与算法的内容,有兴趣的朋友可以关注一下。

      评论
      添加红包

      请填写红包祝福语或标题

      红包个数最小为10个

      红包金额最低5元

      当前余额3.43前往充值 >
      需支付:10.00
      成就一亿技术人!
      领取后你会自动成为博主和红包主的粉丝 规则
      hope_wisdom
      发出的红包
      实付
      使用余额支付
      点击重新获取
      扫码支付
      钱包余额 0

      抵扣说明:

      1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
      2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

      余额充值