加载中...
C++和C的格式化输入输出
第1节:“流”的基本概念与应用
第2节:C++和C标准输入输出同步
第3节:C++和C的格式化输入输出
第4节:重温文件流和字符串流
第5节:为自定义类型重载流操作
第6节:三大关联知识点综合强化复习
第7节:从零开始,实现日志流
第8节:“我的日志流”终版
课文封面

八个实例讲解C++中setw、skipws、setfill、setprecision、dec/hex/oct、boolalpha,以及来自C++14新标的 qutoed 等输入输出操控符的功能与使用;并与C语言的输入输出(scanf、printf)在方便性和安全性方面作了直观的对比。

0. 视频:C++和C的格式输入输出对比与实例演示

1. C 风格输入,设置最大字符串输入长度

// C 风格输入字符数组,容易发生数组越界 void testCStyleFormatIO() { char c = '$'; printf("c = %c\n", c); char name[10]; // 最多9个字母,+1 结束符 '\0' int age; printf("please input your name: "); scanf("%s", name); printf("please input your age: "); scanf("%d", &age); printf("hello %s, you are %d", name, age); printf("\nc = %c\n", c); }

以上代码使用C的scanf库函数实现一个字符串输入,读入内存存储在需预设大小(例中为10)的字符数组中。当用户输入字符个数超过10,即发生数组越界,通常会将该数组相邻(通常是代码中定义在数组前面的临时变量)的数据覆盖掉,相当于篡改了其它数据。

改善方法是在scanf中使用宽度指示,限定本次输入最大可连续读取几个字符:

// C 风格输入字符数组,加输入长度限制,以避免越界 void testCStyleFormatIO() { char c = '$'; printf("c = %c\n", c); char name[10]; // 最多9个字母,+1 结束符 '\0' int age; printf("please input your name: "); scanf("%9s", name); // 加了 9 printf("please input your age: "); scanf("%d", &age); printf("hello %s, you are %d", name, age); printf("\nc = %c\n", c); }

2. C++风格输入字符串

//C++输入字符串,设置极限长度,并检查业务逻辑允许长度 void testCPPInputSetW() { string name; int age; cout << "please input your name: "; cin >> setw(80) >> name; // 80: 允许输入的极限长度,超过认为是恶意攻击 if (name.length() > 9) // 9: 业务逻辑允许的名字最大长度 { cerr << "bad input" << endl; return; } cout << "please input your age: "; cin >> age; cout << "hello " << name << ", you are " << age << endl; }

本例涉及到的一个重要的软件产品人机交互设计上的重要原则:“俯首包容业务错误、横眉冷对恶意攻击”。
软件的强壮性,并不是指对任何来自用户的输入(包括各类操作),都坚持以“客户即上帝”的认识处理;而是应在设计时,就能找到合理的判断逻辑,以区分哪类输入是普通用户日常操作中容易犯的错误,哪些操作可判定为恶意攻击,然后对二者做不同处理。

3. skipws / noskipws

C++输入流在读取数据时,区分为“格式化输入”和“非格式化输入”,流输入操作符 ( >> )默认是前者,此时,输入数据中的空白字符(whitespace),通常被视为用于区分多个数据之间的分隔符号,不会进入输入的结果数据。

类似的格式化输入还有 getline() 方法用于读入一行,它将换行符("\n")作为一行的结束标志,但不视为该行的输入内容,因此读取结果中也不包含结尾的换行符。

3.1 skipws

以下是默认状态下的格式化输入演示:

void testCPPInputSkipWS() { int i,j; cin >> i >> j; cout << i << ',' << j << endl; }

此时,用户输入 “1 2”时,i 读取到 ‘1’, j 读取到 ‘2’,二者中间的空格被视为数据分隔符而自动忽略(跳过)。

3.2 noskipws

void testCPPInputNoSkipWS() { int i, j; char c; cin >> noskipws >> i >> c >> j; cin >> skipws; cout << i << ',' << c << ',' << j << endl; }

在 “cin >> noskipws ” 之后,再遇到输入流中的空白字符,都不会跳过,从而实现后续读取到这些空白的字符。本例中,c 将读到一个空格。
多数时间里,我们都会利用C++的格式化输入,以简化读取各类数据的实现代码;仅在特殊需要时,通过 noskipws 操控切换到 非格式化输入,完成之后,通过显式的 skipws 操作,恢复为格式化输入。

4. setw(输出宽度)、setfill(填充字符)

在C++中 setw 既是输入操控符,也是输出操控符。前者用于设置输入时,最多允许读入多少个字符;后者用于设置输出时,最少需要输出多少个字符。如果输出内容长度不足,默认使用空格进行前置填充。如需使用其它字符填充,可使用 setfill 。

类似的操作,在C语言需要在 printf 函数的第一个参数(格式串)中,设置特定的宽度指示符。

