设计容器类
1. 设计原则
a. 包含什么?
即容器中放入什么东西,是包含对象吗?包含一个对象的确切含义是什么呢?
容器应该包含放在其中的对象的副本,而不是原对象本身。即可以把指向该对象的指针放入到容器中。
b. 复制容器意味着什么?
容器称为模板,而容器内的对象的类型就是模板参数。复制容器是不是也应该复制包含在容器中的对象呢?
Container<T> c1;
Container<T> c2(c1);
或者
Container<T> c2;
c2=c1;
如果复制c2到c1会导致c1和c2指向同一底层对象,那么对c2的改变也会映射到c1中。如果我们定义复制意味着把c2的值放入c1中,则c2的改变对c1就不会影响了。
对于C/C++中的内建集合都实现了两种不同的方法:复制对于这两种方法来说含义各不相同:
结构体实现值的语义;复制完成之后,两个变量都有这个值的独立的副本。
数组实现引用语义:复制完成之后,两个变量都引用同一个底层对象(效率高)。
引用计数和写时复制技术可以减少容器复制的开销。
复制容器定义为复制存储在容器中的值。
c. 怎么样获取容器中的元素?
即当从Container取出对象时候,应该得到类型T还是类型T&的对象呢?
获取容器中对象时复制操作带来的额外开销要比向容器中插入对象时的复制操作的额外开销大得多,当对象本身是某种集合时,更为突出。
如果能够修改容器中的对象对我们来说很重要,那么容器必须提供对象的引用,否则必须提供另外一种方法来改变容器所包含的对象。
d. 怎么样区分读和写?
可以用operator[]用于读,另外再定义一个函数来写元素。但是单独定义的update函数用来写元素对包含容器的容器可能毫无用处。对于容器的容器最好的折中办法就是允许获得引用。并提醒用户只能在创建了引用之后才能使用。
e. 怎么样处理容器增长?
要避免存储一个不存在的元素和取出一个不存在的元素。
如果试图在没有创建元素前就想访问该元素的时候可以抛出异常或者提供一种显示创建先的容器元素,譬如用缺省构造函数返回的值。
如何为容器中的元素分配内存?
例如往数组的尾部添加一个元素,可以按区块增加容器的大小,新的内存空间必须一次分配完毕。但在选择适当的计算新块大小策略的时候要注意。
什么时候讲内存还给系统也很重要。
f. 容器支持哪些操作?
例如是否允许容器包含容器?如果容器内部复制元素,而元素另一种容器,则该种容器必须能够被复制。
除非容器有一个缺省构造函数,否则不可能创建一个容器数组。
顺序地遍历容器中的所有元素。(迭代器)
g. 怎样设想容器元素的类型?
容器至少需要复制、赋和销毁类型为T的对象等操作。根据容器的用途来决定T应具备的操作。
h. 容器和继承
假设有一个基类B和一个派生类D,把一个D对象放入到一个Container<B>中会发生什么呢?
例如:
Class Vehicle{};
Class Airplane:public Vehicle{};
如果有Container<Vehicle>,想放入一个Airplane去,只能得到Airplane的Vihicle部分的副本。如果要记住整个Airplane应该使用Container<Vehicle *>.
如果Contain<Airplane>从Container<Vehicle>继承。
Vehicle v;
Container<Airplane> ca;
Container<Vehicle> &vp =ca;
Vp.insert(v);这样就可以往Container<Vehicle>插入普通的Vehicle.实际上我们并不想这样。
不同类型的容器不应该存在继承关系;Container<Vehicle>和Container<Airplane>是完全不同的类。
2. 设计实例(类似数组的类)
#ifndef _ARRAY_H #define _ARRAY_H #include <iostream> using namespace std; namespace Meditation { template<class T> class Array { public: Array():data(0),sz(0){} //缺省构造函数,保证new T[size] Array(unsigned int size):sz(size),data(new T[size]) { } const T& operator[](unsigned int n)const { if(n >= sz || data == 0) throw "Array subscript out of range"; return data[n]; } T& operator[](unsigned int n) { if(n >= sz || data == 0) throw "Array subscript out of range"; return data[n]; } //数组到指向它的第一个元素的指针的转换 operator const T*() const { return data; } operator T*() { return data; } private: T *data; unsigned int sz; Array(const Array &a); Array&operator=(const Array&); //禁止复制和赋值 }; } #endif
上面类的特点:1.禁止进行复制和赋值
2.允许创建new T[size];
3.提供了从T*到const T*
缺陷:包含元素的Array消失后,它的元素的地址还存在
,允许用户访问它元素的地址。如:
Void f()
{
Int *p
{
Array<int> x(20);
P=&x[10]
}
Cout<<*p<<endl;//此处有问题
}
Array对象x超出了作用域,而p还指向它的一个元素。
a. 访问容器中的元素
上面的容器使得用户能够狠轻易得到一个指向Array内部的指针,即使Array本身不存在了,这个指针仍然保留在那里。如果Array占用的内存发生变化,可能会导致用户错误。,怎么保留指针的表达能力同时又避免这些不足呢?
定义一个识别Array和内部空间的类,这个类应该包括一个下标和一个指向相应Array的指针。这个类的对象的行为类似指针。
template<class T> class Pointer { public: Pointer(Array<T>& a,unsigned int n=0):ap(&a),sub(0) { } Pointer():ap(0),sub(0) { } T operator*()const { if(ap == 0) throw "* of unbound Pointer"; return (*ap)[sub]; } void update(const T&t) { if (ap == 0) { throw "update of unbound Pointer"; } (*ap)[sub] = t; } T operator[](unsigned int n) const { if(n >= sz) { throw "Array subscript out of range"; } return data[n]; } private: Array<T> *ap; unsigned int sub; };
上面使用update而不是operator[]来更新容器中的值,会让容器包含容器失效。
b. 遗留问题
上面只是增加了一个防止出错的中间层,如果Array不存在了,还可能存在一个指向它的某个元素的空悬Pointer.使用Pointer和使用指针一样会造成混乱。如:
Array<int>* ap= new Array<int>(10);
Pointer<int> p(*ap,s);
Delete ap;
*p = 42;
删除了Array,然后再试着给这个元素赋值,这样的错误怎么避免呢?
如果我们能确保只要存在一个指向某个Array对象的Pointer对象,该Array对象就不会消失,情况会不会改变呢?
怎么样设计Array,使得删除Array时不会真正使Array消失?
需要引入一个额外的中间层,使删除了Array对象后仍然保留数据,让Array对象指向数据而不是包含数据。
定义三个类:Array,Pointer,Array_data。每个Array对象都指向一个Array_data对象。
现在还是可以删除Array,每个Pointer对象都指向一个Array_data对象而不是一个Array对象。
何时删除Array_data对象?当没有Array或者Pointer指向某个Array_data对象,就先删除这个Array_data对象。
做法:跟踪这些对象的数目,每个Array_data对象都会包含一个引用计数。这个计数器在创建Array_data的时候设置为1,每次指向这个Array_data的Array或者Pointer被创建时增加1,在指向它的Array或者Pointer被删除时减1.如果引用计数为0,销毁Array_data对象本身。
template<class T> class Array_data { friend class Array<T>; friend class Ptr_to_const<T>; friend class Pointer<T>; Array_data(unsigned size = 0): sz(size), data(new T[size]), use(1) { } ~Array_data() { delete [] data; } const T& operator[](unsigned n) const { if (n >= sz) throw "Array subscript out of range"; return data[n]; } T& operator[](unsigned n) { if (n >= sz) throw "Array subscript out of range"; return data[n]; } void resize(unsigned); void copy(T*, unsigned); void grow(unsigned); void clone(const Array_data&, unsigned); //禁止复制 Array_data(const Array_data&); // not implemented Array_data& operator=(const Array_data&); // not implemented T* data; unsigned sz; int use; };
Array类必须有一个Array_data<T> *,而不是T*,Array<T> *可以将大多数操作转给相应的Array_data对象:
接下来定义一个Pointer ,指向某个Array_data对象,而不是指向Array对象。
template<class T> class Array { friend class Ptr_to_const<T>; friend class Pointer<T>; public: Array(unsigned size): data(new Array_data<T>(size)) { } ~Array() { if (--data->use == 0) delete data; } const T& operator[](unsigned n) const { return (*data)[n]; } T& operator[](unsigned n) { return (*data)[n]; } void resize(unsigned n) { data->resize(n); } void reserve(unsigned new_sz) { if (new_sz >= data->sz) data->grow(new_sz); } Array(const Array& a): data(new Array_data<T>(a.data->sz)) { data->copy(a.data->data, a.data->sz); } Array& operator=(const Array& a) { if (this != &a) data->clone(*a.data, a.data->sz); return *this; } private: Array_data<T>* data; };
//为了能让Pointer指向const Array的元素,定义Ptr_to_const
template<class T> class Ptr_to_const { public: Ptr_to_const(const Array<T>& a, unsigned n = 0): ap(a.data), sub(n) { ++ap->use; } Ptr_to_const(): ap(0), sub(0) { } Ptr_to_const(const Ptr_to_const<T>& p): ap(p.ap), sub(p.sub) { if (ap) ++ap->use; } ~Ptr_to_const() { if (ap && --ap->use == 0) delete ap; } Ptr_to_const& operator=(const Ptr_to_const<T>& p) { if (p.ap) ++p.ap->use; if (ap && --ap->use == 0) delete ap; ap = p.ap; sub = p.sub; return *this; } const T& operator*() const { if (ap == 0) throw "* of unbound Ptr_to_const"; return (*ap)[sub]; } protected: Array_data<T>* ap; unsigned sub; }; template<class T> class Pointer: public Ptr_to_const<T> { public: Pointer(Array<T>& a, unsigned n = 0): Ptr_to_const<T>(a,n) { } T& operator*() const { if (ap == 0) throw "* of unbound Ptr_to_const"; return (*ap)[sub]; } };
现在可以安全的操作该容器了,如下:
Array<int> *ap =new Array<int>(10);
Pointer<int> p(*ap,5);
delete ap;
*p=42;