C++学习笔记-第7单元-文件输入输出流

C++学习笔记-第7单元


注:本部分内容主要来自中国大学MOOC北京邮电大学崔毅东的 《C++程序设计》课程。


第7单元 文件输入输出流

单元导读

  本单元主要学习C++的文件输入与输出。相比于C语言的文件输入与输出,C++使用了“stream(流)”这个概念。提供了很多便捷的输入输出操作。相比于Java,C++的文件流的类没有那么复杂,比较容易掌握。

本单元的内容主要包括:

  1. C++17引入的filesystem这套新东西,提供了path这个类,使得我们可以直接处理很多与文件路径相关的操作,也可以获取磁盘的相关信息。但是这一部分的重点是path类
  2. 创建文件的输入流和输出流对象,并且使用 流提取运算符“>>” 从文件中读数据,使用 流插入运算符“<<” 向文件中写数据。这部分很好理解,因为这些用法与我们使用cin和cout没有太多不同。
  3. 格式化输入输出。有些时候,我们要控制输出数据所占的位宽,或者控制数据的精度。这些都依赖于一些操纵符。你不一定要掌握我们介绍的所有I/O操纵符,但是你需要记住有哪些操纵符。这样等你编程序的时候,根据程序输入输出的需要去查手册。
  4. 二进制输入输出。我们不需要纠结于文本或者二进制的区别。这里最重要的是记住用 write()read() 这两个函数。而且这两个函数接收的第一个参数是 char* 类型的指针。为了将数组、整型变量等等与 char* 类型指针转换,我们还必须学会 reinterpret_cast 这个运算符的用法
  5. 那么最后就是随机文件访问。这一节最重要的是知道如何使用seekx移动文件位置指示器。

  由于C++的I/O流是带有缓冲的,而缓冲这个机制对于程序员来说是不透明的,因此,缓冲会给我们带来一些麻烦。所以我们也要尝试使用 get/getline()等函数编写一些例程,了解 缓冲对输入的影响

7.1 [C++17]文件系统

7.1.1 C++17的文件系统库简介

  C++17的文件系统库被放在名字空间std::filesystem之中。标准库filesystem提供在文件系统与其组件,例如路径、常规文件与目录上进行操作的方法.

一些术语:

  1. File(文件):持有数据的文件系统对象,能被写入或读取。文件有名称和属性,属性之一是文件类型。
  2. Path(路径):标识文件所处位置的一系列元素,可能包含文件名(也可以不包含)。

    绝对路径(Absolute Path):包含完整的路径和驱动器符号,各操作系统的表示形式不同。
    相对路径(Relative Path):指文件存在相对于“当前路径”的位置,不包含驱动器及开头的“ / ”符号。

各操作系统路径表示的不同
OS TypeAbsolute pathDirectory path
Windows(大小写不敏感)c:\example\scores.txtc:\example
Unix/Linux(大小写敏感)/home/cyd/scores.txt/home/cyd
注:windows XP 系统及之后也开始支持" / "的写法

路径类的调用示例:

namespace fs = std::filesystem;//因为名字空间太长,所以就给名字空间起一个别名
fs::path p{ "CheckPath.cpp" };//给路径类p传递一个文件的名字

//namespace fs = std::filesystem::path;//错误,不可以给类起别名
//using std::filesystem::path;         //错误,using只能定义一级的名字

文件和路径在不同操作系统和编程语言中有所不同:

WindowsLinuxC++java
行结束字符\r\n\n-System.getProperty(“line.separator”);
获取当前系统所支持的行结束字符
路径名分隔符‘\’‘/’std::filesystem::path::preferred_separator
获取当前操作系统的路径名分隔符
java.io.File.separator
获取当前操作系统的路径名分隔符
路径名a:\b\c 或\host\b\c/a/b/cstd::filesystem::path
使用path类来处理路径
-

windows系统下定义路径类的代码示例:

namespace fs = std::filesystem;
fs::path p1("d:\\cpp\\hi.txt"); // 字符串中的反斜杠要被转义
fs::path p2("d:/cpp/hi.txt");   // Windows也支持正斜杠
fs::path p3(R"(d:\cpp\hi.txt)");// 使用原始字符串字面量(5.4.2节)

附:更多文件系统库的资料查看cppreference的介绍。

7.1.2 路径类及操作

  本节介绍 路径类(path class)及部分操作函数。路径类的操作函数分为 成员函数非成员函数。下面的两个表格是部分函数的介绍:

