博客首页 注册 建议与交流 排行榜 加入友情链接
推荐 投诉 搜索: 帮助

执着的小雨

  xiaoyuonline.cublog.cn

关于作者
姓名:小雨
职业:IT
年龄:25
位置:北京(为奥运加油!)
个性介绍:做最好的自己!
|| << >> ||
我的分类


C++的I/O的基础
(一)C++ I/O的基础
除了完全支持C的I/O系统外C++还定义了自己的面向对象的I/O系统。和C的I/
O系统一样,C++的I/O系统也完全是集成化的,即c++的I/O系统的那些有差别的地
方,如控制台I/O和磁盘I/O,实际上只是相同机制的不同方面。本章讨论c++面向对象I/
O系统的基础。虽然本章的例子使用的是“控制台”I/O,但这些信息同样适用于其它设备,
如磁盘文件(在第十八章“C++文件I/O”中讨论)。
    C的I/O系统是非常丰富、灵活和强大的。既然如此,为什么c++又定义另外一个系
统?答案是C的I/O系统一点也不了解对象。所以,为了使c++完全支持面向对象的程序
设计,有必要建立一个能对用户定义的对象进行操作的面向对象的I/O系统。除了支持对象
以外,使用C++的I/O系统甚至对不广泛(或任何)使用用户定义的对象的程序也有一些间
接的好处。在本章的稍后读者可以看到一些例子。
    本章将介绍怎样格式化数据,怎样重载C++的<<和>> I/O运算符,使之用于我
们创建的类,还要弄清楚如何创建所谓操纵符的特殊I/O函数,以提高程序效率。
17.1 C++的流
    和C的I/O系统一样C++的I/O系统也是通过流操作的。第九章“ANSI C标准文件
I/O”详细讨论了流,这里不再赘述。概括地讲,流就是既生产信息又消费信息的逻辑设备。
流通过c++系统和物理设备关联。尽管流所关联的物理设备客观上存在着差异,但所有的
流都以同样的方式起作用。正因为如此,相同的c++I/O函数实际上可以操作任何类型的
物理设备。例如,可以用写文件的同一函数写打印机或屏幕。这样做的好处是用户只需要了
解一个接口。
17.2 基本的流类
    C++在首标文件iostream.h里提供了对I/O的支持。这个文件定义了支持I/O操作的
类层次。最底层的类叫做streambuf。该类提供了基本的输入和输出操作。如果用户不派生
自己的I/O的类,就不能直接使用 streambuf。类ios位于下一层,它提供了格式化的I/O。从
ios派生出类istream、ostream和iostream,这些类分别用来创建输入流、输出流和输入/输出
流,在下一章读者将看到,其它从ios派生的类支持磁盘文件和RAM内格式化。
    ios类包含控制或监视流的基本操作的许多成员函数和变量。本章和下一章提供了对其
成员的许多参考。但要记住,如果以通常方式使用 c++I/O系统,任何流都可以使用ios的
成员。
282页
17.2.1 C++的预定义流
      当一个C++程序开始运行时,它就自动打开四个内部流。它们是:
          流            含义                缺省设备
          cin           标准输入            键盘
          cout          标准输出            屏幕
          cerr          标准错误输出        屏幕
          clog          cerr的缓冲形式      屏幕
    流cin、cout和cerr对应于C的stdin、stdout和stderr。
    缺省时,标准流用来和控制台通信。但在支持I/O重定向的环境下(如DOS、UNIX、
Windows和OS/2),可以把标准流重定向于其它设备或文件。为简单起见,假设本章的例子
不发生重定向。
    注:建议的ANSI C++标准也定义了这四种附加流:win、wout、Werr和wlog。这些是标
准流的扩展字符版本。扩展字符类型为wchar_t,通常为16位。扩展字符用来容纳与某些自
然语言相关的大字符集。
17.3 格式化的I/O
    C++I/O系统支持格式化的I/O操作。例如,用户可以设置域宽,指定数字基数或确定
显示到十进制小数点后多少位。从根本上讲,用C的printf()和scanf()函数输入或输出的
任何格式都能用C++的<<和>>I/O运算符输入或输出。
    有两种相关的但概念不同的格式化数据的方法。第一种是,可以直接存取ios类的各种
成员。尤其是可以设置在ios类中定义的各种格式状态位或调用各种i/O成员函数。第二种
是,可以使用作为表达式一部分的所谓操纵符的特殊函数。
    我们将从使用ios成员函数和标志开始讨论格式化的I/O。
17.3.1 用ios成员格式化
    ios格式化标志的集合对应于每个流,它们控制着通过流格式化信息的一些方法。在ios
中,这些标志被命名和赋值,一般使用枚举,请看如下示例程序:
// ios formatting flags
enum {
    skipws = 0x0001,
    left = 0x0002,
    right = 0x0004,
    internal = 0x0008,
    dec = 0x0010,
    Oct=0x0020,
    hex=0x0040,
showbase = 0x0080,
showpoint = 0x0100,
uppercase = 0x0200,
· showpos = 0x0400,
283页
    scientific=0x0800,
    fixed=0x1000,
    unitbuf=0x2000,
};
    与流有关的格式标志编码为一些长整型形式。建议的ANSI C++标准将格式标志的类
