接口类可以用作不同编程方法、数据结构实现技术、编码方法、命名约定以及不同操作系统环境之间的桥梁。事实上,将传统C程序移植到对象世界的最简单途径是为C程序中自由漂浮的函数和数据提供接口类。当需要具备不同OS间或不同硬件平台间的可移植性时,接口类将责无旁贷地为完成这类工作的武器。
1. 接口类详解
接口类用于为代码和数据提供一个新的接口,或更改代码和数据的旧接口。它可以用于修改或改进另一个类或一系列类的接口,许多情况下,还可用来给非面向对象编程工作提供面向对象接口例如过程库、OS API,或者数据库管理系统提供面向对象接口。使用它可以让另一个类更易使用,功能更强,更完全,或者在语义上更正确。
1.1 接口类的类型
作为非面向对象数据和代码的封装器。当我们构建多线程架构技术时,会使用接口类来封装提供多线程处理服务、进程控制服务、进程间通信、文件I/O以及设备I/O的OS API。它的一个重要用途是为数据和需要操作于该数据的函数提供独立域接口。
a.适配器——修改其它类接口的类
类配器类为其它现在面向对象类提供封装器。它的目标不是添加功能性,而是提供一个新接口。一个典型的例子就是作为标准模板库一部分的容器适配器。它为标准模板库中的list、vector和deque容易提供了一个新的公共接口。另外若只是重命名适配类的成员函数,则使用内联(inline)是有利的,它可将重命名的开销降到最低点。
b.蓝图接口类
蓝图接口类提供一个接口,但没有任何实现,它为将来的类提供基础。典型情况下,这样的类不包含数据,其上的所有成员函数都为纯虚拟(pure virtual),而且它的所有基类也为接口类。它们用来计划程序员使用该类时必须实现哪些成员函数。它不仅要求实现一定的函数,而且还要求这些函数有一定的命名约定。
1.2 减小参数和全局变量的数量
使用接口类封装过程代码和数据的一个重要附带作用是减少了调用新成员函数时的参数。
2. C++没有多线程处理的关键字
C++是一种被设计用于生成(build)和重用(reuse)的语言。在速度和空间效率上,它的性能与C语言是相等的。C++语言定义中并不存在并发或多线程的关键字(这一局面在C++11中得到了改关,通过像atomic_t等类型的引入),不过它们都可以在C++中生成。
目前针对并行处理具有几种经典的硬件模型:
a.单指令多数据流(SIMD); b. 多指令单数据流(MISD); c. 多指令多数据流(MIMD); d. 单程序多数据流(SPMD)。
并行处理环境(其中所有的处理器共享单个地址空间)与每个处理器拥有各自地址空间的环境不大一样。对C++程序员而言,面对如此多要求并行处理的不同类型程序员、而且应用的环境又是如此复杂,因此,仅凭单个模型,甚至用少量的模型,是不可能实现的。于是改而按库的形式实现并发,或多线程编程。按此种途径,可以用库来实现所期望的任何模型,而不必强迫每个C++用户掌握并行编程。
如果C++中置入了并行处理的特定模型,程序员将限制于抽象机器所处的表达力。如果将功能类型建成库,而不是语言结构,那么更改功能的实现就不必修改编译器或虚拟机模型。要知道,现实世界中经常会引入一些硬件并行化的新模型,如果采用了关键字的途径,C++语言的定义就要不断地修改。实质上,将并行处理和多线程处理推到库或类库的高度,C++获得的是最灵活的处理方式。此时程序员可以“即插即用”不同的并发模型。
因为C++中没有支持多线程处理或进程间通信的关键字,所以,我们必须用类、类库和应用框架的形式来构建这种支持。另外我们选择重用OS代码。可以设计IPC类、线程控制类以及进程控制类,让它们都重用OS代码。这些类最终生成一个接口类,使用它们我们可以构建任务和线程类。使用任务和线程类,我们可以构建线程和任务库,进而通过使用线程和任务库,我们可以构建多线程应用框架。
3. 面向对象接口到管道
两种可用的最基本IPC机制是匿名管道和命名管道。UNIX/Win32/OS/2都支持这些基本IPC。其中匿名管道的基本功能在每种环境中都是相同的,但命名管道在Win32和OS/2中的功能与大部分UNIX环境中的功能大相径庭。虽然这些环境都支持管道,但系统API对于这些机制来说不是面向对象的,因此不支持封装和继承。如果要使用OS API来构建多线程应用框架的基石,必须首先将必需的OS服务引入对象世界。
对管道的任何面向对象处理方式至少具有5个组件:
a.数据缓冲器; b.数据缓冲器插入操作; c.数据缓冲器提取操作; d.数据缓冲器创建操作; e.数据缓冲器析构操作。
此外管道还有两个终端,一端用于插入数据,另一端则用于提取数据。这两个终端可以在不同的进程中使用。通过这两个终端,简单的管道抽象可以被看作任何类型的IO组件。共有两种创建面向对象管道的技术,第一种技术使用面向对象工具,iostream层次已经为我们提供了这些工具;第二种则使用复合和fstream对象来创建pstream对象(管道类)。
iostream类层次的主要组件可以描述为3种类:缓冲器组件(buffer component)、翻译组件(translation component)以及状态组件(state component)。缓冲器组件用作传送中字节的保存区,翻译组件负责为程序员提供字节流信号量,其中的所有IO,不论其源与目标是哪里,都被看作字节流。状态组件封装面向对象流的状态,它显示可用于缓冲器组件中数据字节的格式类型,还显示流是否在追加(append)、创建、排它性读或排它性写模式已经被打开,或者数字是否被解释成十六进制、八或二进制等;还可用于判断对缓冲器组件的IO操作错误状态。
可以看出,抽象管道的五个基本组件在iostream中已经实现,我们只需要决定如何才能结合逻辑输入和输出端口的概念与iostream。
iostream类中共有三种缓冲器类型如下:
a. streambuf:内存区域集,具有大量定义内存区域行为的受保护方法,它提供了与输入、输出设备间发送数据的接口;
b. strstreambuf:继承了streambuf类。它定义了内存缓冲的基本行为。缓冲器按FIFO列表或字节数组实现;
c. filebuf:继承了streambuf类。它为同文件间的输入或输出提供了一个缓冲器。get/put指针是一个指示当前读取或写入信息位置的指针。
ifstream和ofstream两者都包含filebuf类。fstream类继承了ifstream和ofstream,所以它也包含filebuf类。因此,我们可以使用fstream类家庭的任何一个类来帮助我们创建面向对象管道工具。我们可以通过构造函数,或通过attach()成员函数连接pipe()系统调用返回的文件描述符。
使用提取器和插入器进行自动格式翻译是使用fstream类家族与管道通信的一个主要优点。使用用户自定义提取器和插入器的能力克服了管道编程中碰到的一些困难。fstream类家族也可以使用read()和write()成员函数来读取管道数据和写入数据。
我们也可以通过ostream_iterator和istream_iterator来使用管道。这些迭代器是一般性、面向对象的指针。
4. 使用接口类来实现面向对象命名管道
4.1 C/S架构及术语
当进程使用匿名管道进行通信时,进程是关联的。而通过名字打开管道的进程称做客户进程。服务器进程可以有多个使用管道的客户进程。服务器进程负责建立命名管道的属性。
4.2 名字包含哪些内容
在UNIX环境中,无关联进程可使用命名管道,不过这些进程必须位于同一台计算机上。在Win32和OS/2环境中,命名管道可以通过网络来访问。
基本命名管道的最终抽象至少具备以下组件:
a.管道名字;b.访问权限;c.打开模式;d.管道模式;e.输入端口;f.输出端口;g.输入端口大小;h.输出端口大小;i.数据缓冲器;j.数据缓冲器插入损伤;k.数据缓冲器提取操作;l.数据缓冲器创建操作;m.数据缓冲器析构操作。
如果在UNIX环境中需要与Win32环境中相似的功能,则应当使用socket。
4.3 命名管道和iostream复合
在构建命名管道对象包含Pstream类期间使用复合(composition)。通过与fstream对象的包容关系,npstream类是命名管道功能与iostream提供的IO面向对象模型的合成。
4.4 npstream接口类
此类中包含两个构造函数。其中服务器进程使用第一个构造函数构建命名管道。这个构造函数包含对DosCreateNPipe()的实际调用。创建管道后,使用DosConnectNPipe()函数将它设置成监听模式(listening mode)。一旦将管道放入监听模式后,通过调用attach()成员函数建立与fstream对象NamedPipe的连接。然后客户进程使用第二个构造函数打开与管道的连接,通过传统的打开调用来打开命名管道即DosOpen函数。
4.5 命名管道与STL istream_iterator和ostream_iterator
同样命名管道也可以通过iostream对象进行istream_iterator, ostream_iterator连接。这样大部分可以与istream_iterator或ostream_iterator使用的STL算法也可以通过iostream连接应用于命名管道。为了让npstream类用于复制,必须添加异常处理。事件互斥量和其它同步变量也应当用于协调管道的应用。