C++之“流”-第3课-C++和C的格式化输入输出

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

0 先听课

C++之"流"-第3课:C++和C的格式化输入输出

1. C 风格输入:限制输入长度

cpp 复制代码
// 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中使用宽度指示,限定本次输入最大可连续读取几个字符:

cpp 复制代码
// 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++风格输入字符串

cpp 复制代码
//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

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

cpp 复制代码
void testCPPInputSkipWS()
{
    int i,j;
    cin >> i >> j;
    cout << i << ',' << j << endl;
}

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

3.2 noskipws

cpp 复制代码
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 函数的第一个参数(格式串)中,设置特定的宽度指示符。

cpp 复制代码
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 设置待输出数值的有效位置,其有效位包含整数位和小数位。小数位不足,不会在尾部作填充。

cpp 复制代码
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 分别设置使用十进制、十六进制、八进制输出一个整数。

cpp 复制代码
// 测试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"字面值输出布尔值,基本上仅是为了"好看" :)。算是"颜值即正义"在我们写的小小的控制台程序上的体现。

cpp 复制代码
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将帮我们完成从中读取有效内容。

cpp 复制代码
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 上做了精简:

cpp 复制代码
#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();
}

它将输出:

bash 复制代码
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]
相关推荐
捕鲸叉1 分钟前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer6 分钟前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq8 分钟前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
记录成长java2 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山2 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
hikktn2 小时前
如何在 Rust 中实现内存安全:与 C/C++ 的对比分析
c语言·安全·rust
青花瓷2 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
睡觉谁叫~~~2 小时前
一文解秘Rust如何与Java互操作
java·开发语言·后端·rust
音徽编程2 小时前
Rust异步运行时框架tokio保姆级教程
开发语言·网络·rust
观音山保我别报错2 小时前
C语言扫雷小游戏
c语言·开发语言·算法