型定义为fmtflags,但目前还没有编译程序定义这种类型(当然,不久会有)。实际上,fmtflags
就是通过typedef为长整型定义的一个名称。本书将在引用格式标志时用long类型,因为它
是现在主流C++编译程序所使用的类型。关于这一点,可参考相应的编译程序手册。
    当设置skipws标志时,在流上进行输入操作时丢弃空白字符(空格、制表符和换行符);
当清除skipws时,保留空白字符。
    当设置left标志时,输出左对齐。设置right时,输出右对齐。当设置internal标志时,对
于数值则通过在任何符号或基字符之间插入空格来填充域。(我们将学习如何简短地指定域
宽。)如果这些标志都没设置,按缺省的右对齐输出。
    同样,缺省时数值以十进制输出。数基可以改变,设置oct标志使输出以八进制显示,设
置hex标志使输出以十六进制显示。要使输出回到十进制,需设置dec标志。这些标志也决
定了输入整数值的基。
    设置showbase导致显示数值的基。例如,如果转换基是十六进制,则值1f就显示为
0x1F。
    缺省时,当显示科学记数时,"e"为小写。同样,显示十六进制值时,“x"为小写,当设置
uppercase时,这些字符都显示为大写。
    设置showpos导致在正数前显示加号。
    设置showpoint导致对所有浮点输出,不管需不需要,都显示十进制的小数点和尾0。
    设置scientific标志导致浮点数字值以科学记数法显示。当设置fixed时,浮点值以通常
记数法显示。缺省时,当设置fixed时,显示六位十进制位。当这些标志都没有设置时,编译程
序选择一种适当的方式。
    当设置了unitbuf时,在每个输出操作之后,C++的I/O系统被清理。
    注:上述讨论的格式化标志为所有C++编译程序所支持。但在编写本书的时候,ANSI
C++标准委员会仍在定义ios格式标志的准确特性。例如,增加了标志boolalpha,允许对新
定义的布尔数据类型进行I/O操作。这一标志现在还不为大多数编译程序定义。参阅自己的
编译程序手册以确定哪些格式标志可以使用。
17.3.2 设置格式标志
    用setf()函数设置格式标志。setf()是ios的一个成员,最常用的格式是:
        long setf(long flags);
该函数返回格式标志的前一次设置值并打开由flags指定的那些设置(不影响所有其它的标
志)。例如,用下面的语句打开showpos标志:
stream.setf(ios::showpos);
其中,stream为要影响的流。例如,下面的程序以十六进制显示100并指出其基。
284页
#include<iostream.h>
    main()
        cout.setf(ios::hex);
        cout.setf(ios::showbase);
        cout<<100;//displays 0x64
        return 0;}
      了解setf()是ios类的成员函数并影响该类所创建的类很重要。所以,任何对setf()的
调用都关系到一个特定的流。setf()被它自己调用是没有意义的。换句话说,在c++里没有
全局格式状态的概念。每个流都维护它自己的格式状态。
      虽然前面的程序在理论上没什么毛病,但有更有效的方法书写它。不对setf()进行多次
调用,而是简单地将期望设置的标志值或(OR)在一起。例如,下面的这个语句做了同样的事
    情:
    // You can OR together two or more flags,
    cout.setf(ios::showbase| ios::hex);
      记住:由于格式标志是在ios类里定义的,所以必须用ios fo作用域分辨符来存取它们
的值。例士。showbase本身是无法确认的,必须指定为ios::showbase。
17.3.3清除格式标志
      与setf()互补的是unsetf()。这个ios的成员函数用于清除一个或多个格式标志,其一
    般形式为:
          long unsetf(long flags);
由flags指定的标志被清除(不影响所有其它标志),并返回上次设置的标志值。
      下面的程序说明了unsetf()。程序首先设置了uppercase和scientific标志,然后以科学
记数法输出100.12。在这种情况下,科学记数法中使用的E为大写。接着,程序清除upper-
case标志并用小写"e"再次以科学记数法输出100.12。
    #include<iostream.h>
    main()
          {
    cout.setf(ios:: uppercase|ios:: scientific);
    cout <<100.12;// displays 1.0012E+02
    cout.unsetf(ios::uppercase);//clear uppercase
    cout<<"\n"<< 100;// displays 1.0012e+02
      return 0;}
17.3.4 setf()的重载形式
      下面是一个setf()重载形式的一般形式:

285页
   long setf(long flags1, long flags2);
      在这种形式里,只有flags2指定的标志起作用。它们先复位,然后根据flags1指定的标
志设置。注意,尽管flags1包含flags2没有指定的其它标志,但仅有被flags2指定的那些标
志才起作用,并返回上一次的标志设置。例如,下面的程序设置showpos和showpoint,然后
复位这两个标志并设置showpoint。
#include<iostream.h>
main()

