几个月前重温了数据结构与算法,对数据和结构有了不同的理解,最近又翻出来看了一下,觉得还是做一篇小笔记最好,故便有了此文。
我记得上课时老师曾讲过一句话:编程最重要的是思想,而不是代码;把思路理清了,自然代码就会写了,而且写的代码印像会深刻许多。
作为STL 开头菜,Vector显然会给初学者带来一点困扰,如果你c++学的不怎么扎实的话,其实我们仔细去剖析,你会发现vector真的就是小葱炒鸡蛋,一清二白。
首先你要理解数组、链表本质上是什么,他们其实就是一个容器,是我们开发出来存取数据的两种最基础的数据结构容器。
数组,逻辑连续,物理位置连续,很好用,存取数据非常方便,new一个就行了,不需要编写链表那么多的代码,但它有弊端:删除结点的数据时带来移位,会增加数据处理时的时间,这是我们不希望的,所以就有了链表,删除结点时只需注意一下指针的连接,不需要移位,但每次寻找数据都需要遍历链表,不像数组那么直观。
了解上述两种,再来了解关系式容器和顺序式容器。
顺序性容器 :是一种各元素之间有顺序关系的线性表,是一种线性结构的可序群集。顺序性容器中的每个元素均有固定的位置,除非用删除或插入的操作改变这个位置(数组、链表、vector)。
关联式容器: 和顺序性容器不一样,关联式容器是非线性的树结构,更准确的说是二叉树结构。各元素之间没有严格的物理上的顺序关系,也就是说元素在容器中并没有保存元素置入容器时的逻辑顺序。
今天主要回顾vector,故主讲顺序式容器,vector是stl开发人员做的一个容器,他们能直接调用微软给他们的内存接口,而我们显然是无法调用的,故我们下面的代码全是模拟vector。
好吧,不说废话了,我们开始了:vector 有一个特色,被称为动态数组(可动态扩大,缩小),这可是我们c++的数组做不到的,既然它被称为动态数组,那我们就用数组来做。
首先我们看一看stl的vector最基本的属性。
根据图片,它有两个属性:size(大小) capacity(容量);
按我老师的风格,肯定是要我们自己去探索这两个单词的意义;这里我是做回顾,就直接写出来了:
size:代表vector有多少个数据
capacIty:代表vector最大的数据容量
不懂其分别的话,见下图即可
我用vector的重分配函数给其赋值:5个6,很明显size变为5,capaticy还是10;
我给其赋值 12个6;
很明显size变为12,capaticy变为15;这就是动态分配。
当然,这是assign函数的结果,其它函数有不同的作用,这里我们暂且不表。
既然知道了size,capacity的意义,我们就开始着手写基础的构架。
#pragma once
template <typename T>
class MyVector
{
private:
T*pBuff;
size_t length;
size_t max_size;
public:
MyVector();
~MyVector();
};
把最简单的模板结构给搭出来,pBuff :泛型指针,用来开辟数组
空间。
length :数据个数;(size)
max_size:容器能存储数据的最大个数。(capacity)
构造函数:
template <typename T>
MyVector<T>::MyVector()
{
pBuff = nullptr;
length = 0;
max_size = 0;
}
析构函数:
MyVector<T>::~MyVector()
{
if (pBuff)
{
delete[]pBuff;
}
pBuff = nullptr;
length = 0;
max_size = 0;
}
这样我们就完成了万事开头难的第一步。
最基本的无参构造写完了,肯定要写有参构造和拷贝构造啦:
故代码也就出来了:
public:
CMyVector(int n);
CMyVector(CMyVector const& other);
template <class T>
CMyVector<T>::CMyVector(int n)
{
length = n;
max_size= n;
pBuff = new T[length];
memset(pBuff, 0, sizeof(T)* length );
}
template <class T>
CMyVector<T>::CMyVector(CMyVector const& other)
{
length = other.num;
max_size= other.max_size;
pBuff = NULL;
if (length )
{
pBuff = new T[length];
memcpy(pBuff, other.pBuff, sizeof(T)* length );
}
}
接下来就是vector的重要代码了:被称为动态数组,能根据数据需求改变大小,究竟是怎么实现的呢?
public:
void push_back(T const & srcDate);
template <class T>
void CMyVector<T>::push_back(T const & srcDate)//压入数据
{
if (length>= max_size)//如果当前个数和最大个数相同,证明没有空间
{
//内存重分配及拷贝之前内存数据
max_size= max_size+ ((max_size>> 1) > 1 ? (max_size>> 1) : 1);
T* tempBuff = new T[max_size];
memcpy(tempBuff, pBuff, sizeof(T)* length);
if (pBuff)
delete[]pBuff;
pBuff = tempBuff;
}
pBuff[length++] = srcDate;
}
这句代码就是vector的“点睛之句”;
max_size= max_size+ ((max_size>> 1) > 1 ? (max_size>> 1) : 1);
它就为vector开辟了动态数组一说。
但是vector动态之处远远不止这一个函数之功。
动态即随意变换,故 vector座下还有assign 、pop_back等函数
其中迭代器 iterator()尤为重要;
assign函数为重分配之意,其函数也颇有意思,故在此贴出:
(1)函数原型:assign(int n, T const& srcDate) n:重分配的数据个数,srcDate:重分配的数据。
(2)注意的点:一个合格的代码书写者第一个要考虑的就是代码的完整性,其次是安全性,再是运行速度。
我们要考虑的有下面几种情况:
1. 伪代码: vector<int>v; 当v为空时,运行assign(int n, T const& srcDate)函数结果如何。
2. 伪代码: vector<int>v(m); 当m<n时,运行assign(int n, T const& srcDate)函数结果如何。
3. 伪代码: vector<int>v(m); 当m>n时,运行assign(int n, T const& srcDate)函数结果如何。
4. 伪代码: vector<int>v(m); 当v容器里有数据且发生 2 或3 时,运行assign(int n, T const& srcDate)函数结果如何。
结果希望看到此篇文章的同学自行试下:
1:size:n
max_size:n;
2:size:n
max_size:当n> (max_size>>1)+max_size,取n;
当max_size<n< (max_size/2)+max_size ,取 (max_size/2)+max_size;
当max_size<n<1+max_size, (舍弃)
3:size:n
max_size:m;
4,无论是否有数据,都会被替换,故有无数据运行assign函数结果均一样。
因此我们便可编写函数:
template <class T>
void CMyVector<T>::assign(int n, T const& srcDate)
{
if (pBuff)//如果自身有内存
{
delete[]pBuff;//释放内存
if (max_size< n)//最大长度小于拷贝个数
{
if ((max_size+ (max_size>> 1))<n)
{
max_size= n;//最大长度就是拷贝个数
}
else
{
max_size= max_size+ (max_size>> 1);
}
}
}
else
{
max_size= n;
}
pBuff = new T[max_size];//如果之前有空间在前面已经释放,所以不管怎样都重分配空间
length= n;
for (int i = 0; i < n; ++i)
{
pBuff[i] = srcDate;
}
}
如果代码有错误,欢迎指正。
接下来就是pop_puck函数,非常简单,看了这个你会感叹前面的函数都是假的。
template <class T>
void CMyVector<T>::pop_back()//删除最后一个数据,不返回元素
{
if (length)
length--;
}
好了,到这里我们已经写了vector的构造、析构、添加、删除、重分配,60%的工作都差不多完成了。
当然vector还有很多函数,那些函数就不一一详述了。
一个类当然会有重载运算符,这里我就写一个函数来代表
public:
bool operator == (CMyVector const & srcVector) const;
bool operator != (CMyVector const & srcVector) const;
template <class T>
bool CMyVector<T>::operator==(CMyVector const & srcVector) const
{
if (length== srcVector.length)
{
for (size_t i = 0; i < length; ++i)
{
if (pBuff[i] != srcVector.pBuff[i])
return false;
}
return true;
}
return false;
}
template <class T>
bool CMyVector<T>::operator!=(CMyVector const & srcVector) const
{
return !(*this == srcVector);
}
也是c++里面较为基础的东西了,相信大家使用的都如鱼得水了吧。
上面我们讲vector的时候,讲到迭代器 iterator(),那么它是什么呢?它又有什么意义?
迭代器(iterator)有时又称游标(cursor)是程序设计的软件设计模式,可在容器(container,例如链表或阵列)上遍访的接口,设计人员无需关心容器的内容。
迭代器不是一种成员,它只是实现函数成员的方式。
看到这你可能还一头雾水,那我们直接用实例去理解。
在vector中,迭代器是一个结构体,有趣的是它真正参数只有一个指针,其余全是函数,故其可视为智能指针。
它的作用是给容器提供一个便捷的数据操作方式,程序、代码存在的意义就是为了“偷懒”,就是为了更快速、更有效的达到我们想要的效果,迭代器不外乎如此。
我们先看stl里的迭代器:看看他的用法:
可以清楚的看到:除了非常便捷的插入数据,迭代器的访问方式是域访问,这就代表它不是class的成员函数,
而是类中一种数据结构。
接下来看一个代码
public:
struct MyIterator
{
T *pIt;
};
这个应该很熟悉,我们当初学习结构体的时候应该学到过这个,如果c++和c都有基础,应该知道结构体和class的缘分,
类和结构体很多时候行为是非常相似的。
上面的函数光秃秃的,就一个指针,显然我们要给它加点我们想要的东西,
public:
struct MyIterator
{
T *pIt;
MyIterator& operator=(MyIterator const& srcIt)
{
pIt = srcIt.pIt;
return *this;
}
T operator*()
{
return *pIt;
}
MyIterator operator +(int n)
{
MyIterator it;
it.pIt = pIt;
it.pIt += n;
return it;
}
bool operator != (MyIterator const &srcIt) const
{
return pIt != srcIt.pIt;
}
bool operator -(MyIterator const &srcIt) const
{
return pIt - srcIt.pIt;
}
};
是不是很熟悉这些操作,跟类没什么两样,只不过结构体函数全写成内联函数了,在c++中结构体可以就当成类用的。
如何将这个结构体与我们的vector的数据结合,其实很简单,真的,因为他们就在一起,迭代器就是嵌在容器内部的结构体(或其它)。
public:
MyIterator begin()
{
MyIterator it;
it.pIt = pBuff;
return it;
}
MyIterator end()
{
MyIterator it;
it.pIt = pBuff + num;
return it;
}
到这里,我们迭代器的基本结构就理清了,vector中自然有很多关于迭代器的函数,上面我只写了迭代器自身的一些函数,还有很多函数需要我们自己去尝试,望诸君共勉。
下面我就贴迭代器的一个模拟函数来结束这篇糟糕透顶的文章。
template <class T>
CMyVector<T>::CMyVector(MyIterator beg, MyIterator end)
{
length=0;
max_size= 0;
pBuff = NULL;
int n = 0;
if ((n = end- beg) > 0)
{
max_size= length = n;
pBuff = new T[max_size];
for (int i = 0; i < n; ++i)
pBuff[i] = *(beg+ i);
}
}
再见。