《C++ Primer Plus》第17章:输入、输出和文件(2)

使用 cout 进行输出

正如前面指出,C++将输出看作字节流(根据实现和平台的不同,可能是8位、16位或32位的字节,但都是字节),但在程序中,很多数据被组织成比字节更大的单位。例如,int类型由16位或32位的二进制表示;double 值由64位的二进制数据表示。但在将字节流发送给屏幕时,希望每个字节表示一个字符值。也就是说,要在屏幕上显示数字-2.34,需要将5个字符(-、2、.、3 和 4),而不是这个值的64位内部浮点表示发送到屏幕上。因此,ostream 类最重要的任务之一是将数值类型(如int或float)转换为以文本形式表示的字符流。也就是说,ostream类将数据内部表示(二进制位模式)转换为由字符字节组成的输出流(以后会有仿生移植物,使得能够直接翻译二进制数据。我们把这种开发作为一个练习,留给您)。为执行这些转换任务,ostream类提供了多个类方法。现在就来看看它们,总结本书使用的方法,并介绍能够更精密地控制输出外观的其他方法。

重载的<<运算符

本书常结合使用 cout 和 << 运算符(插入(insertion)运算符):

int clients = 22;
cout << clients;

在 C++ 中,与 C 一样,<< 运算符的默认含义是按位左移运算符。表达式 x << 3 的意思,将 x 的二进制表示中所有的位向左移动 3 位。显然,这与输出的关系不大。但 ostream 类重新定义了 << 运算符,方法是将其重载为输出。在这种情况下,<<叫做插入运算符,而不是左移运算符(左移运算符由于其外观(像向左流动的信息流)而获得这种新角色)。插入运算符被重载,使之能够识别 C++ 中所有的基本类型:

  • unsigned char;
  • signed char;
  • char;
  • short;
  • unsigned short;
  • int;
  • unsigned int;
  • long;
  • unsigned long;
  • long long (C++11);
  • unsigned long long (C++11);
  • float;
  • double;
  • long double;

对于上述每种数据类型,ostream类都提供了 operator<<()函数的定义(第11章讨论过,名称中包含运算符的函数用于重载该运算符)。因此,如果使用下面这样一条语句,而value是前面列出的类型之一,则C++程序将其对应于有相应特征标的运算符函数:

cout << value;

例如,表达式 cout<<88 对应于下面的方法原型:

ostream & operator<<(int);

该原型表明,operator<<()函数接受一个 int 参数,这与上述语句中的 88 匹配。该原型还表明,函数返回一个指向 ostream 对象的引用,这使得可以将输出连接起来,如下所示:

cout << "I'm feeling sedimental over " << boundary << "\n";

如果您是 C 语言程序员,深受%类型说明符过多、说明符类型与值不匹配时将发生问题等痛苦,则使用 cout 非常简单(当然,由于有 cin,C++ 输入也非常简单)。

  1. 输出和指针
    ostream 类还为下面的指针类型定义了插入运算符函数:

    • const signed char *;
    • const unsigned char *;
    • const char *;
    • void *。

    不要忘了,C++用指向字符串存储位置的指针来表示字符串。指针的形式可以是char数组名、显式的char指针或用引号括起的字符串。因此,下面所有的cout语句都显示字符串:

    char name[20] = "Dully Diddlemore";
    char * pn = "Violet D'Amore";
    cout << "Hello!";
    cout << name;
    cout << pn;
    

    方法使用字符串中的终止空字符来确定何时停止显示字符。

    对于其他类型的指针,C++将其对应于 void *,并打印地址的数值表示。如果要获得字符串的地址,则必须将其强制转换为其他类型,如下面的代码片段所示:

        int eggs = 12;
        char * amount = "dozen";
        cout << &egg;				// prints address of eggs variable
        cout << amount;				// prints the string "dozen"
        cout << (void *) amount;	// prints the address of the "dozen" string
    
  2. 拼接输出

    插入运算符的所有化身的返回类型都是 ostream&。也就是说,原型的格式如下:

    ostream & operator<<(type);
    

    (其中,type 是要显示的数据的类型)返回类型 ostream & 意味着使用该运算符将返回一个指向 ostream 对象的引用。哪个对象呢?函数定义指出,引用将指向用于调用该运算符的对象。换句话说,运算符函数的返回值为调用运算符的对象。例如,cout << “potluck” 返回的是 cout 对象。这种特性使得能够通过插入来连接输出。例如,请看下面的语句:

    cout << "We have " << count << " unhatched chickens.\n";
    

    表达式 cout << "We have ”将显示字符串,并返回 cout 对象。至此,上述语句将变为:

    cout << count << " unhatched chickens.\n";
    

    表达式 cout << count 将显示 count 变量的值,并返回 cout。然后 cout 将处理语句中的最后一个参数。这种设计技术确实是一项很好的特性,这也是前几章中重载 << 运算符的示例模仿了这种技术的原因所在。

其他 ostream 方法

除了各种 operator<<()函数外,ostream类还提供了 put() 方法和 write() 方法,前者用于显示字符,后者用于显示字符串。