cout.setf(ios::showpos |  ios::showpoint);
cout << 10.00 <<"\n";// displays +10.000000
cout.setf(ios::showpoint, ios::showpos |  ios:: showpoint);
cout << 10.00;// showpos reset,this displays 10.00000
    return 0;
}
    记住,只有在flags2中指定的标志才能受flags1中指定的标志的影响。例如,下面的程
序不工作:
//This program will not work.
#include<iostream.h>
main()

    cout.setf(ios::showbase |  ios:: hex);
    cout << 100;// displays 0x64
    cout.setf(ios::oct, ios:: hex) ;// error,  oct not set
    cout <<"\n"<< 100;// displays 100- not 0144
    return 0;
}
    其中,在调用setf()时,flags2指出只有hex有效。因为flags1的值是oct, hex被并闭
了,但oct又没有开启。下面的程序是正确的:
// This is now correct.
#include<iostream.h>
main()

    // you can OR together two or more flags
    cout.setf(ios:: showbase| ios:: hex);

286页
cout<<100;//displays 0x64
      // now oct can be afected
      cout.setf(ios::oct, ios::hex| ios::oct);
      cout<<"\n" <<100;// displays 0144
      return 0;
    }
      引用oct、dec和hex可以通过集中地引用ios::basefield达到。同样,left、right和inter-
nal可以用ios::adjustfield实现。
      最后,scientific和fix用ios::floatfield代替。例如,可以把前面的程序改写如下:
    #include<iostream. h>
    main()
    {
    // you can OR together two or more flags
    cout.setf(ios::showbase| ios::hex);
    cout<<100;// displays 0x64
    // use ios:: basefield
    cout.setf(ios:: oct, ios::basefield);
    cout <<"\n"<<100;// displays 0144
      return 0;
    }
      记住,在大多数情况下应使用unsetf()清除标志和单个参数的setf()设置标志。setf()
的setf(long flags1,long flags2)形式用于一些特殊的场合。例如,可能有一个指定所有格式
标志状态的模板,但只希望改变其中的一个或两个。在这种情况下,可以在flags1中指定模
板,而用flags2指定哪些标志将受影响。
17.3.5检查格式标志
      有时用户只需要知道当前格式的设置情况而不做任何改变。为了实现这个目标,ios也
包含了成员函数flags(),它只通过一个长整数返回当前设置的每个格式标志。其原型如下:
          long flags();
      下面的程序用flags()显示关于cout的格式标志的设置情况。要特别注意showflags()
函数。读者会发现它对写程序很有帮助。
    #include<iostream.h>
    void showflags();
    main()

287页
//show default condition of format flags
    showflags();
    cout.setf(ios:: right |  ios:: showpoint| ios:: fixed);
    showflags();
    return 0;

// This function displays the status of the format flags.
void showflags()

    long f,i;
    int j;
    char flgs[15][12] ={
      "skipws",
          "left",
          "right",
          "internal",
          "dec",
          "oct",
          "hex",
          "showbase",
          "showpoint",
          "uppercase",
          "showpos",
          "scientific",
          "fixed",
          "unitbuf",
    };
    f=cout.flags();// get flag settings
    // check each flag
    for(i=1,j=0;i<=0x2000;i=i<<1,j++)
      if(i&f) cout << flgs[j] <<" is on\n";
      else cout <<flgs[j] <<" is off\n";
    cout <<"\n";
}
      程序显示如下输出:
skipws is on
left is off
right is off
internal is off
dec is off
oct is off
hex is off
showbase is off
showpoint is off
288页
    uppercase is off
    showpos is off
    scientific is off
    fixed is off
    unitbuf is off
    skipws is on
    left is off
    right is on
    internal is off
    dec is off
    oct is off
    hex is off
    showbase is off
    showpoint is on
    uppercase is off
    showpos is off
    scientific is off
    fixed is on
    unitbuf is off
      注:由于用户的C++编译程序可能为格式标志定义与前面程序不同的值,所以可能会
看到与上面不同的结果。查阅编译程序手册,确定格式标志的值。
17.3.6设置所有标志
      flags()还有一种形式用来设置与流相关的格式标志,其原型如下:
          long flags(long f);
使用这个函数时,把f以位方式拷贝到容纳关于流的格式标志的变量中,这样即设置了所有
标志。函数返回上一次的设置。
      下面这个程序介绍了flags()的这种形式。程序首先构造一个开启showpos、showbase、
oct和right的标志掩膜。对大多数C++工具来说,这些标志的值为0x0400、0x0080、0x0020
和0x0004。把它们加起来,产生程序使用的值0x04A4。关闭所有其它标志,然后用flags()设
置关联cout的这些设置的变量。函数showflags()证明标志如指示的那样设置(它和前一程
序中使用的一样)。
    #include<iostream.h>
    void showflags();
    main()
    {
      // show default condition of format flags
      showflags();
      // showpos, showbase, oct, right are on, others off
      long f=0x04A4;
      cout.flags(f);// set all flags
      showflags();

289页
return 0;
    }
