《Hands-On System Programming with C++》读书笔记之八

在C++17以前,操作文件使用的是非C++的API,它们是不安全、不通用甚至是不完整的。

打开文件

不同的方法

使用std::fstream对象,如下例:

#include<fstream>#include<iostream>intmain(){if(auto file = std::fstream("text.txt")){
        std::cout <<"success\n";}else{
        std::cout <<"failure\n";}}

可以用 touch test.txt 命令先在当前目录下建立一个文件。默认的权限是read/write。
也可以用is_open()确认文件是否成功打开。

#include<fstream>#include<iostream>intmain(){if(auto file = std::fstream("text.txt"); file.is_open()){
        std::cout <<"success\n";}}

上面的例子是使用了std::fstream()的构造函数打开文件,也可以使用open()函数。

#include<fstream>#include<iostream>intmain(){auto file = std::fstream();if(file.open("test.txt"); file.is_open()){
        std::cout <<"success\n";}}

上面的各个例子中都不需要使用close()来关闭文件,析构函数会自动完成,当然也可以手工完成。

#include<fstream>#include<iostream>intmain(){
    std::cout << std::boolalpha;if(auto file = std::fstream("text.txt")){
        std::cout << file.is_open()<<'\n';// true
        file.close();
        std::cout << file.is_open()<<'\n';// false}}

几种打开模式

打开文件有两种基本模式:

  • std::ios::in:读模式
  • std::ios::out:写模式
    同时还哦于以下一种模式可以与上面的基本模式结合使用:
  • std::ios::binary:二进制格式打开
  • std::ios::app:在文件末尾写入
  • std::ios::ate:打开时操作点在文件末尾
  • std::ios::trunc:打开时删除原文件内容

例如

#include<fstream>#include<iostream>intmain(){constexprauto mode = std::ios::out | std::ios::binary | std::ios::app;if(auto file = std::fstream("text.txt", mode)){
        std::cout <<"success\n";}}

注意在使用std::ios::out模式时,除非指明使用std::ios::ate或std::ios::app模式,否则默认使用std::ios::trunc模式。
在使用了std::ios::ate或std::ios::app模式后,仍然可以通过seekp(0)方法将操作位置回到文件的起始。
当std::ios::in和std::ios::out一起使用时,如果要先清除原有文件内容,还是需要显式指明std::ios::trunc模式。

文件的读与写

读文件

Reading by field

以空格或换行为分解,把字符串写入字符串变量中。假设文件中已经写入了 Hello World ,可以用 echo “Hello World” >text.txt 实现。

#include<fstream>#include<iostream>intmain(){if(auto file = std::fstream("text.txt")){
        std::string str1, str2;
        file >> str1 >> str2;
        std::cout << str1 <<" "<< str2 <<'\n';// Hello World}}

也可以直接读入整型变量,当文件内容不是整型时,读入结果为0。

为了读入自定义类型,可以重载std::fstream操作符。

#include<fstream>#include<iostream>struct myclass
{
    std::string str1;
    std::string str2;};
std::fstream &operator>>(std::fstream &is, myclass &obj){
    is >> obj.str1;
    is >> obj.str2;return is;}