path类的 成员函数
部分重要的成员函数说明
+path(string)构造函数。
+assign(string): path&为路径对象赋值。
连接+append(type p): path&
等价于 /= 运算符。
将p追加到路径后。自动 添加目录分隔符。
type是string、path或const char*。
+concat(type p): path&
等价于 += 运算符
将p追加到路径后。不自动 添加目录分隔符。
type是string、path或const char*。
修改器+clear(): void清空存储的路径名。
+remove_filename(): path&从给定的路径中移除文件名。
+replace_filename(const path& replacement): path&以 replacement 替换文件名。
分解+root_name(): path返回通用格式路径的根名。
+root_directory(): path返回通用格式路径的根目录。
+root_path(): path
等价于 root_name() 、 root_directory()
返回路径的根路径,即“路径的根名 / 路径的根目录”。
+relative_path(): path返回相对于 root-path 的路径。
+parent_path(): path返回到父目录的路径。
+filename(): path返回路径中包含的文件名。
+stem(): path返回路径中包含的文件名,不包括文件的扩展名。
+extension(): path返回路径中包含的文件名的扩展名。
查询+empty(): bool检查路径是否为空。
+has_xxx(): bool其中“ xxx” 是上面“分解”类别中的函数名。这些函数检查路径是否含有相应路径元素。
path类的 非成员函数
部分重要的非成员函数说明
操作符 /
(const path& lhs, const path& rhs )
以偏好目录分隔符连接二个路径成分 lhs 和 rhs。比如:
path p{"C:"}; p = p / "Users" / "batman";
//结果使得p为 C:/Users/batman
操作符 <<, >> (path p)进行路径 p 上的流输入或输出。
文件类型is_regular_file( const path& p ): bool检查路径是否是常规文件。
is_directory( const path& p ): bool检查路径是否是目录
is_empty( const path& p ): bool检查给定路径是否指代一个空文件或目录
查询current_path(): path
current_path( const path& p ): void
返回当前工作目录的绝对路径(类似linux指令 pwd)。
更改当前路径为p (类似linux指令 cd)。
file_size( const path& p ): uintmax_t对于常规文件 p ,返回其大小;尝试确定目录(以及其他非常规文件)的大小的结果是由编译器决定的
space(const path& p): space_info返回路径名 p 定位于其上的文件系统信息。space_info中有三个成员:
capacity ——文件系统的总大小(字节);
free ——文件系统的空闲空间(字节);
available ——普通进程可用的空闲空间(小于或等于 free )。
status(const path& p): file_status返回 p 所标识的文件系统对象的类型与属性。返回的file_status是一个类,其中包含文件的类型(type)和权限(permissions)
修改remove(const path& p): bool
remove_all(const path& p): uintmax_t
删除路径 p 所标识的文件或空目录
递归删除 p 的内容(若它是目录)及其子目录的内容,然后删除 p 自身,返回被删文件及目录数量。
rename(const path& old_p, const path& new_p): void移动或重命名 old_p 所标识的文件系统对象到 new_p(类似linux指令mv)。
copy( const path& from, const path& to ): void复制文件与目录。另外一个函数 bool copy_file(from, to) 拷贝单个文件。
create_directory( const path& p ): bool
create_directories( const path& p ): bool
创建目录 p (父目录必须已经存在),若 p 已经存在,则函数无操作;
创建目录 p (父目录不一定存在),若 p 已经存在,则函数无操作。

下面给出C++17中path对象的用法:

代码目的:创建一个test文件夹,包含Hello.txttest.txt的空文件,放在一个已知的目录下,然后修改9-11行代码相应的路径。

源代码main.cpp

//必须打开C++17标准
#include<iostream>
#include<filesystem>//注意需要包含文件系统库的头文件
#include<string>
int main() {
   namespace fs = std::filesystem;//将名字空间的别名范围限制在主函数

   //定义路径,使用生字符串、转义字符串、正斜杠字符串
   fs::path p1{ "C:\\Users\\liam\\Desktop\\test\\Hello.txt" };//反斜杠要注意转义字符
   fs::path p2{ R"(C:\Users\liam\Desktop\test)" };            //原始字符串字面量
   fs::path p3{ "c:/Users/liam/Desktop/test/hello.txt" };     //windows不区分大小写

   //输出默认文件分隔符
   std::cout << "file separator is <wchar_t>:" << fs::path::preferred_separator << std::endl;

   //判断p2是否是常规文件,如果是,输出文件大小
   //若不是常规文件,判断是否是目录,如果是目录,列出其所有的子目录
   //若不是目录,判断路径是否存在,如果是,输出存在
   //如果不存在,就说不存在
   if (fs::is_regular_file(p2)) {
       std::cout << p2 << "s size is:" << fs::file_size(p2) << std::endl;
   }
   else if (fs::is_directory(p2)) {
       std::cout << p2 << "is a directory,includes: " << std::endl;
       for (auto& e : fs::directory_iterator(p2)) {
           std::cout << " " << e.path() << '\n';
       }
   }
   else if (fs::exists(p2)) {
       std::cout << p2 << "is a special file\n" << std::endl;
   }
   else {
       std::cout << p2 << "does not exist" << std::endl;
   }

   //p1是否存在?若存在,展示path类中一些分解路径长度的函数
   if (fs::exists(p1)) {
       std::cout << std::endl;
       //若存在,根名?根路径?相对路径?
       std::cout << "root_name(): " << p1.root_name() << "\n"
                 << "root_path(): " << p1.root_path() << "\n"
                 << "relative_path(): " << p1.relative_path() << "\n";
       //若存在,父路径?文件名?文件名主干?扩展名?
       std::cout << "parent_path(): " << p1.parent_path() << "\n"
                 << "filename(): " << p1.filename() << "\n"
                 << "stem(): " << p1.stem() << "\n"
                 << "extension(): " << p1.extension() << std::endl;
   }
   
   //p2是否存在?若存在,展示一些path类的特殊运算符的用法
   if (fs::exists(p2)) {
       std::cout << std::endl;
       //append和/=,在当前路径后添加路径
       fs::path pa{ p2 }, pb{ p2 };
       pa.append(R"(users)"); 	//添加路径
       pb /= R"(cyd)"; 			//连接前后的路径名
       std::cout << "pa: " << pa << "\n"
                 << "pb: " << pb << std::endl;
       //concat和+=,直接修改最后一个主干单词
       pa = p2 , pb = p2;
       pa.concat(R"(users)");
       pb += R"(cyd)";
       std::cout << "pa: " << pa << "\n"
                 << "pb: " << pb << std::endl;
       //用运算符/拼凑出一个新路径
       pa = p2;
       pa = pa / R"(temp)" / R"(cyd)";//注意第一个操作数必须是path对象
       std::cout << "p2: " << p2 << std::endl;
       std::cout << "pa: " << pa << std::endl;
   }

   //获取磁盘的空间信息
   if (fs::exists(p3)) {
       fs::path pc{ p3.root_name() };//获取对象根目录(磁盘)
       //需要注意的是,不获取根目录也没关系,当前目录和根目录的空间信息是一致的
       //展示磁盘总大小和剩余大小
       std::cout << std::endl;
       std::cout << "C:total space (GB):" 
                 << static_cast<double>(fs::space(pc).capacity) / 1024 / 1024 / 1024 << std::endl;
       std::cout << "C: free space (GB):" 
                 << static_cast<double>(fs::space(pc).free) / 1024 / 1024 / 1024 << std::endl;
   }

   //std::cin.get();
   return 0;
}

