文章目录
- IO流概述
- iostream 的标准对象
- C++流和C标准库I/O函数的同步 sync_with_stdio
- fstream 文件流
- 文件流的打开标志
- 二进制读写
- 二进制读写的浅拷贝问题
- 文本读写
- 字符串流
- 注意
IO流概述
流是指数据的有序传输序列,路表示数据从一个地方流向另一个地方的过程,流可以是输入流也可以是输出流,具体取决于数据的流动方向;
-
输入流
数据从外部设备(文件,键盘等)流入程序中;
-
输出流
数据从程序流向外部设备(如显示器,文件等);
IO流是指用于处理输入输出操作的流,C++中的IO流用于程序与外部环境(用户,文件系统,网络等)之间交换数据的机制;
IO流通过标准库中的一组类和对象实现允许程序员以统一的方式处理不同类型的输入输出设备;
-
<ios>
<ios>
是C++
中的一个基础头文件,包括了所有输入输出流类的功能;其并不是一个具体的六类型,而是提供了一些基类,如
ios_base
和ios
(basic_ios
);是所有输入输出流类的基石;
<ios>
中的类定义了流对象的状态,格式化标志,异常处理等通用功能; -
<istream>
该头文件定义了
istream
类及相关的输入流操作;流类型是输入流;
其作用为
istream
类用于处理从输入设备(键盘,文件等)读取数据的操作;标准输入流对象
cin
就是istream
类的实例; -
<iostream>
这个头文件包含了输入输出流的基本类,包括
istream
和ostream
;流类型是输入输出流,其既包含了输入流也包含了输出流;
iostream
类是一个组合类,继承自istream
和ostream
,可以同时进行输入和输出操作,是一个被棱形继承的类;该头文件通常用来处理标准输入输出流的操作(如
cin
和cout
); -
<ostream>
这个头文件定义了处理输出操作的
ostream
类;流类型是输出流;
ostream
类用于将数据输出到输出设备,如屏幕,文件等;标准输出流对象
cout
,标准错误流对象cerr
,标准日志流对象clog
均为该类的一个实例; -
<streambuf>
这个头文件定义了一个
streambuf
类,这是所有流类的基础缓冲区类;流类型是流缓冲区;
streambuf
是iostream
,ifstream
,ofstream
等类的核心组成部分,负责具体的数据存取操作;streambuf
通常由更高层次的流类,如istream
,ostream
使用,但它也可悲用户自定义以实现特殊流的缓冲区操作,如自定义的文件格式或网络流操作; -
<fstream>
这个头文件定义了处理文件输入输出的流类,包括
ifstream
,ofstream
和fstream
;流类型是文件流;
ifstream
是输入文件流类,用于从文件读取数据;ofstream
是输出文件流类,用于向文件写入数据;fstream
是读写文件流类,其与iostream
相同既具有读也具有写的功能; -
<sstream>
这个头文件定义了用于操作字符串的流类,包括
istringstream
,ostringstream
和stringstream
;流类型是字符串流;
istringstream
用于从字符串读取数据,ostringstream
用于将数据写入字符串,stringstream
则用于同时进行字符串的读写操作;
这一系列的库用于对标C语言的一系列操作,一个面向过程一个面向对象;
<iostream>
主要对标C语言中的printf
,scanf
等接口,用于处理从输入设备读取数据的操作与处理需要写入至输出设备的数据;
<fstream>
对标C语言中的fprintf
,fscanf
等接口,用于处理向文件内写入与处理从文件中读取的数据;
<sstream>
对标C语言中的sprintf
和sscanf
等接口,用于处理字符串输入输出的流类;
iostream 的标准对象
在C++
标准库中iostream
头文件定义了一些常用的标准流对象,这些对象在全局范围内可用,用于处理常见的输入输出任务;
-
cin
标准输入流对象;
用于从标准输入设备中读取数据(例如键盘);
cin
是istream
类型的一个实例,支持多种输入操作,可以从输入缓冲区中读取字符,整数,浮点数,字符串等不同类型的数据;cin
的输入操作是通过提取运算符>>
来完成的,这个运算符也被称为 “流提取” ;cin
提供了一系列的接口(具体参考 istream - C++ Reference (cplusplus.com)):但最常用的接口还是
operator >>
;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()
等;#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))
的方式进行输入;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>>
其原型为:istream& operator>> (istream& is , string& str);
其返回的是
istream&
的输入流类型引用;这里返回的输入流类型引用可以作为判断条件的原因是该类有一个
operator bool()
的重载,来判断流是否出现错误;同时,
istream
还有一个operator!()
,它返回一个布尔值,指示流是否处于错误状态;因此当输入成功时,条件为
true
,循环继续;当输入失败时,条件为
false
,循环终止;
-
-
cout
标准输出流对象;
用于将数据输出至输出设备中(如显示器);
cout
是ostream
类型的一个实例,支持多种输出和写入操作,可以将缓冲区中的整数,浮点数,字符串等不同类型的数据输出(写入)至对应的设备或文件中;cout
的输出操作是通过插入运算符<<
来完成的,这个运算符也被称为 “流插入” ;cout
提供了一系列的接口(具体参考ostream - C++ Reference (cplusplus.com)):与
cin
相同该对象最常用的成员函数为operator <<
;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<<
函数的自定义类型;cout
是ostream
的实例,继承了其原生的一些成员和接口,如刷新缓冲区的内容的flush()
,设置浮点数的输出精度的precision()
等;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++程序时常常使用C
与C++
混编的方式,如在使用精度控制的时候就可以使用C
语言的精度控制,若是遇到需要使用这些接口的场合查看文档即可;int main() { double d = 3.1415626; printf("d = %.2f\n", d); return 0; } /* 运行结果为: $ ./mytest d = 3.14 */
-
cerr
,clog
cerr
,clog
与cout
一样都是ostream
类的一个实例;其继承了
ostream
的各个成员及接口;本质上
cerr
与clog
分别都是用于打印错误信息与打印日志信息的;-
cerr
cerr
是不带缓冲的,这意味着当使用cerr
打印一个错误信息时该错误信息将被直接打印而不会被存储在缓冲区中,使使用者能够在发生error
时第一时间发现对应的错误信息;std::cerr << "Error: Something went wrong!" << std::endl;
-
clog
clog
与cerr
不同,默认带缓冲,这意味着当使用clog
打印日志信息时日志信息将先被存储在缓冲区中;直到缓冲区被写满或是显示调用刷新缓冲区才会将对应的日志信息进行打印;
std::clog << "Log: Application started" << std::endl;
cerr
与clog
默认都是绑定到标准错误输出流stderr
的,因此通常会直接被显示在终端中;如果需要也可以调用其成员函数
rebuf()
将输出重定向到文件或是其他输出设备;#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
函数时需要小心,以确保不会出现数据竞争或者输出顺序混乱的问题;一旦关闭同步后就无法再重新开启同步;
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/fprintf
和fgetc/fread/fscanf
;
提供三种主要的文件流类主要是支持C++的面向对象,即可以支持将类对象中对应的数据写入至文件当中,C语言只能支持将内置类型写入文件内;
流插入和流提取能够更好的支持内置类型和自定义类型;
文件流的打开标志
在C++中打开文件时通常使用fstream
,ifstream
或ofstream
来打开一个文件流,本质上是定义一个文件流对象;
以fstream
为例;
文件流对象可以通过open
成员函数传入一个const char*
字符串或是const string&
类型作为文件名打开;
也可以使用构造函数传入一个const char*
字符串或是const string&
对象作为文件名将文件打开;
/* 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
成员函数即可;
打开文件后进行读写时同样调用对应的成员函数,write
与read
;
-
write
ostream& write (const char* s, streamsize n);
这里的
const char*
类型并不表示将其作为字符串读取,而是取到该数据的地址后将其以二进制的形式写至文件中,写入大小为streamsize n
; -
read
istream& read (char* s, streamsize n);
这里的
char *
类型同样的是以二进制的形式写进并写至对应的内存中,streamsize n
表示需要读取文件的大小;
二进制读写
二进制读写操作是指以二进制格式从文件中读取或向文件中写入数据;
这种方式直接操作数据的内存表示,更加高效的且没有数据格式的转换,特别适合处理如图像,音频,视频文件等需要保持精度数据格式的文件;
在二进制模式下,数据是以字节流的形式直接读写的,这意味着数据从内存传输到文件或者从文件传输到内存时不会进行任何转换;
但由于不经过任何格式的转换且在内存中数据具有类型而在磁盘/硬盘中数据不存在类型所以写入后无法在磁盘中直接进行查看(数据表示不同);
// 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
对象可以与流(如cout
和cin
)直接交互;
BinIO
用于文件的二进制读写操作;
-
写
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"
;_d2
为3.14
,_date
成员为2001年1月1日
; -
然后创建了一个
BinIO
对象b
,并将文件名"log.txt"
传递给它,准备进行文件操作; -
接着调用
BinIO
类的Write
函数,将t1
的数据以二进制的形式写入到log.txt
文件中;
运行程序数据以二进制的方式被写至文件当中;
-
-
读
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
对象中的内容进行打印;
二进制读写的浅拷贝问题
在进行二进制读写的时候可能会出现浅拷贝问题导致在对数据进行读取的时候出现内存错误;
其他代码与上述无异;
struct testClass {
string _s1; // 字符数组作为字符串,用于存储某些数据
double _d2; // 双精度浮点类型变量,用于存储某些数据
Date _date; // Date类型变量,用于存储日期
};
假设testClass
类中的_s1
是string
类型;
由于string
是一个容器,其自行将在内存的堆中开辟空间并进行管理;
在管理过程中该string
对象被写入至文件中,被写入至文件内的实际上还有该对象在内存中的空间指针(内容可能并未被拷贝);
当分为两个进程对这个文件进行读写操作时(两次执行,一次进行写一次进行读),在进行读的时候原本的string
对象的指针会被读取进新的_s1
成员中,但指针所指向的空间已经被销毁从而导致野指针问题;
$ ./mytest
Segmentation fault
在同一个进程中对文件进行读写操作,可能数据一样会被写入至对应的string
对象中但仍会因为野指针出现问题,故在进行二进制的文件读写操作时需要尽可能避免使用容器,否则需要手动将其序列化;
文本读写
文本读写与二进制读写不同,文本读写需要将所有的内容转换为字节流才能写入至文件当中;
对于内置类型而言可以使用C++中的to_string
将其转换为字符串,在进行读取的时候可以调用C++中的stoi
,stod
函数将其转换为内置类型;
- C语言可以使用
sprintf
与sscanf
将内置类型转换为自定义类型(尤其是double
这种较为复杂的内置类型)
其余代码不变:
// 定义一个结构体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<<
流插入;// 重载<<运算符,用于将Date对象输出到ostream ostream& operator<<(ostream& out, const Date& d) { out << d.year_ << " - " << d.month_ << " - " << d.day_ << endl; return out; }
其中
fstream
能够调用ostream
的本质原因是fstream
是由ostream
派生出来的;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
文件中;hello world 3.14 2001 - 1 - 1
-
读
对于读操作也是相同,直接调用对应的
operator>>
流提取即可;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
的数据;$ ./mytest hello 0 1 - 1 - 1
发现文件读取错误;
这里读取错误的原因有两个;
-
流插入读取字符串
在流插入读取字符串时,当字符串与字符串之间存在空格时默认该空格为分隔符;
此处的
_s1
类型为string
,内容为hello world
;为避免这个问题可以使用
getline
单独读取该字符串;void Read(testClass& rt) { fstream ifs(filename_); getline(ifs, rt._s1); ifs >> rt._d1 >> rt._date; }
同样的这里的
getline
传入的是ifs
文件流,其类型fstream
是IO流中的子类;运行结果为:
$ ./mytest hello world 3.14 2001 - 0 - 1
-
Date
类写入时格式控制不正确同样的在进行读取的时候为避免使用流提取的时候不增加过多的操作,在进行文件写入时就需要对格式进行控制;
Date
类在重载operator<<
时内容为:out << d.year_ << " - " << d.month_ << " - " << d.day_;
同样的在进行流提取时,当出现多个空格时空格会被视作分隔符;
因此在进行写入的时候就应该进行格式控制;
ostream& operator<<(ostream& out, const Date& d) { out << d.year_ << " " << d.month_ << " " << d.day_; return out; }
修改完上述两处问题后重新进行写入再进行读取;
$ 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
可以自定义分隔符;
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语言中的sprintf
和sscanf
接口,提供了对自定义类型的操作与安全性;
字符串流通常应用于以下几种场景:
-
字符串的格式化输出
将不同类型的字符串合并成一个字符串;
-
字符串的解析
将字符串分解成不同的部分(如解析逗号分割的值等);
-
类型转换
在不同数据类型之间进行安全转换;
假设需要生成一条SQL
语句;
利用C语言则是调用sprintf
,即:
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流的最底层类,即最后的派生类,可以调用父类封装的成员函数来进行操作;
// 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上进行 |