std::ostream &operator<<(std::ostream &os,const myclass &obj){
    os << obj.str1;
    os <<' ';
    os << obj.str2;return os;}intmain(){if(auto file = std::fstream("text.txt")){
        myclass obj;
        file >> obj;
        std::cout << obj <<'\n';// Hello World}}
Reading bytes

可以用get()直接从文件中读入字节。假设下面例子中文件的内容是 Hello World

#include<fstream>#include<iostream>intmain(){if(auto file = std::fstream("text.txt")){char c = file.get();
        std::cout << c <<'\n';// H}}

读入多个字节还是不安全的,因为无法使用std::string类,只能读到C风格的数组中。

#include<fstream>#include<iostream>intmain(){if(auto file = std::fstream("text.txt")){char buf[25]={};
        file.read(buf,11);
        std::cout << buf <<'\n';// Hello World}}

可以把read()包在一个模板中,检查读入的长度是否大于缓冲区。

#include<fstream>#include<iostream>template<typename T, std::size_t N>voidmyread(std::fstream &file,T(&str)[N], std::size_t count){if(count >= N){throw std::out_of_range("file.read out of bounds");}
    file.read(static_cast<char*>(str), count);}intmain(){if(auto file = std::fstream("text.txt")){char buf[25]={};myread(file, buf,11);
        std::cout << buf <<'\n';// Hello World}}

使用tellg()方法来获得读操作在文件中的位置。

#include<fstream>#include<iostream>intmain(){if(auto file = std::fstream("text.txt")){
        std::cout << file.tellg()<<'\n';// 0char c = file.get();
        std::cout << file.tellg()<<'\n';// 1
        c = file.peek();
        std::cout << file.tellg()<<'\n';// 1
        file.ignore(1);
        std::cout << file.tellg()<<'\n';// 2}}

其中get()读一个字符且位置前进1;peek()读一个字符而位置不前进;ignore()不读只前进一个位置。
C++还提供了seekg()方法,用来设置当前的读操作在文件中的位置。

#include<fstream>#include<iostream>intmain(){if(auto file = std::fstream("text.txt")){
        std::string str1, str2;
        file >> str1 >> str2;
        std::cout << str1 <<" "<< str2 <<'\n';// Hello World
        file.seekg(1);
        file >> str1 >> str2;
        std::cout << str1 <<" "<< str2 <<'\n';// ello World}}
Reading by line

最后是按行读取的方式。

#include<fstream>#include<iostream>intmain(){if(auto file = std::fstream("text.txt")){char buf[25]={};
        file.getline(buf,25,'\n');
        std::cout << buf <<'\n';// Hello World}}

getline()一直读取,直到文件末尾或分界符出现。由于各操作系统对“行”概念可能定义不一致,所以函数中允许通过参数自行定义分界符。与read()不同,C++提供了安全的,使用std::string类的,非成员函数的getline()方法:

#include<fstream>#include<iostream>intmain(){if(auto file = std::fstream("text.txt")){
        std::string str;
        std::getline(file,str);
        std::cout << str <<'\n';// Hello World}}

当一个已经打开的文件被其它程序修改时,可以用sync()函数同步。

#include<fstream>#include<iostream>#include<unistd.h>intmain(){if(auto file = std::fstream("text.txt")){
        std::string str;
        std::getline(file, str);
        std::cout << str <<'\n';// Hello Worldsleep(10);
        file.sync();
        file.seekg(0);
        std::getline(file, str);
        std::cout << str <<'\n';// Brave new world}}

上例中在程序打印后10秒内将原文件内容从 Hello World 改为 Brave new world ,可以验证。

写文件

Writing by field

可以像std::cout一样对打开的文件使用<<操作符。

#include<fstream>#include<iostream>intmain(){if(auto file = std::fstream("text.txt")){
        std::string str{"Hello"};int num{42};
        file << str <<" World, "<< num <<'\n';}}

上面的代码会向文件中写入 Hello World, 42 ,原有内容清除。
自定义数据结构也可以通过重载支持。

#include<fstream>#include<iostream>struct myclass
{
    std::string str1{"Hello"};
    std::string str2{"World"};};
std::fstream &operator<<(std::fstream &os,const myclass &obj){
    os << obj.str1;
    os <<' ';
    os << obj.str2;return os;}intmain(){if(auto file = std::fstream("text.txt")){
        file << myclass{}<<'\n';}}
Writing bytes

与读取用的get()类似,put()可以向文件写入一个字节。与读取用的read()类似,write()可以向文件写入多个字节。

#include<fstream>#include<iostream>intmain(){if(auto file = std::fstream("text.txt")){
        file.put('H');
        file.put('\n');
        file.write("Hello World\n",20);}}

C风格的write()同样是不安全的,上例中输出的长度20大于字符串的实际长度,会在文件末尾留下随机的不可识别数据。同样可以把wirte()封装一下解决:

