换行符引发的血案

众所周知,换行符在Linux与Win下不同,在Linux下是’\n’,在Win下是’\r\n’。这就是我在文件读写时遇到奇怪的问题的起因。

std::getline

首先我在Win下新建一个文件test.txt,只有一个空格(注意不是用vim,原因等下解释)。

#include <iostream>
#include <fstream>

using namespace std;

int main () 
{
    fstream f("test.txt", fstream::in);
    string s;
    getline(f, s);
    cout << s.size() << endl;
}

输出:1

这时候我把test.txt改一下,再换一行,变为一个空格加一行,代码不变。

#include <iostream>
#include <fstream>

using namespace std;

int main () 
{
    fstream f("test.txt", fstream::in);
    string s;
    getline(f, s);
    cout << s.size() << endl;
}

输出:2

这时候你可能会觉得奇怪,欸难道换行符也被读进来了?于是查了下zeal里面的C++文档。

getline reads characters from an input stream and places them into a string:

  1. Behaves as UnformattedInputFunction, except that input.gcount() is not affected. After constructing and checking the sentry object, performs the following:

    1. Calls str.erase()

    2. Extracts characters from input and appends them to str until one of the following occurs (checked in the order listed)

      1. end-of-file condition on input, in which case, getline sets eofbit.

      2. the next available input character is delim, as tested by Traits::eq(c, delim), in which case the delimiter character is extracted from input, but is not appended to str.

      3. str.max_size() characters have been stored, in which case getline sets failbit and returns.
    3. If no characters were extracted for whatever reason (not even the discarded delimiter), getline sets failbit and returns.
  2. Same as getline(input, str, input.widen(‘\n’)), that is, the default delimiter is the endline character.

可以看到黑体部分,换行符是不会加到尾部的。但是多出来的那一个字符是什么鬼?其实问题就出在我是用bash for win里面的gcc。可以看到,getline是以’\n’作为默认结尾的,因此它getline取的是’ \r’,即一个空格加一个回退符,所以长度为2。因为好奇,我又用dec-cpp内置的MingW gcc进行编译,发现结果还是1。看来MingW确实是对着win的,是处理过的,把’\r\n’作为换行符。

做一个简单测试来证明这一点,先是bash for win

bash for win; x86_64-pc-linux-gnu; gcc version 6.1.0

#include <iostream>
#include <fstream>

using namespace std;

int main () 
{
    fstream f("test.txt", fstream::in);
    string s;
    getline(f, s);
    cout << "nihaoa" << s[s.size() - 1] << "hehe" << endl;
}

输出:heheoa

然后是MingW gcc

x86_64-w64-mingw32; gcc version 4.8.1

#include <iostream>
#include <fstream>

using namespace std;

int main () 
{
    fstream f("test.txt", fstream::in);
    string s;
    getline(f, s);
    cout << "nihaoa" << s[s.size() - 1] << "hehe" << endl;
}

输出:nihaoa hehe 

文件随机访问

但是这还没完,还有随机读写呢。fstream和sstream都是支持随机访问的。因此就有seekg(seekp), tellg(tellp)等函数。测试发现用seekg(xxx, fstream::end)会发现同样奇怪的现象,以致于一开始我没搞明白上面的所发生的事情时候一度认为该函数实现有问题(其实是我自己太菜)。于是乎发现seekg(0, fstream::end)都是指向文件的字符串位置的”尾后迭代器”(算是吧)。然后文件内容是一个空格加一个换行符,我又写了个程序来玩。

先是bash for win

bash for win; x86_64-pc-linux-gnu; gcc version 6.1.0

#include <iostream>
#include <fstream>

using namespace std;

int main () 
{
    fstream f("test.txt", fstream::in | fstream::ate);
    auto end = f.tellg();
    f.seekg(-3, fstream::end);
    cout << boolalpha;
    cout << f.peek() << endl;
    cout << (f.peek() == ' ') << endl;
    f.seekg(-2, fstream::end);
    cout << f.peek() << endl;
    cout << (f.peek() == '\r') << endl;
    f.seekg(-1, fstream::end);
    cout << f.peek() << endl;
    cout << (f.get() == '\n') << endl;
    cout << (f.tellg() == end) << noboolalpha << endl;
}

输出:
32
true
13
true
10
true
true

然后是MingW gcc

x86_64-w64-mingw32; gcc version 4.8.1

#include <iostream>
#include <fstream>

using namespace std;

int main () 
{
    fstream f("test.txt", fstream::in | fstream::ate);
    auto end = f.tellg();
    f.seekg(-3, fstream::end);
    cout << boolalpha;
    cout << f.peek() << endl;
    cout << (f.peek() == ' ') << endl;
    f.seekg(-2, fstream::end);
    cout << f.peek() << endl;
    cout << (f.peek() == '\r') << endl;
    f.seekg(-1, fstream::end);
    cout << f.peek() << endl;
    cout << (f.get() == '\n') << endl;
    cout << (f.tellg() == end) << noboolalpha << endl;
}

输出:
32
true
10
false
10
true
true

这个问题十分诡异。MingW gcc竟然强行变成了两个’\n’,而非’\r\n’。暂时没有找到到底是什么原因。但是可以清楚看到的是,这时候在判断换行符时候就不能像前面getline一样以为是只有一位了。所以在对文件进行随机读写的时候,一定要考虑最后是换行符的情况。在判断结尾的时候尽量避免先用seek跳到结尾字符然后再一个一个get来判断的方式,如果一定要这么做,可以考虑用一下peek, unget, putback这些可以吃后悔药的方法来检测一下,下一个字符到底是什么,避免直接用get()却又没考虑到所有情况,导致程序错误。

  • 文件由换行符结尾
    • Win : 倒数第三个才是最后一个字符
    • Linux : 倒数第二个就是最后一个字符
  • 文件不由换行符结尾 : 你可以庆幸了,两种系统都是一样的

Vim

之前提到说不要用vim来编辑那个text.txt,又是为什么呢?作为一款优秀的编辑器,它肯定要遵守Linux下的基本法。按照POSIX standard,有以下规定(此问题答案从SO发现)

3.206 Line

A sequence of zero or more non- characters plus a terminating character.

所以这样搞之后,只要你的文件不是空ls -hl | grep xxx | awk '{print $5}',你就一定会有一个换行符。但是你看啊,并不是每个人都读过基本法啊(特别是像我这么菜的)。因此vim作为Linux下一款优秀的编辑器,文件当然要按照Linux的基本法去产生。于是默认情形vim会在你保存的文件末尾再强行加一个换行符,以避免忘了加。所以需要加入:set noendofline binary(或者直接修改.vimrc)让其不在你的文件末尾偷偷加上换行符。

参考资料

  1. Why should text files end with a newline?
  2. Definitions
  3. std::getline
  4. 让vim不在末尾添加0a,换行