最初,put() 方法的原型如下:

ostream & put(char);

当前标准与此相同,但被模板化,以适用于 wchar_t。可以用类方法表示法来调用它:

cout.put('W');		// display the W character

其中,cout 是调用方法的对象,put() 是类成员函数。和 << 运算符函数一样,该函数也返回一个指向调用对象的引用,因此可以用它将拼接输出:

cout.put('I').put('t');		// displaying It with two put() calls

函数调用 cout.put(‘I’) 返回cout,cout然后被用作 put(‘t’) 调用的调用对象。

在原型合适的情况下,可以将数值型参数(如int)用于 put(),然函数原型自动将参数转换为正确 char 值。例如,可以这样做:

cout.put(65);		// display the A character
cout.put(66.3);		// display the B character

第一条语句将 int 值 65 转换为一个 char 值,然后显示 ASCII 码为 65 的字符。同样,第二条语句将 double 值 66.3 转换为 char 值 66,并显示对应的字符。

这种行为在 C++2.0 之前可派上用场。在这些版本中,C++语言用 int 值表示字符常量。因此,下面的语句将‘W’解释为一个 int 值,因此将其作为整数 87(即该字符的 ASCII值)显示出来:

cout << 'W';

然而,下面这条语句能够正常工作:

cout.put('W');

因为当前的 C++ 将 char 常量表示为 char 类型,因此现在可以使用上述任何一种方法。
一些老式编译器错误地为 char、unsigned char 和 signed char 3种参数类型重载了 put()。这使得将 int 参数用于 put() 时具有二义性,因为 int 可被转换这3种类型中的任何一种。

write() 方法显示整个字符串,其模板原型如下:

basic_ostream<charT, traits>& write(const char_type * s, streamsize n);

write() 的第一个参数提供了要显示的字符串的地址,第二个参数指出要显示多少个字符。使用 cout 调用 write() 时,将调用 char 具体化,因此返回类型为 ostream&。下面的程序演示了 write() 方法是如何工作的。

// write.cpp -- using cout.write()
#include<iostream>
#include<cstring>   // or else string.h


int main(){
    using std::cout;
    using std::endl;
    const char * state1 = "Florida";
    const char * state2 = "Kansas";
    const char * state3 = "Euphoria";
    int len = std::strlen(state2);
    cout << "Increasing loop index:\n";
    int i;
    for(i=1; i <= len; i++){
        cout.write(state2, i);
        cout << endl;
    }
    // concatenate output
    cout << "Decreasing loop index:\n";
    for(i = len; i > 0; i--) {
        cout.write(state2, i) << endl;
    }

    // exceed string length
    cout << "Exceeding string length:\n";
    cout.write(state2, len+5) << endl;

    return 0;
}

有些编译器可能指出该程序定义了数组 state1 和 state3 但没有使用它们。这不是什么问题,因为这两个数组只是用于提供数组 state2 前面和后面的数据,以便您知道程序错误地存取 state2 时发生的情况。下面是该程序的输出:

Increasing loop index:
K
Ka
Kan
Kans
Kansa
Kansas
Decreasing loop index:
Kansas
Kansa
Kans
Kan
Ka
K
Exceeding string length:
KansasEuph

注意,cout.write() 调用返回 cout 对象。这是因为 wrtie() 方法返回一个指向调用它的对象的引用,这里调用它的对象是 cout。

这使得可以将输出拼接起来,因为 cout.write() 将被其返回值 cout 替换:

cout.write(state2, i) << endl;

还需要注意的是,wrtie() 方法并不会在遇到空字符时自动停止打印字符,而只是打印指定数目的字符,即使超出了字符串的边界!在这个例子中,在字符串 “kansas” 的前后声明了另外两个字符串,以便相邻的内存包含数据。编译器在内存中存储数据的顺序以及调整内存的方式各不相同。例如,“Kansas”占用7个字节,而该编译器使用4个字节的倍数调整字符串,因此“Kansas”被填充成占用8个字节。由于编译器之间的差别,因此输出的最后一行可能不同。

write() 方法也可用于数值数据,您可以将数字的地址强制转换为 char*,然后传递给它:

long val = 560031841;
cout.write( (char *)&val,sizeof(long));

这不会将数字转换为相应的字符,而是传输内存中存储的位表示。例如,4字节的 long 值(如560031841)将作为 4 个独立的字节被传输。输出设备(如显示器)将把每个字节作为 ASCII码进行解释。因此在频幕上,560031841 将被显示为 4 个字符的组合,这很可能是乱码(也可能不是,请试试看)。然而,wrtie() 确实为将数值数据存储在文件中提供了一种简洁、准确的方式,这将在本章后面进行介绍。

刷新输出缓冲区