运行结果:

file separator is <wchar_t>:92
"C:\\Users\\liam\\Desktop\\test"is a directory,includes:
"C:\\Users\\liam\\Desktop\\test\\Hello.txt"
"C:\\Users\\liam\\Desktop\\test\\test.txt"

root_name(): "C:"
root_path(): "C:\\"
relative_path(): "Users\\liam\\Desktop\\test\\Hello.txt"
parent_path(): "C:\\Users\\liam\\Desktop\\test"
filename(): "Hello.txt"
stem(): "Hello"
extension(): ".txt"

pa: "C:\\Users\\liam\\Desktop\\test\\users"
pb: "C:\\Users\\liam\\Desktop\\test\\cyd"
pa: "C:\\Users\\liam\\Desktop\\testusers"
pb: "C:\\Users\\liam\\Desktop\\testcyd"
p2: "C:\\Users\\liam\\Desktop\\test"
pa: "C:\\Users\\liam\\Desktop\\test\\temp\\cyd"

C:total space (GB):465.036
C: free space (GB):261.569

7.2 文件I/O流的基本用法

7.2.1 输入输出类介绍

  本节介绍C++中的 输入输出类(Input and Output Classes)。相比C语言来说,C++提供了更加详细、复杂的文件操作,下面是两者的对比:

C 和 C++ 文件操作对比
文件操作类型C++C
Header File (头文件)file inputifstream (i: input; f:file)stdio.h
file outputofstream (o: ouput; f:file)
file input & outputfstream(统合上述两个)
Read/Write (读写操作)read from file(读文件)>>;(流读取运算符)
get(); get(char); get(char*);
getline();
read(char*,streamsize);
fscanf();
fgets(char*, size_t , FILE*);
fread(void *ptr, size, nitems, FILE *stream);
write to file(写文件)<<;(流插入运算符)
put(char), put(int);
write (const char*, streamsize);
flush()
fprintf();
fwrite(const void *ptr, size, nitems, FILE *stream);
fputs(const char*, FILE *);
Status test(状态测试)eof(); bad(); good(); fail()feof(); ferror();

注意上述 bad()fail() 函数:当流出现错误,fail()返回true;仅当流出现不可恢复的错误,bad()返回true。可见bad()能检测到的错误有限,比如打开一个不存在的文件时,bad()返回false。所以后面在读取文件数据的过程中,判断文件是否成功打开时使用fail()而不是bad()

输入输出流的示意图

  那什么是 ?流是一个数据序列。“输入流”和“输出流”都是从程序的角度来说的,所以上图中,左侧是“输入流”、右侧是“输出流”。进一步可以参考java的 输入输出流 讲义。

输入输出流类的层次

C++ I/O流类具有层次,如上图所示,主要有五类:

  1. 流基类 (黄色,ios_baseios)。由于ios_base这个类的名字很长,所以调用其中的一些静态常量的时候都是用他的派生类ios,如ios::app
  2. 标准输入输出流类(灰色,istreamostreamiostream)。注意到标准输入输出流对象 cincout分别是类 istream 和 ostream 的实例
  3. 字符串流类(橙色,istringstreamostringstream)。字符串流就是将各种不同的数据格式化输出到一个字符串中,可以使用I/O操纵器控制格式;反之也可以从字符串中读入各种不同的数据。
  4. 文件流类(绿色,ifstreamofstreamfstream)。
  5. 缓冲区类(蓝色,streambufstringbuffilebuf)。由于C++在输入输出的时候并不是直接与数据源交互,而是会在内存中开辟一个缓冲区(如字符串buf、文件buf)。即,C++的I/O流是有内部缓冲区的,如下图所示:
输入输出流的缓冲区
#include <iostream>
using namespace std;
int main() {
   char c;
   int i = 0;
   do {
       c = cin.get(); //本行导致2个字符进入缓冲区“a”、“换行符”
       //c = cin.get(void)每次读取一个字符并把由Enter键生成的换行符留在输入队列中
       cout << ++i << " : " << static_cast<int>(c) << endl;
   } while (c != 'q');
   return 0;
}

下面展示缓冲流的演示代码:

#include<iostream>
int main() {
   //拿到cin对象的缓冲区指针
   auto p = std::cin.rdbuf();
   //从键盘读入字符到缓冲区,保留所有字符在缓冲区
   auto x = std::cin.peek();
   //注意peek是只读缓冲区,不会移走字符;而get在读取的同时会移走字符。
   std::cout << "x = " << x << std::endl;
   //显示缓冲区等字符数量
   auto count = p->in_avail();//读入的可用的字符数
   std::cout << "There are " << count << " chasracters in the buffer." << std::endl;
   //把缓冲区的字符都取出来并显示
   for (int i = 0; i < count; i++) {
       std::cout << i + 1 << ":" << std::cin.get() << std::endl;
   }
   //std::cin.get();
   return 0;
}

运行结果:

hello		//这一行是手动输入,下面是自动输出
x = 104
There are 6 chasracters in the buffer.
1:104
2:101
3:108
4:108
5:111
6:10

注:如果控制台会一闪而过,可以右键工程项目名称→属性→链接器→系统→子系统→控制台。就不再需要std::cin.get();了。