void testOutputSetWAndFill() { printf("%d,%4d,%04d\n", 11, 12, 13); cout << 11 << ',' << setw(4) << 12 << ',' << setw(4) << setfill('0') << 13 << ',' << setw(4) << setfill('#') << 14 << endl; }

5. setprecision (数字精度)

如采用C方式的格式化输出,可在 printf 中设置格式指示串 “%N.Mf”来同时设置待输出数值整数部分的宽度和小数位数。整数位不足时,如上一小节所说,默认使用空格在头部填充;小数位不足时,默认使用0在尾部填充。
C++使用 setprecision 设置待输出数值的有效位置,其有效位包含整数位和小数位。小数位不足,不会在尾部作填充。

void testOutputPrecision() { double pi = 3.14159; // C - precision 5, c++ - 6 printf("%.2f\n", pi); // 3.14 printf("%.4f\n", pi); // 3.1416 printf("%.6f\n", pi); // 3.141590 cout << setprecision(3) << pi << '\n'; //3.14 cout << setprecision(5) << pi << '\n'; //3.1416 cout << setprecision(7) << pi << '\n'; //3.14159 }

提示: setprecision 也可用作输入操控符,用于控制读取用户输入数字的精度。

6. 以十进制、十六进制、八进制输出整数

C++输出流可通过:dec、hex、oct 分别设置使用十进制、十六进制、八进制输出一个整数。

// 测试C++以十进制,十六进制,八进制输出整数 void testCPPOutputDecHexOct() { int v = 2023; cout << dec << v << endl; cout << hex << v << endl; cout << oct << v << endl; cout << dec; cout << 100 << endl; }

除了直接使用以上三个操控符设置进制以外,也可以使用 “setbase” 实现。不过,后者并不支持“任意进制”,实际支持仍然是10、8、16三种进制。
另外上,在C++11及更高标准中,还可以使用 hexfloat 实现以十六进制输出浮点数;也提供了 scientific 操控符以实现使用科学计数法输出浮点数;详见 C++浮点数科学计算法输出

7. boolalpha / noboolalpha

使用 “true”、“false”字面值输出布尔值,基本上仅是为了“好看” :)。算是“颜值即正义”在我们写的小小的控制台程序上的体现。

void testCPPOutputBoolAlpha() { cout << true << ',' << false << endl; // 1,0 cout << boolalpha << true << ',' << false << endl; // true,false cout << noboolalpha << true << ',' << false << endl; }

从这个例子中,可以看出:在同一个输出流对象上(本例为 cout),boolalpha / noboolalpha 设置的状态是持久保留的。以前者为例,只需设置一次,后面遇到 bool 值 输出,均能启作用。

8. “引号” 转义输入:quoted

qutoed 的最本质作用,就是允许我们在输入内容中,定义一个特殊字符用于转义,从而改变格式化输入时,将空格视为一次输入读取过程结束标志的默认行为。

典型的,为了读取带有空格的一个词组(或句子),典型的如外国人姓名,要么需要通过 “>>” 多次读取、并自行再组合;要么要求该内容独占一行,然后借助 std::getline() 方法读取一整行内容。借助 quoted 作为输入操控符时,可以要求待读取的内容,使用一对双引号包含起来,流和quoted将帮我们完成从中读取有效内容。

void testCPPQuoted() { string name; int age; cout << "please input your name and age: "; cin >> quoted(name) >> age; cout << "hello " << name << ", you are " << age << endl; cout << "hello " << quoted(name) << ", you are " << age << endl; }

quoted在14年的标准才引入,其时业界已经在广泛的使用这一逻辑。比如在Windows系统 中,使用完全相同的方法,来表达包含空格的文件夹名字或文件路径。各类控制台程序在读取命令行参数时,同样广泛使用双引号来表示一个带有空格的参数值……
C++的quoted操控符,不仅支持使用双引号作为特殊格式控制,还支持通过该操纵符的入参,让用户定制使用其它字符。以下例子的代码,在 cpprefrence.com 之 quoted 上做了精简:

#include <iostream> #include <iomanip> #include <sstream> void custom_delimiter() { const char delim {'$'}; const char escape {'%'}; const std::string in = "std::quoted() quotes this string and embedded $quotes$ $too"; std::stringstream ss; ss << std::quoted(in, delim, escape); std::string out; ss >> std::quoted(out, delim, escape); std::cout << "Custom delimiter case:\n" "read in [" << in << "]\n" "stored as [" << ss.str() << "]\n" "written out [" << out << "]\n\n"; } int main() { custom_delimiter(); }

它将输出:

Custom delimiter case:
read in     [std::quoted() quotes this string and embedded $quotes$ $too]
stored as   [$std::quoted() quotes this string and embedded %$quotes%$ %$too$]
written out [std::quoted() quotes this string and embedded $quotes$ $too]