必須要注意的 C++ 動態記憶體資源管理(六)——vector的簡單實現
阿新 • • 發佈:2019-01-01
十六.myVector分析
我們知道,vector類將其元素存放在連續的記憶體中。為了獲得可接受的效能,vetor預先分配足夠大的記憶體來儲存可能需要的更多元素。vector的每個新增元素的成員函式會檢查是否有空間容納更多的元素。如果有,成員函式會在下一個可用位置構造一個物件。如果沒有可用空間,vector就會重新分配空間;它獲得新的空間,將已有元素移動到新空間中,釋放舊空間,並新增新元素。
既然是動態開闢的記憶體,於是我們在myVector中使用動態陣列來儲存,而每次插入元素的時候需要先判斷開闢的記憶體是否已滿,如果滿了需要重新分配記憶體。
下面給出最初版本的程式碼: |
//myVector.h
#include <memory>
template<typename T>
class myVector
{
public:
typedef myVector<T> _Myt;
myVector() :
elements(nullptr), first_free(nullptr), cap(nullptr){} // allocator成員進行預設初始化
myVector(const _Myt&);
_Myt& operator=(const _Myt&);
~myVector();
T& operator [](size_t i){ return *(elements + i); }
void push_back(const T&); // 新增元素
size_t size()const{ return first_free - elements; }
size_t capacity()const{ return cap - elements; }
T *begin()const{ return elements; }
T *end()const{ return first_free; }
private :
void chk_n() //被新增元素函式使用
{
if (size() == capacity())reallocate();
}
std::pair<T*, T*> n_copy
(const T*, const T*); //被拷貝構造,賦值運算子,解構函式使用
void free(); //銷燬元素並釋放記憶體
void reallocate(); //獲得更多記憶體並拷貝已有元素
T *elements;
T *first_free;
T *cap;
};
template<typename T>
myVector<T>::myVector(const _Myt& v)
{
//呼叫alloc_n_copy 分配空間以容納與s中一樣多的元素
auto newdata = n_copy(v.begin(), v.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
template<typename T>
myVector<T>::~myVector()
{
free();
}
template<typename T>
myVector<T>& myVector<T>::operator=(const _Myt& rhs)
{
//呼叫alloc_n_copy分配記憶體,大小與rhs一樣.
auto data = n_copy(rhs.begin(), rhs.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
template<typename T>
std::pair<T*, T*> myVector<T>::
n_copy(const T *b, const T *e)
{
auto data = new T[e - b];
for (auto i = b; i < e; i++)
data[i-b] = *i;
return{ data, data + (e - b) };
}
template<typename T>
void myVector<T>::push_back(const T& s)
{
chk_n(); //確保已有新空間
*(first_free++) = s;
}
template<typename T>
void myVector<T>::free()
{
delete[] elements;
}
template<typename T>
void myVector<T>::reallocate()
{
//我們將分配當前大小兩倍的記憶體空間
auto newcapacity = size() ? 2 * size() : 1;
//分配新記憶體
auto newdata = new T[newcapacity];
auto dest = newdata;
auto elem = elements;
//將資料從舊地址移動到新地址
for (size_t i = 0; i != size(); ++i)
*(dest++) = *(elem++);
free(); //一旦更新完就要釋放舊記憶體
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
恩,以上程式碼實現了vector的部分功能,實現了vector記憶體的動態分配。不過,我們可以發現以上程式碼還是有幾個可以優化的地方: |
- 1.在分配記憶體的時候,new將記憶體分配和物件構造組合在了一起。但是在vector分配記憶體的時候,事實上有許多記憶體我們可能並用不上;而如果對於這些記憶體進行構造物件,可能就會帶來不必要的開銷。
- 2.在記憶體重新分配的時候,我們涉及到了舊資料的轉移;不對,這裡應該說是拷貝。雖然的確應該是轉移,然而我們實現是通過拷貝。在c++11的時候提供了移動構造語義。它可以對於即將銷燬(保證重新賦值前不再使用)的物件進行移動。這樣對於某些支援移動語義的物件。移動比拷貝就可以帶來更小的開銷。
恩,要進行以上的優化我們要先介紹:allocator類,移動語義。 |
十七.allocator類介紹
new有一些靈活性的侷限,一方面表現在它將記憶體分配和物件構造組合在一起。類似的,delete將物件析構和記憶體釋放組合在一起。我們分配單個物件時,通常我們希望將記憶體和物件初始化放在一起。因為在這樣的情況下,我們幾乎已經知道了物件應當是什麼值。 當分配一大塊記憶體時,我們通常計劃在這塊記憶體上按需構造物件。在這種情況下,我們就應該希望將記憶體分配和物件構造分離。這意味著我們可以分配大塊記憶體,然而只有我們真正需要的時候才執行物件建立操作(同時付出一定開銷)。 標準庫allocator類定義在標頭檔案memory中,它幫助我們將記憶體分配和物件構造分離開來。 |
函式 | 介紹 |
---|---|
allocator<T> a | 定義了一個名為a的allocator物件,它可以為型別為T的物件分配記憶體。 |
a.allocate(n) | 分配一段原始的,未構造的記憶體,儲存n個型別為T的物件。 |
a.deallocate(p,n) | 釋放從T*指標p開始的記憶體,這塊記憶體儲存了n個型別為T的物件;p必須是一個先前由allocate返回的指標,且n必須是p建立時指定的大小。在呼叫deallocate之前,使用者必須對每個在這塊記憶體建立的記憶體呼叫destroy。 |
a.construct(p,args) | p必須是一個型別為T*的指標,指向一塊原始記憶體;args被傳遞給型別為T的建構函式,用來在p指向的記憶體上構造一個物件。 |
a.destroy(p) | p為T*型別指標,此演算法對p指向的物件執行解構函式。 |
下面是一段簡單的程式碼,介紹瞭如何使用allocator進行記憶體分配,物件構造,物件釋放,記憶體回收。 |
int main()
{
allocator<string> alloc; //這個物件可以用來分配 string 型別的記憶體。
string* p = alloc.allocate(5); //使用alloc物件分配5個string物件大的連續記憶體並將頭指標給p。
//allocate分配的記憶體在沒有構造之前是不能使用的!!
alloc.construct(p,"hello world"); //使用"hello world"構造string
cout << *p << endl; //輸出剛才構造的string,輸出hello world
alloc.destroy(p); //銷燬剛才構造的物件
alloc.deallocate(p,5); //釋放記憶體
return 0;
}
不僅這樣,標準庫還提供了一些演算法讓我們使用的時候更加方便: |函式|介紹| |—|—-| |uninitialized_copy(b,e,b2)|從迭代器b和e指出的輸入範圍中拷貝元素到迭代器b2指定的未構造的原始記憶體中。b2指向的記憶體必須足夠大,能容納輸入序列中元素的拷貝。 |uninitialized_copy_n(b,n,b2)|從迭代器b指向的元素開始,拷貝n個元素到b2開始的記憶體中。 |uninitialized_fill(b,e,t)|在迭代器b,e指定的原始記憶體範圍中建立物件,物件的值均為t的拷貝。 |uninitialized_fill_n(b,n,t)|從迭代器b指向的記憶體地址開始建立n個物件。b必須指向足夠大的未構造原始記憶體,能夠容納給定數量的物件。 |
值得注意的是,所有通過allocate分配的記憶體都必須通過deallocate去回收,而所有構造的物件都必須通過destroy去釋放。所以這些拷貝演算法都必須要求原始記憶體,如果記憶體上有物件,請先使用destroy釋放!! |
十八.移動語義介紹
很多情況下都會發生物件拷貝,然而在其中某些情況下,物件拷貝後就會立即被銷燬。在這些情況下,移動而非拷貝物件會大幅度提升效能。還有的情況諸如IO類或者unique_ptr這些類都包含不能共享的資源,因此這些類的物件也不能拷貝只能移動。 為了支援移動操作,在新標準中引入了一種新的引用型別 —— 右值引用。右值引用有個重要的性質:只能繫結到一個即將要銷燬的物件上。因此我們可以自由地將一個右值引用的資源”移動”到另一個物件中。下面給出一些例子來表示哪些是右值: |
int i = 42;
int &r = i; //正確:r引用i
int &&rr = i; //錯誤:不能將一個右值引用繫結到左值上
int &r2 = i * 42; //錯誤:i * 42 是個右值
const int & r3 = i * 432; //正確:我們可以把一個const引用繫結到右值上
int &&rr2 = i * 42; //正確:右值引用繫結右值
考察左值和右值的區別:左值有持久的狀態,而右值要麼是字面常量,要麼是在表示式求值過程中或者是函式返回的時候建立的臨時變數。 因為變數是左值,所以我們不能將一個右值引用繫結到一個變數上,即使這個變數是一個右值引用型別也不行。所以為了解決這個問題,標準庫提供了一個move函式,它可以用來獲得繫結到左值上的右值引用。此函式定義在標頭檔案utility中。 我們可以銷燬一個移後源物件,也可以賦予新值,但不能在賦新值之前使用移後源物件的值。 根據以上所說,如果類也支援移動拷貝和移動賦值,那麼也能在某些時候的初始化(賦值)的時候提高效能。如果要想讓類支援移動語義,我們需要為其定義移動建構函式和移動賦值運算子。這兩個函式的引數都是一個右值引用。就如同上面的程式碼,對於vector的移動我們只需要拷貝三個指標引數,而不是拷貝三個指標引數指向的值。 |
template<typename T>
myVector<T>::myVector(_Myt&& v):elements(v.elements),first_free(v.first_free),cap(v.cap){
v.elements = v.first_free = v.cap = nullptr;
}
值得注意的是,我們要保證移後源物件必須是可析構狀態,而且如果移動構造(賦值)函式不丟擲異常的話必須要標記為noexcept(primer p474)。 對於移動賦值運算子我們要保證能正確處理自我賦值: |
template<typename T>
myVector<T>& myVector<T>::operator=(_Myt&& rhs)
{
if (this != &rhs)
{
free();
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
//將rhs置為可析構狀態
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
}
當然,和其他建構函式一樣,如果我們沒有定義移動建構函式的時候,編譯器會給我們提供預設的移動建構函式。不過,前提是該類沒有定義任何版本的拷貝控制函式以及每個非staitc成員變數都可以移動。編譯器就會預設為它合成移動建構函式和移動賦值運算子。 |
十九.優化過後的Vector
我們使用 allocate 和移動語義對以上的vector進行優化: |
#pragma once
#include <memory>
template<typename T>
class myVector
{
public:
typedef myVector<T> _Myt;
myVector() :
elements(nullptr), first_free(nullptr), cap(nullptr){} // allocator成員進行預設初始化
myVector(const _Myt&);
myVector(_Myt&&);
_Myt& operator=(const _Myt&);
_Myt& operator=(_Myt&&);
~myVector();
T& operator[](size_t i){ return *(elements + i); }
void push_back(const T&); // 新增元素
size_t size()const{ return first_free - elements; }
size_t capacity()const{ return cap - elements; }
T *begin()const{ return elements; }
T *end()const{ return first_free; }
private:
static std::allocator<T> alloc;
void chk_n_alloc() //被新增元素函式使用
{
if (size() == capacity())reallocate();
}
std::pair<T*, T*> alloc_n_copy
(const T*, const T*); //被拷貝構造,賦值運算子,解構函式使用
void free(); //銷燬元素並釋放記憶體
void reallocate(); //獲得更多記憶體並拷貝已有元素
T *elements;
T *first_free;
T *cap;
};
template<typename T>
std::allocator<T> myVector<T>::alloc;
template<typename T>
myVector<T>::myVector(const _Myt& v)
{
//呼叫alloc_n_copy 分配空間以容納與s中一樣多的元素
auto newdata = alloc_n_copy(v.begin(), v.end());
elements = newdata.first;
first_free = cap = newdata.second;
}
template<typename T>
myVector<T>::myVector(_Myt&& v):elements(v.elements),first_free(v.first_free),cap(v.cap){
v.elements = v.first_free = v.cap = nullptr;
}
template<typename T>
myVector<T>::~myVector()
{
free();
}
template<typename T>
myVector<T>& myVector<T>::operator=(const _Myt& rhs)
{
//呼叫alloc_n_copy分配記憶體,大小與rhs一樣.
auto data = alloc_n_copy(rhs.begin(), rhs.end());
free();
elements = data.first;
first_free = cap = data.second;
return *this;
}
template<typename T>
myVector<T>& myVector<T>::operator=(_Myt&& rhs)
{
if (this != &rhs)
{
free();
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
//將rhs置為可析構狀態
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
}
template<typename T>
std::pair<T*, T*> myVector<T>::
alloc_n_copy(const T *b, const T *e)
{
auto data = alloc.allocate(e - b);
//初始化並返回一個pair,該pair由data和uninitialized_copy組成
return{ data, uninitialized_copy(b, e, data) };
}
template<typename T>
void myVector<T>::push_back(const T& s)
{
chk_n_alloc(); //確保已有新空間
alloc.construct(first_free++, s);
}
template<typename T>
void myVector<T>::free()
{
//不能傳遞給deallocate一個空指標,如果elements為NULL,那麼函式什麼都不做
if (elements)
{
//逆序銷燬所有元素
for (auto p = first_free; p != elements;/* 空 */)
alloc.destroy(--p);
alloc.deallocate(elements, cap - elements);
}
}
template<typename T>
void myVector<T>::reallocate()
{
//我們將分配當前大小兩倍的記憶體空間
auto newcapacity = size() ? 2 * size() : 1;
//分配新記憶體
auto newdata = alloc.allocate(newcapacity);
//將資料從舊地址移動到新地址
auto dest = newdata;
auto elem = elements;
for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++));
free(); //一旦更新完就要釋放舊記憶體
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
儘管,以上的程式碼vector只實現了vector很少的一部分功能,而且可能實現方式也有不足的地方。不過,在這裡只是想體現動態記憶體的使用。所以,以上的程式碼還是可以作為c++動態記憶體管理的的示例的。 |
基本上c++動態記憶體管理的就介紹到這裡了。 |