八个实例讲解C++中setw、skipws、setfill、setprecision、dec/hex/oct、boolalpha,以及来自C++14新标的 qutoed 等输入输出操控符的功能与使用;并与C语言的输入输出(scanf、printf)在方便性和安全性方面作了直观的对比。
0 先听课
C++之“流”-第3课: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++浮点数科学计算法输出,或到 d2school 网站学习更多跨语言的计算机编程基础知识。
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]