技术(四):将应用程序库从输入输出中分离出来

应用程序库对I/O设备的选择:捆绑到特定的I/O会限制库的灵活性

  • 用类表示概念:设计一个类表示任意I/O接口

  • 使应用程序性与I/O之间的耦合度降低

问题

  • 表示变长字符串的String类,应该能够用关惯用符号打印String: cout << fullname << endl;

  • 传统方法:

1
2
3
4
ostream& operator<<(ostream& o, const String& s) {
// 在输出流中打印字符串s
return o;
}
  • 两个潜在的严重问题:
    • 使用String类的同时不得不使用iostream类
    • 不是用iostream类而使用另一种I/O机制的程序也不得不包括两个完整的I/O库,带来不必要的空间开销
    • 没有简单方法在另一种I/O机制中打印String
    • 使用专用库如用于进程间通信的库,就很难用这个库打印String的内容

解决方案1:技巧加蛮力

  • 具体问题具体对待:通过将依赖于iostream库的代码划分为各个编译模块,从而把iostream库带来的开销从String中分离出来

  • String.h头文件

1
2
3
4
5
6
7
class ostream;
class String {
// ...
};
ostrean& operator<<(ostream&, const String&);
  • 没有ostream的定义
  • 要打印String就必须包含iostream.h
1
2
3
4
5
6
7
#include <String.h>
#include <iostream.h>
int main() {
String s = "hello\n";
cout << s;
}
  • operator的定义也必须包含iostream.h
1
2
3
4
5
6
7
#include <String.h>
#include <iostream.h>
ostream& operator<<(ostream& o, const String& s) {
// 在输出流中打印字符串s
return o;
}
  • 单独编译这个函数可以避免iostream类带来的开销

  • 不能解决另一个问题:在不是ostream的I/O机制上打印String

解决方案2:抽象输出

  • String类设计者不可能知道怎样对某种可能的输出设备产生输出
  • I/O库作者不可能知道每一种将会依赖于该I/O库的对象类型

  • 抽象出输出操作的最基本的部分,提供连接String和I/O库的纽带

  • dest.send(ptr, n);

    • 不同的dest:需要一个类继承体系
    • send()纯虚函数,虚析构函数
1
2
3
4
5
class Writer {
public:
virtual ~Write();
virtual void send(const char*, int) = 0;
};
  • 将类的声明加入Writer.h中,定义通用String输出函数
1
2
3
4
5
6
7
8
9
#include <Writer.h>
Writer& operator<<(Writer& w, const String& s) {
for (int i = 0; i < s.size(); i++) {
char c = s[i];
w.send(&c, 1);
}
return w;
}
  • 运行得较慢
    • 令该函数为String类的友元函数,获得String的特殊实现细节,一次调用send发送完整的String
  • String类必须知道Writer是如何工作的,仍存在不适当的耦合

  • 为特定的目的地定义特定的Writer类

  • 为C类型的FILE指针定义的Writer类

1
2
3
4
5
6
7
8
9
10
11
12
class FileWriter: public Writer {
public:
FileWriter(FILE* f): fp(f) { }
void send(const char* p, int n) {
for (int i = 0; i < n; i++)
putc(*p++, fp);
}
private:
FILE* fp;
};
  • 在stdout中写String
1
2
3
FileWriter s(stdout);
String hello = "Hello\n";
s << hello;

定义一个表示写任意字符序列到任意目的地的抽象积累Writer
使用继承为每个向使用的I/O库创建一个特殊的Writer类
为每个知道如何使用何种Writer写该类对象的应用类定义输出操作

解决方案3:技巧而无蛮力

  • 设计一个小I/O库,唯一目的是作为其他I/O库的接口
    • 目前的不足之处:应用库需要知道Writer类本身特性
  • 不能编写:

    • String hello = "Hello\n", goodbye = "Goodbye\n";
    • FileWriter(stdout) << hello << goodbye;
  • FileWriter(stdout) 是一个临时值,第一个 << 完成后可能就被销毁

    • 在某些C++编译器实现中会出现这种情况
  • 一种方法:禁止Writer生成临时对象(可以通过显式声明Writer类做到)

  • 另一种方法:完全不用引用

    • 句柄类:WriterSurrogate
    • 指向某个继承自类Writer的底层类
  • 新方法:利用编译期间能得到的关于类的信息

  • 将write当作一个概念,一个write属于一个类型族

  • 不使send作为成员函数:send(dest, ptr, n);

  • 把写入程序与应用类联系起来
1
2
3
4
5
6
7
template <class W>
W& operator<<(W& w, const String& s) {
for (int i = 0; i < n;; i++) {
char c = s[i];
send(w, &c, 1);
}
}
  • 常规的方式访问stdio
1
2
3
4
void send(FILE* f, const char* p, int n) {
for (int i = 0; i < n; i++)
putc(*p++, f);
}
  • 可以以如下方式调用
    • String hello = "Hello\n", goodbye = "Goodbye\n";
    • FileWriter(stdout) << hello << goodbye;

提供I/O独立性方式:
约定应用程序中的类采用使用send模板进行所有的输出操作
每个I/O包都需要一个专为写的适当的send函数(用户提供)
应用程序的库除了要知道如何使用send函数,不必知道任何输出操作的细节

Summary

  • 模板可以提供对操作进行抽象化的方法,就像类对数据结构提供抽象化

  • 可以在编译时利用已知的特定I/O包的信息,而不必等到执行时

    • 编译时就进行类型检查
    • 避免动态绑定在运行时的开销

《C++沉思录(Cplusplus Thinking)》笔记