如何写文本文件

DinS          Written on 2017/12/12

读写文件应该是编程中最基本也是非常重要的操作,因为程序总是把一定的输入转化为特定的输出,这个输入在绝大部分情况下都是来自文件的。
文件读写是程序员的基本功,处理得好会给后续编程带来极大的便利,本专题就这一问题做一个详细的介绍。

一、变量类型与文件类型

在直接进入读写文件之前,先要把变量类型搞清楚,这个不明白则读写文件是没法进行的。

从概念上来说变量总共就有3种类型:数字型、字符型、自定义类型。
数字型细分可以是int,long,double等等。
字符型主要就是char,以及基于char的string。当然还有wchar,这个详见字符集专题,本专题主要以char为基准。
自定义类型就是用户定义的各种class,但是class归根结底还是由数字型和字符型组合而成的,所以一定是可以化简为以上两种情况。
因此重点就是理解数字型和字符型之间的区别。

我们知道,数据在计算机中本质上都是用二进制存储的,即一大串01,所谓类型就是人为约定好用什么样的方式去解释二进制串。
一个二进制位称为1比特(bit),8个bit称为1个字节(byte),一个char是1个byte,一个int是4个byte。
为了更加直观一点,让我们打开计算器,调整到程序员模式,然后输入123456。

注意划横线的部分,这里的32个位就是整数123456对应的二进制表示。
还有一个小细节,在这里我默认左边为高位,右边为低位。当然这是概念上的表示,真正在计算机里怎么存超出了本专题的讨论范围,读写文件没必要深入这么底层。

这么一大串二进制位如果按照int解释就是123456,如果按照字符去解释呢?
一个字符占一个字节,那么结果应该是4个字符。为了避免复杂化,我们使用ASCII来解释(字符集问题见另一个专题)。
通过查ASCII码表,按照这里约定的从左到右的顺序,这一大串二进制解释出来就是”\0☺b@”,第一个是不可打印字符。

不用太纠结具体细节,这里想表达的就是对于同样的二进制数据,用不同类型去解释会得到不一样的结果,这个概念对读写文件很关键,尤其是读写二进制文件时。

在理解了变量类型后,再说说文件类型。文件可以粗略分成两类:文本文件和二进制文件。我们知道计算机上的所有数据都是用二进制存储的,那么区分文本文件和二进制文件有意义吗?
文本文件的意思是可以使用文本编辑器打开并且得到有意义信息的文件,通常的格式是.txt,但是可以是任何后缀名。所谓用文本编辑器,其本质含义就是针对一大串二进制,使用字符型变量去解释,得到了正确的文本信息而不是乱码。
如果一个文件符合这样的描述,就是文本文件,文本文件的优势在于易于人类理解和修改,用的还是很多的。

所谓二进制文件,就是使用文本编辑器打开得到的是乱码的文件,既然是二进制人类肯定无法直接理解了。

二、写文本文件

写文本文件是读写文件中最简单的一项,让我们从易到难逐渐深入。直接上代码

很简单,定义一个ofstream类型的变量fout,于是概念上这个fout就相当于要写的那个文件了。构造函数中指明文件名。(注:构造函数中只接收char*,所以如果是string则需要.c_str())
然后使用<<接变量来实现写文件。<<可以反复重复,看起来数据就像流动的一样。
最后的那个endl= end of line,也就是换行符。不过endl除了换行外还有一个功能,就是flush。在代码执行了<<后其实数据并没有写到文件里,而是停留在内存中。Flush的意思是把内存数据真正写到文件里。如果没有flush那么数据等于丢失了。

让我们执行代码,然后到工程目录下寻找这个文件。

果然有,打开看看

确实是我们输出的内容。
但是故事到这里还没有结束,这里实际上发生了一些神奇的事情,计算机替我们做了很多幕后工作。当我们使用fout<<str时,并没有什么神奇的,str本来就是字符,写出来的也是文本文件。但是当我们fout<<n时,情况却不一样。
这里我们打开的是文本文件,换句话说123456是字符,一个字符一个字节,因此123456占6字节,但是程序里的123456是int类型,一个int4个字节,也就是说123456占4字节。从int到char计算机替我们做了大量工作.
这个文件里显示的一串数字在本质上仅仅是一串字符而已。

