『 C++ 』IO流

文章目录


IO流概述

流是指数据的有序传输序列,路表示数据从一个地方流向另一个地方的过程,流可以是输入流也可以是输出流,具体取决于数据的流动方向;

  • 输入流

    数据从外部设备(文件,键盘等)流入程序中;

  • 输出流

    数据从程序流向外部设备(如显示器,文件等);

IO流是指用于处理输入输出操作的流,C++中的IO流用于程序与外部环境(用户,文件系统,网络等)之间交换数据的机制;

IO流通过标准库中的一组类和对象实现允许程序员以统一的方式处理不同类型的输入输出设备;

  • <ios>

    <ios>C++中的一个基础头文件,包括了所有输入输出流类的功能;

    其并不是一个具体的六类型,而是提供了一些基类,如ios_baseios(basic_ios);

    是所有输入输出流类的基石;

    <ios>中的类定义了流对象的状态,格式化标志,异常处理等通用功能;

  • <istream>

    该头文件定义了istream类及相关的输入流操作;

    流类型是输入流;

    其作用为istream类用于处理从输入设备(键盘,文件等)读取数据的操作;

    标准输入流对象cin就是istream类的实例;

  • <iostream>

    这个头文件包含了输入输出流的基本类,包括istreamostream;

    流类型是输入输出流,其既包含了输入流也包含了输出流;

    iostream类是一个组合类,继承自istreamostream,可以同时进行输入和输出操作,是一个被棱形继承的类;

    该头文件通常用来处理标准输入输出流的操作(如cincout);

  • <ostream>

    这个头文件定义了处理输出操作的ostream类;

    流类型是输出流;

    ostream类用于将数据输出到输出设备,如屏幕,文件等;

    标准输出流对象cout,标准错误流对象cerr,标准日志流对象clog均为该类的一个实例;

  • <streambuf>

    这个头文件定义了一个streambuf类,这是所有流类的基础缓冲区类;

    流类型是流缓冲区;

    streambufiostream,ifstream,ofstream等类的核心组成部分,负责具体的数据存取操作;

    streambuf通常由更高层次的流类,如istream,ostream使用,但它也可悲用户自定义以实现特殊流的缓冲区操作,如自定义的文件格式或网络流操作;

  • <fstream>

    这个头文件定义了处理文件输入输出的流类,包括ifstream,ofstreamfstream;

    流类型是文件流;

    ifstream是输入文件流类,用于从文件读取数据;

    ofstream是输出文件流类,用于向文件写入数据;

    fstream是读写文件流类,其与iostream相同既具有读也具有写的功能;

  • <sstream>

    这个头文件定义了用于操作字符串的流类,包括istringstream,ostringstreamstringstream;

    流类型是字符串流;

    istringstream用于从字符串读取数据,ostringstream用于将数据写入字符串,stringstream则用于同时进行字符串的读写操作;

这一系列的库用于对标C语言的一系列操作,一个面向过程一个面向对象;

<iostream>主要对标C语言中的printf,scanf等接口,用于处理从输入设备读取数据的操作与处理需要写入至输出设备的数据;

<fstream>对标C语言中的fprintf,fscanf等接口,用于处理向文件内写入与处理从文件中读取的数据;

<sstream>对标C语言中的sprintfsscanf等接口,用于处理字符串输入输出的流类;


iostream 的标准对象

