C语言中我们学习过文件IO的相关函数,那么在C++中也一定有各种IO流的
函数或者功能,由我今天来简单介绍一下C++中IO流的大致原理及使用。
在C语言中我们经常会使用到scanf、printf、sscanf、sprintf等等来实现进程和文件之间数据的流动,在C++中虽然由于面向对象的特性使得C++和C语言的文件IO不一样,但他们都有IO流的说法,那么说到很多次“流”这个字,那么“流”究竟是什么?
1. 流的简单认识
“流”即是流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方
向性的数据( 其单位可以是bit,byte,packet )的抽象描述。C++流是指信息从外部输入
设备(如键盘)向计算机内部(如内存)输入和从内存向外部输出设备(显示器)输出
的过程。这种输入输出的过程被形象的比喻为“流”。
它的特性是:有序连续、具有方向性。
那我们们直接步入主题,来认识C++中的IO流
2. C++IO流
a. C++IO特点
C++中的IO体系中,使用了基类派生类的方式,以一个最基本的IO方式来衍生出各种合适的IO方式。和C语言有区别的是,C语言中是有着stdin、stdout、stderr。而C++中有cin、cout、cerr、clog,多了个clog,它们的基本特性还是差不多,例如cin和scanf一样默认以空格和换行作为隔断,不同的是C语言中是格式化输入输出,而C++中没有这一直接功能,而是需要使用函数来实现:
比如这一函数就是用来控制浮点数的精度:
这里后面出现了26是因为计算机存储浮点数精度的问题。
但是C++中解决了一个问题,那就是C语言无法对自定义类型有着很好的输出输入:
class Student
{friend istream& operator>>(istream& in, Student& st);friend ostream& operator<<(ostream& out, Student& st);
private:string name;string id;int age;
};istream& operator>>(istream& in, Student& st)
{in >> st.name >> st.id >> st.age;return in;
}ostream& operator<<(ostream& out, Student& st)
{out << "姓名:" << st.name << endl <<"学号:" << st.id << endl << "年龄:" << st.age << endl;return out;
}int main()
{Student s;cin >> s;cout << s;return 0;
}
这在C语言中写的可就不止这两行了。
还有在oj题中我们经常会遇到下面的代码:
int main()
{int a = 0;while (cin >> a){}return 0;
}
可是我们知道,流提取的返回值是ostream&类型的,但是我们while中判断的值直接判断的是bool,或者整型值也可以作为判断的依据,那么凭什么ostream类型能够作为判断依据呢?这是因为:
它的成员函数中重载了bool,可以让ostream转为bool类型,这也算是一种重载,但是跟普通的重载不一样,它没有返回值,就可以实现这样的玩法:
class Student
{friend istream& operator>>(istream& in, Student& st);friend ostream& operator<<(ostream& out, Student& st);public:operator bool(){if (name == "aaa")return false;else return true;}operator int(){return 1;}private:string name;string id;int age;
};istream& operator>>(istream& in, Student& st)
{in >> st.name >> st.id >> st.age;return in;
}ostream& operator<<(ostream& out, Student& st)
{out << "姓名:" << st.name << endl <<"学号:" << st.id << endl << "年龄:" << st.age << endl;return out;
}int main()
{Student st;while(cin >> st){if (st)cout << "hello world" << endl;else{int a = st;cout << a << endl;}}return 0;
}
b. C++文件IO
跟C语言一样对于文件IO操作都是以f开头。
关于文件IO,有两种方式,一种是二进读写,一种是文本读写,二进制读写是当你写到文件的数据形式是二进制,无法直接观看,而文本读写就是,以字符串的方式来写入文件中,可以直接观察:
我们使用一个类来封装一下文件IO:
class Student
{friend istream& operator>>(istream& in, Student& st);friend ostream& operator<<(ostream& out, Student& st);public:operator bool(){if (name == "aaa")return false;else return true;}operator int(){return 1;}//private:char name[10];char id[20];int age;
};istream& operator>>(istream& in, Student& st)
{in >> st.name >> st.id >> st.age;return in;
}ostream& operator<<(ostream& out, Student& st)
{out << "姓名:" << st.name << endl <<"学号:" << st.id << endl << "年龄:" << st.age << endl;return out;
}class FileIO
{
public:FileIO(string s):_filename(s){}void BinRead(Student& st){ifstream ifs(_filename, ios_base::in | ios_base::binary);ifs.read((char*)&st, sizeof(st));}void BinWrite(Student& st){ofstream ofs(_filename, ios_base::out | ios_base::binary);ofs.write((const char*)&st, sizeof(st));}void TextRead(Student& st){ifstream ifs(_filename);ifs >> st.name >> st.id >> st.age;}void TextWrite(Student& st){ofstream ofs(_filename);ofs << st.name <<endl << st.id << endl<< st.age;}private:string _filename;
};int main()
{FileIO fio1("test1.txt");Student st, st1, st2;cin >> st;fio1.BinWrite(st);fio1.BinRead(st1);cout << st1 << endl;FileIO fio2("test2.txt");cin >> st;fio2.TextWrite(st);fio2.TextRead(st2);cout << st2 << endl;return 0;
}
有人可能注意到,我对Student类做了一些小改动,我改变了它的成员变量,将string变成了char的数组,那我们假如使用string呢?
VS2022下
文件中
这与我们刚开始的结果并不一样。这里明显多了一些东西,那假如我们,重新运行程序只进行读呢?
存的东西再长一些呢?
直接崩了
这是因为自定义类型数组,是在栈上,在二进制读写的时候就是把自己数组的内容直接写进文件中了,但是自定义类型string不一样,它的内部结构我们大致直到应该是一个char*、size_t、size_t类型的,他的字符串是存在堆上的我们要进行写的时候,会把string中的char*的地址写到文件中,两个整形写到文件中。
如果我们这个时候结束程序,系统会回收我们的资源,但是我们文件中存的还是那个地址,那么再次打开程序读那个地址,发生访问越界问题那自然就崩了。
而在同一程序下进行读写的时候则是因为同一进程还没有回收资源,那个地址指向的仍然是我们的字符串,那为什么还是崩了呢?这个是因为,发生了浅拷贝的原因,st和st1中的string指向的地址是同一个,在main函数结束的时候,会释放两次,所以还是会崩。
还有一个问题,那为什么短一点的字符串看起来没有上面的在两个进程下读写崩了呢?那是因为VS编译器中的string中有一段自己的缓存区。
所以:使用二进制读写时,尽量不要用容器
c. 内存间IO
我们在C语言中如果要将一个整形转成字符串或者字符串转成整形会怎么办呢?我们可能会使用函数atoi、itoa,但是我们还有一种方法,就是使用ssprintf、sscanf:
负数也可以:
而在C++中自然也可以有这种方式:
那就是sstream。
1). 字符串整数之间转换
int main()
{//整数转字符串int a = 123456;string s;stringstream ssm;ssm << a;ssm >> s;cout << s << endl;//字符串转整数ssm.clear();ssm << s;int b = 0;ssm >> b;cout << b << endl;return 0;
}
clear()注意多次转换时,必须使用clear将上次转换状态清空掉, stringstreams在转换结尾时(即最后一个转换后),会将其内部状态设置为badbit,因此下一次转换是必须调用clear()将状态重置为goodbit才可以转换,但是clear()不会将stringstreams底层字符串清空掉s.str("");
将stringstream底层管理string对象设置成"", 否则多次转换时,会将结果全部累积在底
层string对象中
2). 字符串拼接
int main()
{string s1 = "hello";string s2 = "world";stringstream ssm;ssm << s1;ssm << s2;string s3;ssm >> s3;cout << s3 << endl;return 0;
}
3). 结构化数据
class Student
{friend istream& operator>>(istream& in, Student& st);friend ostream& operator<<(ostream& out, Student& st);
public:Student(string name = "", string id = "", int age = 0):_name(name),_id(id),_age(age){}string _name;string _id;int _age;
};istream& operator>>(istream& in, Student& st)
{in >> st._name >> st._id >> st._age;return in;
}ostream& operator<<(ostream& out, Student& st)
{out << "姓名:" << st._name << endl <<"学号:" << st._id << endl << "年龄:" << st._age << endl;return out;
}int main()
{Student st("张三", "2024", 12);stringstream ssm;ssm << st._name << " " << st._id << " " << st._age;Student st1;ssm >> st1._name >> st1._id >> st1._age;cout << st1 << endl;return 0;
}
4). 小点总结
- stringstream实际是在其底层维护了一个string类型的对象用来保存结果。
- 多次数据类型转化时,一定要用clear()来清空,才能正确转化,但clear()不会将
stringstream底层的string对象清空。 - 可以使用s. str(“”)方法将底层string对象设置为""空字符串。
- 可以使用s.str()将让stringstream返回其底层的string对象。
- stringstream使用string类对象代替字符数组,可以避免缓冲区溢出的危险,而且其会对参
数类型进行推演,不需要格式化控制,也不会出现格式化失败的风险,因此使用更方便,更
安全。