7.2.2 向文件写入数据

  向文件中写入数据需要用到ofstream类。本节中使用ofstrem文本文件 中写数据,其他的文件类型(如二进制文件会在后面介绍)。如果文件已存在,那么其中的内容会默认直接清除。流程图如下:

向文件中写入数据的流程

注意到,在C++中,不管是向标准输出流写入数据,还是向文件中写数据,都可以使用“流插入运算符”(也叫流输出运算符)。下面的代码给出了三种可行的打开一个输出文件流的方法(前两种对应上述左侧流程,第三种对应右侧流程):

/**********方法一************/
std::filesystem::path p{"out.txt"};
std::ofstream output{p};
/**********方法二************/
std::ofstream output{"out.txt"};
/**********方法三************/
std::filesystem::path p{"out.txt"};
std::ofstream output{};
output.open(p);

  使用“流插入运算符”的一个好处是可以自动进行类型识别。假如现在需要向文件"score.txt"中写入数据,格式是“姓名空格数字”(如下图所示),那么流插入运算符就可以自动将“姓名”和“空格”转化成字符串,“数字”(int/double)转化成ASCII码:

流插入运算符自动进行类型识别

下面是示例代码:
源文件mian.cpp

//std:c++17
#include<iostream>
#include<filesystem>//路径类
#include<fstream>//输入输出(文件操作)类
int main() {
   namespace fs = std::filesystem;
   fs::path p{ "scores.txt" }; //创建路径类对象(相对路径)
   std::ofstream output{ p };  //创建文件
   double lileiScore{ 90.5 };  //李磊的数据
   int hanmeimeiScore{ 84 };   //韩梅的数据
   //输出数据到文件中
   output << "LiLei " << lileiScore << std::endl;
   output << "HanMei " << hanmeimeiScore << std::endl;
   //关闭文件
   output.close();
   //检测文件大小
   std::cout << "size of " << p << "is: " << fs::file_size(p) << std::endl;
   //std::cin.get();
   return 0;
}

运行结果:

size of "scores.txt"is: 23

7.2.3 从文件读数据

  向文件中读取数据需要用到ifstream类。本节中使用ifstrem文本文件 中读取数据,其他的文件类型(如二进制文件会在后面介绍)。流程图如下:

读取文件数据的流程图

注意到相比于向文件中写入数据,使用ifstrem从文件读取数据要多一个 “检测文件是否成功打开” 的步骤。并且读取过程中,流提取运算符不会自动识别数据类型,若想正确读出数据,必须确切了解数据的存储格式。如下图所示(图片右侧为错误代码,最后一位是0的原因是无法将字符串转化成整数所以就为默认值):

使用流插入运算符的时候要明确数据类型

  上述已经讨论了读取时的注意事项,现在来关心 “如何检测文件是否成功打开” 这个问题。在打开文件时可能出现的错误有:读文件时文件不存在、写文件时介质只读(比如CD光盘)等。那么在上述使用ifstram的构造函数或open()函数打开文件后,立即调用 fail() 进行检测,若返回true,则证明文件未打开。如下面的代码所示:

ofstream output("scores.txt");
if (output.fail())  {
   cout << R"(Can't open file "scores.txt"!)";
 }

  好的,现在可以打开文件了,也可以读取文件了,那么读取到哪里停止呢?这就需要 “检测是否已到文件末尾”。可以使用 eof() 函数检查是否是文件末尾,若返回true,则证明已到达文件结尾。如下面的代码所示:

ifstream in("scores.txt");
while (in.eof() == false) {
   cout << static_cast<char>(in.get());//需要类型转换是因为get()默认返回int
}

下面来展示文件读取的可执行代码,注意需要先有一个上节所生成的“score.txt”文件:
源文件main.cpp

//std:c++17
#include<fstream>//使用文件流操作
#include<iostream>
#include<filesystem>//使用路径类
#include<string>
int main() {
   namespace fs = std::filesystem;
   //打开文件
   fs::path p{ "scores.txt" };
   std::ifstream input{ p };
   //检测是否成功打开文件
   if (input.fail()) {
       std::cout << "Can't open file " << p << std::endl;
       //std::cin.get();
       return 0;
   }
   //检测是否读取到文件结尾,若没有就一直读取数据
   std::string name{ " " };
   double score{ 0.0 };
   char x;
   while (!input.eof()) {
       input.get(x);   //注意以前的标准里需要在判断条件中读取,现在不需要
       std::cout << x; //暴力读取
       //注意下面这一段代码会连续输出两次最后一行
       //这是因为需要再读一次,若读不出才会判断到达文档末尾
       /*input >> name >> score;
       std::cout << name << " " << score << std::endl;*/
   }
   //不使用open()函数,就不用close(),会默认调用析构函数
   //std::cin.get();
   return 0;
}

运行结果

LiLei 90.5
HanMei 84

7.3 格式化输出、I/O流函数

7.3.1 格式化输出

   格式化输出(Formating Output) 就是按照期望的格式将信息输出到文件中/显示到屏幕上。首先介绍两个词,manipulator和 <iomanip>manipulator 翻译成控制符/操作符,意思更多的是以希望的方式操控目标,与 operator 不同。而在使用 manipulators 时必须要包含头文件 <iomanip>。下面介绍三个控制符:“设置域宽”控制符、“设置浮点精度”控制符、“设置填充字符”控制符。

  首先来看 setw(int n)“设置域宽”控制符,w表示位宽width),用于设置域宽,即数据所占的总字符数(setw()默认为setw(0),按实际输出)。并且,setw(n)控制符 只对其后输出的 第一个 数据有效, 其他控制符 则对其后的 所有 输入输出产生影响。最后,如果输出的数值实际占用的宽度超过setw(int n)设置的宽度,则按实际宽度输出。如下面的代码所示:

/***************一:正常调用****************/
std::cout << std::setw(3) << 'a' <<  std::endl;
//C++标准输出默认右对齐:所以输出:_ _ a(两个空格+一个a)
/***************二:多个输出****************/
std::cout << std::setw(5) << 'a' << 'b' << std::endl;
//输出:_ _ _ _ ab(四个空格+一个a,一个b)
/************三:实际位宽>设定值************/
float f=0.12345;
std::cout << std::setw(3) << f << std::endl;
//输出:0.12345(位宽太小,就按原来的来)

  接下来看 setprecision(int n)“设置浮点精度”控制符,n代表数字总位数),用于控制显示浮点数的有效位。注意到setprecision(0)的效果取决于编译器,不同编译器的实现是不同的(按理说应该是保持默认宽度,所以VS比较合理)。代码如下:

#include <iostream>
#include <iomanip>
int main() {
   using std::cout;
   using std::endl;
   float f = 17 / 7.0;
   cout <<                         f << endl;//VS:2.42857   ;Eclipse CDT + GCC 8.2:2.42857
   cout << std::setprecision(0) << f << endl;//VS:2.42857   ;Eclipse CDT + GCC 8.2:2
   cout << std::setprecision(1) << f << endl;//VS:2         ;Eclipse CDT + GCC 8.2:2
   cout << std::setprecision(2) << f << endl;//VS:2.4       ;Eclipse CDT + GCC 8.2:2.4
   cout << std::setprecision(3) << f << endl;//VS:2.43      ;Eclipse CDT + GCC 8.2:2.43
   cout << std::setprecision(6) << f << endl;//VS:2.42857   ;Eclipse CDT + GCC 8.2:2.42857
   cout << std::setprecision(8) << f << endl;//VS:2.4285715 ;Eclipse CDT + GCC 8.2:2.4285715
   return 0;
}

  最后来看 setfill(c)“设置填充字符”控制符,c是一个字符型的参数),即“<<"符号后面的数据长度小于域宽时,使用什么字符进行填充。示例代码如下:

std::cout << std::setfill('*') << std::setw(5) << 'a' << std::endl;
//输出:****a

  下面这张表给出了在文件操作中,进行格式化输入/输出所有的控制符。注意到,有些流控制符同样可以用于文件输入(上面介绍的三个例子都是输出)。

控制符用途
setw(width)设置输出字段的宽度(仅对其后第一个输出有效)
setprecision(n)设置浮点数的输/入出精度(总有效数字个数等于n)
fixed将浮点数以定点数形式输入/出(小数点后有效数字个数等于setprecision指定的n)
showpoint将浮点数以带小数点和结尾0的形式输入/出,即便该浮点数没有小数部分
left输出内容左对齐
right输出内容右对齐
hexfloat / defaultfloatC++11新增;前者以定点科学记数法的形式输出十六进制浮点数,后者还原默认浮点格式
get_money(money)
put_money(money)
C++11新增;从流中读取货币值,或者将货币值输出到流。支持不同语言和地区的货币格式
https://en.cppreference.com/w/cpp/io/manip/get_money
https://en.cppreference.com/w/cpp/io/manip/put_money
get_time(tm, format)
put_time(tm,format)
C++11新增;从流中读取日期时间值,或者将日期时间值输出到流。
https://en.cppreference.com/w/cpp/io/manip/get_time
https://en.cppreference.com/w/cpp/io/manip/put_time

下面给出使用流输入输出控制符的代码示例:
源文件main.cpp

#include<iostream>
#include<iomanip>
int main() {
   //展示setw和setfill
   std::cout << std::setw(4) << std::setfill('#') << "a" << "\n";
   std::cout << std::setfill('*');
   for (int i = 0; i < 5; i++) {
       std::cout << std::setw(i + 2) << "\n";
       // std::cout << std::setw(i + 1) << "" << std::endl;//这行代码与上一行作用相同
   }

   //展示setprecision/fixed/shoepoint/left/right
   double pi = 3.1415926535;
   std::cout << std::setprecision(6) << pi << std::endl;
   std::cout << std::setprecision(6) << std::fixed << pi << std::endl;
   double y = 3.0;
   std::cout << std::defaultfloat << y << std::endl;//注意这里清除了上面的配置
   std::cout << std::showpoint << y << std::endl;
   std::cout << std::setw(20) << std::left << pi << std::endl;
   std::cout << std::setw(20) << std::right << pi << std::endl;

   //展示hexfloat
   double z = 4.4;
   std::cout << std::hexfloat << z << std::endl;
   std::cout << std::defaultfloat << z << std::endl;
   std::cout << std::showpoint << z << std::endl;
   //std::cin.get();
   return 0;
}

运行结果

###a
*
**
***
****
*****
3.14159
3.141593
3
3.00000
3.14159*************
*************3.14159
0x1.199999999999ap+2
4.40000
4.40000

7.3.2 用于输入/输出流的函数

  本节介绍一些用于输入/输出流的函数,这些函数包括 成员函数/非成员函数。

  1. getline()函数