C++标准库中iostream头文件定义了一些常用的标准流对象,这些对象在全局范围内可用,用于处理常见的输入输出任务;

  • cin

    标准输入流对象;

    用于从标准输入设备中读取数据(例如键盘);

    cinistream类型的一个实例,支持多种输入操作,可以从输入缓冲区中读取字符,整数,浮点数,字符串等不同类型的数据;

    cin的输入操作是通过提取运算符>>来完成的,这个运算符也被称为 "流提取" ;

    cin提供了一系列的接口(具体参考 istream - C++ Reference (cplusplus.com)):

    但最常用的接口还是operator >>;

    cpp 复制代码
    arithmetic types (1)	
    istream& operator>> (bool& val);
    istream& operator>> (short& val);
    istream& operator>> (unsigned short& val);
    istream& operator>> (int& val);
    istream& operator>> (unsigned int& val);
    istream& operator>> (long& val);
    istream& operator>> (unsigned long& val);
    istream& operator>> (long long& val);
    istream& operator>> (unsigned long long& val);
    istream& operator>> (float& val);
    istream& operator>> (double& val);
    istream& operator>> (long double& val);
    istream& operator>> (void*& val);
    stream buffers (2)	
    istream& operator>> (streambuf* sb );
    manipulators (3)	
    istream& operator>> (istream& (*pf)(istream&));
    istream& operator>> (ios& (*pf)(ios&));
    istream& operator>> (ios_base& (*pf)(ios_base&));

    这个操作符用于从输入流中提取数据并存储到对应的变量中;

    >>操作符会根据变量的类型自动进行推导,这种操作可以针对多种基本数据类型(内置类型,布尔值,容器等)以及一些特殊的类型(指针,streambuf对象等)进行处理;

    除此之外cin作为istream的实例,还提供了其他的成员函数,如getline(),ignore(),get()等;

    cpp 复制代码
    #include <iostream>
    
    int main() {
        int num;
        char ch;
    
        std::cout << "Enter a number: ";
        std::cin >> num;
    
        // 输出读取到的数字
        std::cout << "You entered the number: " << num << std::endl;
    
        // 忽略输入缓冲区中的下一个字符(通常是换行符)
        std::cin.ignore(1, '\n');
    
        std::cout << "Enter a character: ";
        std::cin.get(ch);
        std::cout << "You entered the character: " << ch << std::endl;
    
        return 0;
    }
        /*
        	运行结果为:
        	$ ./mytest 
            Enter a number: 42
            You entered the number: 42
            Enter a character: a
            You entered the character: a
        */

    在这段代码中使用流提取读取一个整型数据,调用cin.ignore()成员函数忽略一个字符,即\n;

    忽略该字符后该字符不会被下面的cin.get()读取从而程序不会出现不符合需求的现象,否则\n将会被cin.get()读取;

    通常情况下这些函数在使用时查看对应的文档即可;

    • 使用cin进行循环输入

      在一些OJ题目中需要循环进行输入,这种情况下可以使用while(operator>> (std::cin,variable))的方式进行输入;

      cpp 复制代码
      int main() {
        string s1;
      while (cin>>s1) {
      //  while (operator>>(cin, s1)) {
          cout << s1 << endl;
        }
        return 0;
      }
      
      /*
          $ ./mytest 
          hello
          hello
          world
          world
          ^Z
          [1]+  Stopped                 ./mytest
      */

      这里的operator>>其原型为:

      cpp 复制代码
      istream& operator>> (istream& is , string& str);

      其返回的是istream&的输入流类型引用;

      这里返回的输入流类型引用可以作为判断条件的原因是该类有一个operator bool()的重载,来判断流是否出现错误;

      同时,istream还有一个operator!(),它返回一个布尔值,指示流是否处于错误状态;

      因此当输入成功时,条件为true,循环继续;

      当输入失败时,条件为false,循环终止;

  • cout

    标准输出流对象;

    用于将数据输出至输出设备中(如显示器);

    coutostream类型的一个实例,支持多种输出和写入操作,可以将缓冲区中的整数,浮点数,字符串等不同类型的数据输出(写入)至对应的设备或文件中;

    cout的输出操作是通过插入运算符<<来完成的,这个运算符也被称为 "流插入" ;

    cout提供了一系列的接口(具体参考ostream - C++ Reference (cplusplus.com)):

    cin相同该对象最常用的成员函数为operator <<;

    cpp 复制代码
    arithmetic types (1)	
    ostream& operator<< (bool val);
    ostream& operator<< (short val);
    ostream& operator<< (unsigned short val);
    ostream& operator<< (int val);
    ostream& operator<< (unsigned int val);
    ostream& operator<< (long val);
    ostream& operator<< (unsigned long val);
    ostream& operator<< (long long val);
    ostream& operator<< (unsigned long long val);
    ostream& operator<< (float val);
    ostream& operator<< (double val);
    ostream& operator<< (long double val);
    ostream& operator<< (void* val);
    stream buffers (2)	
    ostream& operator<< (streambuf* sb );
    manipulators (3)	
    ostream& operator<< (ostream& (*pf)(ostream&));
    ostream& operator<< (ios& (*pf)(ios&));
    ostream& operator<< (ios_base& (*pf)(ios_base&));

    该运算符被称为 "流插入" 运算符,将数据从右侧的表达式插入到左侧的输入流中,即std::cout;

    同样的<<操作符会根据变量的类型自动进行推导,这种操作可以针对多种基本数据类型(内置类型,布尔值,容器等)以及一些重载了operator<<函数的自定义类型;

    coutostream的实例,继承了其原生的一些成员和接口,如刷新缓冲区的内容的flush(),设置浮点数的输出精度的precision()等;

    cpp 复制代码
    int main() {
      std::cout.precision(3);
      std::cout << 3.14159265 << std::endl;
    
      std::cout << "Processing..." << std::flush;
      // 一些耗时的操作
      std::cout << "Done!" << std::endl;
    
      return 0;
    }
    /*
    	运行结果为:
    	$ ./mytest 
        3.14
        Processing...Done!
    */

    但通常情况下由于C++C 兼容,编写C++程序时常常使用CC++混编的方式,如在使用精度控制的时候就可以使用C语言的精度控制,若是遇到需要使用这些接口的场合查看文档即可;

    cpp 复制代码
    int main() {
      double d = 3.1415626;
      printf("d = %.2f\n", d);
      return 0;
    }
    /*
    	运行结果为:
    	$ ./mytest 
        d = 3.14	
    */
  • cerr,clog

    cerr,clogcout一样都是ostream类的一个实例;

    其继承了ostream的各个成员及接口;

    本质上cerrclog分别都是用于打印错误信息与打印日志信息的;

    • cerr

      cerr是不带缓冲的,这意味着当使用cerr打印一个错误信息时该错误信息将被直接打印而不会被存储在缓冲区中,使使用者能够在发生error时第一时间发现对应的错误信息;

      cpp 复制代码
      std::cerr << "Error: Something went wrong!" << std::endl;
    • clog

      clogcerr不同,默认带缓冲,这意味着当使用clog打印日志信息时日志信息将先被存储在缓冲区中;

      直到缓冲区被写满或是显示调用刷新缓冲区才会将对应的日志信息进行打印;

      cpp 复制代码
      std::clog << "Log: Application started" << std::endl;

    cerrclog默认都是绑定到标准错误输出流stderr的,因此通常会直接被显示在终端中;

    如果需要也可以调用其成员函数rebuf()将输出重定向到文件或是其他输出设备;

    cpp 复制代码
    #include <iostream>
    #include <fstream>
    
    int main() {
        std::ofstream error_file("errors.log"); // 实例化对应的 ofstream 对象
        std::ofstream log_file("logs.log");
    
        std::cerr.rdbuf(error_file.rdbuf()); // 重定向 cerr 到 error_file 
        std::clog.rdbuf(log_file.rdbuf());   // 重定向 clog 到 log_file
    
        std::cerr << "Error: This will go to errors.log" << std::endl;
        std::clog << "Log: This will go to logs.log" << std::endl;
    
        return 0;
    }

