访问容器中的元素

上一篇遗留的问题

在上一篇中我们实现了一个类似内建数组的容器,但是这个容器包含了内建数组的缺陷

  • 由于operator[]返回的类型T&导致用户可以获取到容器内部元素的地址,在容器不存在以后这个指针依然存在。
  • 由于维护了容器到数据的指针关系,我们过多的暴漏了容器的内部机制。用户可以使用指针直接访问容器内部,一旦容器内部占用的内存发生变化,将导致用户错误。导致resize这类的函数很难实现。

模拟指针

c++中最基本的设计原则就是使用类来表示概念。为了不让用户直接操作裸指针,我们需要引入一个中间层来封装。这个类应该包含一个指向Array的指针,还应该包含一个下标,从而可以识别一个Array及其内部空间。

template
class Pointer
{
private:
    unsigned int sub;
    Array* ap;
};

现在考虑这个类需要哪些操作。首先从:构造函数,拷贝构造函数,赋值运算符,析构函数考虑是一个很好的起点。
我们的Pointer需要默认构造函数吗?可能用户需要定义Pointer的数组,所以应该有。但是apsub的默认值应该设置为多少呢?我们可以假设默认初始化的Pointer不指向任何容器。
然后考虑我们是不是需要两个参数将成员都初始化呢?我们的用户可能希望既可以Pointer p(array);又可以Pointer p(array, 4);。前者定义一个指向第零个元素的Pointer对象,后者指向指定元素的Pointer对象。
销毁Pointer对象时,由于我们持有了Array的指针,但是我们不拥有这个对象,所以没啥可做的。复制构造函数和赋值运算符也可以是用默认的实现

template
class Pointer
{
public:
    Pointer():ap(nullptr),sub(0) {}
    Pointer(Array& a, unsigned int n = 0): ap(a), sub(n) {} // 使用默认参数
    Pointer(const Pointer&) = default;
    ~Pointer() = default;
    Pointer& operator=(const Pointer&) = default;
private:
    unsigned int sub;
    Array* ap;
};

获取数据

因为我们的Pointer类是模仿指针的,所以很容易就能想到下面的方案:

template
class Pointer 
{
public:
    T& operator*() {
        if (ap == nullptr) {
            throw "* of unbound Pointer";
        }
        return (*ap)[sub];
    }
};

不过我们再次向用户暴漏了隐藏在Pointer类中Array中数据结构。
如果我们让operator*()放回T类型,将导致元素的复制,但是将禁止掉*p = new T();的功能。返回引用也很难防止用户采用T* tp = &*p;来获取Array内部数据元素的地址。我们引入了中间层,但是问题还是没有解决。这里我们会发现我们之所以定义Pointer类就是为了防止用户直接操作指针,如果我们的Pointer类完美的实现了指针的操作,用户就没有必要在使用指针了,所以我们这里的选择依旧是采用返回引用的形式。但是这里还有个空悬Pointer的问题存在

空悬Pointer

void f() {
    Array ap = new Array(10);
    Pointer p(*ap, 3);
    delete ap;
    *p = 42; // 这里会发生什么?此时p指向的内存还是有效的吗?
}

由于delete ap已经释放了持有的资源,所以*p = 42;会导致非法的内存引用,可能会引起未定义行为。
现在让我们回顾一下:

  1. 为了解决Array的resize问题,我们引入了中间层Pointer来模拟指针。
  2. 为了不让用户直接操作指针,我们还是通过引入中间层Pointer来模拟指针的操作。
    但是却保留了指针的空悬问题。如果我们可以保证在Array被delete的时候,如果还有Pointer指向资源就不进行释放就解决了空悬Pointer带来的问题。由此我们可以想到通过引用计数来实现。即:Array析构的时候仅仅修改引用计数,不释放资源。所以我们的Array类需要的不再是T* data属性,而是指向持有资源,并包含引用计数的类。所以我们需要第三个类来处理这个问题。
    因为持有Array本来持有的数据,所以这个类的名字我们可以叫:Array_data
template
class Array_data
{
    friend class Array; // 操作引用计数
    friend class Pointer;

    Array_data(unsigned int n = 0): sz(n), use(1), data(new T[n]) {} // 通过设置默认参数实现,省去了写多个构造函数的问题。
    ~Array_data() { delete data; }
    Array_data(const Array_data& other) = delete;
    Array_data& operator=(const Array_data& other) = delete; // 我们希望对于内存中的一块空间只有一个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;
};

现在我们来看Array类的实现。因为定义了包含引用计数的Array_data类,所以Array和Pointer都应该持有指向Array_data的指针,但是Pointer的构造函数的参数是:Pointer(Array&, unsigned int)所以Pointer应该可以直接访问Array的私有成员Array_data* data

template 
class Array
{
    friend class Pointer;
public:
    Array(unsigned n): data(new Array_data(n)) {}
    ~Array() {
        if (-- data->use == 0)
            delete data;
    }
    Array(const Array& other) = delete;
    Array& operator=(const Array&) = delete;
    const T& operator[](unsigned int i) const {
        return (*data)[i]; // 直接调用Array_data的operator[],也可以写作: data->operator[](n);
    }
    T& operator[](unsigned int i) {
        return (*data)[i]; // 直接调用Array_data的operator[],也可以写作: data->operator[](n);
    }
private:
    Array_data* data;
};

