三根指针动态数组
任务要求
在面向对象程序设计这门课上,老师给我们布置了一个任务,要我们完成类似于STL中的vector容器,说白了就是创建一个动态数组。
分析
创建一个的方法可以说有很多,其中常见的有利用一个数组指针+数组大小的类创建,但是该种会造成插入与删除极其不方便的问题(一般来说,用这种方式创建的数组完成删除、增添等操作的时候都要重新申请一个数组然后赋值的方式)。于是可以想出另一种方式:利用一个数组指针+数组当前的大小+数组最大容量的方式创建类。
class myArray_
{
public:
int m_max_size = 0;
int m_pre_size = 0;
int* my_array;
public:
//ctor
myArray_();
myArray_(int size);
//functions
void push_back(int num);
void delet(int i); //i为下标位置,从0开始索引
void print();
};
其中my_arrary数组指针不用解释了吧,m_max_size就是数组的最大容量,在构造函数中会将它初始化为用户给出的大小的2倍(即构造函数中形参size的2倍),目的很简单,就是面对增加操作时,尽量少的重新设置指针申请新空间然后再赋值。有了m_max_size的话,只要当前数组的数量(m_pre_size)不超过m_max_size,就不用上述操作,而是可以简单地的my_array(m_ptr_size) = num;m_pre_size ++;就可以了。
以下是简单的实现,有问题请指出。
myArray_::myArray_()
{
m_max_size = 5; //缺省参数的构造函数,给出最小大小为5
m_pre_size = 0;
my_array = new int[m_max_size];
}
myArray_::myArray_(int size)
{
m_max_size = 2 * size;
my_array = new int[m_max_size];
m_pre_size = 0;
}
void myArray_::push_back(int num)
{
//首先要判断是否需要扩容
if (this->m_pre_size + 1 <= this->m_max_size) //不需要扩容很简单了
{
m_pre_size++;
my_array[m_pre_size - 1] = num;
}
else //需要扩容的情况,需要重新申请地址
{
m_max_size = 2 * m_max_size; //实行扩容操作,将最大容器扩大为原来的2倍
int *new_my_array = new int[m_max_size];
for (int i = 0; i < m_pre_size - 1; i++) //完成数组拷贝操作
{
new_my_array[i] = my_array[i];
}
m_pre_size++;
new_my_array[m_pre_size - 1] = num;
my_array = new_my_array;
}
}
void myArray_::delet(int pos)
{
if (m_pre_size == 0 || pos >=m_pre_size) //判断数组是否为空,为空则退出
{
return;
}
//不为空的情况
int* new_my_arrary = new int[m_max_size]; //重新申请地址
for (int i = 0; i < pos ; i++)
{
new_my_arrary[i] = my_array[i]; //拷贝
}
for (int i = pos; i < m_pre_size - 1; i++) //拷贝剩下的元素,正好漏下一个删除元素没拷贝
{
new_my_arrary[i] = my_array[i + 1];
}
m_pre_size--;
my_array = new_my_arrary; //别忘了把删除后的地址重新赋值回去
}
void myArray_::print() //debug函数
{
for (int i = 0; i < m_pre_size ; i++)
{
cout << my_array[i] << " ";
}
cout << endl;
}
这里的删除操作只是其中一种简单的实现,还可以利用swap函数对数组中要删除元素的后面所有元素进行交换,同样可以完成删除操作,相比较代码给出的方法,此种方法时间复杂度会大大减小,尤其是当删除的元素的位置靠近数组末尾时。
我的方式
当然上述方法好像并不满足题目上要求的利用三个指针的方式实现vector容器。但是不慌,有了刚才的思路,可以想到创建出三个指针:指向数组首地址的指针(数组名)+指向当前最后一个元素下一位置的指针 + 当前数组最大容量下一位置的指针,就可以完成和上面核心思想一样的操作了。三根指针的示意图如图所示:(利用的是c++风格的指针示意,有点像c++风格迭代器的样式,即指针不直接指向任何一个元素,相信大家应该都懂……)
理解清楚了这个,剩下的就不难了。利用c++指针的特性,刚才那个类的m_pre_size就是present-begin,而m_max_size就是end-begin!这不是我瞎说的,大家可以随意创建一个动态数组试一下,指针相减就是他们的位置差。所以直接可以对刚才那个类在进行一次包装就能完成该任务了!
直接看看类的属性和行为吧:
template <class T>
class MyVector
{
private:
T* begin;
T* present;
T* end;
public:
MyVector();
MyVector(int size);
MyVector(MyVector<T>& obj);
~MyVector();
void push_back(T num);
void delet(int i);
void print();
T operator[](int pos);
void BubbleSort();
void operator=(MyVector<T>& obj);
};
template <class T>
MyVector<T>::MyVector()
{
begin = new T[defaultSize]; //默认创建大小为10的一个数组
present = begin; //因为现在还是空数组,所以present指针与begin重合
end = begin + defaultSize; //末尾指针指向最大位置
}
template <class T>
MyVector<T>::MyVector(int size)
{
begin = new T[size * 2]; //这里同样将数组最大空间设为给出的2倍,原因上面已经讲出
present = begin; //因为现在还是空数组,所以present指针与begin重合
end = begin + size * 2; //末尾指针指向最大位置
}
template<class T> //copy ctor,深拷贝
MyVector<T>::MyVector(MyVector<T>& obj)
{
int size = obj.end - obj.begin;
begin = new T[size];
present = begin;
end = begin + size;
for (int i = 0; i < (obj.present - obj.begin); i++)
{
begin[i] = obj.begin[i];
present++;
}
}
template <class T>
MyVector<T>::~MyVector()
{
//对于析构函数来说,begin、end、preseent只能delete一次,因为实质上三者指向的都是基于begin创建的内存空间
//所以只用delete begin,不能delete present与end,否则会造成重复delete的问题
//执行完delete begin后,应让begin、end、presen三者都指向空即可,防止野指针的产生
//注:此处如果只删除掉begin,不对end和present操作的话,必然会造成野指针的产生
delete begin;
begin = present = end = NULL;
}
构造函数里面的拷贝构造函数不能直接利用系统默认的,实际上那只是浅拷贝,在析构的时候会造成重复删除的问题。所以要我们自己写copy ctor,自己写的才是深拷贝。(深拷贝和浅拷贝这里就不多说了,毕竟那么多大牛已经讲得清清楚楚了)
还有就是令人头疼的析构函数……(这个东西很重要)我尝试过delete begin,present,end;begin = present = end = NULL,但是编译器总是报错。后来才想到,因为present和end指针都是在begin创建出来的内存之上的,所以我原来的那种写法会造成指针的重复删除,才导致的编译不通过,所以只需要delete掉bigin指针就行了。为了防止野指针的产生,同时也要对所以的三个指针都要 = NULL操作(实际上,如果只删除掉begin指针,而不对present和end进行任何处理的话,就一定会造成野指针的出现:利用解引用的方式(*present与*end)访问该指针指向的值,会出现意料之外的数字……)。
剩下的就是一下成员函数的实现了,没有什么需要注意的事情,就是考验对指针是否能灵活应用。(比如present- begin就是当前数组中存储数字的个数,*begin就能访问到第一个元素,*(begin + 1)就能访问到第二个元素……)
template <class T>
void MyVector<T>::push_back(T i)
{
if (present != end) //未满
{
*present = i;
present++;
}
else
{
T* temp = new T[(end - begin) * 2];
present = temp;
for (int i = 0; i < (end - begin); i++)
{
temp[i] = begin[i];
present++;
}
end = temp + (end - begin) * 2;
begin = temp;
delete temp;
temp = NULL;
*present = i;
present++;
}
}
template <class T>
void MyVector<T>::delet(int pos) //i从0开始索引
{
if (present == begin || pos >=(present -begin))
{
return;
}
T* new_begin = new T[end - begin];
for (int i = 0; i < pos; i++)
{
new_begin[i] = begin[i];
}
for (int i = pos; i < (end - begin - 1); i++)
{
new_begin[i] = begin[i + 1];
}
end = new_begin + (end - begin);
present = new_begin + (present - begin) - 1;
begin = new_begin;
}
template <class T>
void MyVector<T>::print()
{
T* temp = begin;
while (temp != present)
{
cout << *temp << " ";
temp++;
}
cout << endl;
}
小小小总结
c++中的指针系统虽然复杂但是很有趣,并且最能接近代码编译的真实情况。得益于完备的c++指针体系(之所以称为体系是因为指针真的有好多重载操作啊),使我们较为简单的完成了该题目。希望在以后的学习生活中能够勤于思考,敢于试错,知其然更要知其所以然,发挥出自身优势。
最后想以一句话来结尾:千里之行,始于足下。事在人为!