#include<fstream>#include<iostream>#include<string.h>voidmywrite(std::fstream &file,constchar*str, std::size_t count){if(count >strlen(str)){throw std::out_of_range("file.write out of bounds");}
    file.write(str, count);}intmain(){if(auto file = std::fstream("text.txt")){
        file.write("Hello World\n",10);}}

上面的做法仍不是完全安全的,总的来说应该避免read()和write()这种字节读写的使用。
与读操作的方法对应,tellp()获得当前写操作的位置,seekp()设置当前的写操作位置。

#include<fstream>#include<iostream>intmain(){if(auto file = std::fstream("text.txt")){
        std::cout << file.tellp()<<'\n';// 0
        file <<"Hello";
        std::cout << file.tellp()<<'\n';// 5
        file.seekp(0);
        std::cout << file.tellp()<<'\n';// 0}}

当文件对象被析构时,其内容会自动从内存写入文件系统中,也可以手动采用flush()方法。

#include<fstream>#include<iostream>intmain(){if(auto file = std::fstream("text.txt")){
        file.flush();}}

std::fstream提供了四种方法确定流的状态:

  • good():没有错误,未到文件末尾
  • eof():已到文件尾,没有影响功能的错误
  • fail():已发生错误,但还能执行
  • bad():已发生错误,流无法继续执行
    另外bool操作符也有相当于good()的作用。
