小心为上:注意C++ fstream给你设下的陷阱
透过名字看本质:到底什么是stream?
stream的定义
stream的中文翻译为“流”,不是很好理解,我们来看英文关于stream的定义,比较常见的有两个:
1. A stream is an abstraction that represents a device on which input and output operations are performed.
2. A stream is a "stream of data" in which character sequences "flow."
英文看起来比较累,总结一下,提炼如下几个关键点:
1. abstraction that represents a device:代表一个设备;
2. stream of data:数据流,隐含了FIFO的一个特性;
3. character sequences flow:字符序列在其中流动,而不是二进制在其中流动;
多说无益,还是看图说话:
注:上图中暗含一个玄机:stream的数据是设备数据的一个子集,因为stream只是代表一个设备,而并不是完全等于一个设备。
关公战秦琼: stream vs buffer
一个容易和stream混淆的概念就是大家常见的buffer,buffer的定义为(参见WIKI Data Buffer):A buffer is a region of memory used to temporarily hold data while it is being moved from one place to another.
英文看起来也比较累,还是总结一下:
1. region of memory:一块内存区域,因此就隐含了随机访问和二进制的操作方式;
2. one place to another:数据移动过程中使用
经过对两个术语定义的分析总结,相信大家都明白了这两者之间的差异了,其实这就是一个关公战秦琼的例子,因为它们两个实际上并不是一个竞争关系。
汇总对比点如下:
对比点 |
stream |
buffer |
作用 |
代表一个设备 |
数据临时存储 |
访问方式 |
FIFO |
随机访问 |
数据内容 |
字符流 |
二进制 |
当然,并不是说stream和buffer就毫无关系了,stream为了提高性能,实现的时候就用到了buffer。
小心为上:注意fstream设下的陷阱
2.1 char和wchar_t的操作
当你使用<<输出的时候,任何字符都可以输出;但当使用>>进行输入的时候,开头的空白字符缺省情况下是跳过的。
例如:假设文件中有这么一个字符串“ test”,则使用>>读入的时候,直接就读到了字母‘t’,而不会是空格。
避免这个陷阱的招数:
1. 清除skipws标志:unsetf(ios::skipws)
2. 使用get()函数
2.2 指针类型的操作
1. char*
char*类型的数据输出时会将字符串全部输出,但输入的时候你千万要注意,输入的时候默认会将前面的空白字符全部去掉;而且输入时默认是以空格来作为分隔符的,例如:“This is a test C-string.”可以全部输出,但如果文件中有这么一串字符串,那么用<<是无法全部读入的,只能读入“This”“is”“a”“test”“C-string”.
避免这个陷阱的招数:
1. 如果想改变默认去掉头部空白字符的操作方式,请参考skipws
2. 如果想改变以空格作为结束符的操作方式,对不起,用<<是没有办法的,只能用istream& istream::get (char* str, streamsize count, char delim)或者
istream& istream::getline (char* str, streamsize count, char delim)
2. void*
直接输出指针地址。
3. 其它指针
不管是指向int/double等标准数据类型,还是自定义的struct/class类型,都是输出指针指向的地址,而不是输出指针指向的对象。
2.3 eof和fail
2.3.1 eof
eof从字面意思来看,当然是end of file,用于表明当前已经到了文件末尾,不能再读了。
但这里有一个很迷惑的陷阱:只要遇到结束符,流就会将状态置为EOF,而不管置位前的操作是否成功。
例如,使用getline函数读取文件的最后一行,如果这一行是因为遇到了EOF而结束的,那么getline操作是成功的,但eof还是会置位。
因此,不能在调用函数后通过eof来判断函数调用是否读到文件末尾了,而应该直接判断调用本身是否成功,具体样例请看fail。
2.3.2 fail
导致fail标志位置位的有如下常见的情况:
1. 文件不存在;
2. 文件不能创建;
3. eof标志位置位;
4. 非法的格式,例如当你期望数字的时候,而文件里面却是字母
注意第三种情况,在文件eof的时候也会同时置fail,所以,循环读取文件的时候,要将fail和eof结合起来使用:在循环判断中使用fail,fail失败后再使用eof。
错误的用法:
1: std::ifstream file("test.txt");
2: std::string word;
3: double value;
4: while ( true ) {
5: // A word and a double value were both read successfully
6 file >> word >> value;
7 if( file.eof() )
8 break;
9: }
正确的用法
1: std::ifstream file("test.txt");
2: std::string word;
3: double value;
4: while (file >> word >> value) {
5: // A word and a double value were both read successfully
6: }
7: if (!file.eof()) throw std::runtime_error("Invalid data from file");
2.4 fstream的binary打开方式
文件流的打开方式中有一个binary,从字面意思来看,应该是按照二进制打开文件,然后进行二进制读写。
然而这样理解的话,就陷入了C++的陷阱:binary实际上和二进制读写没有关系,binary只是为了告诉系统是否将不同操作系统间特定的字符替换,最典型的是换行符,在windows上是/r/n,而在Unix类系统上是/n,如果加了这个binary标志,流就不会自动替换。
那C++的fstream流如何进行二进制读写呢?其实很简单:只需要调用get/read和put/write即可。也就是说:是否是二进制读写和文件打开方式无关,而是和调用函数有关。如果使用<<和>>,则就是按照字符流进行读写;如果使用get/read和put/write,则按照二进制读写。
前面说过流是character sequences flow,那为什么这里又说fstream能够按照二进制读写呢?由于没有研究过相关的实现代码,因此这里无法给出分析,个人推断应该是流的内部实现将字符流转换为二进制了。