C++流和C标准库I/O函数的同步 sync_with_stdio

sync_with_stdio是C++标准库中用于控制C++流和C标准库I/O函数(如printf,scanf)之间同步行为的函数;

默认情况下C++流(如cin,cout)与C标准库的I/O函数是同步的,意味着每次进行输入或是输出操作时C++流都会刷新其内部缓冲区并与C标准库的I/O缓冲区同步;

这种同步操作是为了确保使用 C++流 和 C标准库 I/O函数时,输出结果的一致,但这种同步操作也会带来一定的性能开销,特别是在需要频繁进行I/O操作的情况下;

可以使用std::ios::sync_with_stdio(false)来关闭 C++流 和 C标准库 I/O函数之间的同步;

关闭同步后,C++流不再与其内部缓冲区和C标准库I/O缓冲区进行同步;

这可以提高I/O操作的效率,特比是对于大量数据的输入和输出;

  • 注意事项

    关闭同步之后,如果在程序中混合使用 C++流 和 C标准库 I/O函数时需要小心,以确保不会出现数据竞争或者输出顺序混乱的问题;

    一旦关闭同步后就无法再重新开启同步;

    cpp 复制代码
    int main() {
      // 关闭 C++ 流和 C 标准库 I/O 函数之间的同步
      std::ios::sync_with_stdio(false);
    
      int age = 25;
    
      // 使用 C++ 流输出提示信息
      std::cout << "请输入您的年龄: ";
    
      // 使用 C 标准库函数读取用户输入
      scanf("%d", &age);
    
      // 使用 C++ 流输出用户输入的年龄
      std::cout << "您的年龄是: " << age << std::endl;
    
      return 0;
    }
    /*
    	运行结果为:
    	$ ./mytest 
        27
        请输入您的年龄: 您的年龄是: 27
    */

    在这个例子中再关闭同步之后先使用std::cout输出提示信息,然后使用std::scanf读取用户输入;

    由于关闭了同步,std::cout的输出缓冲区可能没有被立即刷新到屏幕上;

    因此当程序执行到scanf时用户可能看不到提示信息导致程序行为异常;


