訪問容器中的元素

上一篇遺留的問題

在上一篇中我們實現(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ù)組嘀掸,所以應該有。但是apsub的默認值應該設置為多少呢规惰?我們可以假設默認初始化的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)在讓我們回顧一下:

  1. 為了解決Array的resize問題衔掸,我們引入了中間層Pointer來模擬指針烫幕。
  2. 為了不讓用戶直接操作指針,我們還是通過引入中間層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);
}

總結

  1. 我們通過引入Pointer類中間層解決了Array resize以后指針失效的問題蕾管。
  2. 通過解決Pointer的空懸問題,我們采用了引用計數(shù)的方案進行優(yōu)化菩暗。
  3. 解決綁定到const Array的問題掰曾,我們采用了Const_pointer,Pointer的繼承關系模擬指針和const指針停团。
  4. 通過引入中間層旷坦,我們的用戶現(xiàn)在可以只使用Pointer來替代內(nèi)建指針了,但是用戶現(xiàn)在還不能進行遍歷操作佑稠。想要進行遍歷仍然需要使用內(nèi)建指針進行操作秒梅。
    下一篇我們將通過迭代器來實現(xiàn)遍歷的問題。
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末舌胶,一起剝皮案震驚了整個濱河市捆蜀,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌幔嫂,老刑警劉巖辆它,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異履恩,居然都是意外死亡锰茉,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進店門切心,熙熙樓的掌柜王于貴愁眉苦臉地迎上來飒筑,“玉大人,你說我怎么就攤上這事绽昏⊙锼” “怎么了?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵而涉,是天一觀的道長著瓶。 經(jīng)常有香客問我,道長啼县,這世上最難降的妖魔是什么材原? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任乒裆,我火速辦了婚禮见间,結果婚禮上墓猎,老公的妹妹穿的比我還像新娘燕锥。我一直安慰自己,他們只是感情好威酒,可當我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布窑睁。 她就那樣靜靜地躺著葵孤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪尤仍。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天苏遥,我揣著相機與錄音赡模,去河邊找鬼。 笑死教硫,一個胖子當著我的面吹牛欺缘,可吹牛的內(nèi)容都是我干的挤安。 我是一名探鬼主播,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼嫩絮,長吁一口氣:“原來是場噩夢啊……” “哼围肥!你這毒婦竟也來了?” 一聲冷哼從身側響起置尔,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤氢伟,失蹤者是張志新(化名)和其女友劉穎幽歼,沒想到半個月后谬盐,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡皇型,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年砸烦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寡键。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡雪隧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出藕畔,到底是詐尸還是另有隱情庄拇,我是刑警寧澤,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布溶弟,位于F島的核電站瞭郑,受9級特大地震影響,放射性物質發(fā)生泄漏屈张。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一碳抄、第九天 我趴在偏房一處隱蔽的房頂上張望场绿。 院中可真熱鬧,春花似錦璧尸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至哼拔,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間譬正,已是汗流浹背檬姥。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留抒巢,地道東北人秉犹。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像型诚,于是被迫代替她去往敵國和親鸳劳。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,901評論 2 355

推薦閱讀更多精彩內(nèi)容