如果程序使用 cout 将字节发送给标准输出,情况将如何?由于 ostrream 类对 cout 对象处理的输出进行缓冲,所以输出不会立即发送到目标地址,而是被存储在缓冲区中,直到缓冲区填满。然后,程序将刷新(flush)缓冲区,把内容发送出去,并清空缓冲区,以存储新的数据。通常,缓冲区为512字节或其整数倍。当标准输出连接的是硬盘上的文件时,缓冲可以节省大量的时间。毕竟,不希望程序为发送512个字节,而存取磁盘512次。将512个字节收集到缓冲区中,然后一次性将它们写入硬盘的效率要高得多。

然而,对于屏幕输出来说,首先缓冲区的重要性要低得多。如果必须重述消息“Press any key to continue” 以便使用 512 个字节来填充缓冲区,实在是太不方便了。所幸的是,在屏幕输出时,程序不必等到缓冲区被填满。例如,将换行符发送到缓冲区后,将刷新缓冲区。另外,正如前面指出的,多数 C++ 实现都会在输入即将发生时刷新缓冲区。也就是说,假设有下面的代码:

cout << "Enter a number: ";
float num;
cin >> num;

程序期待输入这一事实,将导致它立刻显示 cout 消息(即刷新“Enter a number: " 消息),即使输出字符串中没有换行符。如果没有这种特性,程序将等待输入,而无法通过 cout 消息来提示用户。

如果实现不能在所希望时刷新输出,可以使用两个控制符中的一个来强行进行刷新。控制符 flush 刷新缓冲区,而控制符 endl 刷新缓冲区,并插入一个换行符。这两个控制符的使用方式与变量名相同:

cout << "Hello, good-looking! " << flush;
cout << "Wait just a moment, please." << endl;

事实上,控制符也是函数。例如,可以直接调用 flush() 来刷新 cout 缓冲区:

flush(cout);

然而,ostream 类对 << 插入运算符进行了重载,使得下述表达式将被替换为函数调用 flush(cout):

cout << flush;

因此,可以用更为方便的插入表示法来成功地进行刷新。

用 cout 进行格式化

ostream 插入运算符将值转换为文本格式。在默认情况下,格式化值的方式如下。

  • 对于 char 值,如果它代表的是可打印字符,则将被作为一个字符显示在宽度为一个字符的字段中。
  • 对于数值整型,将以十进制方式显示在一个刚好容纳该数字及负号(如果有的话)的字段中。
  • 字符串被显示在宽度等于该字符串长度的字段中。

浮点数的默认行为有变化。下面详细说明了老式实现和新实现之间的区别。

  • 新式:浮点类型被显示为6位,末尾的0不显示(注意,显示的数字位数与数字被存储时精度没有任何关系)。数字以顶点表示法显示还是以科学计数法表示(参见第3章),取决于它的值。具体来说,当指数大于等于6或小于等于-5时,将使用科学计数法表示。另外,字段宽度恰好容纳数字和负号(如果有的话)。默认的行为对应于带 %g 说明符的标准 C 库函数 fprintf()。
  • 老式:浮点类型显示为带 6 位小数,末尾的 0 不显示(注意,显示的数字位数与数字被存储时的精度没有任何关系)。数字以定点表示法显示还是以科学计数法表示(参见第3章),取决于它的值。另外,字段宽度恰好容纳数字和负号(如果有的话)。

因为每个值的显示宽度都等于它的长度,因此必须显式地在值之间提供空格;否则,相邻的值将不会被分开。

下面的程序演示了默认的输出情况,它在每个值后面都显示一个冒号(:),以便可以知道每种情况下的字段宽度。该程序使用表达式 1.0/9.0 来生成一个无穷小数,以便能够知道打印了多少位。

注意,并非所有的编译器都能生成符合当前 C++ 标准格式的输出。另外,当前标准允许区域性变化。例如,欧洲实现可能遵循欧洲人的风格:使用逗号而不是句点来表示小数点。也就是说,2.54 将被写成 2, 54。区域库(头文件 locale)提供了用特定的风格影响(imbuing)输入或输出流的机制,所以同一个编译器能够提供多个区域选项。本章使用美国格式。

// defaults.cpp -- cout default formats
#include <iostream>


int main(){

    using std::cout;
    cout << "12345678901234567890\n";
    char ch = 'K';
    int t = 273;
    cout << ch << ":\n";
    cout << t << ":\n";
    cout << -t << ":\n";

    double f1 = 1.200;
    cout << f1 << ":\n";
    cout << (f1 + 1.0 / 9.0) << ":\n";

    double f2 = 1.67E2;
    cout << f2 << ":\n";
    f2 += 1.0 / 9.0;
    cout << f2 << ":\n";
    cout << (f2 * 1.0e4) << ":\n";

    double f3 = 2.3e-4;
    cout << f3 << ":\n";
    cout << f3 / 10 << ":\n";

    return 0;
}

该程序的输出如下:

12345678901234567890
K:
273:
-273:
1.2:
1.31111:
167:
167.111:
1.67111e+06:
0.00023:
2.3e-05:

每个值都填充自己的字段。注意,1.200 末尾的 0 没有显示出来,但末尾不带 0 的浮点值后面将有 6 个空格。另外,该实现将指数显示为 3 位,而其他实现可能为两位。

  1. 修改显示时使用的计数系统
    ostream 类是从 ios 类派生而来的,而后者是从 ios_base 类派生而来的。ios_base 类存储了描述格式状态的信息。例如,一个类成员中某些位决定了使用的计数系统,而另一个成员则决定了字段宽度。通过使用控制符(manipulator),可以控制显示整数时使用的计数系统。通过使用 ios_base 的成员函数,可以控制字段宽度和小数位数。由于 ios_base 类是 ostream 的间接基类,因此可以将其方法用于 ostream 对象(或子代),如 cout。

    注意:ios_base 类中的成员和方法以前位于 ios 类中。现在,ios_base 是 ios 的基类。在新系统中,ios 是包含 char 和 wchar_t 具体化的模板,而 ios_base 包含了非模板特性。

    来看如何设置显示整数时使用的计数系统。要控制整数以十进制、十六进制还是八进制显示,可以使用 dec、hex 和 oct 控制符。例如,下面的函数调用将 cout 对象的计数系统格式状态设置为十六进制:

    hex(cout);
    

    完成上述设置后,程序将以十六进制形式打印整数值,直到将格式状态设置位其他选项为止。注意,控制符不是成员函数,因此不必通过对象来调用。

    虽然控制符实际上是函数,但它们通常的使用方式为:

    cout << hex;
    

    ostream 类重载了 << 运算符,这使得上述用法与函数调用 hex(cout) 等价。控制符位于名称空间 std 中。下面的程序演示了这些控制符的用法,它以3种不同的计数系统显示了一个整数的值及其平方。注意,可以单独使用控制符,也可将其作为一系列插入的组成部分。

    // manip.cpp -- using format manipulators
    #include<iostream>
    int main(){
        using namespace std;
        cout << "Enter an integer: ";
        int n;
        cin >> n;
    
        cout << "n      n*n\n";
        cout << n << "      " << n * n << " (decimal)\n";
    
        //set to hex mode
        cout << hex;
        cout << n << "      ";
        cout << n * n << " (hexadecimal)\n";
    
        //set to octal mode
        cout << oct << n << "       " << n*n << " (octal)\n";
    
        // alternative way to call a manipulator
        dec(cout);
        cout << n << "      " << n * n << " (decimal)\n";
    
        return 0;
    }
    

    该程序的运行情况:

    Enter an integer: 8
    n      n*n
    8      64 (decimal)
    8      40 (hexadecimal)
    10       100 (octal)
    8      64 (decimal)
    
  2. 调整字段宽度
    您可能已经注意到,在上面的程序的输出中各列并没有对齐,这是因为数字的字段宽度不相同。可以使用 width 成员函数将长度不同的数字放到宽度相同的字段中,该方法的原型为:

    int width();
    int width(int i);
    

    第一种格式返回字段宽度的当前设置;第二种格式将字段宽度设置为 i 个空格,并返回以前的字段宽度值。这使得能够保存以前的值,以便以后恢复宽度值时使用。

    width() 方法只影响将显示的下一个项目,然后字段宽度将恢复为默认值。例如,请看下面的语句:

    cout << '#';
    cout.width(12);
    cout << 12 << "#" << 24 << "#\n";
    

    由于 width() 是成员函数,因此必须使用对象(这里为 cout)来调用它。输出语句生成的输出如下:

    #          12#24#
    

    12 被放到宽度为 12 个字符的字段的最右边,这被称为右对齐。然后,字段宽度恢复为默认值,并将两个#符号以及24放在宽度与它们的长度相等的字段中。

    警告:width() 方法只影响接下来显示的一个项目,然后字段宽度将恢复为默认值。

    C++ 永远不会截短数据,因此如果试图在宽度为2的字段中打印一个7位值,C++将增宽字段,以容纳该数据(在有些语言中,如果数据长度与字段宽度不匹配,将用星号填充字段。C/C++ 的原则是:显示所有的数据比保持列的整洁更重要。C++ 视内容重于形式)。下面的程序演示了 width() 成员函数是如何工作的。

    // width.cpp -- using the width method
    #include<iostream>
    
    int main(){
        using std::cout;
        int w = cout.width(30);
        cout << "default field width = " << w << ":\n";
        cout.width(5);
        cout << "N" << ':';
        cout.width(8);
        cout << "N * N" << ":\n";
    
        for (long i = 1; i <= 100; i *= 10) {
            cout.width(5);
            cout << i << ':';
            cout.width(8);
            cout << i * i << ":\n";
        }
    
        return 0;
    }
    

    程序的输出如下:

            default field width = 0:
        N:   N * N:
        1:       1:
       10:     100:
      100:   10000:
    

    在上述输出中,值在字段中右对齐。输出中包含空格,也就是说,cout 通过加入空格来填满整个字段。右对齐时,空格被插入到值的左侧。用来填充的字符叫做填充字符(fill character)。右对齐是默认的。

    注意,在上面的程序中,第一条 cout 语句显示字符串时,字段宽度被设置为30,但在显示 w 的值时,字段宽度不是 30。这是由于 width() 方法只影响接下来被显示的一个项目。另外,w 的值为0。这是由于 cout.width(30) 返回的是以前的字段宽度,而不是刚设置的值。w 为 0 表明,默认的字段宽度为0。由于 C++ 总会增长字段,以容纳数据,因此这种值适用于所有的数据。最后,程序使用 width() 来对齐列标题和数据,方法是将第1列宽度设置为5个字符,将第2列的宽度设置为8个字符。

  3. 填充字符
    在默认情况下,cout 用空格填充字段中未被使用的部分,可以用 fill() 成员函数来改变填充字符。例如,下面的函数调用将填充字符改为星号:

    cout.fill('*');
    

    这对于检查打印结果,防止接收方添加数字很有用。下面的程序演示了该成员函数的用法。

    // fill.cpp -- changing fill character for fields
    #include<iostream>
    
    int main() {
        using std::cout;
        cout.fill('*');
        const char * staff[2] = { "Waldo Whipsnade", "Willmarie Wooper" };
        long bonus[2] = {900, 1350};
    
        for( int i = 0; i < 2; i++){
            cout << staff[i] << ": $";
            cout.width(7);
            cout << bonus[i] << "\n";
        }
    
        return 0;
    }
    

    下面是程序的输出:

    Waldo Whipsnade: $****900
    Willmarie Wooper: $***1350
    

    注意,与字段宽度不同的是,新的填充字符将一直有效,直到更改它为止。

  4. 设置浮点数的显示精度
    浮点数精度的含义取决于输出模式。在默认模式下,它指的是显示的总位数。在定点模式和科学模式下(稍后将讨论),精度指的是小数点后面的位数。已经知道,C++ 的默认精度为6位(但末尾的0将不显示)。precision() 成员函数使得能够选择其他值。例如,下面语句将 cout 的精度设置为2:

    cout.precision(2);
    

    和 width() 的情况不同,但与 fill() 类似,新的精度设置将一直有效,知道被重新设置。下面的程序准确地说明了这一点。

    // precise.cpp -- setting the precision
    #include<iostream>
    
    int main(){
        using std::cout;
        float price1 = 20.40;
        float price2 = 1.9 + 8.0 / 9.0;
    
        cout << "\"Furry Friends\" is $" << price1 << "!\n";
        cout << "\"Fiery Friends\" is $" << price2 << "!\n";
    
        cout.precision(2);
        cout << "\"Furry Friends\" is $" << price1 << "!\n";
        cout << "\"Fiery Fiends\" is $" << price2 << "!\n";
    
        return 0;
    }
    

    下面是程序的输出:

    "Furry Friends" is $20.4!
    "Fiery Friends" is $2.78889!
    "Furry Friends" is $20!
    "Fiery Fiends" is $2.8!
    

    注意:第3行没有打印小数点及其后面的内容。另外,第4行显式的总位数为2位。

  5. 打印末尾的 0 和小数点
    对于有些输出(如价格或栏中的数字),保留末尾的0将更为美观。例如,对于上面的程序$20.40 将比 $20.4 更美观。iostream 系列类没有提供专门用于完成这项任务的函数,但 ios_base 类提供了一个 setf() 函数(用于 set 标记),能够控制多种格式化特性。这个类还定义了多个常量,可用作该函数的参数。例如,下面的函数调用使 cout 显示末尾小数点:

    cout.setf(ios_base::showpoint);
    

    使用默认的浮点格式时,上述语句还将导致末尾的0被显示出来。也就是说,如果使用默认精度(6位)时,cout 不会将 2.00 显示为 2, 而是将它显示为 2.000000。下面的程序添加了这条语句。

    您可能对表示法 ios_base::showpoint 有疑问,showpoint 是 ios_base 类声明中定义的类级静态常量。类级意味着如果在成员函数定义的外面使用它,则必须在常量名前面加上作用域运算符(::)。因此 ios_base::showpoint 指的是在 ios_base 类中定义的一个常量。

    // showpt.cpp -- setting the precision, showing trailing point
    #include<iostream>
    
    int main(){
        using std::cout;
        using std::ios_base;
    
        float price1 = 20.40;
        float price2 = 1.9 + 8.0 / 9.0;
    
        cout.setf(ios_base::showpoint);
        cout << "\"Furry Friends\" is $" << price1 << "!\n";
        cout << "\"Fiery Friends\" is $" << price2 << "!\n";
    
        cout.precision(2);
        cout << "\"Furry Friends\" is $" << price1 << "!\n";
        cout << "\"Fiery Friends\" is $" << price2 << "!\n";
    
        return 0;
    }
    

    下面是使用当前 C++ 格式时,程序的输出:

    "Furry Friends" is $20.4000!
    "Fiery Friends" is $2.78889!
    "Furry Friends" is $20.!
    "Fiery Friends" is $2.8!
    

    在上述输出中,第一行显示了末尾的0,第三行显示了小数点,但没有显示末尾的0,这是因为精度被设置为2,而小数点前面已经包含两位。

  6. 再谈 setf()
    setf() 方法控制了小数点被显示时其他几个格式选项,因此来仔细研究一下它。ios_base 类有一个受保护的数据成员,其中的各位(这里叫作标记)分别控制着格式化的各个方面,例如计数系统、是否显示末尾的0等。打开一个标记称为设置标记(或位),并意味着相应的位被设置为1.位标记是编程开关,相当于设置 DIP 开关以配置计算机硬件。例如,hex、dec 和 oct 控制符调整控制计数系统的 3 个标记位。setf() 函数提供了另一种调整标记位的途径。

    setf() 函数有两个原型。第一个为:

    fmtflags setf(fmtflags);
    

    其中,fmtflags 是 bitmask类型的 typedef 名,用于存储格式标记。该名称是在 ios_base 类中定义的。这个版本的 setf() 用来设置单个位控制的格式信息。参数是一个 fmtflags 值,指出要设置哪一位。返回值是类型为 fmtlfags 的数字,指出所有标记以前的设置。如果打算以后恢复原始设置,则可以保存这个值。应给 setf() 传递什么呢?如果要第11位设置为1,则可以传递一个第11位为1的数字。返回值的第11位将被设置为1。对位进行跟踪好像单调乏味(实际上也是这样)。然而,您不必做这这项工作,ios_base 类定义了代表位值的常量,下表列出了其中的一些定义。

    常量含义
    ios_base::boolalpha输入和输出bool值,可以为 true 或 false
    ios_base::showbase对于输出,使用C++基数前缀(0,0x)
    ios_base::showpoint显示末尾的小数点
    ios_base::uppercase对于16 进制输出,使用大写字母,E 表示法
    ios_base::showpos在正数前面加上 +

    注意:bitmask 类型是一种用来存储各个位值的类型。它可以是整型、枚举,也可以是 STL bitset 容器。这里的主要思想是,每一位都是可以单独访问的,都有自己的含义。iostream 软件包使用 bitmask 来存储状态信息。

    由于这些格式常量都是在 ios_base 类中定义的,因此使用它们时,必须加上作用域解析运算符。也就是说,应使用 ios_base::uppercase,而不是 uppercase。如果不想使用 using 编译指令或 using 声明,可以使用作用域运算符来指出这些名称位于名称空间 std 中。修改将一直有效,直到被覆盖为止。下面的程序演示了如何使用其中一些常量。

    // setf.cpp -- using setf() to control formatting
    #include <ios>
    #include<iostream>
    
    
    int main(){
        using std::cout;
        using std::endl;
        using std::ios_base;
    
        int temperature = 63;
        cout << "Today's water temperature: ";
        cout.setf(ios_base::showpos);       // show plus sign
        cout << temperature << endl;
    
        cout << "For our programming friends, that's\n";
        cout << std::hex << temperature << endl;    // use hex
        cout.setf(ios_base::uppercase);     // use uppercase in hex
        cout.setf(ios_base::showbase);      // use 0X prefix for hex
        cout << "or\n";
        cout << temperature << endl;
        cout << "How " << true << "!  oops -- How ";
        cout.setf(ios_base::boolalpha);
        cout << true << "!\n";
    
        return 0;
    }
    

    下面是该程序的输出:

    Today's water temperature: +63
    For our programming friends, that's
    3f
    or
    0X3F
    How 0X1!  oops -- How true!
    

    注意,仅当基数为10时才使用加号。C++将十六进制和八进制都视为无符号的,因此对它们,无需使用符号(然而,有些 C++ 实现可能仍然会显示加号)。

    第二个 setf() 原型接受两个参数,并返回以前的设置:

    fmtflags setf(fmtflags, fmtflags);
    

    函数的这种重载格式用于设置由多位控制的格式选项。第一参数和以前一样,也是一个包含了所需设置的 fmtflags 值。第二参数指出要清除第一个参数中的哪些位。例如,将第3位设置为1表示以10为基数,将第4位设置为1表示以8为基数,将第5位设置为1表示以16为基数。假设输出是以10为基数的,而要将它设置为以16为基数,则不仅需要将第5位设置为1,还需要将第3位设置为0——这叫作清除位(clearing the bit)。聪明的十六进制控制符可自动完成这两项任务。使用函数 setf() 时,要做的工作多些,因为要用第二参数指出要清除哪些位,用第一参数指出要设置哪些位。这并不像听上去那么复杂,因为 ios_base 位此定义了常量。具体地说,要修改基数,可以将常量 ios_base::basefield 用作第二参数,将 ios_base::hex 用作第一参数。也就是说,下面的函数调用与使用十六进制控制符的作用相同:

    cout.setf(ios_base::hex, ios_base::basefield);
    

    setf(long, long) 的参数

    第二个参数第一个参数含义
    ios_base::basefieldios_base::dec使用基数10
    ios_base::basefieldios_base::oct使用基数 8
    ios_base::basefieldios_base::hex使用基数 16
    ios_base::floatfieldios_base::fixed使用定点计数法
    ios_base::floatfieldios_base::scientific使用科学计数法
    ios_base::adjustfieldios_base::left使用左对齐
    ios_base::adjustfieldios_base::right使用右对齐
    ios_base::adjustfieldios_base::internal符号或基数前缀左对齐,值右对齐

    ios_base 类定义了可按这种方式处理的 3 组格式标记。每组标记都由一个可用作第二参数的常量和两三个可用作第一参数的常量组成。第二参数清除一批相关的位,然后第一参数将其中一位设置位1。上表列出了用作 setf() 的第二参数的常量的名称、可用作第一参数的相关常量以及它们的含义。例如,要选择左对齐,可将 ios_bass::adjustfield 用作第二参数,将 ios_base::left 作为第一参数。左对齐意味着将值放在字段的左端,右对齐则表示将值放在字段的右端。内部对齐表示将符号或基数前缀放在字段左侧,余下的数字放在字段的右侧(遗憾的是,C++没有提供自对齐模式)。

    定点表示法意味着使用格式 123.4 来表示浮点值,而不管数字的长度如何,科学表示法则意味着使用格式 1.23e04,而不考虑数字的长度。如果您熟悉C语言中 printf() 的说明符,则可能知道,默认的 C++ 模式对应于 %g 说明符,定点表示法对应于 %f 说明符,而科学表示法对应于 %e 说明符。

    在 C++ 标准中,定点表示法和科学表示法都有下面的两个特征:

    • 精度指的是小数位数,而不是总位数;
    • 显示末尾的0.

    setf() 函数是 ios_base 类的一个成员函数。由于这个类是 ostream 类的基类,因此可以使用 cout 对象来调用该函数。例如,要左对齐,可使用下面的调用:

    ios_base::fmtflags old = cout.setf(ios::left, ios::adjustfield);
    

    要恢复以前的设置,可以这样做:

    cout.setf(old,ios::adjustfield);
    

    下面的程序是一个使用两个参数的 setf() 的示例。
    注意:下面的程序使用了一个数学函数,有些 C++ 系统不自动搜索数学库。例如,有些 UNIX 系统要求这样做:

    $ CC setf2.C -lm
    

    -lm 选项命令链接程序搜索数学库。同样,有些使用 g++ 的 Linux 系统也要求这样做。

    // setf2.cpp -- using setf() with 2 arguments to control formatting
    #include<iostream>
    #include<cmath>
    
    int main(){
        using namespace std;
        // use left justification, show the plus sign, show trailing
        // zeros, with a precision of 3
        cout.setf(ios_base::left, ios_base::adjustfield);
        cout.setf(ios_base::showpos);
        cout.setf(ios_base::showpoint);
        cout.precision(3);
        // use e-notation and save old format setting
        ios_base::fmtflags old = cout.setf(ios_base::scientific, ios_base::floatfield);
        cout << "Left Justification:\n";
        long n;
        for (n = 1; n <= 41; n += 10){
            cout.width(4);
            cout << n << "|";
            cout.width(12);
            cout << sqrt(double(n)) << "|\n";
        }
    
        // change to internal justification
        cout.setf(ios_base::internal, ios_base::adjustfield);
        // restore default floating-point display style
        cout.setf(old, ios_base::floatfield);
        cout << "Internal Justification:\n";
        for (n=1; n<=41; n+=10){
            cout.width(4);
            cout << n << "|";
            cout.width(12);
            cout << sqrt(double(n)) << "|\n";
        }
        
        // use right justification, fixed notation
        cout.setf(ios_base::right, ios_base::adjustfield);
        cout.setf(ios_base::fixed, ios_base::floatfield);
        cout << "Right Justification:\n";
        for(n=1; n<=41; n+=10) {
            cout.width(4);
            cout << n << "|";
            cout.width(12);
            cout << sqrt(double(n)) << "|\n";
        }
    
        return 0;
    }
    

    下面是程序的输出:

    Left Justification:
    +1  |+1.000e+00  |
    +11 |+3.317e+00  |
    +21 |+4.583e+00  |
    +31 |+5.568e+00  |
    +41 |+6.403e+00  |
    Internal Justification:
    +  1|+       1.00|
    + 11|+       3.32|
    + 21|+       4.58|
    + 31|+       5.57|
    + 41|+       6.40|
    Right Justification:
      +1|      +1.000|
     +11|      +3.317|
     +21|      +4.583|
     +31|      +5.568|
     +41|      +6.403|
    

    注意到精度 3 让默认的浮点显示(在这个程序中用于内部对齐)总共显示 3 位,而定点模式和科学模式只显示3位小数(e表示法的指数位数取决于实现)。

    调用 setf() 的效果可以通过 unsetf() 消除,后者的原型如下:

    void unsetf(fmtflags mask);
    

    其中,mask 是位模式。mask 中所有的位都设置为1,将使得对应的位被复位。也就是说,setf() 将位设置为1,unsetf() 将位恢复为0。例如:

    cout.setf(ios_base::showpoint);		// show trailing decimal point
    cout.unsetf(ios_base::showpoint);		// don't show trailing decimal point
    cout.setf(ios_base::boolalpha);		// display true, false
    cout.unsetf(ios_base::boolalpha);		// display 1, 0
    

    您可能注意到了,没有专门指示浮点数默认显示模式的标记。系统的工作原理如下;仅当只有定点位被设置时使用定点表示法;仅当只有科学位被设置时使用科学表示法;对于其他组合,如没有位被设置或两位都被设置时,将使用默认模式。因此,启用默认模式的方法之一如下:

    cout.setf(0, ios_base::floatfield);		// go to default mode
    

    第二个参数关闭这两位,而第一个参数不设置任何位。一种实现同样目标的简捷方式是,使用参数 ios::floatfield 来调用函数 unsetf():

    cout.unsetf(ios_base::floatfield);		// go to default mode
    

    如果已知 cout 处于定点状态,则可以使用参数 ios_base::fixed 调用函数 unsetf() 来切换到默认模式;然而,无论 cout 的当前状态如何,使用参数 ios_base::floatfield 调用函数 unsetf() 都将切换到默认模式,因此这是一种更好的选择。

  7. 标准控制符

    使用 setf() 不是进行格式化、对用户最为友好的方法,C++ 提供了多个控制符,能够调用 setf(),并自动提供正确的参数。前面已经介绍过 dec、hex 和 oct,这些控制符(多数都不适用于老式 C++ 实现)的工作方式都与 hex 相似。例如,下面的语句打开左对齐和定点选项:

    cout << left << fixed;
    

    下表列出了这些控制符以及其他一些控制符:
    一些标准控制符

    控制符调用
    boolalphasetf(ios_base::boolalpha)
    noboolalphaunsetf(ios_base::boolalpha)
    showbasesetf(ios_base::showbase)
    noshowbaseunsetf(ios_base::showbase)
    showpointsetf(ios_base::showpoint)
    noshowpointunsetf(ios_base::showpoint)
    showpossetf(ios_base::showpos)
    noshowposunsetf(ios_base::showpos)
    uppercasesetf(ios_base::uppercase)
    nouppercaseunsetf(ios_base::uppercase)
    internalsetf(ios_base::internal, ios_base::adjustfield
    leftsetf(ios_base::left, ios_base::adjustfield
    rightsetf(ios_base::right, ios_base::adjustfield
    decsetf(ios_base::dec, ios_base::basefield
    hexsetf(ios_base::hex, ios_base::basefield
    octsetf(ios_base::oct, ios_base::basefield
    fixedsetf(ios_base::fixed, ios_base::floatfield
    scientificsetf(ios_base::scientific::fixed, ios_base::floatfield

    提示:如果系统支持这些控制符,请使用它们;否则,仍然可以使用 setf()。

  8. 头文件 iomanip
    使用 iostream 工具来设置一些格式值(如字段宽度)不太方便。为简化工作,C++ 在头文件 iomanip 中提供了其他一些控制符,它们能够提供前面讨论过的服务,但表示起来更方便。3 个最常用的控制符分别是 setprecision()、setfill() 和 setw(),它们分别用来设置精度、填充字符和字段宽度。与前面讨论的控制符不同的是,这3个控制符带参数。setprecision() 控制符接受一个指定精度的整数参数;setfill() 控制符接受一个指定填充字符的 char 参数;setw() 控制符接受一个指定字段宽度的整数参数。由于它们都是控制符,因此可以用 cout 语句连接起来。这样 setw() 控制符在显示多列值时尤其方便。下面的程序演示了这一点,它对于每一行输出,都多次修改了字段宽度和填充字符,同时使用了一些较新的标准控制符。

    注意:有些 C++ 系统不自动搜索数学库。前面说过,有些 UNIX 系统要求使用如下命令选项来访问数学库:

    $ CC iomanip.C -lm
    
    // iomanip.cpp -- using manipulators from iomanip
    // some systems require explicitly linking the math library
    
    #include <iostream>
    #include <iomanip>
    #include <cmath>
    
    int main() {
        using namespace std;
        // use new standard manipulators
        cout << fixed << right;
    
        // use iomanip munipulators
        cout << setw(6) << "N" << setw(14) << "square root"
             << setw(15) << "fourth root\n";
        double root;
        for (int n = 10; n <= 100; n += 10) {
            root = sqrt(double(n));
            cout << setw(6) << setfill('.') << n << setfill(' ')
                << setw(12) << setprecision(3) << root
                << setw(14) << setprecision(4) << sqrt(root)
                << endl;
        }
    
        return 0;
    }
    

    下面时该程序的输出:

         N   square root   fourth root
    ....10       3.162        1.7783
    ....20       4.472        2.1147
    ....30       5.477        2.3403
    ....40       6.325        2.5149
    ....50       7.071        2.6591
    ....60       7.746        2.7832
    ....70       8.367        2.8925
    ....80       8.944        2.9907
    ....90       9.487        3.0801
    ...100      10.000        3.1623
    

    现在可以生成几乎完全对齐的列了。使用 fixed 控制符导致显示末尾的0.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值