fstream 文件流

C++提供了一个文件流用于处理文件输入输出的对象,可以从文件中读取数据到程序中或者将程序中的数据写到文件中;

C++标准库提供了三种主要的文件流类:

  • ofstream

    用于向文件写入数据(输出);

  • ifstream

    用于从文件读取数据(输入);

  • fstream

    用于同时进行文件的读写操作;

这三个流类存在对应的函数get/read/>>put/write/<<分别对标C语言的fputc/fwrite/fprintffgetc/fread/fscanf;

提供三种主要的文件流类主要是支持C++的面向对象,即可以支持将类对象中对应的数据写入至文件当中,C语言只能支持将内置类型写入文件内;

流插入和流提取能够更好的支持内置类型和自定义类型;


文件流的打开标志

在C++中打开文件时通常使用fstream,ifstreamofstream来打开一个文件流,本质上是定义一个文件流对象;

fstream为例;

文件流对象可以通过open成员函数传入一个const char*字符串或是const string&类型作为文件名打开;

也可以使用构造函数传入一个const char*字符串或是const string&对象作为文件名将文件打开;

cpp 复制代码
/* open 成员函数声名 */
void open (const char* filename,
           ios_base::openmode mode = ios_base::in | ios_base::out);
void open (const string& filename,
           ios_base::openmode mode = ios_base::in | ios_base::out);
    
/* 构造函数 */
explicit fstream (const char* filename,
                  ios_base::openmode mode = ios_base::in | ios_base::out);
explicit fstream (const string& filename,
                  ios_base::openmode mode = ios_base::in | ios_base::out);

其中mode表示打开文件时的模式,可以使用一些标志来控制文件流的行为;

这些标志与Linux底层的文件打开接口相似,以二进制标志位的方式标明文件的打开模式,即mode = a | b | c;

常见的标识有:

  • in

    以输入模式打开(读取);

  • out

    以输出模式打开文件(写入),如果文件不存在将创建文件;

    如果文件存在则清空文件内容(除非另有其他标志影响,如app);

  • binary

    以二进制模式打开文件,这意味着数据将以字节流的形式读写,不进行任何格式的转换;

  • ate

    打开文件后,将文件指针定位到文件末尾,但可以在文件的任意位置读写数据;

  • app

    以追加模式打开文件,数据写入时会自动定位到文件末尾,不会覆盖文件中的现有内容;

  • trunc

    如果文件存在,打开时会清空其内容,这是out模式的默认行为;

关闭文件时调用其close成员函数即可;

打开文件后进行读写时同样调用对应的成员函数,writeread;

  • write

    cpp 复制代码
    ostream& write (const char* s, streamsize n);

    这里的const char*类型并不表示将其作为字符串读取,而是取到该数据的地址后将其以二进制的形式写至文件中,写入大小为streamsize n;

  • read

    cpp 复制代码
    istream& read (char* s, streamsize n);

    这里的char *类型同样的是以二进制的形式写进并写至对应的内存中,streamsize n表示需要读取文件的大小;


二进制读写

二进制读写操作是指以二进制格式从文件中读取或向文件中写入数据;

这种方式直接操作数据的内存表示,更加高效的且没有数据格式的转换,特别适合处理如图像,音频,视频文件等需要保持精度数据格式的文件;

在二进制模式下,数据是以字节流的形式直接读写的,这意味着数据从内存传输到文件或者从文件传输到内存时不会进行任何转换;

但由于不经过任何格式的转换且在内存中数据具有类型而在磁盘/硬盘中数据不存在类型所以写入后无法在磁盘中直接进行查看(数据表示不同);

