上一篇遗留的问题
在上一篇中我们实现了一个类似内建数组的容器,但是这个容器包含了内建数组的缺陷
- 由于
operator[]
返回的类型T&
导致用户可以获取到容器内部元素的地址,在容器不存在以后这个指针依然存在。 - 由于维护了容器到数据的指针关系,我们过多的暴漏了容器的内部机制。用户可以使用指针直接访问容器内部,一旦容器内部占用的内存发生变化,将导致用户错误。导致resize这类的函数很难实现。
模拟指针
c++中最基本的设计原则就是使用类来表示概念。为了不让用户直接操作裸指针,我们需要引入一个中间层来封装。这个类应该包含一个指向Array的指针,还应该包含一个下标,从而可以识别一个Array及其内部空间。
template
class Pointer
{
private:
unsigned int sub;
Array* ap;
};
现在考虑这个类需要哪些操作。首先从:构造函数,拷贝构造函数,赋值运算符,析构函数考虑是一个很好的起点。
我们的Pointer需要默认构造函数吗?可能用户需要定义Pointer的数组,所以应该有。但是ap
和sub
的默认值应该设置为多少呢?我们可以假设默认初始化的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;
会导致非法的内存引用,可能会引起未定义行为。
现在让我们回顾一下:
- 为了解决Array的resize问题,我们引入了中间层Pointer来模拟指针。
- 为了不让用户直接操作指针,我们还是通过引入中间层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
所以Pointer应该可以直接访问Array的私有成员Array_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);
}
总结
- 我们通过引入Pointer类中间层解决了Array resize以后指针失效的问题。
- 通过解决Pointer的空悬问题,我们采用了引用计数的方案进行优化。
- 解决绑定到const Array的问题,我们采用了Const_pointer,Pointer的继承关系模拟指针和const指针。
- 通过引入中间层,我们的用户现在可以只使用Pointer来替代内建指针了,但是用户现在还不能进行遍历操作。想要进行遍历仍然需要使用内建指针进行操作。
下一篇我们将通过迭代器来实现遍历的问题。