Li Lei#Han Meimei#Adam

  首先来看一下 getline()函数出现的场合。当使用流提取运算符“ >> ”的时候,数据会被空格分隔开。比如对于上面的文件内容,使用“ >> ”(如下代码)只能读入“Li”。此时,如果想一次性读取Li Lei(也就是用#分隔的字符),就需要使用 getline() 函数。其中包括输入流的成员函数 input.getline()非成员函数 std::getline()非成员的 getline()函数更常用,以下依次进行介绍。

//使用流提取运算符读取文件内容
ifstream input("name.txt");
std::string name;
input >> name;

  对于 输入流的成员函数 ,调用格式见下,其中 char* buf表示已经开辟好的缓冲区; int size表示读入的字节数; char delimiter表示用于分隔的字符(默认空格、制表、换行符)。

/************调用格式************/
input.getline(char* buf, int size, char delimiter);
/************代码示例************/
constexpr int SIZE{ 40 };   //编译时常量
std::array<char , SIZE> name{};
while (!input.eof()) {      //没有读到文件的末尾
   input.getline(&name[ 0 ] , SIZE , '#');
   std::cout << &name[ 0 ] << std::endl;
}

  可以注意到上述输入流的成员函数的第一个参数调用非常麻烦,所以可以使用getline()非成员函数,调用格式见下,其中 istream& is是输入流对象引用; string& str是string类型的对象引用(即,读入的对象不再进入缓冲区,而是直接存入字符串对象); char delimiter是用于判断分隔的符号(默认空格、制表、换行符)。

/*****************调用格式******************/
std::getline(istream& is, string& str, char delimiter);
/*****************代码示例******************/
std::string name2{};
while (!input.eof()) {
   std::getline(input, name2, '#');
   std::cout << n << std::endl;
}
  1. get()put()函数

  两个函数都是对于单个字符的操作, get() 用于读取一个字符,有两个重载函数(无参/有参); put() 用于输出一个字符,只有一个有参的函数。

/****************get函数调用格式*****************/
int istream::get();//所以读取字符时要强制类型转换
//比如:char c = static<char>(in.get());
istream& get (char& c);
//比如:char c;  in.get(c);
/****************put函数调用格式*****************/
ostream& put (char c);
  1. flush()函数

   flush() 函数是专门针对带缓冲的输出流所使用的。在正常情况下,使用流输出运算符“ << ”进行输出的时候,信息会先进入缓冲区,当缓冲区没有满的时候,信息不会输出。此时,就可以调用 flush()函数,强制将缓冲区中的内容输出到屏幕上/文件中。

/*****************调用格式******************/
ostream& flush();
/*****************代码示例******************/
cout.flush();                   // 用法一:其它输出流对象也可以调用 flush()
cout << "Hello" << std::flush;  // 用法二:与endl类似,作为manipulator的调用方式

下面给出getline函数的测试代码:
首先需要创建一个’greatwall.txt’文件,并写入如下内容:

Shanhai Guan#Juyong Guan#Zijing Guan#Yanmen Guan#Niangzi Guan#Piantou Guan#Jiayu Guan#Yumen Guan

源文件main.cpp

//语言最低标准:C++17
#include<iostream>
#include<filesystem>//路径类
#include<fstream>//文件操作
#include<array>
#include<string>
int main() {
   using std::cout;
   using std::endl;
   using std::ifstream;
   //打开文件
   std::filesystem::path p{ "greatwall.txt" };
   ifstream in{ p };
   if (!in) {//!in.fail();效果相同
       cout << "Cann't open file" << p << endl;
       std::abort();//程序直接退出
   }

   //istream::getline函数
   constexpr int SIZE = 1024;//数组大小
   std::array<char, SIZE> buf;
   while (!in.eof()) {
       //注意&buf是对象的地址,&buf[0]是数组的地址
       in.getline(&buf[0], SIZE, '#');//只读一个
       cout << &buf[0] << endl;//注意输出的也是数组首地址
   }
   
   //std::getline函数的用法
   in.close();//手动关闭文件
   in.open(p);
   std::string name1{ "" };
   while (!in.eof()) {
       std::getline(in, name1, '#');
       cout << name1 << endl;
   }
   //std::cin.get();
   return 0;
}

运行结果

Shanhai Guan
Juyong Guan
Zijing Guan
Yanmen Guan
Niangzi Guan
Piantou Guan
Jiayu Guan
Yumen Guan

Shanhai Guan
Juyong Guan
Zijing Guan
Yanmen Guan
Niangzi Guan
Piantou Guan
Jiayu Guan
Yumen Guan

7.4 二进制输入输出

7.4.1 文件的打开模式

  前面已经介绍过输入流 ifstream(读数据)和输出流 ofstream(写数据),但有时需要文件可以一边写一边读,这时候就需要 fstream(相当于ofstream 与 ifstream 的叠加)。需要注意的是,创建fstream对象时,应指定文件打开模式(定义在所有的文件流的基类 ios_base中):

文件打开模式
Mode(模式)Description(描述)
ios::in打开文件读数据。
ios::out打开文件写数据。
ios::app把输出追加到文件末尾。app = append
ios::ate打开文件,把文件光标移到末尾。ate = at end
ios::trunc若文件存在则舍弃其内容。这是ios::out的默认行为。trunc = truncate
ios::binary打开文件以二进制模式读写。

  Open Mode都是由编译器自己实现的,且可以使用“位或”运算符将几种模式可以组合在一起(称为模式组合,Combining Modes)。

/***************open mode的定义*****************/
// std::ios_base::openmode 被ios继承
typedef /*implementation defined*/ openmode;
static constexpr openmode app = /*implementation defined*/
/*****************模式组合示例*******************/
//期望打开文件name.txt追加数据
stream.open("name.txt", ios::out | ios::app);

注:如果想在命令行窗口发送EOF符号给程序,则可以使用 crtl+z(windows)、 ctrl+d(Unix)快捷键。

下面给出对于文件模式的测试代码:
源文件main.cpp

#include<iostream>
#include<fstream>
#include<filesystem>
int main() {
   namespace fs = std::filesystem; //对名字空间起别名用namespace
   using fo = std::ios;            //对类起别名用using
   fs::path p1{ "city1.txt" }, p2{ "city2.txt" };
   //创建两个输出文件流,分别为app和trunc模式
   std::ofstream out1{ p1, fo::out | fo::app };
   std::ofstream out2{ p2, fo::out | fo::trunc };
   //从键盘读入字符,输入到两个文件流中
   char c;
   std::cout << "//需要手动输入" << std::endl;
   while (!std::cin.get(c).eof()) {
       out1.put(c);
       out2.put(c);
   }
   //关闭文件流
   out1.close();
   out2.close();
   //读模式打开两个IO文件流,其中一个使用ate模式
   std::fstream in1{ p1,fo::in };
   std::fstream in2{ p2,fo::in | fo::ate };
   std::cout << "//以下为文件输出" << std::endl;
   //读取两个文件的内容到窗口
   std::cout << p1 << std::endl;
   while (!in1.get(c).eof()) {
       std::cout << c;
   }
   std::cout << p2 << std::endl;
   while (!in2.get(c).eof()) {
       std::cout << c;
   }
   //关闭IO文件流
   in1.close();
   in2.close();
   //std::cin.get();
   return 0;
}

运行结果

//需要手动输入
Bei Jing
Shang Hai
Shen Zhen
Guang Zhou
^Z
//以下为文件输出
"city1.txt"
Bei Jing
Shang Hai
Shen Zhen
Guang Zhou
"city2.txt"

注:多运行几次程序可以观察写入信息的差别。

7.4.2 二进制输入输出简介

  二进制文件输入输出与前面所述的文本文件输入输出有所不同。笼统的说,文本文件(Text File)与二进制文件(Binary File)本质上都是按照二进制格式存储比特序列,区别是文本文件解释为一系列字符;而二进制文件解释为一系列比特。比如说对于十进制整数199:

(1)在文本文件中存为3个字符: ‘1’, ‘9’, ‘9’;三个字符的ASCII码占3个字节:0x31, 0x39, 0x39
(2)在二进制文件中存为字节类型的值:C7;十进制 199 = 十六进制 C7

文本文件和二进制文件的区别
表 换行字符的转义
系统换行的缩写ASCII码转移
WindowsCRLF\r\n
Unix / Linux / Mac OS XLF\n

再以换行符 “\n” 为例,根据上表,windows系统在进行文本文件读写时会自动编码为 “\r\n” 两个字符;二进制文件则不会转换 “\n” 字符。而在Unix/Linux上用C++读写文件,无论时文本读写还是二进制读写,都不涉及到 “\n” 字符的转换。具体介绍可以查看CSDN博文“CRLF、CR、LF详解”。

  总的来说,文本模式的读写是建立在二进制模式读写的基础上的,只不过是将二进制信息进行了字符编解码。想要对二进制文件进行读写,需要选择文件打开模式为 ios::binary(默认文件打开模式是文本文件)。注意,二进制读写无需信息转换,以下两种将信息写入二进制文件的方式等价(即内存信息和文件信息相同):

numeric value   →  write (bin I/O)       → file
value in memory →  copy (no conversion)  → file
两种模式下的读写函数
Text I/O (文本模式)Binary I/O function:(二进制模式)
operator >>; get(); getline();read();
operator <<; put();write();

7.4.3 如何实现二进制读写

  本节介绍有关二进制读写的两个函数 write()read()、类型转换运算符reinterpret_cast

  首先来看 write()函数,其函数原型见下面代码,其中 const char* s表示一个字符指针类型的参数,指向将要写到文件中的信息块(以字节块的形式存在); std::streamsize count则是指写入文件的字节数。由于字符串存在于字符数组中,所以将字符串写入二进制文件很简单(见下面的代码示例)。而如果需要写入非字符数据,则需要先将数据转换为字节序列(即字节流),再用write()函数将字节序列写入文件。

/*****************write函数原型******************/
ostream& write( const char* s, std::streamsize count )
/**********直接将字符串写入文件代码示例************/
fstream fs("GreatWall.dat", ios::binary|ios::trunc);
char s[] = "ShanHaiGuan\nJuYongGuan";//不会转义\n
fs.write(s, sizeof(s));

  那如何将信息转换为字节流呢?就需要 reinterpret_cast类型转换运算符。前面学过两种转换运算符: static_cast用作基础类型的转换; dynamic_cast动态类型转换,用于继承链上的基类或派生类 指引/引用 的转换。而第三种类型转换 reinterpret_cast主要有以下两种用途(调用原型和代码示例如下):

(1) 将一种类型的地址转为另一种类型的地址;
(2) 将地址转换为数值,比如转换为整数(将指针转换为数值)。

/*************reinterpret_cast调用语法*************/
reinterpret_cast<dataType>(address) 
//dataType是要转至的目标类型;address是待转换的数据的起始地址。
//对于二进制I/O来说,dataType 始终是 char*。

/*************reinterpret_cast代码示例*************/
long int x {0};                           //定义变量
int a[3] {21,42,63};                      //定义数组
std::string str{"Hello"};                 //定义字符串对象
char* p1 = reinterpret_cast<char*>(&x);   //变量地址
char* p2 = reinterpret_cast<char*>(a);    //数组地址
char* p3 = reinterpret_cast<char*>(&str); //对象地址

  最后来介绍一下读二进制文件 read()函数。下面直接给出函数原型见下面代码,其中 char* s表示一个指针吗,指向要将读入的信息所存入的内存区域; std::streamsize count则是指读入的字节数。代码示例如下:

/*****************read函数原型******************/
istream& read ( char* s, std::streamsize count );

/**********直接读入字符串代码示例************/
fstream bio("GreatWall.dat", ios::in | ios::binary);
char s[10];
bio.read(s, 5); //一次性读入5个字节的信息
s[5] = '\0';    //字符串结束符
cout << s;
bio.close();
/*********读入其他类型数据代码示例***********/
// 读其它类型数据(整数),需要使用 reinterpret_cast
fstream bio("temp.dat", ios::in | ios::binary);
int value;
bio.read(reinterpret_cast<char *>(&value), sizeof(value));
cout << value;

下面给出二进制输入输出的代码示例:
源文件main.cpp

//必须使用C++17编译
#include<iostream>
#include<fstream>//文件操作
#include<filesystem>//路径类
#include<array>
int main() {
   namespace fs = std::filesystem;
   using io = std::ios;
   fs::path p{ "array.dat" };
   //创建二进制输出流
   std::fstream out{ p,io::out | io::app };
   //判断流是否成功打开
   if (out.fail()) {
       std::cout << "文件打开错误!" << std::endl;
       return 0;
   }
   //将一个整型数组的内容输出到二进制文件中
   std::array a{ 21L,34L,56L };//L表示长整型
   std::streamsize size = a.size() * sizeof(a[0]);
   out.write(reinterpret_cast<char*>(&a[0]), size);
   //以读取模式重新打开二进制文件,或者将文件光标定位到文件头fstream::seekg()
   out.close();
   out.open(p, io::in);
   //从二进制流中读入所有整数并显示到屏幕上
   auto x{ 0L };
   for (auto i = 0; i < a.size(); i++) {
       out.read(reinterpret_cast<char*>(&x), sizeof(x));
       std::cout << x << std::endl;
   }
   //std::cin.get();
   return 0;
}

运行结果

21
34
56

7.5 随机访问文件

7.5.1 文件位置指示器

  本节介绍 文件位置指示器File Positioner/文件指针,简称fp)。文件由字节序列构成,有一个特殊标记指向其中一个字节,就称为文件位置指示器。读写操作都是从文件位置指示器所标记的位置开始(这个位置也叫做 “当前位置” current position)。比如说以默认的形式打开文件,fp会指向文件头,读写文件时,fp会向后移动到下一个数据项(注意不是下一个字节,如 int = 4bit)。比如现在调用一个输入文件流的 aFileStream.get()函数,会导致 fp = fp + 1,如下图:

文件位置指示器(数据单元为 char = 1bit)

文件位置指示器的其它说法:

File Pointer(文件指针):易与C语言的FILE* 混淆,不推荐。
File Cursor(文件光标):借用数据库中的“光标”概念,很形象。

7.5.2 随机访问文件

   随机访问(Random Access) 意味着可以读写文件的任意位置。那要做到随机访问就需要:知道文件定位器的位置、能在文件中移动文件定位器。进一步的,为了满足对一个文件同时进行读/写的操作,就需要有两个文件位置指示器(一个用于读、一个用于写)。但是上述并没有在C++标准中定义,而是依赖于编译器的实现。与文件定位指示器的相关函数如下:

文件定位指示器相关函数
For reading (读文件时用)For writing(写文件时用)
获知文件定位器指到哪里tellg(); tell是获知,g是get表示读文件tellp(); tell是获知,p是put表示写文件
移动文件定位器到指定位置seekg(); seek是寻找,g是get表示读文件seekp(); seek是寻找,p是put表示写文件

注:上述可以进行等价 tell = getseek = setg = readp = write

/**************seek的函数原型*****************/
xxx_stream& seekg/seekp( pos_type pos );                            //将fp移动到一个绝对位置
xxx_stream& seekg/seekp( off_type off, std::ios_base::seekdir dir); //将fp移动到一个相对于dir偏移off的位置

  seek的函数原型主要有以上两个重载函数,其中 std::ios_base::seekdir称为 文件定位方向类型,只包含以下三种(调用实例也一同给出):