cpp 复制代码
// Date类的定义,包含友元函数声明和私有成员变量
class Date {
  // 友元函数声明,允许<<运算符访问Date类的私有成员
  friend ostream& operator<<(ostream& out, const Date& d);
  // 友元函数声明,允许>>运算符访问Date类的私有成员
  friend istream& operator>>(istream& in, Date& d);

 public:
  // 构造函数,默认值为1年1月1日
  Date(int year = 1, int month = 1, int day = 1)
      : year_(year), month_(month), day_(day) {}

  // bool类型转换函数,当year_为0时返回false,否则返回true
  operator bool() {
    if (year_ == 0) {
      return false;
    }
    return true;
  }
    
  // 打印操作  
  void Print(){
    printf("year= %d ,month= %d ,day= %d\n",year_,month_,day_);
  }
    
 private:
  // 年份
  int year_;
  // 月份
  int month_;
  // 日期
  int day_;
};

// 重载<<运算符,用于将Date对象输出到ostream
ostream& operator<<(ostream& out, const Date& d) {
  out << d.year_ << " - " << d.month_ << " - " << d.day_ << endl;
  return out;
}

// 重载>>运算符,用于从istream输入流中读取Date对象
istream& operator>>(istream& in, Date& d) {
  in >> d.year_ >> d.month_ >> d.day_;
  return in;
}

// 定义一个结构体testClass,包含成员变量_d1,_d2和_date
struct testClass {
  char _s1[32];  // 字符数组作为字符串,用于存储某些数据
  double _d2;  // 双精度浮点类型变量,用于存储某些数据
  Date _date;  // Date类型变量,用于存储日期
};

class BinIO {
 public:
  // 构造函数 传入一个 const char* 类型作为文件名
  BinIO(const char* filename) : filename_(filename) {}

  // 写
  void Write(const testClass& wt) {
    // fstream 实例化 一个对象ofs用于打开文件(以二进制的形式)用于写入操作
    fstream ofs(filename_, fstream::out | fstream::binary);
    // 调用write成员函数进行写入
    ofs.write((const char*)&wt,sizeof(wt));
  }
  // 读
  void Read(testClass& rt) {
    // fstream 实例化 一个对象ifs用于打开文件(以二进制的形式)用于读取操作
    fstream ifs(filename_, fstream::in | fstream::binary);
    // 调用read成员函数进行读取
    ifs.read((char*)&rt,sizeof(rt));
  }

 private:
  string filename_;  // 文件名
};

这段代码定义了一个用于处理日期的Date类型和一个包含日期及其他数据的结构testClass,并实现了一个用于二进制文件读写的类BinIO;

其中Date类重载了>>运算符和<<运算符使得Date对象可以与流(如coutcin)直接交互;

BinIO用于文件的二进制读写操作;

  • cpp 复制代码
    int main() {
      // 创建一个 testClass 对象 t1,初始化 _d1 为 "hello world",
      // _d2 为 3.14,_date 为 2001年1月1日
      testClass t1{"hello world", 3.14, {2001, 1, 1}};
      
      // 创建一个 BinIO 对象 b,初始化文件名为 "log.txt"
      BinIO b("log.txt");
      
      // 将 testClass 对象 t1 以二进制形式写入到 "log.txt" 文件中
      b.Write(t1);
      
      return 0;  // 返回 0,表示程序正常结束
    }

    这段代码的主要功能是将一个 testClass 对象以二进制形式写入到文件 log.txt 中。

    • 首先使用初始化列表创建了一个 testClass 对象 t1;

      t1_d1 成员变量为字符串 "hello world";

      _d23.14_date 成员为 2001年1月1日;

    • 然后创建了一个 BinIO 对象 b,并将文件名 "log.txt" 传递给它,准备进行文件操作;

    • 接着调用 BinIO 类的 Write 函数,将 t1 的数据以二进制的形式写入到 log.txt 文件中;

    运行程序数据以二进制的方式被写至文件当中;

  • cpp 复制代码
    int main() {
      // 创建另一个 testClass 实例 t2,用于从文件中读取数据
      testClass t2;
      
      // 从文件 "log.bin" 中读取数据到 t2 对象
      b.Read(t2);
      
      // 输出读取到的字符串和 double 类型数据
      cout << "s1 : " << t2._s1 << " d1 : " << t2._d1 << endl;
      
      // 输出读取到的 Date 类型数据
      cout << "date :" << t2._date << endl;
    
      return 0;  
    }
    /*
    	运行结果为:
    	$ ./mytest 
        s1 : hello world d1 : 3.14
        date :2001 - 1 - 1
    */

    在这段代码中创建了另一个testClass类型对象t2用于将文件数据进行读取;

    读取后依次将t2对象中的内容进行打印;


