I/O流库提供了针对文本和数值的输入输出功能。这种格式化和非格式化的I/O都带有缓冲。它不但是类型安全的,而且可以象支持内置类型一样,支持用户自定义类型。
文件系统库提供了操作文件和目录的基本工具。
ostream类型将有类型的对象转换为字符(字节)流:
istream类型将字符(字节)流转换为有类型的对象:
在istream和ostream上的任何操作,都是类型安全且类型敏感的,甚至可以被扩展,以支持用户自定义的类型。
其它形式的用户交互,比如图形用户界面(GUI),需要借助其它库的支持。这些库并不是标准库的一部分。
I/O流库可用于二进制数据的I/O,支持不同的字符类型和区域设置,还可使用高级缓冲策略。这些内容涉及有关I/O流的高级话题。
I/O流既可用于针对文件的I/O,也可用于针对内存的I/O,甚至可以向标准库的string对象,或从该对象中,写入或读取格式化的数据。
所有的I/O流类都带有析构函数,用于释放所持有的资源,如内存缓冲区、文件句柄等。它们是资源获取即初始化(RAII)的典范。
在<ostream>头文件中,I/O流库为所有的内置类型都定义了输出操作。而且,为用户自定义类型定义输出操作也很简单。“<<”是输出(流插入)操作符,作用于ostream类型的对象。I/O流库预定义了两个ostream类型的对象,cout表示标准输出流,cerr表示标准出错流。默认情况下,输出(写入)到cout的值被转换为一个字符序列。例如下面的代码可将整数10以十进制的形式输出:
1cout << 10;
此代码先将字符1放到标准输出流中,接着又放入字符0。
实现该功能的另一种等价的写法是:
xxxxxxxxxx
21int i{ 10 };
2cout << i;
不同类型值的输出可以一种很直观的方法组合在一起。例如:
xxxxxxxxxx
31cout << "the value of i is ";
2cout << i;
3cout << '\n';
执行这段代码,将在标准输出上看到:
xxxxxxxxxx
11the value of i is 10
重复书写“cout <<”难免令人感到厌倦。幸运的是,输出表达式的结果,即输出操作符(<<)函数的返回值,是对输出流对象的引用,因此可在此基础上继续执行输出操作。例如:
xxxxxxxxxx
11cout << "the value of i is " << i << '\n';
这段代码的执行结果,与上一段代码完全一样。
字符常量是被单引号引起的一个字符。注意,输出一个字符的结果就是其字符形式,而非其数值形式。例如:
xxxxxxxxxx
31int b = 'b'; // char隐式转换为int
2char c = 'c';
3cout << 'a' << b << c;
字符'b'的值是98('b'的ASCII编码值是98),因此这段代码的输出是:a98c。
在<istream>头文件中,I/O流库为所有的内置类型都定义了输入操作。而且,为用户自定义类型定义输入操作也很简单。“>>”是输入(流提取)操作符,作用于istream类型的对象。“>>”操作符的右操作数决定了输入什么类型的值,以及输入的值被保存到哪里。I/O流库预定义了一个istream类型的对象——cin,表示标准输入流。默认情况下,被cin输入(读取)值是其右操作数对象的字符串形式。例如下面的代码可将一个整数以十进制形式输入到指定的变量中:
xxxxxxxxxx
21int i;
2cin >> i; // 将整数读入到i中
类似地,下面的代码可用于读取一个双精度浮点数:
xxxxxxxxxx
21double d;
2cin >> d; // 将双精度浮点数读入到d中
与输出的情况类似,输入表达式的结果,即输入操作符(>>)函数的返回值,是对输入流对象的引用,因此可将多个输入操作链接起来。例如:
xxxxxxxxxx
11cin >> i >> d; // 先将整数读入到i中,再将双精度浮点数读入到d中
在输入的过程中,如果遇到与目标数据类型不相匹配内容,比如在读取整数的过程中遇到非数字字符,会终止读取。默认情况下,起始空白字符(空格、制表或换行)会被跳过,中间的空白字符会被作为多个数据项的分隔符。
如果需要输入一个字符串,最简单的方法是使用string类型的对象。例如:
xxxxxxxxxx
21string str;
2cin >> str;
默认情况下,空白字符会终止输入。例如输入“hello world”,实际读取到str中的只有“hello”。
getline函数可以读取一整行字符串,并丢弃位于行尾的换行符。例如:
xxxxxxxxxx
11getline(cin, str);
因为位于行尾的换行符被丢弃了,所以接下来从cin的输入会从下一行开始。
使用格式化I/O通常不那么容易出错,且更有效率,比逐个操作字符的代码量要少得多。特别地,istream会把内存管理和范围检查处理得很好。借助格式化I/O,向字符串流和内存流写入格式化内容,或者从字符串流和内存流读取格式化内容,也会非常方便。
标准库的string对象有一个非常好的性质,它可以自动扩展空间以容纳被放入的内容。这样就无需预先计算被格式化后的文本需要占用多大内存,既不会浪费有限的内存资源,又能有效地规避内存溢出的风险。
每个iostream对象都用状态。可以通过对状态的检查,判断流操作是否成功。流状态检查常被作为值序列读取循环的控制条件。例如:
xxxxxxxxxx
21vector<int> ints;
2for (int n; cin >> n; ints.push_back(n));
for循环持续从cin读取整数到ints中,直到遇到非整数字符(比如'a')为止。这段代码的关键是,“cin >> n”操作返回一个对cin对象的引用。该对象在布尔上下文中的值因其状态而异。若cin对象的状态正常,并可继续执行后续输入,则该布尔值为true,循环继续,否则为false,循环退出。
一般而言,I/O状态包含了执行读写操作所需要的全部信息,例如格式化信息、是否发生错误、是否读至文件末尾、使用何种缓冲区等。用户可以人为设置状态表示发生了错误,或在错误不严重的情况下予以清除。假设要将从标准输入读到的整数序列求和,遇到文件尾或“end”字符串则停止读取并输出计算结果,其它情况则报告错误:
xxxxxxxxxx
141int sum = 0;
2for (int n; cin >> n; sum += n);
3if (cin.eof()) // 遇到文件尾
4 cout << "eof: " << sum << endl;
5else if (cin.fail()) { // 遇到非整数字符
6 cin.clear(); // 清除错误
7 string s;
8 if (cin >> s && s == "end") // 遇到“end”字符串
9 cout << "end: " << sum << endl;
10 else {
11 cout << "input error" << endl;
12 cin.setstate(ios_base::failbit); // 设置错误
13 }
14}
通过Windows的标准输入模拟文件尾的方法是,于新行开始处按下<Ctrl+C>组合键。
除了支持内置类型和标准库string对象的I/O,iostream还允许程序员为自己的类型定义I/O操作。例如有一个名为Entry的自定义类型,表示电话本中的一个条目,包含姓名和电话号码两个数据项:
xxxxxxxxxx
41struct Entry {
2 string name;
3 unsigned long long number;
4};
可以定义一个针对该类型的输出操作符函数,以形如“{"Zhang Fei", 13910110072}”的格式打印该类型的对象:
xxxxxxxxxx
31ostream& operator<<(ostream& os, const Entry& e) {
2 return os << "{\"" << e.name << "\", " << e.number << '}';
3}
一个用户自定义的输出操作符函数,名为“operator<<”,接受输出流对象的引用作为其第一个参数,第二个参数为所要输出的自定义类型对象的常引用,根据所期望的输出格式编写函数体中的代码,最后再返回输出流对象的引用。这样就可以象输出任何内置类型的数据一样输出Entry类型的对象了。例如:
xxxxxxxxxx
21Entry e1{ "Zhang Fei", 13910110072 };
2cout << e1 << endl;
相对于输出,输入操作符函数的定义则要复杂得多,它必须检查格式是否正确,并处理可能发生的错误。例如:
xxxxxxxxxx
191istream& operator>>(istream& is, Entry& e) {
2 char c, q;
3 if (is >> c && c == '{' && is >> q && q == '"') { // 以“{”开始,后跟“"”,跳过空白字符
4 string name; // string的默认值是空串
5 while (is.get(c) && c != '"') // 读取两个“"”之间的姓名,空白字符也要读取
6 name += c;
7
8 if (is >> c && c == ',') { // 姓名和电话号码之间以“,”分隔,跳过空白字符
9 unsigned long long number = 0;
10 if (is >> number >> c && c == '}') { // 读取“}”之前的电话号码,跳过空白字符
11 e = { name, number }; // 赋值给Entry对象
12 return is;
13 }
14 }
15 }
16
17 is.setstate(ios_base::failbit); // 设置错误
18 return is;
19}
一个用户自定义的输入操作符函数,名为“operator>>”,接受输入流对象的引用作为其第一个参数,第二个参数为所要输入的自定义类型对象的引用,根据所要求的输入格式编写函数体中的代码,最后再返回输入流对象的引用。这样就可以象输入任何内置类型的数据一样输入Entry类型的对象了。例如:
xxxxxxxxxx
21Entry e2;
2cin >> e2;
注意,形如“is >>”的操作会跳过空白字符,而get函数则不会:
xxxxxxxxxx
61____{____"____Zhang____Fei____"____,____13910110072____}
2\___/\___/|||||||||||||||||||||\___/\_____________/\___/
3{ " ____Zhang____Fei____" , 13910110072 }
4c q ccccccccccccccccccccc c number c
5\__________________/
6name
上图中的“_”代表一个空白字符。上面的代码会忽略姓名双引号以外的空白字符,而姓名双引号以内的空白字符,则作为姓名字符串的一部分,被如实地读取出来。
I/O流库和format库提供了很多操作来控制输入和输出的格式。I/O流库与C++语言的历史一样悠久,它更专注于对算术数字的格式化。而format库则比较新,它由C++20提供,可以类似printf函数的风格,格式化各种类型数据的组合。输出格式化还提供对Unicode字符集的支持。
最简单的格式化方式,就是使用格式控制符(manipulator)。它们被定义在<ios>、<istream>、<ostream>和<iomanip>(带参数的格式控制符)等几个头文件中。例如下面的代码分别以默认(十进制)格式、十六进制格式、八进制格式和十进制格式,输出一个整数:
xxxxxxxxxx
41cout << 1234 << ' ' // 默认(十进制)格式
2 << hex << 1234 << ' ' // 十六进制格式
3 << oct << 1234 << ' ' // 八进制格式
4 << dec << 1234 << endl; // 十进制格式
也可以显式设置浮点数的输出格式。例如:
xxxxxxxxxx
61constexpr double d = 123.456;
2cout << d << ' ' // 默认格式
3 << fixed << d << ' ' // 定点小数格式
4 << scientific << d << ' ' // 科学计数法格式
5 << hexfloat << d << ' ' // 十六进制格式
6 << defaultfloat << d << endl; // 默认格式
精度是在显示浮点数时用于控制数字位数的一个整数。
格式控制符 | 格式 | 显示 | 例如 | 精度 |
---|---|---|---|---|
fixed | 定点小数 | 整数部分、小数点和小数部分 | 123.456000 | 小数部分的位数 |
scientific | 科学计数法 | 一位整数部分、小数点、小数部分、字符“e”和指数 | 1.234560e+02 | 小数部分的位数 |
defaultfloat | 默认 | 根据可用空间的大小,自动选择最佳形式 | 123.456 | 最多显示总位数 |
精度的默认值为6,通过precision函数可以人为设置精度。浮点值并非简单截断,而是遵循四舍五入规则。precision函数不影响整数显示。例如:
xxxxxxxxxx
221constexpr double d = 123.456;
2...
3cout.precision(8);
4cout << d << ' ' // 123.456
5 << fixed << d << ' ' // 123.45600000
6 << scientific << d << ' ' // 1.23456000e+02
7 << hexfloat << d << ' ' // 0x1.edd2f1a9fbe77p+6
8 << defaultfloat << d << endl; // 123.456
9...
10cout.precision(4);
11cout << d << ' ' // 123.5
12 << fixed << d << ' ' // 123.4560
13 << scientific << d << ' ' // 1.2346e+02
14 << hexfloat << d << ' ' // 0x1.edd2f1a9fbe77p+6
15 << defaultfloat << d << endl; // 123.5
16...
17cout.precision(2);
18cout << d << ' ' // 1.2e+02
19 << fixed << d << ' ' // 123.46
20 << scientific << d << ' ' // 1.23e+02
21 << hexfloat << d << ' ' // 0x1.edd2f1a9fbe77p+6
22 << defaultfloat << d << endl; // 1.2e+02
上面这些格式控制符,都是为流式格式化一系列值而设计的,因此它们都是有粘性的,即一经设置就会一直起作用,直到被显式改变。
还有一些格式控制符,可用于设置显示区域的大小(占几个字符宽度)和对齐方式等。
除了基本数字,“<<”操作符还支持诸如duration、time_point、year_month_date、weekday、month、zoned_time等日期时间类型数据的输出。例如:
xxxxxxxxxx
21cout << November / 2 / 2024 << endl;
2cout << zoned_time{ current_zone(), system_clock::now() } << endl;
这会输出:
xxxxxxxxxx
212024-11-02
22024-10-28 12:09:47.3753837 GMT+8
标准库也同样定义了可用于其它数据类型的“<<”操作符,以支持诸如complex、bitset、错误代码、指针等类型数据的输出。流式I/O可以被扩展,即针对用户自定义的数据类型,提供专门的“<<”操作符函数。
C++17:十六进制浮点数字面量
很多人坚信,printf是C语言里最受欢迎的函数,也是C语言获得成功的重要因素之一。例如:
xxxxxxxxxx
11printf("An int %d and a string '%s'.", 123, "Hello!");
类似这样“在一个格式化字符串后面紧跟一系列参数”的格式化风格,从BCPL时代就被C语言采纳,并一直延续至今。更有甚者,时至今日,仍有许多现代编程语言竞相步其后尘。自然,printf函数也是C++标准库的一部分,但它缺乏类型安全,可扩展性也不佳,不能很方便地处理用户自定义的数据类型。
在<format>头文件中,标准库提供了类型安全但同样缺乏可扩展性的printf风格的格式化机制。这个机制的核心是一个名为format的函数,该函数的返回值是一个表示格式化结果的string对象。例如:
xxxxxxxxxx
11string s1 = format("An int {} and a string '{}'.", 123, "Hello!");
格式化字符串中的普通字符,会被原样放到该函数返回的string对象中。格式化字符串中形如“{ ... }”的占位符,指定了按位置对应的后续参数,将以何种方式被插入到该string对象中。
最简单的格式化占位符由一对形如“{}”的空花括号组成,它会将参数列表中的下一个参数直接以默认“<<”操作符的方式输出到结果字符串中。上例中的整数字面量123,输出为其十进制形式的“123”,而字符串字面量“Hello!”,也直接输出为其本来的形式“Hello!”。
在表示格式化占位符的一对花括号中,添加其它格式化标记,可以刻画更多格式化细节。例如下面的代码分别以默认(十进制)格式、十六进制格式、八进制格式、十进制格式和二进制格式,输出一个整数:
xxxxxxxxxx
11cout << format("{} {:x} {:o} {:d} {:b}\n", 1234, 1234, 1234, 1234, 1234);
每个格式化标记必须以“:”作为前缀。格式化标记“b”表示以二进制格式输出整数。流式格式化并不直接支持整数的二进制格式输出。
在默认情况下,格式化字符串中的占位符,与后面的参数,按顺序一一对应,也可以人为指定与每个占位符对应的参数。例如:
xxxxxxxxxx
11cout << format("{4} {3:x} {2:o} {1:d} {0:b}\n", 2, 10, 8, 16, 10);
花括号中的数字代表参数的序号。为了维持最佳的C++风格,参数序号从零开始。借助这种方法,甚至可以多次使用同一个参数。例如:
xxxxxxxxxx
11cout << format("{0} {0:x} {0:o} {0:d} {0:b}\n", 1234);
按指定顺序对应参数的能力非常重要,尤其在需要对不同自然语言文本做格式化的场合,比如国际化。
针对浮点数的格式化标记包括:“f”表示定点小数格式、“e”表示科学计数法格式、“a”表示十六进制格式、“g”表示默认格式。例如:
xxxxxxxxxx
11cout << format("{0} {0:f} {0:e} {0:a} {0:g}\n", 123.456); // 123.456 123.456000 1.234560e+02 1.edd2f1a9fbe77p+6 123.456
输出结果与使用流式格式化的情形基本一致,除了十六进制格式浮点数开头没有“0x”前缀。
输出浮点数可以指定精度。例如:
xxxxxxxxxx
31cout << format("{0:.8} {0:.8f} {0:.8e} {0:.8a} {0:.8g}\n", 123.456); // 123.456 123.45600000 1.23456000e+02 1.edd2f1aap+6 123.456
2cout << format("{0:.4} {0:.4f} {0:.4e} {0:.4a} {0:.4g}\n", 123.456); // 123.5 123.4560 1.2346e+02 1.edd3p+6 123.5
3cout << format("{0:.2} {0:.2f} {0:.2e} {0:.2a} {0:.2g}\n", 123.456); // 1.2e+02 123.46 1.23e+02 1.eep+6 1.2e+02
与流式格式化不同,format函数的格式化标记并不具有粘性,因此需要为每个被输出的数据单独指定。
与流式格式化类似,在format函数的格式化标记中,也可以设置显示区域的大小和对齐方式等。此外,format函数也支持诸如duration、time_point、year_month_date、weekday、month、zoned_time等日期时间类型数据的格式化。例如:
xxxxxxxxxx
21cout << format("{}\n", November / 2 / 2024);
2cout << format("{}\n", zoned_time{ current_zone(), system_clock::now() });
此外,format函数提供了一种迷你语言,包含大约60种格式化标记,用以详尽地控制日期和时间的格式。例如:
xxxxxxxxxx
31auto date = 2024y / November / 2;
2cout << format("{3:%A}, {1:%B} {2}, {0}\n",
3 date.year(), date.month(), date.day(), weekday(date));
这会输出:
xxxxxxxxxx
11Saturday, November 02, 2024
所有与日期和时间有关的格式化标记都以“%”开头。
大量的格式化标记,在带来空前的灵活性的同时,也增加了更多的犯错误的机会。如果在运行时发生了格式化错误,format函数会抛出format_error异常。例如:
xxxxxxxxxx
61try {
2 cout << format("{:e}\n", 1234); // Invalid presentation type for integer
3}
4catch (const format_error& ex) {
5 cerr << ex.what() << endl;
6}
format函数接受变长参数表形式的被格式化数据,vformat函数则可以接受向量形式的被格式化数据。例如:
xxxxxxxxxx
51void printDouble(int p, char f, double d) {
2 string fmt = format(":.{}{}", p, f);
3 fmt = '{' + fmt + "}\n";
4 cout << vformat(fmt, make_format_args(d));
5}
这更灵活。例如:
xxxxxxxxxx
41printDouble(8, 'f', 123.456); // 123.45600000
2printDouble(6, 'e', 123.456); // 1.234560e+02
3printDouble(4, 'a', 123.456); // 1.edd3p+6
4printDouble(2, 'g', 123.456); // 1.2e+02
也更容易出错。例如:
xxxxxxxxxx
11printDouble(8, 'd', 123.456); // Invalid presentation type for floating-point
与format函数直接返回格式化结果不同,format_to函数将格式化结果写入一个由迭代器定义的缓冲区。例如:
xxxxxxxxxx
21string s2;
2format_to(back_inserter(s2), "An int {} and a string '{}'.", 123, "Hello!");
如果直接使用流的缓冲区或将缓冲区用于其它输出设备,可能会获得更加显著的性能提升。
C++20:基于format和vformat的printf风格格式化
标准库直接支持下列流:
标准流:与标准输入、标准输出等标准设备关联的流
文件流:与文件关联的流
字符串流:与字符串关联的流
内存流:与特定内存区域关联的流
同步流:在多线程中避免数据竞争的流
此外,还可以定义自己的流,比如与特定通信信道关联的流。
所有标准库流都是不可复制的,因此只能以引用的形式传递流类型的参数和返回值。
所有标准库流都是模板化的类,其类型参数为某种形式的字符,比如char或者wchar_t。ostream和wostream实际上分别是basic_ostream<char>和basic_ostream<wchar_t>的别名。
标准库预定义了四个标准流对象:
cin:用于输入
cout:用于一般性的数据输出
cerr:用于无缓冲的错误输出
clog:用于有缓冲的日志输出
在<fstream>头文件中,标准库提供了用于读写文件的流:
ifstream:用于读文件
ofstream:用于写文件
fstream:用于读写文件
打开文件,准备写入。例如:
xxxxxxxxxx
51ofstream ofs{ "target" }; // 字母“o”表示“output”,即输出
2if (!ofs) {
3 cerr << "couldn't open 'target' for writing" << endl;
4 return -1;
5}
通过检查流对象的状态判断文件是否被成功打开。
打开文件,准备读取。例如:
xxxxxxxxxx
51ifstream ifs{ "source" }; // 字母“i”表示“input”,即输入
2if (!ifs) {
3 cerr << "couldn't open 'source' for reading" << endl;
4 return -1;
5}
文件一旦被成功打开,就可以象普通ostream对象(如cout)一样,向ofs对象输出(<<)数据。例如:
xxxxxxxxxx
81double d = 123.456;
2ofs.precision(4);
3ofs << d << ' '
4 << fixed << d << ' '
5 << scientific << d << ' '
6 << hexfloat << d << ' '
7 << defaultfloat << d << endl;
8ofs << format("{0:.4} {0:.4f} {0:.4e} {0:.4a} {0:.4g}\n", d);
或象普通istream对象(如cin)一样,从ifs对象输入(>>)数据。例如:
xxxxxxxxxx
181int i;
2ifs >> i;
3cout << i << endl;
4
5ifs >> d;
6cout << d << endl;
7
8ifs >> i >> d;
9cout << i << ' ' << d << endl;
10
11string str;
12ifs >> str;
13cout << str << endl;
14
15getline(ifs, str);
16
17getline(ifs, str);
18cout << str << endl;
借助文件流,还可以设置文件的读写位置,或对文件的打开方式做更精细化的控制。
文件流并没有提供诸如修改文件名、删除文件、复制文件、访问目录等功能。这要涉及与文件系统有关的操作。
在<sstream>头文件中,标准库提供了用于读写字符串的流:
istringstream:用于读字符串
ostringstream:用于写字符串
stringstream:用于读写字符串
例如:
xxxxxxxxxx
111ostringstream oss;
2oss << "{temperature: " << scientific << 123.4567890 << "}";
3cout << oss.view() << endl;
4
5string str = oss.str();
6
7istringstream iss{ str };
8string head, tail;
9double temperature;
10iss >> head >> temperature >> tail;
11cout << head << ' ' << scientific << temperature << tail << endl;
ostringstream中的内容,可以通过view或str函数,获得string_view类型的视图或string类型的拷贝。
ostringstream的一个最常见的应用,是先通过它对需要输出的数据进行格式化,然后再将格式化的结果渲染到GUI(图形用户界面)组件上。
反过来,也可以先从GUI组件上收集用户输入的字符串,并将其放入一个istringstream对象,再通过它格式化输入到用于存储数据的变量中。
stringstream既可用于从字符串中读取数据,也可用于将数据写入到字符串中。例如下面的函数用于在两种都有字符串表示的类型间实现转换:
xxxxxxxxxx
171template<typename Target = string, typename Source = string>
2Target castTo(const Source& src) {
3 stringstream ss;
4 if (!(ss << src)) // 将src写入字符串流
5 throw runtime_error{ "castTo failed" };
6
7 Target tar;
8 if (!(ss >> tar)) // 从字符串流读取tar
9 throw runtime_error{ "castTo failed" };
10
11 ss >> ws; // 跳过连续的空白字符,直到遇到第一个非空白字符或文件尾为止
12
13 if (!ss.eof()) // 字符串流中还有其它东西
14 throw runtime_error{ "castTo failed" };
15
16 return tar;
17}
“ws”是一个针对输入流的格式控制符,表示跳过连续的空白字符,直到遇到第一个非空白字符或文件尾为止。调用该函数,实现类型转换。例如:
xxxxxxxxxx
51auto x1 = castTo<string, double>(temperature); // 类型参数给全,最啰嗦
2auto x2 = castTo<string>(temperature); // 根据temperature的类型,将Source推断为double
3auto x3 = castTo<>(temperature); // Target默认为string
4auto x4 = castTo(temperature); // 空“<>”可以省略
5cout << format("{} {} {} {}\n", x1, x2, x3, x4);
只有当函数模板的类型参数无法被推断出来,同时又没有指定默认值时,才需要显式指定它。如果函数模板的模板参数表为空,则可以省略表示模板参数表边界的一对尖括号。
这个例子充分展现了,组合使用语言特性和标准库工具,提高代码通用性和易用性的方法。
从早期的C++开始,就有为用户设计的内存流,直接通过流来读写内存。这类流的最古老的实例,比如strstream,数十年前就已经被废弃了。作为它们的替代品,ospanstream、ispanstream和spanstream,在C++23之前还没有被正式吸纳为官方标准。尽管如此,它们在很多C++实现中已经可以使用了。在GitHub上,也能找到它们的第三方实现。
ospanstream的行为与ostringstream非常相似,初始化方法也基本一致,除了ospanstream可以接受span而非string作为构造参数。例如:
xxxxxxxxxx
31array<char, 1024> buf{};
2ospanstream oss(buf);
3oss << 123 << ' ' << 4.56 << ' ' << 'c' << ' ' << "string";
如果在插入流的过程中发生了缓冲区溢出,那么流状态将会变为failure。
类似地,ispanstream的行为与istringstream也很相似。例如:
xxxxxxxxxx
71ispanstream iss(buf);
2int n;
3double d;
4char c;
5string s;
6iss >> n >> d >> c >> s;
7cout << n << ' ' << d << ' ' << c << ' ' << s << endl;
在多线程系统中,对I/O流的操作可能会变得非常不可靠,除非:
只有一个线程在使用流
对I/O流的操作进行了同步,确保在任何时候都只有一个线程拥有对流的访问权限
osyncstream保证每个针对它的写操作都能顺利完成,且结果符合预期,即便有其它线程也试图写入。例如:
xxxxxxxxxx
41void unsafe(int n, const string& s) {
2 cout << n;
3 cout << s;
4}
当该函数在多个线程中被同时调用时,可能引发竞争,最终导致奇怪的输出。借助osyncstream,可以避免这种情况的发生。例如:
xxxxxxxxxx
51void safe(int n, const string& s) {
2 osyncstream oss(cout);
3 oss << n;
4 oss << s;
5}
当该函数在多个线程中被同时调用时,不会相互影响。但如果其它线程仍然直接使用cout,则因竞争而导致的混乱依然会发生。因此,要么在所有线程中统一使用osyncstream,要么借助其它同步机制,确保任何时候只有一个线程写入输出流。
为多线程建立同步,需要一些技巧。只要有可能,应尽量避免在多线程间共享对象,包括I/O流对象,如cout等。
C++标准库继承了C标准库的I/O函数,如printf、scanf等。这些函数通常都不是类型安全的,而且也不支持用户自定义的数据类型,因此不建议使用它们。继续使用它们的唯一理由,可能源于对代码简洁性的追求,为此承担适度且可控的安全风险,从某方面讲也是值得的。
如果决定不再使用C风格的I/O,同时又很在意I/O的性能,可执行如下调用:
xxxxxxxxxx
11ios_base::sync_with_stdio(false); // 避免兼容性开销
默认情况下,标准库的iostream对象,如cin、cout等,会因为要兼容C风格的I/O而牺牲一部分性能。执行以上调用,可以避免这方面的兼容性开销。
如果对printf风格的格式化情有独钟,不妨使用format函数取而代之。它类型安全、易读易用,且灵活迅速。
大多是操作系统都有文件系统的概念,提供对文件形式的持久化存储的管理和访问。不幸的是,不同文件系统的属性和操作方式存在很大差异。为了解决这个问题,标准库的文件系统库,为大多数文件系统的大多数工具,提供了统一的封装。只要使用<filesystem>头文件,就能够以可移植的方式实现:
借助路径字符串,在文件系统中导航
检查文件的类型及其附加的权限许可
另外,C++的文件系统库也支持Unicode字符集。
C++17:文件系统
考虑如下代码:
x1path f = "FileSystem.cpp"; // 指定文件路径
2
3assert(exists(f)); // 确定文件存在
4
5if (is_regular_file(f)) // 若为普通文件
6 cout << f << " is a file; its size is " << file_size(f) << " bytes" << endl;
请注意,文件系统作为一种系统级的全局资源,可能会被多个程序(进程)同时访问。即使刚刚通过断言确定了文件存在,也不能保证接下来判断文件是否为普通文件时,该路径还依然有效。
path是一个非常复杂的类,有能力处理各种各样的字符集,并能与不同类型的操作系统和文件系统保持兼容。特别地,它还可以接受来自main函数的命令行参数。例如:
xxxxxxxxxx
61path p{ argv[1] }; // 通过命令行参数构造path对象
2
3if (exists(p))
4 cout << p << " exists" << endl; // path可以象字符串一样被打印输出
5else
6 cout << format("{} doesn't exist\n", p.string()); // 通过string成员函数获取路径字符串
path直到使用时才被检查其有效性。过早检查,即便有效,也不能保证使用时还有效。
path可用于打开文件。例如:
xxxxxxxxxx
91ifstream ifs{ p };
2if (!ifs) {
3 cerr << format("couldn't open {} for reading\n", p.string());
4 return -1;
5}
6
7string s;
8while (getline(ifs, s))
9 cout << s << endl;
除了path以外,<filesystem>头文件提供了用于遍历目录及查询文件属性的类:
用于遍历目录的类 | 说明 |
---|---|
path | 文件或目录的路径 |
filesystem_error | 访问文件系统过程中可能抛出的异常 |
directory_entry | 目录条目 |
directory_iterator | 用于遍历目录迭代器 |
recursive_directory_iterator | 用于遍历目录及其子目录的迭代器 |
下面的代码用于遍历给定目录及其子目录中的所有条目:
xxxxxxxxxx
101try {
2 for (const directory_entry& e : recursive_directory_iterator{ path{ ".." } })
3 if (is_directory(e.path()))
4 cout << e.path().generic_string() << '/' << endl;
5 else
6 cout << e.path().generic_string() << endl;
7}
8catch (const filesystem_error& ex) {
9 cerr << ex.what() << endl;
10}
如果不想遍历给定目录下子目录中的内容,可以使用directory_iterator。如果想以字典顺序输出目录条目,可以将遍历所得路径字符串放入一个vector,排序后再输出。
path类提供了很多有用的公有操作:
path类的公有操作 | 说明 |
---|---|
value_type | 符合文件系统自然编码的字符类型,POSIX系统为char,Windows系统为wchar_t |
string_type | std::basic_string<value_type>的别名 |
const_iterator | const双向迭代器,迭代器的元素类型value_type为path |
iterator | const_iterator的别名 |
p=q | 将q赋值给p |
p/=q | 用系统定义的路径分隔符连接p和q |
p+=q | 不使用路径分隔符,直接连接p和q |
s=p.native() | 获得p的原生格式字符串的引用 |
s=p.string() | 获得p的原生格式字符串的副本 |
s=p.generic_string() | 获得p的通用格式字符串的副本 |
q=p.filename() | 获得p的文件名部分 |
q=p.stem() | 获得p中不带扩展名的文件名部分 |
q=p.extension() | 获得p的文件扩展名部分(含“.”) |
i=p.begin() | 获得p的起始迭代器 |
i=p.end() | 获得p的终止迭代器 |
p==q、p!=q | 不等和相等性比较 |
p<q、p<=q、p>q、 p>=q | 基于字典序的小于、小于等于、大于、大于等于比较 |
os<<p、is>>p | 插入到输出流和从输入流提取 |
p=u8path(s) | 从UTF-8编码的string中生成path |
(p和q是path类型的对象,s和i分别是string和迭代器类型的对象)
例如:
xxxxxxxxxx
71for (const directory_entry& e : recursive_directory_iterator{ path{ ".." } }) {
2 const path& p = e;
3 if (p.extension() == ".exe")
4 cout << p.filename() << " is a Windows executable file" << endl;
5 else if (p.extension() == ".cpp")
6 cout << p.filename() << " is a C++ source file" << endl;
7}
filename、stem、extension等函数的返回值都path类型对象,但可以被转换为string对象。如果将它们直接插入到输出流,会在实际的路径、文件名、扩展名字符串的两端添加一对双引号。例如:
xxxxxxxxxx
11cout << p.filename() << " is a Windows executable file" << endl;
的输出可能会是下面这个样子:
xxxxxxxxxx
11"FileSystem.exe" is a Windows executable file
而如果写成:
xxxxxxxxxx
11cout << p.filename().string() << " is a Windows executable file" << endl;
则会输出:
xxxxxxxxxx
11FileSystem.exe is a Windows executable file
这样的写法:
xxxxxxxxxx
11if (p.extension() == ".exe")
实际等价于:
xxxxxxxxxx
11if (p.extension().string() == ".exe")
但凡涉及到文件名约定、自然语言和字符编码的问题,都不会是个简单问题。标准库的文件系统库,通过适度的抽象,极大地简化了对这类问题的处理,而且具有很好的可移植性。
文件系统提供了许多针对文件和目录的操作。自然地,不同操作系统中的不同文件系统,提供了不同的操作集。标准库提供了一套可以操作任何文件系统的统一接口:
文件系统操作 | 说明 |
---|---|
exists(p) | p所表示文件或目录是否存在 |
copy(p,q) | 从p向q复制文件或目录,出错抛异常 |
copy(p,q,e) | 从p向q复制文件或目录,出错输出错误码 |
copy_file(p,q) | 从p向q复制文件,出错抛异常 |
b=create_directory(p) | 创建新目录,p中的中间目录必须已经存在 |
b=create_directories(p) | 创建新目录,p中的中间目录如果不存在,则一并创建 |
p=current_path() | 获取当前工作目录 |
current_path(p) | 设置当前工作目录 |
s=file_size(p) | 获取文件大小(以字节为单位) |
b=remove(p) | 删除文件或空目录 |
(p和q是path类型的对象,e、s和b分别是error_code、整型和布尔型的变量,b为true、false表示成功或失败)
许多操作拥有可接受更多参数(如操作系统权限描述字等)的重载版本。
与copy类似,所有操作其实都有两种报告错误的方式:抛出filesystem_error类型的异常,或输出error_code类型的错误码。如果操作失败属于正常情况,则使用错误码,而如果操作失败被视作一种意外,则最好抛出异常。
通常情况下,判断文件属性,最简单、直接的方法,就是调用查询函数。文件系统库可以识别一些常见的文件类型,并将它识别不了的类型统一标注为“其它类型”。常见的文件类型如下表所示:
用于判断文件类型的函数 | 说明 |
---|---|
is_block_file(p) | p是一个块设备文件吗? |
is_character_file(p) | p是一个字符设备文件吗? |
is_regular_file(p) | p是一个普通文件吗? |
is_directory(p) | p是一个目录吗? |
is_empty(p) | p是一个空文件或目录吗? |
is_symlink(p) | p是一个符号链接文件吗? |
is_fifo(p) | p是一个有名管道文件吗? |
is_socket(p) | p是一个本地套接字文件吗? |
is_other(p) | p是一个其它类型的文件吗? |
status_known(p) | p是一个状态未知的文件吗? |
(p是path或file_status类型的对象)
iostream是类型安全、类型敏感且易于扩展的
只在必要时才使用字符级输入
处理输入时,必须考虑到数据格式不符合预期的情况
避免使用endl
如果用户自定义的类型,存在有意义的文本表示形式,就应该为其重载“<<”和“>>”操作符
cout用于一般性输出,cerr用于报告错误
标准库提供了针对普通字符(char)和宽字符(wchar_t)的I/O流,还可以定义针对任何字符类型的I/O流
标准库支持二进制I/O
标准库提供了用于表示标准流、文件流、字符串流、内存流和同步流的预定义类型
将多个“<<”操作链接起来可以简化输出语句
将多个“>>”操作链接起来可以简化输入语句
通过字符串流向string对象中输入数据,无需担心内存溢出问题
默认情况下,“>>”操作会跳过起始空白字符
可以通过流状态获取、设置和清除I/O错误
可以为自己定义的类型重载“<<”和“>>”操作符
添加针对自定义类型的“<<”或“>>”操作符,并不需要修改istream或ostream类
借助格式控制符或format函数,处理输入输出过程中的数据格式化问题
有关precision的格式控制,对后续浮点数的输出,持续有效
有关浮点数的格式控制符,如scientific等,对后续浮点数的输出,持续有效
标准库将格式控制符定义在<ios>、<iostream>和<iomanip>三个头文件中
绝大多数格式控制符都是有粘性的,一经设置,持续有效
那些带有参数的格式控制符,都被定义在<iomanip>头文件中
可以标准格式输出日期、时间等
任何流对象都不能被复制,但可以被转移
在使用一个文件流对象之前,必须检查它是否与文件关联
若希望在内存中完成格式化,可以使用stringstream或者内存流
对任意两种类型,只要它们都有字符串表示形式,就可以为其定义相互类型转换
C风格的I/O不是类型安全的
除非要使用printf函数族,否则建议执行“ios_base::sync_with_stdio(false)”,以保证性能
访问文件系统,倾向于使用定义在<filesystem>头文件中的接口,而不要直接使用,特定平台提供的接口