三种文件定位方向类型
seekdir 文件定位方向类型解释
std::ios_base::beg流的开始;beg = begin
std::ios_base::end流的结尾
std::ios_base::cur流位置指示器的当前位置;cur = current
几种seek函数的调用实例
例子解释
seekg(42L);将文件位置指示器移动到文件的第42字节处
seekg(10L, std::ios::beg);将文件位置指示器移动到从文件开头算起的第10字节处
seekp(-20L, std::ios::end);将文件位置指示器移动到从文件末尾开始,倒数第20字节处
seekp(-36L, std::ios::cur);将文件位置指示器移动到从当前位置开始,倒数第36字节处

下面给出随机存取文件的代码示例:
源文件main.cpp

//使用C++17标准
#include<iostream>
#include<fstream>//文件操作
#include<filesystem>//路径类
#include<array>
#include<vector>
int main() {
   namespace fs = std::filesystem;
   using std::cout;
   using std::endl;
   using std::fstream;
   //在文件中存2个long ling int 和"Hello World"字符串
   fs::path p{ "test.dat" };
   fstream file{ p,std::ios::out | std::ios::in | std::ios::trunc };//写/读数据、覆写内容
   auto x{ 12LL }, y{ 24LL };
   char str[]{ "Hello World" };//使用string会涉及到C++的对象序列化,非常复杂,以后进阶课程讨论
   file.write(reinterpret_cast<char*>(&x), sizeof(x));
   file.write(reinterpret_cast<char*>(&y), sizeof(long long int));
   file.write(str, sizeof(str));
   //在文件中读取Hello字符串
   char buf[100]{ "ShenZhen" };
   std::cout << "Original buf  : " << buf << std::endl;
   file.seekg(2 * sizeof(long long int), std::ios::beg);//移动读文件位置指示器
   file.read(buf, sizeof("Hello"));//注意这里是6个字符
   std::cout << "After Read buf: " << buf << std::endl;
   //std::cin.get();
   return 0;
}

运行结果

Original buf  : ShenZhen
After Read buf: Hello en
  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

虎慕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值