二进制读写的浅拷贝问题

在进行二进制读写的时候可能会出现浅拷贝问题导致在对数据进行读取的时候出现内存错误;

其他代码与上述无异;

cpp 复制代码
struct testClass {
  string _s1;  // 字符数组作为字符串,用于存储某些数据
  double _d2;  // 双精度浮点类型变量,用于存储某些数据
  Date _date;  // Date类型变量,用于存储日期
};

假设testClass类中的_s1string类型;

由于string是一个容器,其自行将在内存的堆中开辟空间并进行管理;

在管理过程中该string对象被写入至文件中,被写入至文件内的实际上还有该对象在内存中的空间指针(内容可能并未被拷贝);

当分为两个进程对这个文件进行读写操作时(两次执行,一次进行写一次进行读),在进行读的时候原本的string对象的指针会被读取进新的_s1成员中,但指针所指向的空间已经被销毁从而导致野指针问题;

bash 复制代码
$ ./mytest 
Segmentation fault

在同一个进程中对文件进行读写操作,可能数据一样会被写入至对应的string对象中但仍会因为野指针出现问题,故在进行二进制的文件读写操作时需要尽可能避免使用容器,否则需要手动将其序列化;


文本读写

文本读写与二进制读写不同,文本读写需要将所有的内容转换为字节流才能写入至文件当中;

对于内置类型而言可以使用C++中的to_string将其转换为字符串,在进行读取的时候可以调用C++中的stoi,stod函数将其转换为内置类型;

  • C语言可以使用sprintfsscanf将内置类型转换为自定义类型(尤其是double这种较为复杂的内置类型)

其余代码不变:

cpp 复制代码
// 定义一个结构体testClass,包含成员变量_s1,_d2和_date
struct testClass {
  string _s1;
  double _d1;  // 双精度浮点类型变量,用于存储某些数据
  Date _date;  // Date类型变量,用于存储日期
};

class TextIO {
 public:
  // 构造函数 传入一个 const char* 类型作为文件名
  TextIO(const char* filename) : filename_(filename) {}

  // 写
  void Write(const testClass& wt) {
    fstream ofs(filename_, fstream::out | fstream::trunc);
    // 不同的编译器/平台实现不同
    // 有些平台可以直接使用默认打开方式

    if (!ofs) {
      cerr << "Error opening file for writing!" << endl;
      return;
    }

    ofs << wt._s1 << endl;
    ofs << wt._d1 << endl;
    ofs << wt._date << endl;  // 这里调用了 Date 类的 operator << 流插入
                              // 调用的本质是因为 fstream 继承于 iostream
                              // 这里将对应的数据写入至ofs文件流中
  }

  // 读
  void Read(testClass& rt) { 	
    fstream ifs(filename_);
    ifs >> rt._s1; // 调用内置类型的流提取
    ifs >> rt._d1;
    ifs >> rt._date; // 调用Date重载的流提取
  }

 private:
  string filename_;  // 文件名
};

