上一篇遺留的問題
在上一篇中我們實現(xiàn)了一個類似內(nèi)建數(shù)組的容器芒粹,但是這個容器包含了內(nèi)建數(shù)組的缺陷
- 由于
operator[]
返回的類型T&
導致用戶可以獲取到容器內(nèi)部元素的地址,在容器不存在以后這個指針依然存在。 - 由于維護了容器到數(shù)據(jù)的指針關系嘉竟,我們過多的暴漏了容器的內(nèi)部機制徙融。用戶可以使用指針直接訪問容器內(nèi)部毁菱,一旦容器內(nèi)部占用的內(nèi)存發(fā)生變化,將導致用戶錯誤衷佃。導致resize這類的函數(shù)很難實現(xiàn)趟卸。
模擬指針
c++中最基本的設計原則就是使用類來表示概念。為了不讓用戶直接操作裸指針氏义,我們需要引入一個中間層來封裝锄列。這個類應該包含一個指向Array的指針,還應該包含一個下標惯悠,從而可以識別一個Array及其內(nèi)部空間邻邮。
template<typename T>
class Pointer
{
private:
unsigned int sub;
Array<T>* ap;
};
現(xiàn)在考慮這個類需要哪些操作。首先從:構造函數(shù)克婶,拷貝構造函數(shù)饶囚,賦值運算符帕翻,析構函數(shù)考慮是一個很好的起點。
我們的Pointer需要默認構造函數(shù)嗎萝风?可能用戶需要定義Pointer的數(shù)組嘀掸,所以應該有。但是ap
和sub
的默認值應該設置為多少呢规惰?我們可以假設默認初始化的Pointer不指向任何容器睬塌。
然后考慮我們是不是需要兩個參數(shù)將成員都初始化呢?我們的用戶可能希望既可以Pointer p(array);
又可以Pointer p(array, 4);
歇万。前者定義一個指向第零個元素的Pointer對象揩晴,后者指向指定元素的Pointer對象。
銷毀Pointer對象時贪磺,由于我們持有了Array<T>的指針硫兰,但是我們不擁有這個對象,所以沒啥可做的寒锚。復制構造函數(shù)和賦值運算符也可以是用默認的實現(xiàn)
template<typename T>
class Pointer
{
public:
Pointer():ap(nullptr),sub(0) {}
Pointer(Array<T>& a, unsigned int n = 0): ap(a), sub(n) {} // 使用默認參數(shù)
Pointer(const Pointer<T>&) = default;
~Pointer() = default;
Pointer<T>& operator=(const Pointer<T>&) = default;
private:
unsigned int sub;
Array<T>* ap;
};
獲取數(shù)據(jù)
因為我們的Pointer類是模仿指針的劫映,所以很容易就能想到下面的方案:
template<typename T>
class Pointer
{
public:
T& operator*() {
if (ap == nullptr) {
throw "* of unbound Pointer";
}
return (*ap)[sub];
}
};
不過我們再次向用戶暴漏了隱藏在Pointer類中Array中數(shù)據(jù)結構。
如果我們讓operator*()
放回T
類型刹前,將導致元素的復制泳赋,但是將禁止掉*p = new T();
的功能。返回引用也很難防止用戶采用T* tp = &*p;
來獲取Array內(nèi)部數(shù)據(jù)元素的地址喇喉。我們引入了中間層祖今,但是問題還是沒有解決。這里我們會發(fā)現(xiàn)我們之所以定義Pointer類就是為了防止用戶直接操作指針拣技,如果我們的Pointer類完美的實現(xiàn)了指針的操作千诬,用戶就沒有必要在使用指針了,所以我們這里的選擇依舊是采用返回引用的形式膏斤。但是這里還有個空懸Pointer的問題存在
空懸Pointer
void f() {
Array<int> ap = new Array<int>(10);
Pointer<int> p(*ap, 3);
delete ap;
*p = 42; // 這里會發(fā)生什么大渤?此時p指向的內(nèi)存還是有效的嗎?
}
由于delete ap
已經(jīng)釋放了持有的資源掸绞,所以*p = 42;
會導致非法的內(nèi)存引用泵三,可能會引起未定義行為。
現(xiàn)在讓我們回顧一下:
- 為了解決Array的resize問題衔掸,我們引入了中間層Pointer來模擬指針烫幕。
- 為了不讓用戶直接操作指針,我們還是通過引入中間層Pointer來模擬指針的操作敞映。
但是卻保留了指針的空懸問題较曼。如果我們可以保證在Array被delete的時候,如果還有Pointer指向資源就不進行釋放就解決了空懸Pointer帶來的問題振愿。由此我們可以想到通過引用計數(shù)來實現(xiàn)捷犹。即:Array析構的時候僅僅修改引用計數(shù)弛饭,不釋放資源。所以我們的Array類需要的不再是T* data
屬性萍歉,而是指向持有資源侣颂,并包含引用計數(shù)的類。所以我們需要第三個類來處理這個問題枪孩。
因為持有Array本來持有的數(shù)據(jù)憔晒,所以這個類的名字我們可以叫:Array_data
template<typename T>
class Array_data
{
friend class Array<T>; // 操作引用計數(shù)
friend class Pointer<T>;
Array_data(unsigned int n = 0): sz(n), use(1), data(new T[n]) {} // 通過設置默認參數(shù)實現(xiàn),省去了寫多個構造函數(shù)的問題蔑舞。
~Array_data() { delete data; }
Array_data(const Array_data& other) = delete;
Array_data& operator=(const Array_data& other) = delete; // 我們希望對于內(nèi)存中的一塊空間只有一個Array_data的對象持有
const T& operator[](unsigned int i) const { // 用于讀取
if (i >= sz) {
throw "Array index out of range";
}
std::cout << "into const []" << std::endl;
return data[i];
}
T& operator[](unsigned int i) { // 用于修改
if (i >= sz) {
throw "Array index out of range";
}
std::cout << "into non-const []" << std::endl;
return data[i];
}
T* data;
unsigned int sz;
int use;
};
現(xiàn)在我們來看Array類的實現(xiàn)拒担。因為定義了包含引用計數(shù)的Array_data類,所以Array和Pointer都應該持有指向Array_data的指針攻询,但是Pointer的構造函數(shù)的參數(shù)是:Pointer(Array<T>&, unsigned int)
所以Pointer應該可以直接訪問Array的私有成員Array_data<T>* data
template <typename T>
class Array
{
friend class Pointer<T>;
public:
Array(unsigned n): data(new Array_data<T>(n)) {}
~Array() {
if (-- data->use == 0)
delete data;
}
Array(const Array& other) = delete;
Array<T>& operator=(const Array<T>&) = delete;
const T& operator[](unsigned int i) const {
return (*data)[i]; // 直接調(diào)用Array_data的operator[],也可以寫作: data->operator[](n);
}
T& operator[](unsigned int i) {
return (*data)[i]; // 直接調(diào)用Array_data的operator[],也可以寫作: data->operator[](n);
}
private:
Array_data<T>* data;
};
按照上面所說从撼,Pointer應該也擁有一個Array_data<T>的指針,同時處理引用計數(shù)問題钧栖。
class Pointer
{
public:
Pointer():sub(0),ap(nullptr) {}
Pointer(Array<T>& a, unsigned int i = 0): ap(a.data), sub(i) {}
~Pointer() {
if (ap && --ap->use == 0) {
delete ap;
}
}
// 注意這里是復制了ap這個指針低零,不會調(diào)用Array_data的拷貝構造函數(shù)。
Pointer(const Pointer& p):ap(p.ap),sub(p.sub) {
if(ap)
++ap->use;
}
Pointer& operator=(const Pointer& other) {
if(other.ap)
other.ap->use++;
if(ap && --ap->use == 0)
delete ap;
ap = other.ap;
sub = other.sub;
return *this;
}
T& operator*() const {
if(ap == nullptr) {
throw "* of unbound Pointer";
}
return (*ap)[sub]; // 這里調(diào)用了Array_data的 T& operator[](unsigned int i)
}
T& operator*() {
if(ap == nullptr) {
throw "* of unbound Pointer";
}
std::cout << "in here!" << std::endl;
return (*ap)[sub]; // 這里調(diào)用了Array_data的 T& operator[](unsigned int i)
}
private:
unsigned int sub;
Array_data<T>* ap;
};
指向const Array的Pointer
雖然Pointer類針對operator*
進行了const 和非const的重載桐经,但是調(diào)用*p
的表達式只調(diào)用了Array_data的T& operator[](unsigned int i)
。所以當
void f(const Array<int>& a) {
Pointer<int> p(a); // 這里會編譯報錯浙滤!
}
時會出現(xiàn)問題阴挣。因為我們的Pointer的構造函數(shù)沒有Pointer(const Array<T>&);
接受const Array&參數(shù)的。所以假設我們重載一下:
template<typename T>
class Pointer
{
public:
/*其他都不變*/
Pointer(const Array<T>& a) : ap(a.data),sub(0) {}
};
此時可以編譯通過了纺腊,但是如果我們執(zhí)行*p
實際調(diào)用的還是Array_data的T& operator[](unsigned int i)
畔咧,我們只是假裝綁定到了一個const Array。依舊可以修改const Array中的元素揖膜。即:*p = 10
可以編譯過誓沸。
所以我們需要一個可以綁定到const Array的類似Pointer的類,同時確保調(diào)用const T& operator[]
的重載壹粟。
同時為了模擬內(nèi)建指針:
int n = 11;
int* p4 = &n;
const int* p5 = p4;
這種隱式轉換的操作拜隧,Pointer可以轉換到這個新類但不產(chǎn)生副作用。
Pointer類與這個類存在相似性趁仙,僅僅是使用的重載不一樣洪添,所以我們采用繼承的方式實現(xiàn):
template <typename T>
class Const_pointer
{
public:
Const_pointer():ap(nullptr), sub(0) {}
Const_pointer(const Array<T>& a, unsigned int i = 0):ap(a.data), sub(i) {}
Const_pointer(const Const_pointer& other):ap(other.ap), sub(other.sub) {
if (ap != nullptr) {
++ap->use;
}
}
Const_pointer& operator=(const Const_pointer& other) {
if (other.ap != nullptr) {
other.ap->use++;
}
if (ap != nullptr && --ap->use == 0) {
delete ap;
}
ap = other.ap;
sub = other.sub;
return *this;
}
~Const_pointer() {
if (ap != nullptr && --ap->use == 0) {
delete ap;
}
}
const T& operator*() const {
if (ap == nullptr) {
throw "* of unbound Const_pointer!";
}
std::cout << "Const_pointer *" << std::endl;
return ((const Array_data<T>*)ap)->operator[](sub); // 保證調(diào)用的是const T& Array_data<T>::operater[](unsigned int i) const;
}
protected:
unsigned int sub;
Array_data<T>* ap;
};
template <typename T>
class Pointer : public Const_pointer<T>
{
public:
Pointer(){}
Pointer(Array<T>& a, unsigned int i = 0): Const_pointer<T>(a, i) {}
T& operator*() const {
// 這里需要知名this->ap否則會報錯。
if(this->ap == nullptr) {
throw "* of unbound Pointer";
}
return (*(this->ap))[this->sub]; // 這里調(diào)用了Array_data的 T& operator[](unsigned int i)
}
};
現(xiàn)在執(zhí)行
Array<int>* array_ptr = new Array<int>(10);
Pointer<int> p(*array_ptr, 1);
*p = 10;
Const_pointer<int> p1 = p;
*p1 = 10; // 這里是非法的雀费。
將會遇到編譯錯誤干奢。上面這種處理方式也可以通過實現(xiàn)Pointer(const Array& a)重載構造函數(shù)綁定到const Array。然后重載const T& operator*() const
一樣可以得到這個結果盏袄。但是如果拆分為兩個有繼承關系的類更能描述清楚內(nèi)建指針和const 指針的關系忿峻。
有用的增強操作
現(xiàn)在我們可以為Array編寫resize方法了薄啥,因為Array不直接持有資源了,所以可以交給Array_data來完成操作逛尚,這樣不會導致Array中data指針失效垄惧,同時也不會導致Pointer中ap指針失效,這樣就做到了進行resize操作不會影響用戶保存的Pointer對象失效黑低。
template <typename T>
void Array<T>::resize(unsigned int new_size) {
data->resize(new_size);
}
template <typename T>
void Array_data<T>::resize(unsigned int new_size) {
if (new_size == sz) return;
T* old_data = data;
data = new T[new_size];
copy(old_data, sz > new_size ? new_size : sz);
delete [] old_data;
sz = new_size;
}
template <typename T>
void Array_data<T>::copy(T *arr, unsigned int size) {
for (int i = 0; i < size; ++i) {
*(data+i) = *(arr+i);
}
}
然后為了支持用戶可以創(chuàng)建Array的Array赘艳,又因為我們應該復制的是元素,所以我們應該要來實現(xiàn)Array的復制構造函數(shù)和賦值運算符克握。
template <typename T>
Array<T>::Array(const Array &other):data(new Array_data<T>(other.data->sz)) {
data->copy(other.data->data, other.data->sz);
}
template<typename T>
Array<T> &Array<T>::operator=(const Array<T> &other) {
if(*this != other) {
data->clone(*other.data);
}
return *this;
}
template <typename T>
void Array_data<T>::clone(const Array_data<T> a) {
delete [] data;
data = new T[a.sz];
sz = a.sz;
copy(a.data, sz);
}
總結
- 我們通過引入Pointer類中間層解決了Array resize以后指針失效的問題蕾管。
- 通過解決Pointer的空懸問題,我們采用了引用計數(shù)的方案進行優(yōu)化菩暗。
- 解決綁定到const Array的問題掰曾,我們采用了Const_pointer,Pointer的繼承關系模擬指針和const指針停团。
- 通過引入中間層旷坦,我們的用戶現(xiàn)在可以只使用Pointer來替代內(nèi)建指針了,但是用戶現(xiàn)在還不能進行遍歷操作佑稠。想要進行遍歷仍然需要使用內(nèi)建指針進行操作秒梅。
下一篇我們將通過迭代器來實現(xiàn)遍歷的問題。