#include<fstream>#include<iostream>intmain(){
    std::cout << std::boolalpha;if(auto file = std::fstream("text.txt")){
        std::string str1{"Hello"}, str2{"World"};
        file << str1 <<' '<< str2 <<'\n';
        std::cout <<"good: "<< file.good()<<'\n';// true
        std::cout <<"fail: "<< file.fail()<<'\n';// false
        std::cout <<"bad: "<< file.bad()<<'\n';// false
        std::cout <<"eof: "<< file.eof()<<'\n';// falseif(!file) std::cout <<"failed\n";// no displayif(file) std::cout <<"success\n";// success}}

当错误发生时,错误标志一直保留直到流被关闭或使用clear()方法手动清除。

#include<fstream>#include<iostream>intmain(){
    std::cout << std::boolalpha;if(auto file = std::fstream("text.txt")){int answer;
        std::cout << file.good()<<'\n';// true
        file >> answer;
        std::cout << file.good()<<'\n';//false
        file.clear();
        std::cout << file.good()<<'\n';// true}}

理解文件工具

前面的API在C++17之前的版本就有,C++17中增加了std::filesystem工具来进一步管理文件系统。

路径

路径就是表示节点在文件系统中位置的一个字符串。节点可以是文件、目录、链接或设备等等。在UNIX系统中主要指:

  • Block devices:块设备,如/dev/sda
  • Character devices:字符设备,如/dev/random
  • Directories:目录
  • Fifo:管道或其它形式的进程间通信
  • Socket:POSIX socket
  • Symlink:POSIX链接
  • Files:文件

C++17提供了下面的方法来确定节点的类型。

#include<iostream>#include<experimental/filesystem>intmain(){usingnamespace std::experimental::filesystem;
    std::cout << std::boolalpha;
    std::cout <<is_block_file("/dev/sda")<<'\n';//true
    std::cout <<is_character_file("/dev/random")<<'\n';//true
    std::cout <<is_directory("/dev")<<'\n';// true
    std::cout <<is_empty("/dev")<<'\n';// false
    std::cout <<is_fifo("scratchpad.cpp")<<'\n';// false
    std::cout <<is_other("scratchpad.cpp")<<'\n';// false
    std::cout <<is_regular_file("scratchpad.cpp")<<'\n';//true
    std::cout <<is_socket("scratchpad.cpp")<<'\n';// false
    std::cout <<is_symlink("scratchpad.cpp")<<'\n';// false}

原书中没有使用 experimental 命名空间,我这里不加没法编译。使用 g++ -std=c++17 scratchpad.cpp -lstdc++fs 命令编译。
判断节点是否存在,使用exists()函数;当前目录使用current_path()函数;使用relative()函数获得相对路径(我用gcc7.5找不到此函数,在gcc9.3中就可以了,此时已经不用 experimental 命名空间了);从相对路径转化为绝对路径,使用canonical()函数;而absolute()函数给出另一种以用当前绝对路径的形式。

#include<iostream>#include<experimental/filesystem>intmain(){usingnamespace std::experimental::filesystem;
    std::cout << std::boolalpha;
    std::cout <<exists("/dev")<<'\n';// true
    std::cout <<exists("no_exist.txt")<<'\n';// false
    std::cout <<current_path()<<'\n';// "/home/cenf/HandsOnSystemProgramming"
    std::cout <<relative("/home/cenf");// "/HandsOnSystemProgramming"
    std::cout <<canonical(".")<<'\n';// "/home/cenf/HandsOnSystemProgramming"
    std::cout <<canonical("..")<<'\n';// "/home/cenf"}

C++17中还有equivalent()函数用来比较多种表示路径的方法是否等效。而==操作符只是判断这些路径表示是否在字符上相等。

#include<iostream>#include<experimental/filesystem>intmain(){usingnamespace std::experimental::filesystem;auto path1 = path{"."};auto path2 = path{"../HandsOnSystemProgramming"};auto path3 =current_path();auto path4 =absolute("../HandsOnSystemProgramming");
    std::cout << std::boolalpha;
    std::cout <<equivalent(path1, path2)<<'\n';// true
    std::cout <<equivalent(path1, path3)<<'\n';// true
    std::cout <<equivalent(path1, path4)<<'\n';// true
    std::cout <<(path1 == path2)<<'\n';// false}

为了便于操作路径,/、/=、+=等操作符被重载为路径拼接的操作符。下面例子中还演示了replace_extension()、remove_filename()和replace_filename()的效果。

#include<iostream>#include<experimental/filesystem>intmain(){usingnamespace std::experimental::filesystem;auto path =current_path();
    path /="scratchpad.cpp";
    std::cout << path <<'\n';// "/home/cenf/HandsOnSystemProgramming/scratchpad.cpp"
    path.replace_filename("scratchpad.h");
    std::cout << path <<'\n';// "/home/cenf/HandsOnSystemProgramming/scratchpad.h"
    path.replace_extension("hpp");
    std::cout << path <<'\n';// "/home/cenf/HandsOnSystemProgramming/scratchpad.hpp"
    path.remove_filename();
    std::cout << path <<'\n';// "/home/cenf/HandsOnSystemProgramming/"}

可以使用clear()方法来清空路径。

#include<iostream>#include<experimental/filesystem>intmain(){usingnamespace std::experimental::filesystem;auto path =current_path();
    path +="scratchpad.cpp";
    std::cout << path <<'\n';// "/home/cenf/HandsOnSystemProgrammingscratchpad.cpp"
    path.clear();
    std::cout << path <<'\n';// ""}

路径由一些固定的格式元素组成,C++17提供了一些函数帮助识别或判断它们。

#include<iostream>#include<experimental/filesystem>intmain(){usingnamespace std::experimental::filesystem;auto path =current_path();
    path /="scratchpad.cpp";
    std::cout << std::boolalpha;
    std::cout << path <<'\n';// "/home/cenf/HandsOnSystemProgramming/scratchpad.cpp"
    std::cout << path.root_name()<<'\n';// ""
    std::cout << path.root_directory()<<'\n';// "/"
    std::cout << path.root_path()<<'\n';// "/"
    std::cout << path.relative_path()<<'\n';// "home/cenf/HandsOnSystemProgramming/scratchpad.cpp"
    std::cout << path.parent_path()<<'\n';// "/home/cenf/HandsOnSystemProgramming"
    std::cout << path.filename()<<'\n';// "scratchpad.cpp"
    std::cout << path.stem()<<'\n';// "scratchpad"
    std::cout << path.extension()<<'\n';// ".cpp"
    std::cout << path.empty()<<'\n';// flase
    std::cout << path.has_root_path()<<'\n';// true
    std::cout << path.has_root_name()<<'\n';// false
    std::cout << path.has_root_directory()<<'\n';// true
    std::cout << path.has_relative_path()<<'\n';// true
    std::cout << path.has_parent_path()<<'\n';// true
    std::cout << path.has_filename()<<'\n';// true
    std::cout << path.has_stem()<<'\n';// true
    std::cout << path.has_extension()<<'\n';// true
    std::cout << path.is_absolute()<<'\n';// true
    std::cout << path.is_relative()<<'\n';// false}

C++17提供了管理文件系统的一些方法,包括创建目录、重命名和删除节点。成功的操作返回true,失败的操作返回false。

#include<iostream>#include<experimental/filesystem>intmain(){usingnamespace std::experimental::filesystem;auto path1 =current_path();auto path2 =current_path();
    path1 /="test1";
    path2 /="test2";
    std::cout << std::boolalpha;
    std::cout <<create_directory(path1)<<'\n';// truerename(path1, path2);
    std::cout <<remove(path1)<<'\n';// false
    std::cout <<remove(path2)<<'\n';// true}

remove_all()可以用来删除非空的文件夹;directory_iterator()可以用来便利文件夹。

#include<iostream>#include<experimental/filesystem>#include<fstream>intmain(){usingnamespace std::experimental::filesystem;auto path =current_path();
    path /="test";
    std::cout << std::boolalpha;
    std::cout <<create_directory(path)<<'\n';
    std::fstream(path /"test1.txt", std::ios::app);
    std::fstream(path /"test2.txt", std::ios::app);
    std::fstream(path /"test3.txt", std::ios::app);
    std::cout <<remove_all(path)<<'\n';// true \n 4}

输出

true
“/home/cenf/HandsOnSystemProgramming/test/test2.txt”
“/home/cenf/HandsOnSystemProgramming/test/test1.txt”
“/home/cenf/HandsOnSystemProgramming/test/test3.txt”
4

最后,C++17还提供了一个方法确定系统的临时文件夹的路径。尽管在UNIX系统下临时文件夹通常是/tmp,但还是建议通过这个函数来获得,而不是直接使用。

#include<iostream>#include<experimental/filesystem>intmain(){usingnamespace std::experimental::filesystem;
    std::cout <<temp_directory_path()<<'\n';// "/tmp"}

例一:Understanding the logger example

这里学习实现如何把std::clog流写入日志文件,而不仅仅是显示到控制台。

#include<sstream>#include<fstream>#include<iostream>#ifdef DEBUG_LEVELconstexprauto g_debug_level = DEBUG_LEVEL;#elseconstexprauto g_debug_level =0;#endif#ifdef NDEBUGconstexprauto g_ndebug =true;#elseconstexprauto g_ndebug =false;#endif
std::fstream g_log{"log.txt", std::ios::out | std::ios::app};template<std::size_t LEVEL>constexprvoidlog(void(*func)()){ifconstexpr(!g_ndebug &&(LEVEL <= g_debug_level)){
        std::stringstream buf;auto g_buf = std::clog.rdbuf();
        std::clog.rdbuf(buf.rdbuf());func();
        std::clog.rdbuf(g_buf);
        std::clog <<"\033[1;32mDEBUG\033[0m: ";
        std::clog << buf.str();
        g_log <<"\033[1;32mDEBUG\033[0m: ";
        g_log << buf.str();};}intprotected_main(int argc,char** argv){(void) argc;(void) argv;
    log<0>([]{
        std::clog <<"Hello World\n";});
    std::clog <<"Hello World\n";return EXIT_SUCCESS;}intmain(int argc,char** argv){try{returnprotected_main(argc, argv);}catch(const std::exception &e){
        std::cerr <<"Caught unhandled exception:\n";
        std::cerr <<" - what(): "<< e.what()<<'\n';}catch(...){
        std::cerr <<"Caught unknown exception\n";}return EXIT_FAILURE;}

上面的例程中,首先定义了两个常量表达式,用来控制是否打印调试信息及打印的程度。
然后是log文件的全局变量的定义,模式上它只允许写入,而且是在文件末尾。
接着是log()函数的定义。函数只有在允许调试且调试级别低于等级时才会执行,否则在编译阶段就会被优化掉。当条件满足时,首先创建一个string流buf,然后再用g_buf来保存std::clog的原有指针(即stderr),然后将其替换为buf的地址,然后执行调试打印func(),然后从g_buf恢复std::clog的缓冲区指针(恢复为stderr),然后指定std::clog的输出格式(加绿色DEBUG字样),将buf保存的内容输出到std::clog(此时以恢复为控制台显示),同时将同样的buf内容输出到g_log(即写入log文件)。
组后是protect_main()函数,它输出调试信息Hello World,同时用了自定义的log()和正常的std::clog。
不需要按照书中的使用cmake编译,直接用命令 g++ -std=c++17 scratchpad.cpp -lstdc++fs 就可以编译,输出

DEBUG: Hello World
Hello World

同时会生成log.txt文件,内容

DEBUG: Hello World

例二:Learning about the tail file example

书中提供了另一个比较完整的例子,实现监视一个指定文件,将它的新增内容逐行打印到控制台的功能。例子利用了前面的各种工具方法,还演示了如何处理命令参数。具体代码见
这个例程需要用cmake编译。

例三:Comparing C++ versus mmap benchmark

这个例程对比了两种读文件的方法的性能——std::fstream和mmap()。前者是C++ API,后者是系统调用,前者会多执行一次内存复制,所以原则上应该会更慢。例程中使用了std::chrono库,主要是用于计时。

首先是定义要读取的文件大小size。
然后benchmark函数用来计时,这个使用了高分辨率时钟的方法是比较通用的。
主函数中*(void) argc; (void) argv;*是表示这两个参数没有使用,编译器不必警告。主函数首先写入了test.txt文件准备用于测试,写入的都是0。接着用std::fstream方式读文件并计时。被计时的函数以lambda表达式的形式传入benchmark模板。 duration_cast 是std::chrono库中定义的模板,用于将类型转换为时间段。最后用mmap方式读取并计时。注意这里使用的open()函数来自C library <fcntl.h>,不是我们前面介绍的C++ API。

#include<sys/stat.h>#include<sys/mman.h>#include<fcntl.h>#include<chrono>#include<fstream>#include<iostream>constexprauto size =0x1000;template<typename FUNC>autobenchmark(FUNC func){auto stime = std::chrono::high_resolution_clock::now();func();auto etime = std::chrono::high_resolution_clock::now();return etime - stime;}intprotected_main(int argc,char** argv){(void) argc;(void) argv;usingnamespace std::chrono;{char buf[size]={};if(auto file = std::fstream("test.txt", std::ios::out)){
            file.write(buf, size);}}{char buf[size];if(auto file = std::fstream("test.txt", std::ios::in)){auto time =benchmark([&file,&buf]{
                file.read(buf, size);});
            std::cout <<"c++ time: "<< duration_cast<microseconds>(time).count()<<'\n';}}{void*buf;if(int fd =open("test.txt", O_RDONLY); fd !=0){auto time =benchmark([&fd,&buf]{
                buf =mmap(NULL, size, PROT_READ,0, fd,0);});munmap(buf, size);
            std::cout <<"mmap time: "<< duration_cast<microseconds>(time).count()<<'\n';}}return EXIT_SUCCESS;}intmain(int argc,char** argv){try{returnprotected_main(argc, argv);}catch(const std::exception &e){
        std::cerr <<"Caught unhandled exception:\n";
        std::cerr <<" - what(): "<< e.what()<<'\n';}catch(...){
        std::cerr <<"Caught unknown exception\n";}return EXIT_FAILURE;}

不需要使用cmake直接编译,运行打印结果

c++ time: 8
mmap time: 1

mmap()方法确实快很多。