这段代码其他代码与上文例子相同不变,定义了一个TextIO的文本读写操作,对应的testClass类中的_s1成员的类型换成string类型();

  • 写入操作直接使用流插入<<操作符即可;

    对于内置类型而言会去调用库中的operator<<流插入;

    对于自定义类型而言会去调用在Date中重载的operator<<流插入;

    cpp 复制代码
    // 重载<<运算符,用于将Date对象输出到ostream
    ostream& operator<<(ostream& out, const Date& d) {
      out << d.year_ << " - " << d.month_ << " - " << d.day_ << endl;
      return out;
    }

    其中fstream能够调用ostream的本质原因是fstream是由ostream派生出来的;

    cpp 复制代码
    int main() {
      testClass t1{"hello world", 3.14, {2001, 1, 1}};
      TextIO b("log.txt");
      b.Write(t1);
      return 0;
    }

    在这段代码中定义了一个testClass类型对象t1,并创建了一个TextIO类型的对象b类作为文本文件读写操作;

    调用Write成员函数,运行结果文件被写入至log.txt文件中;

    tex 复制代码
    hello world
    3.14
    2001 - 1 - 1
  • 对于读操作也是相同,直接调用对应的operator>>流提取即可;

    cpp 复制代码
    int main() {
    
      testClass t;
      TextIO b("log.txt");
      b.Read(t);
    
      cout << t._s1 << endl;
      cout << t._d1 << endl;
      cout << t._date << endl;
      return 0;
    }

    这里创建一个t用于接收数据,创建一个b对象用于文件的读写;

    读写后打印出t的数据;

    cpp 复制代码
    $ ./mytest 
    hello
    0
    1 - 1 - 1

    发现文件读取错误;

    这里读取错误的原因有两个;

    • 流插入读取字符串

      在流插入读取字符串时,当字符串与字符串之间存在空格时默认该空格为分隔符;

      此处的_s1类型为string,内容为hello world;

      为避免这个问题可以使用getline单独读取该字符串;

      cpp 复制代码
      void Read(testClass& rt) {
          fstream ifs(filename_);
          getline(ifs, rt._s1);
          ifs >> rt._d1 >> rt._date;
        }

      同样的这里的getline传入的是ifs文件流,其类型fstream是IO流中的子类;

      运行结果为:

      bash 复制代码
      $ ./mytest 
      hello world
      3.14
      2001 - 0 - 1
    • Date类写入时格式控制不正确

      同样的在进行读取的时候为避免使用流提取的时候不增加过多的操作,在进行文件写入时就需要对格式进行控制;

      Date类在重载operator<<时内容为:

      cpp 复制代码
        out << d.year_ << " - " << d.month_ << " - " << d.day_;

      同样的在进行流提取时,当出现多个空格时空格会被视作分隔符;

      因此在进行写入的时候就应该进行格式控制;

      cpp 复制代码
      ostream& operator<<(ostream& out, const Date& d) {
        out << d.year_ << " " << d.month_ << " " << d.day_;
        return out;
      }

    修改完上述两处问题后重新进行写入再进行读取;

    bash 复制代码
    $ make # 写入文件
    g++ -o mytest Main.cc -g -Wall -std=c++11
    $ ./mytest 
    
    # 文件内容正确 
    $ cat log.txt 
    hello world
    3.14
    2001 1 1
    
    # 代码修改为读取代码后重新编译
    $ make
    g++ -o mytest Main.cc -g -Wall -std=c++11
    $ ./mytest 
    hello world
    3.14
    2001 1 1
    # 结果正确

同样的可以使用getline,其中getline可以自定义分隔符;

cpp 复制代码
istream& getline (istream&  is, string& str, char delim);
istream& getline (istream&& is, string& str, char delim);

字符串流

C++字符串流stringstream是C++标准库中的一种流类,用于再内存中处理字符串,属于标准库中的sstream;

字符串流允许我们像处理文件流一样处理字符串,它将字符串作为数据源或数据目的地,使得再内存中进行字符串流更加灵活方便;

文件流是以 内存->磁盘 的方式,字符串流则是以 内存->内存 的方式(内存中数据具有类型);

C++中的字符串流主要有三种类型:

  • std::istringstream(输入字符串流)

    用于从字符串中提取数据,相当于字符串作为输入源;

  • std::ostringstream(输出字符串流)

    用于将数据格式化为字符串并存储,相当于将字符串作为输出目的地;

  • std::stringstream(输入/输出字符串流)

    可以同时进行输入和输出操作;

这三种类型实际上是对标C语言中的sprintfsscanf接口,提供了对自定义类型的操作与安全性;

字符串流通常应用于以下几种场景:

  • 字符串的格式化输出

    将不同类型的字符串合并成一个字符串;

  • 字符串的解析

    将字符串分解成不同的部分(如解析逗号分割的值等);

  • 类型转换

    在不同数据类型之间进行安全转换;

假设需要生成一条SQL语句;

利用C语言则是调用sprintf,即:

cpp 复制代码
int main() {
  char sql[128];
  char name[24];
  scanf("%s", &name);
  sprintf(sql, "select * from t_test name = '%s' ", name);
  printf("%s\n", sql);
  return 0;
}
/*
	运行结果为:
	$ ./mytest 
    张三
    select * from t_test name = '张三' 
*/

此处查找的为内置类型,但若是需要查找自定义类型则需要更复杂的操作;

