第11章 输入输出
第11章 输入输出
简介
输出
输入
I/O 状态
用户自定义I/O类型
格式化
流格式化;
printf() 风格的格式化
流
标准流;
文件流;
字符串流;
内存流;
同步流
C 风格的 I/O
- 文件系统
路径;
文件和目录
- 建议
11.1 简介
I/O 流库提供了文本和数值的格式化与非格式化缓冲I/O。 它具有扩展性,能够像内置类型一样支持用户自定义类型,并保证类型安全。
文件系统库提供了操作文件和目录的基本功能。
ostream 类将类型化的对象转换为字符(字节)流:
istream 类将字符(字节)流转换为类型化的对象:
对 istream 和 ostream 的操作在 §11.2 和 §11.3 中有描述。这些操作是类型安全、类型敏感的,并且可扩展以处理用户自定义类型(§11.5)。
其他形式的用户交互,例如图形I/O,是通过非ISO标准部分的库来处理的,因此这里不做描述。
这些流可以用于二进制I/O,可以用于多种字符类型,可以是特定区域设置的,并且可以使用高级缓冲策略,但这些主题超出了本书的范围。
这些流可以用于从 字符串 输入和向 字符串 输出。(§11.3),用于格式化到 字符串 缓冲区(§11.7.3),到内存区域(§11.7.4),以及用于文件I/O(§11.9)。
I/O 流类都具有析构函数,用于释放所有拥有的资源(如缓冲区和文件句柄)。也就是说,它们是“资源获取即初始化”(RAII; §6.3)的一个实例。
11.2 输出
在 <ostream> 中,I/O 流库为每种内置类型定义了输出功能。此外,定义用户自定义类型的输出也非常简单(§11.5)。操作符 << (“放入”)用作 ostream 类型对象上的输出操作符; cout 是标准输出流,而 cerr 是用于报告错误的标准流。默认情况下,写入 cout 的值会被转换为一系列字符。例如,要输出十进制数10,我们可以写:
cout << 10;
这将在标准输出流上放置字符 1 后跟字符 0 。
等效地,我们也可以写:
int x {10};
cout << x;
不同类型的数据可以通过显而易见的方式组合输出:
void h(int i)
{
cout << "the value of i is ";
cout << i;
cout << '\n';
}
对于 h(10) ,输出将是:
the value of i is 10
当输出多个相关项时,人们很快就会厌倦重复输出流的名字。幸运的是,输出表达式的结果本身也可用于进一步的输出。例如:
void h2(int i)
{
cout << "the value of i is " << i << '\n';
}
这个 h2() 函数产生的输出与 h() 相同。
字符常量是用单引号包围的单个字符。请注意,字符作为字符输出而不是数值。例如:
int b = 'b'; // 注意:char 类型被隐式转换为 int
char c = 'c';
cout << 'a' << b << c;
字符 'b' 的整数值是 98 (在我使用的 C++ 实现所采用的 ASCII 编码中),因此这将输出 a98c 。
11.3 输入
在 <istream> 中,标准库提供了用于输入的 istreams 。与 ostreams 类似, istreams 处理内置类型的字符的字符串表示,并且可以轻松扩展以处理用户自定义类型。
操作符 >> (“从...获取”)用作输入操作符; cin 是标准输入流。 >> 的右操作数的类型决定了接受何种输入及输入操作的目标。例如:
int i;
cin >> i; // 从标准输入读取一个整数到 i
double d;
cin >> d; // 读取一个双精度浮点数到 d
这会从标准输入读取一个数字,如 1234 ,到整型变量 i 中,以及一个浮点数,如 12.34e5 ,到双精度浮点型变量 d 中。
和输出操作类似,输入操作也可以链接,所以我也可以等效地这样写:
int i;
double d;
cin >> i >> d; // 读取到 i 和 d 中
在这两种情况中,整数的读取都会被任何非数字字符终止。默认情况下, >> 会跳过开头的空白字符,所以一个合适的完整输入序列可以是:
1234
12.34e5
通常,我们希望读取一串字符。一种方便的方法是读入一个 字符串 。例如:
cout << "请输入您的名字\n";
string str;
cin >> str;
cout << "你好," << str << "!\n";
如果你输入 "南风",响应将会是:
你好,南风!
默认情况下,一个空白字符,如空格或换行符,会终止读取,因此如果你输入 "南风 知我意" ,响应仍然是:
你好,南风!
你可以使用 getline() 函数读取整行。例如:
cout << "请输入您的名字\n";
string str;
getline(cin,str);
cout << "你好," << str << "!\n";
使用此程序,输入 "南风 知我意" 会产生期望的输出:
你好,南风 知我意!
终止行的换行符会被丢弃,因此 cin 准备好接收下一行输入。
使用格式化的 I/O 操作通常比逐个操纵字符更不易出错、更高效且代码更少。特别是, istream 负责内存管理和范围检查。我们可以使用 字符串流 (§11.7.3)或 内存流 (§11.7.4)进行到内存的格式化输入输出。
标准字符串有一个很好的特性,即它们会根据你放入的内容自动扩展大小;你不必预先计算最大尺寸。因此,如果你输入几兆字节的分号, hello_line() 函数会回显多页的分号给你。
11.4 I/O
iostream 有一个状态,可以检查该状态以确定操作是否成功。最常见用途是从流中读取一系列值:
vector<int> read_ints(istream& is)
{
vector<int> res;
for (int i; is >> i; )
res.push_back(i);
return res;
}
这会从 is 读取直到遇到非整数为止。这种情况通常是到达输入结束。这里发生的事情是,操作 is >> i 返回对 is 的引用,测试 iostream 如果流准备好进行下一次操作,则返回 true 。
一般来说,I/O 状态保存了读取或写入所需的所有信息,比如格式化信息(§11.6.2)、错误状态(例如,是否已结束输入?)以及使用了哪种类型的缓冲。特别是,用户可以设置状态以反映发生了错误(§11.5),并且如果错误不严重则清除状态。例如,我们可以想象一个版本的 read_ints() 函数,它接受一个终止字符串:
vector<int> read_ints(istream& is, const string& terminator)
{
vector<int> res;
for (int i; is >> i; )
res.push_back(i);
if (is.eof())// 正常:文件结束
return res;
if (is.fail()) { // 我们未能读取一个整数;是终止符吗?
is.clear(); // 重置状态为良好状态
string s;
if (is >> s && s == terminator)
return res;
is.setstate(ios_base::failbit); // 向 is 的状态添加失败标志
}
return res;
}
auto v = read_ints(cin, "stop");
11.5 用户自定义I/O类型
除了内置类型和标准 字符串 的I/O, iostream 库还允许我们为自己的类型定义I/O。例如,考虑一个简单的类型 Entry ,我们可能用它来表示电话簿中的条目:
struct Entry {
string name;
int number;
};
我们可以定义一个简单的输出运算符,以 {"name", number} 的格式写入 Entry ,类似于我们在代码中初始化时使用的格式:
ostream& operator<<(ostream& os, const Entry& e) {
return os << "{\"" << e.name << "\", " << e.number << "}";
}
用户自定义的输出运算符将其输出流(通过引用)作为其第一个参数,并将其作为结果返回。
相应的输入运算符更为复杂,因为它必须检查正确的格式并处理错误:
istream& operator>>(istream& is, Entry& e) {
// 读取 {"name", number} 对。注意:使用 { " ", } 格式化
char c, c2;
if (is >> c && c == '{' && is >> c2 && c2 == '"') { // 以 { 后跟 " 开始
string name; // 字符串的默认值是空字符串:""
while (is.get(c) && c != '"') { // 在 " 之前的所有内容都是名称的一部分
name += c;
}
}
if (is >> c && c == ',') {
int number = 0;
if (is >> number >> c && c == '}') { // 读取数字和 }
e = {name, number}; // 将值赋给entry
return is;
}
}
is.setstate(ios_base::failbit); // 在流中注册失败
return is;
}
输入操作返回对其 istream 的引用,可用于测试操作是否成功。例如,当用作条件时, is>>c 意味着“我们是否成功地从 is 读取了一个字符到 c 中?”
默认情况下, is>>c 会跳过空白字符,但 is.get(c) 不会,因此此 Entry 输入运算符会忽略(跳过)名称字符串之外的空白字符,但在字符串内部则不会。例如:
{ "John Marwood Cleese", 123456 }
{ "Michael Edward Palin", 987654 }
我们可以像这样从输入中读取这样一对值到 Entry 中:
for (Entry ee; cin >> ee; ) // 从cin读取到ee
cout << ee << '\n'; // 将ee写入cout
输出为:
{"John Marwood Cleese", 123456}
{"Michael Edward Palin", 987654}
有关识别字符流中的模式(正则表达式匹配)的更系统技术,请参阅第10.4节。
11.6 输出格式化
iostream 和格式化库提供了用于控制输入和输出格式的操作。 iostream 设施几乎与 C++ 一样历史悠久,主要集中在数字流的格式化上。 format 设施(§11.6.2)是最近的(C++20)侧重于 printf() -风格(§11.8)的格式指定,用于值的组合格式化。
输出格式化也提供了对 Unicode 的支持,但这超出了本书的范围。
11.6.1 流格式化
最简单的格式化控制称为 操纵符 ( manipulators),可以在 <ios> 、 <istream> 、 <ostream> 和 <iomanip> (对于带参数的操纵符)中找到。例如,我们可以将整数以十进制(默认)、八进制或十六进制的形式输出:
cout << 1234 << ' ' << hex << 1234 << ' ' << oct << 1234 << dec << 1234 << '\n';
// 输出: 1234 4d2 2322 1234
我们可以显式地为浮点数设置输出格式:
constexpr double d = 123.456;
cout << d << "; " // 使用d的默认格式
<< scientific << d << "; "// 使用1.123e2风格的格式表示d
<< hexfloat << d << "; " // 使用十六进制表示法表示d
<< fixed << d << "; " // 使用123.456风格的格式表示d
<< defaultfloat << d << '\n';// 使用d的默认格式
这将产生以下输出:
123.456
1.234560e+002
0x1.edd2f2p+6
123.456000
123.456
精度是一个整数,它决定了用于显示浮点数的小数位数:
- 一般格式( defaultfloat )让实现选择一种格式,以在可用空间内最好地保留数值的形式展示值。精度指定了最大位数。
- 科学计数法格式( scientific )以小数点前有一位数字和指数的形式展示值。精度指定了小数点后最大位数。
- 固定格式( fixed )以整数部分加上小数点和小数部分的形式展示值。精度指定了小数点后最大位数。
浮点数值会进行四舍五入处理,而不仅仅是截断,并且 precision() 不会影响整数的输出。例如:
cout.precision(8);
cout << "precision(8): " << 1234.56789 << ' ' << 1234.56789 << ' ' << 123456 << '\n';
cout.precision(4);
cout << "precision(4): " << 1234.56789 << ' ' << 1234.56789 << ' ' << 123456 << '\n';
cout << 1234.56789 << '\n';
这将产生以下输出:
precision(8): 1234.5679 1234.5679 123456
precision(4): 1235 1235 123456
1235
这些浮点数操纵符是“粘性的”;也就是说,它们的效果会在后续的浮点数操作中持续存在。也就是说,它们主要是为了格式化值的流而设计的。
我们还可以指定数字要放置的字段大小及其在该字段中的对齐方式。
除了基本的数字外, << 还可以处理时间与日期: duration , time_point , year_month_day , weekday , month ,以及 zoned_time (§16.2)。例如:
cout << "birthday: " << November/28/2021 << '\n';
cout << "zt: " << zoned_time{current_zone(), system_clock::now()} << '\n';
这产生了如下输出:
birthday: 2021-11-28
zt: 2021-12-05 11:03:13.5945638 EST
标准还为 complex 、 bitset (§15.3.2)、错误码和指针定义了 << 。
流I/O是可扩展的,因此我们可以为自己的(用户定义)类型定义 << (§11.5)。
11.6.2 printf() 风格的格式化
有人有理有据地指出, printf() 是 C 语言中最受欢迎的函数,也是其成功的重要因素之一。例如:
printf("an int %g and a string '%s'\n",123,"Hello!");
这种“格式字符串后跟参数”的风格起源于 BCPL 并被 C 语言采纳,随后被许多编程语言效仿。自然而然地, printf() 一直是 C++ 标准库的一部分,但它缺乏类型安全性,且难以扩展以处理用户自定义类型。
在 <format> 中,标准库提供了一种类型安全(尽管不可扩展)的 printf() 风格格式化机制。基础函数 format() 生成一个字符串:
string s = format("Hello, {}\n", val);
格式字符串 中的“普通字符”直接插入到输出 字符串 中。由 { 和 } 分隔的格式字符串指定跟随格式字符串的参数如何插入到字符串中。最简单的格式字符串是空字符串 {} ,它从参数列表中取出下一个参数,并根据其默认的 << (如果有的话)进行打印。所以,如果 val 是 " world ",我们得到的是 "Hello, World\n" 。如果 val 是 127 ,我们得到的是 "你好,127\n" 。
format() 最常见的用法是输出其结果:
cout << format("Hello, {}\n", val);
为了了解其工作原理,让我们首先重复一下 §11.6.1 中的例子:
cout << format("{} {:x} {:o} {:d} {:b}\n", 1234, 1234, 1234, 1234, 1234);
这给出了与 §11.6.1 中整数示例相同的输出,只是我增加了 b 表示二进制,这是 ostream 直接不支持的:
1234 4d2 2322 1234 10011010010
格式指令前有一个冒号。整数格式选项有: x 表示十六进制, o 表示八进制, d 表示十进制, b 表示二进制。
默认情况下, format() 按顺序获取其参数。然而,我们可以指定任意顺序。例如:
cout << format("{3:} {1:x} {2:o} {0:b}\n", 000, 111, 222, 333);
这将打印 333 6f 336 0 。冒号前的数字表示要格式化的参数序号。按照 C++ 的最佳实践,编号从 零 开始。这使我们能够多次格式化同一个参数:
cout << format("{0:} {0:x} {0:o} {0:d} {0:b}\n", 1234); // 默认,十六进制,八进制,十进制,二进制
浮点数格式与 ostream 相同: e 表示 scientific , a 表示 hexfloat , f 表示 fixed , g 表示 default 。例如:
cout << format("{0:}; {0:e}; {0:a}; {0:f}; {0:g}\n", 123.456);
// default, scientific, hexfloat, fixed, default
结果与从 ostream 获得的相同,只是十六进制数字前面没有 0x :
123.456; 1.234560e+002; 1.edd2f2p+6; 123.456000; 123.456
点号前导一个精度 指定符 (specifier):
cout << format("precision(8): {:.8} {} {}\n", 1234.56789, 1234.56789, 123456);
cout << format("precision(4): {:.4} {} {}\n", 1234.56789, 1234.56789, 123456);
cout << format("{}\n", 1234.56789);
与流不同, 指定符 不是“粘性的”,所以我们得到:
precision(8): 1234.5679 1234.56789 123456
precision(4): 1235 1234.56789 123456
1234.56789
与流格式器一样,我们也可以指定数字要放入的字段大小及其在该字段中的对齐方式。
与流格式器一样, format() 也能处理时间和日期(§16.2.2)。例如:
cout << format("birthday: {}\n",November/28/2021);
cout << format("zt: {}", zoned_time{current_zone(), system_clock::now()});
和往常一样,值的默认格式与默认的流输出格式相同。但是, format() 提供了大约 60 个格式 指定符 的小型语言,可以对数字和日期的格式化进行非常详细的控制。例如:
auto ymd = 2021y/March/30;
cout << format("ymd: {3:%A},{1:} {2:%B},{0:}\n", ymd.year(), ymd.month(), ymd.day(), weekday(ymd));
这产生了:
ymd: Tuesday, March 30, 2021
所有时间和日期的格式字符串都以 % 开头。
众多格式 指定符 提供的灵活性很重要,但也带来了许多出错的机会。一些格式 指定符 带有可选的或依赖于区域性的语义。如果格式化错误在运行时被捕获,将抛出 format_error 异常。例如:
string ss = format("{:%F}", 2);// 错误:参数不匹配;可能在编译时被捕获
string sss = format("{%F}", 2);// 错误:格式错误;可能在编译时被捕获
迄今为止的例子都具有可在编译时检查的常量格式。互补(complimentary)函数 vformat() 接受一个变量作为格式,大大提高了灵活性和运行时出错的机会:
string fmt = "{}";
cout << vformat(fmt, make_format_args(2));// OK
fmt = "{:%F}";
cout << vformat(fmt, make_format_args(2)); // 错误:格式与参数不匹配;在运行时捕获
最后,格式器也可以直接写入由迭代器定义的缓冲区中。例如:
string buf;
format_to(back_inserter(buf), "iterator: {} {}\n", "Hi! ", 2022);
cout << buf; // iterator: Hi! 2022
如果我们直接使用流的缓冲区或其他输出设备的缓冲区,这对性能来说会很有趣。
11.7 流
标准库直接支持以下几种流:
• 标准流:与系统标准I/O流关联的流(§11.7.1)
• 文件流:与文件关联的流(§11.7.2)
• 字符串流:与字符串关联的流(§11.7.3)
• 内存流:与内存特定区域关联的流(§11.7.4)
• 同步流:可以从多个 线程 中使用而不会产生数据竞争的流(§11.7.5)
此外,我们还可以定义自己的流,例如,与通信通道关联的流。
流不能被复制;应始终通过引用传递它们。
所有标准库中的流都是模板,其字符类型作为参数。我在这里使用的名称所对应的版本处理 char 。例如, ostream 是 basic_ostream<char> 的一个实例。对于这样的每个流,标准库还提供了处理 wchar_t 的版本。例如, wostream 是 basic_ostream<wchar_t> 的一个实例。宽字符流可以用于Unicode字符。
11.7.1 标准流
标准流包括:
• cout 用于“普通输出”
• cerr 用于无缓冲的“错误输出”
• clog 用于有缓冲的“日志输出”
• cin 用于标准输入。
11.7.2 文件流
在 <fstream> 中,标准库提供了与文件进行读写操作的流:
• ifstreams 用于从文件读取
• ofstreams 用于向文件写入
• fstreams 用于同时从文件读取和写入
例如:
ofstream ofs {"target"}; // 'o' 表示“输出”
if (!ofs)
error("无法打开'target'进行写入");
检查文件流是否已正确打开,通常通过检查其状态来完成。
ifstream ifs {"source"}; // 'i' 表示“输入”
if (!ifs)
error("无法打开'source'进行读取");
假设这些测试成功, ofs 可以像 cout 一样用作普通的 ostream ,而 ifs 则可以像 cin 一样用作普通的 istream 。
文件定位以及对文件打开方式的更详细控制是可能的,但这超出了本书的范围。
有关文件名的组合和文件系统操作,请参阅 §11.9。
11.7.3 字符串流
在 <sstream> 中,标准库提供了与 字符串 进行读写操作的流:
• istringstream 用于从 字符串 读取
• ostringstream 用于向 字符串 写入
• stringstream 用于同时从 字符串 读取和写入。
例如:
void test() {
ostringstream oss;
oss << "{temperature," << scientific << 123.4567890 << "}";
cout << oss.str() << '\n'; // 使用str()获取写入的内容
}
ostringstream 中的内容可以通过 str() (内容的字符串副本)或 view() (内容的 string_view )来读取。 ostringstream 的一个常见用途是在将格式化后的结果传递给GUI之前进行格式化。类似地,从GUI接收到的字符串也可以通过将其放入 istringstream 中,使用格式化的输入操作(§11.3)来读取。
stringstream 可用于读写。例如,我们可以定义一个操作,它可以将任何具有字符串表示形式的类型转换为另一种也可以表示为字符串的类型:
template<typename Target = string, typename Source = string>
Target to(Source arg) { // 将Source转换为Target
stringstream buf;
Target result;
if (!(buf << arg) // 将arg写入流
|| !(buf >> result) // 从流中读取result
|| !(buf >> std::ws).eof()) { // 流中是否还有剩余内容?
throw runtime_error{"to<>() failed"};
}
return result;
}
函数模板的参数仅在无法推导出或没有默认值时才需要明确提及(§8.2.4),因此我们可以这样写:
auto x1 = to<string, double>(1.2); // 非常明确(但冗长)
auto x2 = to<string>(1.2); // Source 自动推导为double
auto x3 = to<>(1.2); // <>是多余的,Target 默认为string,Source 自动推导为double
auto x4 = to(1.2); // Target 默认为string,Source 自动推导为double
如果所有函数模板参数都已默认,则可以省略 <> 。
我认为这是结合语言特性和标准库设施所能达到的通用性和易用性的一个很好的例子。
11.7.4 内存流
从C++的早期开始,用户就可以指定内存中的某个部分与流关联,以便我们能够直接从中读取/写入。这类古老的流, strstream ,已经被弃用数十年,但其替代品 spanstream 、 ispanstream 和 ospanstream 要到C++23才会正式成为标准。然而,它们已经广泛可用;您可以尝试您的实现或在GitHub上搜索。
ospanstream 的行为类似于 ostringstream (§11.7.3),初始化方式也类似,不同之处在于 ospanstream 接受一个 span 而不是字符串作为参数。例如:
void user(int arg) {
array<char, 128> buf;
ospanstream ss(buf);
ss << "write " << arg << " to memory\n";
// ...
}
如果写入操作超出目标缓冲区的容量,会将字符串状态设置为 failure (§11.4)。
类似地, ispanstream 类似于 istringstream ,用于从内存中的 span 读取数据。
11.7.5 同步流
在多线程系统中,除非满足以下条件之一,否则I/O会变得不可靠且混乱:
• 只有一个 线程 使用该流。
• 对流的访问是同步的,确保同一时间只有一个 线程 能获得访问权限。
osyncstream 确保一系列输出操作将完整执行,并且即使其他 线程 尝试写入,其结果也会按照预期出现在输出缓冲区中。例如:
void unsafe(int x, string& s) {
cout << x;
cout << s;
}
另一个线程可能会引入数据竞争(§18.2),导致输出结果出乎意料。使用 osyncstream 可以避免这种情况:
void safer(int x, string& s) {
osyncstream oss(cout);
oss << x;
oss << s;
}
其他同样使用 osyncstream 的 线程 将不会互相干扰。但如果另一个 线程 直接使用 cout ,则仍可能产生干扰,因此要么始终一致地使用 ostringstream ,要么确保只有一个 线程 向特定的输出流产生输出。
并发编程可能相当复杂,因此需谨慎处理(参见第18章)。只要可行,就尽量避免 线程 间的数据共享。
11.8 C风格I/O
C++标准库同时也支持C标准库的I/O功能,包括 printf() 和 scanf() 。从类型安全和安全性角度来看,这种用法存在很多隐患,因此我不建议使用。特别是对于安全且便捷的输入处理,它难以胜任。并且它不支持用户自定义类型。如果你不打算使用C风格的I/O,并且关心I/O性能,可以调用:
ios_base::sync_with_stdio(false); // 避免显著的开销
如果不进行此调用,标准 iostream (如 cin 和 cout )可能会为了与C风格I/O兼容而显著降低运行速度。
如果你喜欢 printf() 风格的格式化输出,可以使用 format (§11.6.2);它是类型安全的,更易于使用,同样灵活且速度快。
11.9 文件系统
大多数系统都有文件系统的概念,用于访问以文件形式存储的永久信息。不幸的是,文件系统的属性及操作它们的方式差异很大。为了解决这个问题,位于 <filesystem> 中的文件系统库为大多数文件系统的大部分功能提供了一个统一的接口。利用 <filesystem> ,我们可以可移植地:
- 表达文件系统路径并浏览文件系统
- 检查文件类型及与之相关的权限
文件系统库能够处理Unicode,但具体说明如何处理超出了本书的范围。对于详细信息,我推荐查阅 [Cppreference] 和 [Boost] 文件系统文档。
11.9.1 路径
考虑一个示例:
path f = "dir/hypothetical.cpp"; // 命名一个文件
assert(exists(f)); // f 必须存在
if (is_regular_file(f)) // f 是否是一个普通文件?
cout << f << " 是一个文件;它的大小是 " << file_size(f) << '\n';
请注意,操作文件系统的程序通常与其他程序一起在计算机上运行。因此,在两个命令之间,文件系统的具体内容可能会发生变化。例如,尽管我们首先仔细断言了 f 的存在,但在下一行我们询问 f 是否为常规文件时,这可能不再成立。
路径 是一个相当复杂的类,能够处理多种操作系统所使用的多变字符集和约定。特别是,它可以处理由 main() 提供的命令行中的文件名。例如:
int main(int argc, char* argv[]) {
if (argc < 2) {
cerr << "需要参数\n";
return 1;
}
path p {argv[1]}; // 从命令行创建路径
cout << p << " " << exists(p) << '\n'; // 注意:路径可以像字符串一样打印
// ...
}
路径 在使用之前不会被检查有效性。即便如此,其有效性也取决于程序运行所在系统的约定。
自然地, 路径 可以用来打开文件:
void use(path p) {
ofstream f {p};
if (!f) error("不合法的文件名: ", p);
f << "你好,文件!";
}
除了 path 之外, <filesystem> 还提供了遍历目录和查询找到的文件属性的类型:
path | 路径 |
filesystem_error | 文件系统异常 |
directory_entry | 目录条目 |
directory_iterator | 用于遍历目录的迭代器 |
recursive_directory_iterator | 用于递归遍历目录及其子目录的迭代器 |
考虑一个简单但并非完全不切实际的例子:
void print_directory(path p) // 打印p中所有文件的名称
try {
if (is_directory(p)) {
cout << p << ":\n";
for (const directory_entry& x : directory_iterator{p})
cout << " " << x.path() << '\n';
}
} catch (const filesystem_error& ex) {
cerr << ex.what() << '\n';
}
字符串可以隐式转换为路径,所以我们可以通过以下方式调用 print_directory :
void use() {
print_directory("."); // 当前目录
print_directory(".."); // 父目录
print_directory("/"); // Unix根目录
print_directory("c:"); // Windows C盘
}
for (string s; cin >> s;) {
print_directory(s);
}
如果我还想列出子目录,我就会使用 recursive_directory_iterator{p} 。如果我想按字典顺序打印条目,我会将路径复制到一个 vector 中,并在打印之前对其进行排序。
path 类提供了许多常见且有用的操作:
value_type | 用于表示路径组件的字符类型,POSIX上为 char ,Windows上为 wchar_t |
string_type | 基于 value_type 的 std::basic_string 类型 |
const_iterator | 具有 value_type 为 path 的双向常量迭代器 |
iterator | 类似于 const_iterator 的别名 |
p=p2 | 将 p2 赋值给 p |
p/=p2 | 使用文件名分隔符(默认为 / )连接 p 和 p2 的结果赋值给 p |
p+=p2 | 直接连接 p 和 p2 的结果赋值给 p (无分隔符) |
s=p.native() | 获取 p 的本机格式引用 |
s=p.string() | 获取 p 的本机字符串格式 |
s=p.generic_string() | 获取 p 的通用格式字符串 |
p2=p.filename() | 获取 p 的文件名部分 |
p2=p.stem() | 获取 p 的基本文件名部分(不含扩展名) |
p2=p.extension() | 获取 p 的文件扩展名部分 |
i=p.begin() | 获取 p 的元素序列的开始迭代器 |
i=p.end() | 获取 p 的元素序列的结束迭代器 |
p==p2, p!=p2 | 判断 p 和 p2 是否相等或不等 |
p<p2, p<=p2, p>p2, p>=p2 | 对 p 和 p2 进行字典序比较 |
is>>p, os<<p | 输入流和输出流与 p 的I/O操作 |
u8path(s) | 从UTF-8编码的源 s 创建一个路径对象 |
例如:
void test(path p)
{
if (is_directory(p)) {
cout << p << ":\n";
for (const directory_entry& x : directory_iterator(p)) {
const path& f = x; // 引用目录条目的路径部分
if (f.extension() == ".exe")
cout << f.stem() << " 是一个Windows可执行文件\n";
else {
string n = f.extension().string();
if (n == ".cpp" || n == ".C" || n == ".cxx")
cout << f.stem() << " 是一个C++源文件\n";
}
}
}
}
我们把 路径 当作字符串来使用(例如, f.extension ),并且可以从 路径 中抽取各种类型的字符串(例如, f.extension().string() )。
命名规范、自然语言和字符串编码富含复杂性。标准库中的文件系统抽象提供了可移植性和极大的简化。
11.9.2 文件和目录
很自然地,文件系统提供了许多操作,并且不同的操作系统提供了不同的一组操作。标准库提供了一些能够在各种系统上合理实现的操作。
exists(p) | 检查 p 是否指向一个存在的文件系统对象 |
copy(p1, p2) | 从 p1 复制文件或目录到 p2 ;错误以异常形式报告 |
copy(p1, p2, e) | 从 p1 复制文件或目录;错误以错误码形式报告,其中 e 指定错误处理策略 |
b=copy_file(p1, p2) | 复制 p1 的文件内容到 p2 ;错误以异常形式报告,返回布尔值表示操作成功与否 |
b=create_directory(p) | 创建名为 p 的新目录; p 的所有上级目录必须已存在 |
b=create_directories(p) | 创建名为 p 的新目录以及所有不存在的上级目录 |
p=current_path() | 返回当前工作目录的路径 |
current_path(p) | 设置当前工作目录为 p |
s=file_size(p) | 返回 p 所指文件的字节数 |
b=remove(p) | 如果 p 是一个文件或空目录,则删除之 |
许多操作都有额外参数的重载版本,例如操作系统权限的处理。这类处理远超出了本书的范围,因此如果你需要的话,请查阅相关资料。
和 copy() 一样,所有操作都分为两个版本:
• 基础版本,如表格中所列,例如 exists(p) 。如果操作失败,函数会抛出 filesystem_error 。
• 额外带有 error_code 参数的版本,例如 exists(p, e) 。通过检查 e 来判断操作是否成功。
当操作在正常使用中预期频繁失败时,我们使用错误码;而当错误被视为异常情况时,则使用抛出异常的版本。
通常,使用查询函数是检查文件属性最简单、最直接的方法。 <filesystem> 库了解几种常见的文件类型,并将其他归类为“其他”:
is_block_file(f) | f 是否为块设备文件? |
is_character_file(f) | f 是否为字符设备文件? |
is_directory(f) | f 是否为目录? |
is_empty(f) | f 是否为空文件或目录? |
is_fifo(f) | f 是否为命名管道(FIFO)? |
is_other(f) | f 是否为其他类型的文件? |
is_regular_file(f) | f 是否为普通文件? |
is_socket(f) | f 是否为命名的IPC套接字? |
is_symlink(f) | f 是否为符号链接? |
status_known(f) | f 的文件状态是否已知? |
11.1 建议
- iostream 具有类型安全、类型敏感和可扩展性;§11.1。
- 仅在必须时使用字符级输入;§11.3;[CG: SL.io.1]。
- 读取时,始终考虑不规范的输入;§11.3;[CG: SL.io.2]。
- 避免使用 endl ;[CG: SL.io.50]。
- 为具有有意义文本表示的用户定义类型定义 << 和 >> ;§11.1, §11.2, §11.3。
- 使用 cout 进行常规输出, cerr 用于错误输出;§11.1。
- 存在用于普通字符和宽字符的 iostream ,且可以为任何类型的字符定义 iostream ;§11.1。
- 支持二进制I/O;§11.1。
- 标准 iostream 适用于标准I/O流、文件和 字符串 ;§11.2, §11.3, §11.7.2, §11.7.3。
- 串联 << 操作以简化记法;§11.2。
- 串联 >> 操作以简化记法;§11.3。
- 字符串 输入不会溢出;§11.3。
- 默认情况下, >> 跳过初始空白字符;§11.3。
- 使用流状态 fail 处理可能恢复的I/O错误;§11.4。
- 可以为我们自己的类型定义<<和>>运算符;§11.5。
- 添加新的 << 和 >> 运算符不需要修改 istream 或 ostream ;§11.5。
- 使用 manipulator 或 format() 控制格式化;§11.6.1, §11.6.2。
- precision() 指定应用于随后的所有浮点数输出操作;§11.6.1。
- 浮点数格式指定(如 科学计数法 )应用于随后的所有浮点数输出操作;§11.6.1。
- 使用标准 manipulator 时包含 <ios> 或 <iostream> ;§11.6。
- 流格式化 manipulator 对流中多个值的使用是“粘性的”;§11.6.1。
- 使用带参数的标准 manipulator 时 #include <iomanip> ;§11.6。
- 可以按照标准格式输出时间、日期等;§11.6.1, §11.6.2。
- 不要尝试复制流:流只能移动;§11.7。
- 在使用文件流之前,记得检查是否已连接到文件;§11.7.2。
- 对于内存格式化,使用 stringstream 或 memory streams ;§11.7.3; §11.7.4。
- 可以定义任意两种具有字符串表示的类型之间的转换;§11.7.3。
- C风格的I/O不是类型安全的;§11.8。
- 除非使用printf家族函数,否则调用 ios_base::sync_with_stdio(false) ;§11.8; [CG: SL.io.10]。
- 优先使用 <filesystem> 而非直接使用平台特定接口;§11.9。