然后还有一点,注意到这个文本文件有4行,但是实际上我们貌似只输出了3行,最后一行是结尾那个endl造成的,在读文件的时候最后一个空行可能造成一些麻烦
因此如果可能,在最后一行输出中使用flush
对文本文件问题还不算大,但是对于二进制文件关系很大。这个稍后再说。

运行结果

三、写文本文件的模式

写文件还有一件事需要注意,如果打开的文件已经存在,则里面的内容会清空。
换句话说,如果你把这个程序执行两遍,只有一个WriteTxt.txt,里面的内容就是3行。
大多数情况下这就是我们想要的效果,不过有的时候我们不希望清空内容,而是不断增加内容,这时需要用到文件模式,看代码。

我们在构造函数中指定了文件模式为app,固定写法,意为append。
还有其他哪些模式可以参考cplusplus reference。之后还会介绍到二进制模式。
常用的就这两个。

运行结果(假设文件存在)

你能解释为什么只有5行吗?
因为我们末尾使用的是flush而不是endl,于是下一次的Hello File直接接到了上次的末尾。

关于写文本文件暂时就介绍这么多了,能满足大部分常规需要,一些其他的技巧在介绍读文本文件时在合起来讲解。

四、关于文件的路径问题

虽然文件路径并不跟读写文件直接相关,但是仍然很重要,本专题以windows为例,其他的操作系统可能有差异,但是思路是相同的。貌似c++17标准引入了path和systemfile,有了这些就可以跨平台操作文件系统了。

1.相对路径表示法。

考察上一个例子,有一个问题:为什么WriteTxt会出现在那个位置?
我们在构造函数里并没有指定文件路径,对于这种情况程序使用默认路径。每一个应用程序启动时,默认路径自动设置为exe所在路径。不过对于VS是个例外,从VS里调试程序默认路径为项目所在路径。
当前路径使用.\表示,所以在上例中构造函数实际全部填充完后为.\WriteTxt.txt。注意\在c++中是转义符号,因此为了表示\还需要加一个\,于是用代码描述就变为.\\WriteTxt.txt。
更直接的理解就是如果不指定路径,则文件生成到应用程序同级目录下,为了验证这一点,我们直接运行编译好的程序。

结果

这种路径表示方法称为相对路径。引申出两个问题:如何深入文件目录和如何返回上级目录。

第一个问题的回答就是不断使用\

对于这样的代码,表达的意思就是将WriteTxt.txt生成在exe所在目录下的Test文件夹里,注意Test文件夹必须存在,否则会失败。
于是我们编译程序,然后建立Test文件夹。

然后运行exe。结果

第二个问题的回答是使用..\

这里代码的意思是将WriteTxt.txt生成在exe所在目录的上级目录当中
编译运行。

于是我们可以结合.\和..\达到我们想要的特定目录。

2.绝对路径表示法

另一种路径表示方法称为绝对路径。

顾名思义就是指定文件的绝对路径,于是不管exe在哪里文件都生成在那个路径下。

3.与字符集有关的路径问题

对于路径还有一点要说明。
我们所有的例子都是使用了只有英文的路径,如果路径中有非英文路径呢?这是一个问题,对于windows而言,如果路径中有非英文,比如中文,则像上面那样使用fstream可能会失败。
为什么叫可能?跟编译器版本有关系,对于老编译器会失败,新编译器貌似可以。
最保险的是使用wstring,这是因为windows原生态使用的是utf-16,因此路径实际上在操作系统里都是utf-16编码,使用wstring可以保证非英文路径成功。

看例子

使用L””表示这是一个宽字符串,L是VS的标准。

文件概念与写文本文件到此结束,如何读入文本文件见《如何读文本文件