C++中的字符串流则是可以解决对于自定义类型的操作问题,本质上是重载自定类型的operator<<operator>>,即流插入和流提取操作,而stringstream流则是所有IO流的最底层类,即最后的派生类,可以调用父类封装的成员函数来进行操作;

cpp 复制代码
// Date类的定义,包含友元函数声明和私有成员变量
class Date {
  // 友元函数声明,允许<<运算符访问Date类的私有成员
  friend ostream& operator<<(ostream& out, const Date& d);
  // 友元函数声明,允许>>运算符访问Date类的私有成员
  friend istream& operator>>(istream& in, Date& d);

 public:
  // 构造函数,默认值为1年1月1日
  Date(int year = 1, int month = 1, int day = 1)
      : year_(year), month_(month), day_(day) {}

  // bool类型转换函数,当year_为0时返回false,否则返回true
  operator bool() {
    if (year_ == 0) {
      return false;
    }
    return true;
  }
  void Print() {
    printf("year= %d ,month= %d ,day= %d\n", year_, month_, day_);
  }

 private:
  // 年份
  int year_;
  // 月份
  int month_;
  // 日期
  int day_;
};

// 重载<<运算符,用于将Date对象输出到ostream
ostream& operator<<(ostream& out, const Date& d) {
  out << d.year_ << "/" << d.month_ << "/" << d.day_;
  return out;
}

// 重载>>运算符,用于从istream输入流中读取Date对象
istream& operator>>(istream& in, Date& d) {
  in >> d.year_ >> d.month_ >> d.day_;
  return in;
}

int main() {
  Date d = {2023, 12, 1};  // 初始化日期对象
  string sql;
  stringstream st;
  st << d;  // 将日期对象格式化为字符串
  sql += "select * from t_data date = '";
  sql += st.str();  // 插入格式化后的日期
  sql += "'";
  cout << sql << endl;  // 输出SQL语句
  return 0;
}

/*
	运行结果为:
	$ ./mytest 
    select * from t_test name = '2023/12/1'
*/

在这段代码中创建了一个Date类,初始化为{2023, 12, 1};

创建了一个string对象sql作为sql语句的存储地;

定义了一个stringstream字符串流st用于自定义类型Date的转换;

当使用流插入时将去调用对应的operator<<将自定义类型中的数据转换为字符串并存储在stringstream字符串流的缓冲区中;

最后使用string类的+=将字符串进行组装;

  • stringstream的序列化和反序列化

    序列化是指将对象的状态转换为一种可存储或传输的格式的过程;

    这样可以将对象保存到文件,数据库,或者通过网络传输;

    序列化之后的数据可以在以后反序列化(也称为反序列化或解串)回原来的对象,从而恢复对象的状态;

    stringstream支持一些较为简单的序列化和反序列化;

    本质上就是调用自定义类型重载的operator<<operator<<,即流插入与流提取;

    但由于stringstream不能很好的支持复杂类型,如标准库的容器等等;

    通常情况下复杂内容的序列化和反序列化可以使用第三方库如Json等;


注意

|---------------------------------|
| 本文中所有代码测试均在Linux-centOS7上进行 |

相关推荐
问道飞鱼1 分钟前
【前端知识】强大的js动画组件anime.js
开发语言·前端·javascript·anime.js
拓端研究室1 分钟前
R基于贝叶斯加法回归树BART、MCMC的DLNM分布滞后非线性模型分析母婴PM2.5暴露与出生体重数据及GAM模型对比、关键窗口识别
android·开发语言·kotlin
Code成立2 分钟前
《Java核心技术I》Swing的网格包布局
java·开发语言·swing
Auc247 分钟前
使用scrapy框架爬取微博热搜榜
开发语言·python
QQ同步助手14 分钟前
C++ 指针进阶:动态内存与复杂应用
开发语言·c++
凯子坚持 c20 分钟前
仓颉编程语言深入教程:基础概念和数据类型
开发语言·华为
小爬虫程序猿22 分钟前
利用Java爬虫速卖通按关键字搜索AliExpress商品
java·开发语言·爬虫
程序猿-瑞瑞24 分钟前
24 go语言(golang) - gorm框架安装及使用案例详解
开发语言·后端·golang·gorm
qq_4335545424 分钟前
C++ 面向对象编程:递增重载
开发语言·c++·算法
易码智能32 分钟前
【EtherCATBasics】- KRTS C++示例精讲(2)
开发语言·c++·kithara·windows 实时套件·krts