17.3.7使用width()、precision()和fill()
      除了格式标志外,ios还定义了三个成员函数来设置这些格式参数:域宽、精度和填充字
符。做这些事情的函数分别是width()、precision()和fill()。下面依次讨论这些函数。
      缺省时,当输出一个值时,它只占所具有的字符数个位置显示。但用width()函数可以
指定最小域宽。它的原型为:
          int width(int w);
其中,w成为新的域宽,函数返回以前的域宽。在一些工具中,域宽必须在输出前设置,否则
使用缺省的域宽。
      设置最小域宽之后,当值小于指定的域宽时,可用当前的填充字符(缺省为空格)将其补
充到指定的域宽。当值的长度超出最小域宽时,则域超限,没有值会被截断。
      在输出浮点数时,可以用precision()函数设置显示在小数点后的位数,其原型为:
          int precision(int p);
其中,p为设置的精度,函数返回原来的精度值。缺省精度为6。在一些工具中,精度必须在浮
点输出前定义,否则使用缺省精度。
      缺省时,用空格填充域。但是,用fill()函数可以指定填充字符,其原型为:
          char fill(char ch);
调用fill()后, ch变为新的填充字符,函数返回原来的值。
下面的程序介绍了这些函数:
    #include <iostream.h>
main()
    {
      cout.precision(d4);
      cout.width(10);
      cout << 10.12345 <<"\n";// displays 10.12
      cout.fill(’*’);
      cout.width(10);
      cout << 10.12345 <<"\n";// displays *****10.12
    // field width applies to strings, too
    cout.width(10);
    cout << "Hi!" <<"\n";// displays *******Hi!
    cout.width(10);
    cout.setf(ios::left);// left justify
    cout << 10.12345;// displays 10.12** * * *

290页
        return 0;
      }
        程序输出如下:
                    10.12
      *****10.12
      ******Hi!
      10.12*****
    17.3.8用操纵符格式化i/o
      改变流的格式参数的第二种方法是,通过包含在i/o表达式中的所谓操纵符的特殊函
    数实现的。表17-1列出了标准的操纵符。从这个表可以看出,许多i/o操纵符和ios类的成
    员函数是并列的。
        注:表17-1中所列操纵符为所有C++编译程序支持。但在写本书时,ANSI C++标准委
    员会仍在定义i/O操纵符的准确特性。例如,增加了boolalpha()操纵符以提供对新定义的
    布尔数据类型的i/o操作。用户目前的编译程序可能不支持这一操纵符。查阅自己的编译程
    序手册找出哪些操纵符可以使用。
                                  表17-1 C++的操纵符
              操纵符                用途                  输入/输出
              dec                 输入/输出十进制数  输入和输出
              endl                输出一个换行符并清空流  输出
              ends                输出空                  输出
              flush               清空流                  输出
              hex                 输入/输出十六进制数    输入和输出
             oct                输入/输出八进制数      输入和输出
             resetiosflags(long f)   关闭f指定的标志      输入和输出
              setbase(int base) 设置数字基为base        输出
              setfill(int ch     设置填充字符为ch        输出
              setiosflags(long f) 开启f指定的标志       输入和输出
              setprecision(int p)设置精度
              setw(int w)         设置域宽为w          输出
              ws                    跳过前导空白          输入
        要存取带参数的操纵符(如setw()),程序中必须包含iomanip.h.下面是一个使用一些
    操纵符的例子:
      #include<iostream.h>
      #include<iomanip.h>
      main()
      {
        cout<<hex<<100<< endl;
        cout<<setfill(’?’) <<setw(10)<<2343.0;
        return 0;
      }

291页
   显示如下:
      64
      ??????2343
    注意操纵符是如何在I/O操作链中出现的。同时也要注意当操纵符不带变元时,如例
中的endl(),它后面不跟括号。这是因为它是传递给重载((运算符的函数的地址。
        作为对照,下面的程序和上面的程序在功能上是等价的,它使用了ios的成员函数
    获得同样的结果。
    #include <iostream.h>
    #include <iomanip.h>
    main()
    {
      cout.setf(ios::hex);
      cout <<100<<"\n";// 100 in hex
      cout.fill(’?’);
      cout.width(10);
      cout<<2343.0;
      return 0;
    }
    这个例子暗示,使用操纵符比使用ios成员函通数常能获得更短的代码。
    使用setiosflags()操纵符可以直接给流设置几个格式标志。例如,下面的程序用setios-
flags()设置showbase和showpos标志:
    #include <iostream.h>
    #include <iomanip. h>
      main()
    {
      cout << setiosflags(ios::showpos);
      cout << setiosflags(ios::showbase);
      cout<<123<<" " <<hex<<123;
      return 0;
    }
    操纵符setiosflags()实现成员函数setf()同样的功能。
17.4重载<< 和>>
    C++重载<< 和>> 运算符用来执行C++的内部类型的i/o操作。用户还可以重载
这些运算符来执行自定义类型的i/o操作。
    在C++里,输出运算符<< 由于向流中插入字符,所以称为插入运算符。与此相似,输
入运算符>>由于从流中提取字符则被称作提取运算符。通常,重载插入运算符和提取运
算符的运算符函数分别叫做插入符和提取符。

292页
       17.4.1创建自己的插入符
      为自己的类创建插入符非常简单。所有插入符函数的一般形式是:
        ostream&operator<<(ostream&stream, class.type obj
                    {
            //body of inserter
                  return stream;
注意,函数返回类型ostream的流的引用(记住, ostream是从ios派生的、支持输出的类)。
进一步讲,传给函数的第一个参数是对输出流的引用,第二个参数是被插入的对象。插入符
在退出之前必须做的最后一件事是返回stream,这使得插入符可以用在插入链中。
    在插入符函数中,可以放置用户希望的任何类型的过程或运算符。也就是说,一个插入
符的作为完全取决于用户自己。为了保持好的编程风格,应该限制插入符向流输出信息的操
作。例如,用插入符计算PI到30个十进制位作为插入符的副作用大概不是一个好主意。
    看一个例子,我们为类型phonebook的对象创建一个插入符:
class phonebook{
public:
    char name[80];
      int areacode;
      int prefix;
      int num;
      phonebook(char *n,int a,int p,int nm)
      {
        strcpy(name,n);
        areacode=a;
      prefix=p;
            num=nm;
      }
    };
      该类包含了一个人的姓名和电话号码。下面是一种为类型phonebook的对象创建插入
符函数的方法。
// Display name and phone number
ostream &operator<<(ostream &stream, phonebook o)
    {
      stream << o.name <<" ";
      stream<< "("<<o.areacode<< ")";
      stream <<o.prefix<<"-" <<o.num <<"\n";
      return stream;// must return stream
    }
下面的短程序介绍了phonebook插入符函数。
    #include <iostream. h>
    #include <string.h>
    class phonebook{
    public:

293页
char name[80];
    int areacode;
    int prefix;
      int num;
    phonebook(char *n,int a,int p,int nm)
      {
      strcpy(name,n);
      areacode=a;
      prefix=p;
            num=nm;
      }
    };
//Display name and phone number.
ostream&operator<<(ostream&stream, phonebook o)
    {
      stream<<o. name<<" ";
      stream<<"(" <<o.areacode<<")";
      stream <<o.prefix <<"-"<< o.num <<"\n";
      return stream;// must return stream
        }
    main()
    {
      phonebook a("Ted", 111, 555,1234);
    phonebook b("Alice", 312, 555, 5768);
    phonebook c("Tom", 212, 555, 9991 );
    cout<<a<<b<<c;
      return 0;
    }
      该程序产生如下输出:
    Ted (111)555-1234
    Alice(312) 555-5768
    Tom(212)555-9991
      在这个程序里,注意phonebook的插入符不是phonebook的成员。尽管这种方法初看
起来似乎很费解,但还是很容易理解的。当一个任意类型的运算符函数是类的成员时,左操
作数(通过this隐式传递)是产生调用运算符函数的对象。而且,这个对象是运算符函数作为
其成员的类的对象。没有办法改变这一点。如果一个重载的运算符函数是类的成员,左操作
数必须是该类的对象。重载插入符时,左操作数是一个流,而右操作数是那个类的对象。所
以,重载的插入符不能是为之重载的类的成员。变量name、areacode、prefix和num在前面的
程序是公有的,所以插入符能存取它们。
      插入符不能是它为之定义的类的成员的事实似乎是C++的一个严重缺陷。既然重载
的插入符不是成员,它们又怎么能存取类的专有元素呢?在上面的程序里,所有成员都是公
有的。然而,封装性是面向对象程序设计的基本部仲。要求用插入符输出的所有数据成为公
有与本原则相冲突。解决这个难题的办法是,让插入符成为类的友元。这既满足了传给重载
的插入符的参量是流的要求并授权函数存取它为之重载的类的专有部分。下面是和前面相

294页
       同的程序,修改后插入符就成为友元函数。
    #include<iostream.h>
    #include<string.h>
class phonebook{
    //now private
    char name[80];
    int areacode;
    int prefix;
        int num;
    public:
      phonebook(char*n,int a, int p, int nm)
      {
        strcpy(name,n);
        areacode=a;
        prefix=p;
            num=nm;
      }
    friend ostream &operator<<(ostream&stream,phonebook o);
    };
    // Display name and phone number.
ostream &operator<<(ostream &stream, phonebook o)
    {
      stream<<o.name<<" ";
      stream<<"(" <<o.areacode<<")";
    stream<<o.prefix<<"-"<<o.num<<"\n";
    return stream;// must return stream
    }
    main()
    {
      phonebook a("Ted" , 111, 555, 1234);
      phonebook b("Alice", 312, 555, 5768);
      phonebook c("Tom", 212, 555, 9991);
      cout<<a<<b<<c;
      return 0;
    }
      在定义插入符函数体时,要尽可能使其一般化,如上面例子所示的插入符。由于函数体
把输出定向到请求这个插入符的stream,所以它可以用于任何流,并且,将
    stream<<o.name <<" ";
    改写为
    cout<<o.name<<" ";
也是对的。这有把硬件编码的cout作为输出流的效果,原始的形式可以作用于任何流,包括
那些与磁盘文件相联系的流。虽然在某些情况下,特别是在涉及特殊输出设备的地方,用户

295页
希望给输出流硬件编码,但在大多数情况下是不需要的。一般地讲,插入符越灵活,其价值越
高。
注:除非num的值是一些象0034这样的值,在这种情况下其前导0不显示,类phone-
book的插入符还是工作得很好的。为证明这一点,可以把num转换成一个串或者设置填充
字符为0,并用width()格式函数产生前导0。这个问题作为练习留给读者解答。
在切入提取符之前,再看一个插入符函数的例子。插入符不应只局限于处理直接文本,
还应能用来输出人们知道的任何形式的数据,例如,CAD系统的某个类的一个插入符可以
输出绘图仪指令,另一个插入符可产生图形图象。基于Windows程序的插入符,可以显示一
个对话框。看下面的程序在屏幕上绘制方框(由于C和C++都没定义图形,程序用字符绘制
方框,但如果系统支持图形也很容易替换)。
#include <iostream.h>
class box{
Int x,y;
public:
box(int i, int j){x=i; y=j;}
friend ostream&operator<<(ostream&Stream, box o);
};
//Output a box.
ostream&operator<<(ostream&stream, box o){
register int i, j;
for(i=0;i<o.X; i++)
      stream<<"*";
stream<<"\n";
for(j=1; j<o.y-1;j++){
    for(i=0; i<o.x;i++)
        if(i==0||i==o.x-1) stream<<"*";
       else stream<<" ";
    stream <<"\n";

for(i=0; i<o.x; i++)
      stream<<"*" ;
stream<<"\n";
return stream;

main()

box a(14,6),b(30,7),c(40,5);
    cout <<"Here are some boxes:\n";
    cout <<a<<b<<c;

296页
          return 0;
        程序显示如下∶
    Here are  some boxes:
          * * * * * * * * * * * * *
          *                       *
          *                       *
          *                       *
          *                       *
          * * * * * * * * * * * * *
          * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
          *                                                       *
          *                                                       *
          *                                                       *
          *                                                       *
          *                                                       *
          * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
        * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
        *                                                                     *
        *                                                                     *
        *                                                                     *
        * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
17.4.2创建自己的提取符
      提取符与插入符互补。提取符函数的一般格式为:
        istream &operator<<(istream &stream, class_type &obj)
            {
              // body of extra
                  return Stream;
                }
      提取符返回一个类型istream的输入流的引用。第一个参数也必须是类型istream的流
的引用。注意,第二个参数必须是提取符为之重载的类的对象的引用,这样使得对象能被输
入(提取)操作所修改。
      沿用phonebook类,下面是书写提取函数的一种方法:
istream &operator>>(istream &stream, phonebook &o)
      {
      cout<<"Enter name:";
      stream>>o.name;
      cout<<"Enter area code:";
      stream>>o.areacode;
      cout<<"Enter prefix :";
      stream >> o.prefix;
      cout << "Enter number :";
      stream>>o.num;
      cout <<"\n";
      return stream;
        }

297页
      注:虽然这是一个输入函数,但它也可以通过提示用户执行输出。关键是,虽然提取符的
主要用途是输入,但它也能执行一些为达此目的必要的操作。然而,和使用插入符一样,最
好让提取符执行的操作直接关联于输入。如果不这样做,将严重破坏结构和清晰性。
    下面是一个介绍phonebook提取符的程序:
#include <iostream.h>
#include<string.h>
class phonebook{
    char name[80];
    int areacode;
    int prefix;
      int  num;
public:
    phonebook(){};
    phonebook(char *n, int a, int p, int nm)
              {
      strcpy(name,n);
      areacode=a;
      prefix=p;
        num=nm;
    }
    friend ostream&operator<<(ostream&stream,phonebook o);
    friend istream&operator>>(istream&stream, phonebook &o);
};
//Display name and phone number.
ostream &operator<<(ostream&stream, phonebook o)

    stream<<o.name <<" ";
    stream<<"(" <<o.areacode<<")";
   stream<<o.prefix<<"-"<<o.num<<"\n";
    return stream;// must return stream

// Input name and telephone number.
istream&operator>>(istream&stream, phonebook &o)

    cout<<"Enter name:";
    stream>>o.name;
    cout<<"Enter area code:";
    stream>>o.areacode;
    cout <<"Enter prefix:";
    stream >> o.prefix;
    cout <<"Enter number:";
    stream>>o.num;
    cout <<"\n";
      return stream;

main()


298页
      phonebook a;
      cin >>a;
      cout<<a;
      return 0;
                }
17.5创建自己的操纵符函数
      除了重载插入和提取运算符以外,用户还可以通过创建自己的操纵符函数进一步用户
化c++的I/o系统。定制的操纵符之所以重要有两个主要原因。其一,可以将一串分离的I/
O操作合并为一个操纵符。例如,在一个程序里频繁出现同样顺序的I/O操作的情况是常见
的。如果用定制的操纵符完成这些操作,则可以简化源代码并避免发生意外错误。当需要对
非标准设备执行I/O操作时,定制的操纵符也显得很重要,例如,可以用一个操纵符给特殊
类型的打印机或光识别系统发送控制码。
      定制的操纵符是c++支持OOP的一个特征,但对非面向对象的程序也有好处。下面读
者会看出,定制的操纵符有助于使任何I/O密集的程序变得更加清晰高效。
    有两种基本类型的操纵符:一种作用于输入流,一种作用于输出流。除了这两个广义的
分类以外,还有一种分类方法,即带一个变元的操纵符和不带变元的操纵符。在无参数的操
纵符和有参数的操纵符创建的方法上,存在着明显的区别。本节从创建无参数的操纵符着
手,讨论如何创建每一种类型的操纵符。
17.5.1创建无参数的操纵符
      所有无参数的操纵符输出函数都有如下框架:
          ostream&manip_name(ostream&stream)
            {
               // your code here
                  return stream;
                }
其中,manip_name是操纵符名。注意,返回的是对类型ostream的流的引用。如果操纵符用
作大的I/O表达式的一部分时,这是必要的。注意到这一点也很重要,即使操纵符有一个对
它操作的流的引用作为它的单个变元,但在它被插入到一个输出操作中时,也不会使用变
    元。
    作为一个简单的例子,下面的程序创建了一个叫sethex()的操纵符,它开启showbase
标志并设置输出为十六进制。
    #include <iostream.h>
    #include<iomanip. h>
// A simple output manipulator.

299页
    ostream&sethex(ostream&stream)
          {
      stream.setf(ios::showbase);
      stream. setf(ios::hex);
        return Stream;
    }
    main()
    {
      cout<<256<<" " <<sethex<<256;
      return 0;
    }
    程序显示256为0x100。可以看出,sethex按和其它任何内部的操纵符相同的方法被用作
I/O表达式的一部分。
    定制的操纵符不必复合着用。例如,简单的操纵符la()和ra()分别显示左箭头和右箭
头,如下所示:
    #include <iostream.h>
    #include<iomanip.h>
    // Right Arrow
    ostream&ra(ostream&stream)
            {
      stream<<"------->";
      return stream;
        }
    // Left Arrow
    ostream&la(ostream&stream)
    {
      stream<<"<—-------";
      return stream;
    }
    main()
    {
    cout<<"High balance"<<ra<<1233.23<<"\n";
    cout<<"Over draft"<<ra<<567.66<<la;
      return 0;
    }
      程序显示:
High balance------>1233.23
over draft------->567.66<--------
    如果经常使用,这些简单的操纵符可避免一些乏味的打字。
    使用输出操纵符对给设备发送特殊代码特别有用。例如,一台打印机可能要接收一些代
码,如改变打印字符的大小、字库或设置打印头的位置等。如果这些调整要经常进行,则操纵

300页
  符就是最佳的候选者。
    所有的无参数输入的操纵符函数都有如下框架:
        istream &manip_name(istream &stream)
            {
            // your code here
                    return stream;
                  }
    输入操纵符接受访问它的流的引用。这个流必须被操纵符返回。下面的程序刨建了输入
操纵符getpass(),响铃并提示口令:
    #include<iostream.h>
    #include <string. h>
    // A simple input manipulator
istream&getpass(istream &stream)
    {
    cout <<’\a’;// sound bell
    cout <<"Enter password :" ;
        return stream;
            }
    main()
        {
    Char pw[80];
    do{
      cin>>getpass>> pw;
      }while(strcmp(pw, "password"));
      cout<("Logon complete\n";
      return 0;
        }
    操纵符返回stream是至关重要的。如果不这样,操纵符就不能用在输入或输出操作的
    系列里。
17.5.2创建带参数的操纵符
    创建带变元的操纵符没有创建不带变元的操纵符那么直接了当。一个原因是带参数的
操纵符涉及到通用类。通用类通过使用template关键字创建。(模板和通用类将在第二十一
章讨论。)如果不知道通用类如口何操作,就不能完全了解带参数操纵符的创建过程。操纵符更
复杂的另一个原因就是用于创建操纵符的具体方法随编译程序而不同。也就是说,对一个编
译程序有效的方法可能对另一个编译程序并不必要。虽然ANSI C++委员会对此作了最后
定夺,但并不是所有的编译程序均按标准实现带参数的操纵符。由于这些原因,在创建属于
自己的带参数操纵符时,需查阅相应的编译程序手册了解细节。

301页
    注:本节中介绍的带参数的操纵符示例程序符合ANSI C++标准,可在最新版的C++
编译程序上运行。
    创建带参数的操纵符必须在文件中包含IOMANIP.H。IOMANIP.H中定义了三种通
用类:omanip、imamip和smanip。omanip用于创建带一个变元的输出操纵符;imanip用于创
建带参数的输入操纵符;smanip用于创建能够用在输入和输出流中带参数的操纵符。(请查
阅自己的编译程序手册,找出其中IOMANIP.H中有关这些类的定义。)
    通常,不管何时需要创建带一个变元的操纵符,都要创建两个重载的操纵符函数。其中
一个要定义两个参数。第一个参数是对流的引用,第二个参数是要传给函数的参数。操纵符
的第二种形式只定义一个参数,即当操纵符用在I/O表达式中时说明的那个。这种形式产生
了对第一种形式的调用。通常,对输出操纵符,可以使用这些形式创建参数化操纵符:
        ostream&manip_name(ostream &stream, type param)
          {
            //your code here
                  return stream;
            }
              // overload
        omanip <type> manip_name(type param){
          return omanip<type>(manip_name, param);
          }
其中,manip_name为操纵符名,type指明操纵符使用的参数类型。由于omanip是一个通用
类,故type也变成操纵符返回的特定omanip对象操作的数据类型。
    下面的程序创建了一个调用indent()的参数化的操纵符,indent()缩进指定数目的空
格。
#include<iostream.h>
#include <iomanip.h>
// Indent length number of spaces.
ostream&indent(ostream&stream, int length)

    register int i;
   for(i=0;i<length;i++) cout <<" ";
      return stream;

omanip<int> indent(int length){
    return omanip <int>(indent, length);

main()

    cout<<indent(10)<<"This is atest\n";
    cout << indent(20) <<"of the indent manipulator.\n";
    cout << indent(5)<< " it works!\n";

302页
        return 0;
}
    可以看出,indent()象前面描述的那样重载。当在输出表达式中遇到indent(10)时,将执
行indent()的第二种形式,并把值10传给参数length。这种形式然后用通过length传递的值
10执行第一种形式。这个过程对每个indent()调用都要重复。
    希望操纵符具有的参数类型可以由用户控制,且由操纵符函数的第二个参数的类型确
定。在为操纵符的重载版本创建通用类时则使用相同的类型。例如,下面的程序示出了如何
把一个double值传给操纵符函数,然后以美元-美分的格式输出其值。
#include<iostream.h>
#include<iomanip.h>
ostream&dollars(ostream&stream, double amount)
    stream. setf(ios::showpoint);
    stream<<“$”<<setw(10) << setprecision(2)<<amount;
    return stream;
omanip<double>dollars(double amount)<
    return omanip(double>(dollars , amount);
    }
main()
{
    cout<< dollars(123.123456);
    cout<<"\n"<< dollars(10.0);
    cout<<"\n"<< dollars(1234.23);
    cout <<"\n"  <<dollar(0.0);
    return 0;
}
    输入操纵符也可以带一个参数。下面的程序对前面的getpass()作了改进。这种形式带
有一个变元,这个变元指明用户要正确输入口令必须努力的次数。
// This program uses a manipulator to input a password.
#include<iostream.h>
#include<iomanip.h>
#include <string.h>
#include<stdlib.h>
Char *password="I like C++";
char pw[80];
// Input a password
istream&getpass(istream&stream, int tries)
    {
    do{
      cout<<"Enter password:";
      stream>>pw;

303页
if(!strcmp(password,pw)) return stream;
      cout<<"\a";//bell
          tries--;
    }while(tries>0);
    cout <<"All tries failed!\n";
    exit(1);//didn’t enter password
      return stream;

imanip<int> getpass(int tries){
    return imanip <int>(getpass, tries);

main()
{
    // give 3 tries to enter password
    cin >> getpass(3);
    cout<< "Login complete!\n";
      return 0;
    }
      注意,格式和输出操纵符完全一样,但有两点例外:必须使用输入流istream,并指明类
IMANIP。
      和用C++工作一样,用户会发现定制的操纵符有助于精简i/o语句。
17.6关于老式流类库的简短说明
      当发明C++时,就创建了一个较小的且稍有差别的i/o类库。这个库在文件stream.h
中定义。但在C++诞生时,老的i/o库已被本书介绍的i/o库代替。由于和老的C++程序
兼容,所以大多数C++编译程序仍然支持老的流库。但在编写新程序时,应该使用
IOSTREAM.H中定义的现代i/o库。
 

 原文地址 http://www.yourblog.org/Sort/20059/207404_2.html
发表于: 2007-08-19,修改于: 2007-09-03 17:25,已浏览846次,有评论0条 推荐 投诉


网友评论
 发表评论