按照上面所说,Pointer应该也拥有一个Array_data的指针,同时处理引用计数问题。

class Pointer
{
public:
    Pointer():sub(0),ap(nullptr) {}
    Pointer(Array& a, unsigned int i = 0): ap(a.data), sub(i) {}
    ~Pointer() {
        if (ap && --ap->use == 0) {
            delete ap;
        }
    }
    // 注意这里是复制了ap这个指针,不会调用Array_data的拷贝构造函数。
    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]; // 这里调用了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]; // 这里调用了Array_data的 T& operator[](unsigned int i)
    }
private:
    unsigned int sub;
    Array_data* ap;
};

指向const Array的Pointer

虽然Pointer类针对operator*进行了const 和非const的重载,但是调用*p的表达式只调用了Array_data的T& operator[](unsigned int i)。所以当

void f(const Array& a) {
    Pointer p(a); // 这里会编译报错!
}

时会出现问题。因为我们的Pointer的构造函数没有Pointer(const Array&);接受const Array&参数的。所以假设我们重载一下:

template
class Pointer
{
public:
    /*其他都不变*/
    Pointer(const Array& a) : ap(a.data),sub(0) {}
};

此时可以编译通过了,但是如果我们执行*p实际调用的还是Array_data的T& operator[](unsigned int i),我们只是假装绑定到了一个const Array。依旧可以修改const Array中的元素。即:*p = 10可以编译过。
所以我们需要一个可以绑定到const Array的类似Pointer的类,同时确保调用const T& operator[]的重载。
同时为了模拟内建指针:

    int n = 11;
    int* p4 = &n;
    const int* p5 = p4;

这种隐式转换的操作,Pointer可以转换到这个新类但不产生副作用。
Pointer类与这个类存在相似性,仅仅是使用的重载不一样,所以我们采用继承的方式实现:

template 
class Const_pointer
{
public:
    Const_pointer():ap(nullptr), sub(0) {}
    Const_pointer(const Array& 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*)ap)->operator[](sub); // 保证调用的是const T& Array_data::operater[](unsigned int i) const;
    }

protected:
    unsigned int sub;
    Array_data* ap;
};

template 
class Pointer : public Const_pointer
{
public:
    Pointer(){}
    Pointer(Array& a, unsigned int i = 0): Const_pointer(a, i) {}

    T& operator*() const {
        // 这里需要知名this->ap否则会报错。
        if(this->ap == nullptr) {
            throw "* of unbound Pointer";
        }
        return (*(this->ap))[this->sub]; // 这里调用了Array_data的 T& operator[](unsigned int i)
    }
};

现在执行

    Array* array_ptr = new Array(10);
    Pointer p(*array_ptr, 1);
    *p = 10;
    Const_pointer p1 = p;
    *p1 = 10; // 这里是非法的。

将会遇到编译错误。上面这种处理方式也可以通过实现Pointer(const Array& a)重载构造函数绑定到const Array。然后重载const T& operator*() const一样可以得到这个结果。但是如果拆分为两个有继承关系的类更能描述清楚内建指针和const 指针的关系。

有用的增强操作

现在我们可以为Array编写resize方法了,因为Array不直接持有资源了,所以可以交给Array_data来完成操作,这样不会导致Array中data指针失效,同时也不会导致Pointer中ap指针失效,这样就做到了进行resize操作不会影响用户保存的Pointer对象失效。

template 
void Array::resize(unsigned int new_size) {
    data->resize(new_size);
}

template 
void Array_data::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 
void Array_data::copy(T *arr, unsigned int size) {
    for (int i = 0; i < size; ++i) {
        *(data+i) = *(arr+i);
    }
}

然后为了支持用户可以创建Array的Array,又因为我们应该复制的是元素,所以我们应该要来实现Array的复制构造函数和赋值运算符。

template 
Array::Array(const Array &other):data(new Array_data(other.data->sz)) {
    data->copy(other.data->data, other.data->sz);
}

template
Array &Array::operator=(const Array &other) {
    if(*this != other)  {
        data->clone(*other.data);
    }
    return *this;
}

template 
void Array_data::clone(const Array_data a) {
    delete [] data;
    data = new T[a.sz];
    sz = a.sz;
    copy(a.data, sz);
}

总结

  1. 我们通过引入Pointer类中间层解决了Array resize以后指针失效的问题。
  2. 通过解决Pointer的空悬问题,我们采用了引用计数的方案进行优化。
  3. 解决绑定到const Array的问题,我们采用了Const_pointer,Pointer的继承关系模拟指针和const指针。
  4. 通过引入中间层,我们的用户现在可以只使用Pointer来替代内建指针了,但是用户现在还不能进行遍历操作。想要进行遍历仍然需要使用内建指针进行操作。
    下一篇我们将通过迭代器来实现遍历的问题。

你可能感兴趣的:(访问容器中的元素)