幾個月前重溫了數據結構與算法,對數據和結構有了不同的理解,最近又翻出來看了一下,覺得還是做一篇小筆記最好,故便有了此文。
我記得上課時老師曾講過一句話:編程最重要的是思想,而不是代碼;把思路理清了,自然代碼就會寫了,而且寫的代碼印像會深刻許多。
作爲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);
}
}
再見。