設計容器類
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;