模板(五):抽象接口

前面探讨了模板的经典用途: 建立容器类

  • 描述一个或一组程序的接口的通用方式
  • 使用模板提供抽象接口,可以写出独立于任何一种实际类型的函数

问题

系统的在概念上具有不同特性的部分能谨慎地分隔开,有利于(代码)复用

小例子

  • 一个将整数数组元素相加的函数
1
2
3
4
5
6
int sum(int * p, int n) {
int result = 0;
for (int i = 0; i < n; i++)
result += p[i];
return result;
}
  • 使用这个函数:
1
2
3
4
5
6
int main() {
int x[10];
for (int i = 0; i < 10; i++)
x[i] = i;
cout << sum(x, 10) << endl;
}
  • sum函数知道三件事:

    1. 它把一组数加在一起
    1. 它所加的数是整数
    1. 它所加的整数以一种特殊的方式存储了起来

划分这个程序使把每一个特征分离到不同的部分中

分离迭代方式

  • 函数的一个依赖: 添加的元素是放在数组中

  • 用类抽象这种依赖: 迭代器

    • 一个构造函数用来创建要处理的数据
    • 请求序列中下一个元素的方法
    • 告知遍历元素何时完成的方法
    • 赋值操作符、复制构造函数、析构函数,使对象可以当作值使用
1
2
3
4
5
6
7
8
9
10
11
class Int_iterator {
public:
Int_iterator(int*, int); // 第一个元素的地址、元素数量
~Int_iterator();
int valid() const;
int next();
Int_iterator(const Int_iterator&);
Int_iterator& operator=(const Int_iterator&);
};
  • 改写sum函数
1
2
3
4
5
6
int sum(Int_iterator ir) {
int result = 0;
while (ir.valid())
result += ir.next()
return result;
}
  • 不需要再直接知道正在添加的元素是如何保存的: 封装在Int_iterator中

  • 使用新的函数:

1
2
3
4
5
6
int main() {
int x[10];
for (int i = 0; i < 10; i++)
x[i] = i;
cout << sum(Int_iterator(x, 10)) << endl;
}
  • 实现未泛型化的迭代器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Int_iterator {
public:
Int_iterator(int* p, int c): data(p), len(c) { }
int valid() const {
return len > 0;
}
int next() {
--len;
return *data++;
}
private:
int* data;
int len;
};

遍历任意类型

  • 通用的Iterator模板
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<class T>
class Iterator {
public:
Iterator(T* p, int c): data(p), len(c) { }
int valid() const {
return len > 0;
}
T next() {
--len;
return *data++;
}
private:
T* data;
int len;
};
  • typedef Iterator<int> Int_iterator使Int_iterator类型等价于Iterator<int>

增加其他类型

求出任意类型的值的和

  • 将sum函数做成模板
1
2
3
4
5
6
7
template <class T>
T sum(Iterator<T> ir) {
T result = 0;
while (ir.valid())
result += ir.next();
return result;
}
  • 类型T满足:
    • 可以把0转换成该类的对象
    • 对该类的对象定义类
    • 对象具有类似值的语义,sum函数可以把对象作为值返回

存储技术抽象化

该迭代器只能访问存储在数组中的值

  • 值可以保存在链表、文件中

  • 需要反映不同数据结构的不同迭代器

  • 把Iterator类转变成一个抽象基类,可以表示许多不同类型的迭代器类中的任何一个

1
2
3
4
5
6
7
template <class T>
class Iterator {
public:
virtual int valid() const = 0;
virtual T next() const = 0;
virtual ~Iterator() {}
};
  • Array_iterator<T>就是一种Iterator<T>:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <class T>
class Array_iterator : public Iterator<T> {
public:
Array_iterator(T* p, int c): data(p), len(c) {}
int valid() const {
return len > 0;
}
T next() {
--len;
return *data++;
}
private:
T* data;
int len;
};
  • sum函数接受指向Iterator的引用作为参数以允许动态绑定
1
2
3
4
5
6
7
template <class T>
T sum(Iterator<T>& ir) {
T result = 0;
while (ir.valid())
result += ir.next();
return result;
}
  • 调用sum时将迭代器传给它
1
2
3
4
5
6
7
int main() {
int x[10];
for (int i = 0; i < 10; i++)
x[i] = i;
Array_iterator<int> it(x, 10);
cout << sum(it) << endl;
}
  • 不能使用如下的调用方式:
    • cout << sum(Array_iterator<int>(x, 10)) << endl
    • 子表达式Array_iterator<int>(x, 10)不是左值,没有绑定其上的非const引用
  • 每次遍历sum函数中的内部循环时都需要调用虚函数: 动态绑定(开销较大,尤其对象复杂时)

    • 可以取消动态绑定减少额外开销,但需要在编译时知道相加元素类型
  • 另一种实现方式(不采用继承)

    • 让sum函数有两种类型的参数: 迭代器类型以及被加对象的类型
1
2
template <class Iter, class T>
T sum(Iter it) {/*...*/}
  • 并不奏效: 定义了一个返回类型与参数无关的函数
    • sum(x)的类型独立于x的类型(可能在C++中非法)
    • 需要检查大量上下文条件才有办法判断表达式的类型
  • solution: 定义sum接收一个对求和结果的引用

1
2
3
4
5
6
template <class T, class Iter>
void sum2(T& result, Iter it) {
result = 0;
while (ir.valid())
result += ir.next();
}
  • 重写main程序
1
2
3
4
5
6
7
8
int main() {
int x[10];
for (int i = 0; i < 10; ++i)
x[i] = i;
int r;
sum2(r, Iterator<int>(x, 10));
cout << r << endl;
}
  • 可以通过保留sum接口使得原来的主程序可以运行
1
2
3
4
5
6
7
8
9
10
11
12
13
template <class T>
T sum(T* p, int n) {
T r = 0;
sum2(r, Iterator<T>(p, n));
return r;
}
int main() {
int x[10];
for (int i = 0; i < 10; ++i)
x[i] = i;
cout << sum(x, 10) << endl;
}

实证

  • 利用sum2对一个从istream中生成的、数量不限的数字集合求和

  • 可以当做迭代器的类: Reader

    • Reader<T>对象从一个istream中读取一个T值的序列
  • 必须在读取数据前检查istream中是否还有数据存在

    • 从istream中读取数据看读取是否有效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template <class T>
class Reader {
public:
Reader(istream& is): i(is) { advance(); }
int valid() const {
return status;
}
T next() {
T result = data;
advance();
return result;
}
private:
istream& i;
int status;
T data;
void advance() {
i >> data;
status = i != 0;
}
};
  • 每个Reader对象绑定一个给予构造函数的istream的引用
    • Reader<double>(cin)是一个从cin中取回double值的Reader对象
  • 从输入中读取的数字相加

1
2
3
4
5
6
int main() {
cout << "Enter numbers: " << endl;
double r = 0;
sum2(r, Reader<double>(cin));
cout << r << endl;
}

Summary

  • 任何一个大规模系统的关键在于将它划分成可以独立处理的小模块
    • 在小模块之间定义清晰的接口
  • 学生注册系统:

    • 窗口接口 + 数据库接口
1
2
3
4
5
6
7
8
9
10
11
template <class W, class DB>
class Registration {
public:
// 学生注册所需的任何操作
};
// 为正在使用的任何窗口和数据库系统实例化一个Registration对象
int main() {
Registration<wizzy_window, dazzling_DB> r;
// ...
}
  • 只要wizzy_window满足窗口系统的规范,dazzling_DB满足数据库系统的规范

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