支援外部記憶體功能的STL容器使用方法分享

孔子?孟子?小柱子!發表於2024-09-17

一、分享簡介

C++的STL支援了多種容器供開發者操作,然而這些容器使用的是系統記憶體,使用者無法直接管理。邊緣端的嵌入式裝置通常會要求對使用的記憶體進行管理,因此封裝出支援外部記憶體功能的STL容器就顯得十分必要。本案例針對被封裝容器的使用方法進行了經驗分享,具體涉及3種順序容器(CVector/CList/CString),以及7種關聯容器(CSet/CMultiset/CUnordered_set/CMap/CMultimap/CUnordered_map/CUnordered_multimap)。

二、封裝容器使用方法

2.1 CVector容器

// 使用示例
using VectorContainer = CVector<int, MyAlloc::CAllocator<int, MEM_TYPE, nullptr>>;

使用CVector容器時,需要依次指定容器的元素型別、記憶體分配器的資料型別、記憶體型別和記憶體池控制代碼,其中後三個引數屬於記憶體分配器的內容,可以省略,但此時封裝容器不再支援外部記憶體,將使用STL標準容器的記憶體分配器分配系統記憶體。在上述程式碼示例中,元素型別為int,記憶體分配器的資料型別為int,記憶體型別為MEM_TYPE,記憶體池控制代碼為空。

為了方便使用,建議使用using關鍵字為CVector容器取一個新的別名,如VectorContainer。封裝後的CVector容器支援的操作包括(若無特別宣告,使用方法與STL標準容器中vector的相應操作一致):

  • Constructor:無參構造一個容器;複製構造一個容器;移動構造一個容器;賦值構造一個容器(operator"=");構造含有n個元素,每個元素的值均為value的一個容器;構造元素範圍為[first, last]的一個容器。
  • Operator overloads:"<";"=="。
  • Iterators:begin();end();cbegin();cend()。
  • Capacity:size();resize();capacity();empty();reserve()。
  • Element access:front();back();data();operator"[]"。
  • Modifiers:push_back();emplace_back();pop_back();erase();insert();clear();assign()。
  • Non-member function overloads:swap()。

CVector容器在記憶體操作上同STL中的vector,底層是一段記憶體連續的陣列,當記憶體不夠時會申請2倍大小的記憶體(不同編譯器申請的記憶體不同)擴容,並將陣列的內容複製至新記憶體中。在清除元素時,被清除元素的記憶體空間並不會被釋放,只是改變了容器的size(),並沒有改變容器的capacity(),因此整個容器的記憶體大小隻增不減,若希望釋放空間的記憶體,可以透過swap()方法將原容器與一個臨時的匿名容器交換的方式實現,當交換之後,臨時容器被銷燬時,其記憶體也會被釋放。

CVector容器支援隨機存取,但對中間元素進行新增或刪除時,需要移動記憶體,且可能會執行構造和析構的操作,進而影響效能,適用於元素結構簡單,需要經常進行隨機訪問,且不需要經常對中間元素執行新增或刪除操作的場景。

2.2 CList容器

// 使用示例
using ListContainer = CList<int, MyAlloc::CAllocator<int, MEM_TYPE, nullptr>>;

CList容器的使用方法同CVector容器的使用方法一致。為了方便使用,建議使用using關鍵字為CList容器取一個新的別名,如ListContainer。封裝後的CList容器支援的操作包括(若無特別宣告,使用方法與STL標準容器中list的相應操作一致):

  • Constructor:無參構造一個容器;複製構造一個容器;移動構造一個容器;賦值構造一個容器(operator"=");構造含有n個元素,每個元素的值均為value的一個容器;構造元素範圍為[first, last]的一個容器。
  • Operator overloads:"<";"=="。
  • Iterators:begin();end();cbegin();cend()。
  • Element access:front();back()。
  • Modifiers:push_back();pop_back();push_front();pop_front();erase();insert();clear()。
  • Capacity:size()。
  • Operations:remove()。

CList容器的begin()迭代器和end()迭代器不是隨機訪問迭代器,均屬於雙向迭代器,它們不能執行加減整數的運算,所以修改本容器迭代器的唯一方法是使用自增或自減運算。

CList容器的底層為雙向連結串列,記憶體空間可以不連續,透過指標來訪問資料,每分配一個元素都會從記憶體中分配儲存空間,每刪除一個元素都會釋放它佔用的記憶體。CList容器在隨機存取元素時執行效率低,佔用記憶體較多,但可以高效的支援在任意地方刪除和插入元素,適用於需要經常執行快速插入或刪除中間元素的場景。

2.3 CString容器

// 使用示例
using StringContainer = CString<MyAlloc::CAllocator<char, MEM_TYPE, nullptr>>;

Cstring容器是基於容器類别範本std::basic_string封裝而成的,該類模版管理的物件是標準的C++字串序列,而標準C++字串類是一個容器,所以可以基於std::basic_string封裝容器。常用的std::string其實是形參為char的basic_string類模版的一個別名,它使用了std::basic_string預設的迭代器型別,不能支援外部記憶體,所以不能基於std::string封裝CString容器。

由於希望把CString容器當作std::string使用,所以在封裝時已經為std::basic_string指定了字串中單個字元的資料型別為char型,因此在使用本容器時,僅需要指定記憶體分配器的資料型別、記憶體型別和記憶體池控制代碼,這三個引數均屬於記憶體分配器的內容,可以省略,但此時封裝容器不再支援外部記憶體,將使用STL標準容器的記憶體分配器分配系統記憶體。在上述程式碼中,記憶體分配器的資料型別為char,記憶體型別為MEM_TYPE,記憶體池控制代碼為空。

為了方便使用,建議使用using關鍵字為CString容器取一個新的別名,如StringContainer。封裝後的CString容器支援的操作包括(若無特別宣告,使用方法與STL標準容器中basic_string的相應操作一致):

  • Constructor:無參構造一個容器;複製構造一個容器(標準C++字串/字串陣列);移動構造一個容器;賦值構造一個容器(operator"=")。
  • Operator overloads:"<";"=="。
  • Element access:operator"[]"。
  • Iterators:begin();end();cbegin();cend()。
  • Modifiers:operator"+=";push_back();append();erase()。
  • String operations:find();rfind();compare();substr()。
  • Capacity:size();resize()。
  • Non-member function overloads:operator"<<";operator">>"。

CString容器儲存了可變長度的字元序列,雖然不用考慮字元越界的問題,但操作效率較低,適用於操作未知長度的字串。當在字串中執行查詢操作時,若沒有匹配到查詢位置,此時操作結果會返回一個常數std::string::npos,它用來表示一個不存在的位置,可以保證大於任何有效的下標值,該常數具體為2的64次方-1,即64位機的最大值18446744073709551615。

2.4 CSet容器

// 使用示例
using SetContainer = CSet<int, MyAlloc::CAllocator<int, MEM_TYPE, nullptr>>;

CSet容器的使用方法同CVector容器的使用方法一致。為了方便使用,建議使用using關鍵字為CSet容器取一個新的別名,如SetContainer。封裝後的CSet容器支援的操作包括(若無特別宣告,使用方法與STL標準容器中set的相應操作一致):

  • Operator overloads:"<";"=="。
  • Iterators:begin();end();cbegin();cend()。
  • Modifiers:insert ()。
  • Operations:count();find()。
  • Capacity:size()。

CSet容器基於紅黑樹(RB Tree)實現,其內部元素依照規則自動排序,每個元素是唯一的,無重複的,且是有序的,其插入、刪除、搜尋等操作的效率穩定且較高,時間複雜度均為O(log n),但不能直接改變元素值,要改變元素值必須先刪除舊元素,再插入新元素,整個容器的記憶體消耗較高。CSet容器適用於儲存有排序要求的無重複元素,且要求經常快速查詢或刪除的場景。

CSet容器會對元素進行比較和排序,當CString等封裝容器作為CSet容器的元素時,相當於CSet容器儲存了自定義型別的元素,需要過載CString等封裝容器的比較運算子"<",其他自定義的資料型別如類、結構體也需要在資料型別內部過載比較運算子,給自定義的資料一個比較準則,而CSet容器對內建的基本資料型別則有預設的比較準則std::less<T>。

2.5 CMultiset容器

// 使用示例
using MultiSetContainer = CMultiset<int, MyAlloc::CAllocator<int, MEM_TYPE, nullptr>>;

CMultiset容器的使用方法同CSet容器的使用方法一致。為了方便使用,建議使用using關鍵字為CMultiset容器取一個新的別名,如MultiSetContainer。封裝後的CMultiset容器支援的操作包括(若無特別宣告,使用方法與STL標準容器中multiset的相應操作一致):

  • Operator overloads:"<";"=="。
  • Iterators:begin();end();cbegin();cend()。
  • Modifiers:insert ()。

CMultiset容器基於紅黑樹(RB Tree)實現,其內部元素依照規則自動排序,允許有重複的元素。本容器插入、刪除、搜尋等操作的效率穩定且較高,時間複雜度均為O(log n),但和CSet容器一樣,不能直接改變元素值,要改變元素值必須先刪除舊元素,再插入新元素,整個容器的記憶體消耗較高。CMultiset容器適用於儲存有排序要求的元素,且要求經常快速查詢或刪除的場景。

CMultiset容器會對元素進行比較和排序,所以和CSet容器一樣,當CString等封裝容器以及其他自定義的資料型別如類、結構體作為CMultiset容器的元素時,需要過載關於它們的比較運算子"<",給自定義的資料一個比較準則,同時本容器對內建的基本資料型別也有預設的比較準則std::less<T>。

2.6 CUnordered_set容器

// 使用示例,元素為內建基本資料型別
using UnordSetContainer = CUnordered_set<int, MyAlloc::CAllocator<int, MEM_TYPE, nullptr>>;
// 使用示例,元素為自定義資料型別,如CString
using UnordSetContainer = CUnordered_set<StringContainer, MyAlloc::CAllocator<StringContainer, MEM_TYPE, nullptr>, CString_Hash>;

當內建的基本資料型別作為CUnordered_set容器的元素時,本容器的使用方法同CSet容器的使用方法一致;當CString等封裝容器以及其他自定義的資料型別作為CUnordered_set容器的元素時,本容器還需要額外指定一個特例化的雜湊函式,如CString_Hash。需要注意的是,雜湊函式只是一個稱謂(便於理解),其本體並不是普通的函式形式,而是一個函式物件類。

為了方便使用,建議使用using關鍵字為CUnordered_set容器取一個新的別名,如UnordSetContainer。封裝後的CUnordered_set容器支援的操作包括(若無特別宣告,使用方法與STL標準容器中unordered_set的相應操作一致):

  • Operator overloads:"<";"=="。
  • Iterators:begin();end();cbegin();cend()。
  • Modifiers:insert ()。
  • Element lookup:find()。

CUnordered_set容器基於雜湊表實現,其元素無序且唯一,容器透過元素值的雜湊值將元素分組放置到各個槽。本容器可以高效地訪問各個元素,時間複雜度為O(1),但消耗較多的記憶體,且可能會遇到雜湊衝突,適用於元素無序,但需要快速訪問的場景。

當內建的基本資料型別作為CUnordered_set容器的元素時,容器內部會使用預設的雜湊函式模板std::hash<T>來獲取雜湊值,而當CString等封裝容器以及其他自定義的資料型別作為本容器的元素時,本容器則要求以函式物件類的方式,透過定義一個一元運算子operator()來特例化實現雜湊函式,如CString_Hash:

// 關於StringContainer容器的雜湊函式
class CString_Hash
{
public:
    std::size_t operator()(const StringContainer &str) const
    {
        return std::hash<std::size_t>()(str.size() *2024);
    }
};

由於容器底層基於雜湊表,需要解決雜湊碰撞的問題,因此必須判斷兩個物件是否相等。當基本資料型別作為CUnordered_set容器的元素時,容器內部會使用預設的判等函式模板std::equal_to<T>來指定容器內的元素是否相等,對於這種判等規則,其在底層實現過程中,直接用"=="運算子判斷容器中任意兩個元素是否相等,這意味著如果容器中儲存的元素型別,支援直接用"=="運算子判斷是否相等,則CUnordered_set等無序容器可以使用預設的std::equal_to<T>,反之就不可以使用。因此,當自定義的資料型別作為CUnordered_set容器的元素時,容器要求在元素內部過載"=="運算子,使得std::equal_to<T>判等規則中使用的"=="運算子變得合法。

std::equal_to<T>在本質也是一個函式物件類,因此當自定義的資料型別作為CUnordered_set容器的元素時,也可以完全捨棄std::equal_to<T>,使用函式物件類的方式自定義一個判等規則,類似於特例化雜湊函式的做法。

2.7 CMap容器

// 使用示例
using MapContainer = CMap<int, int, MyAlloc::CAllocator<std::pair<const int, int>, MEM_TYPE, nullptr>>;

使用CMap容器時,需要依次指定容器內元素的鍵(key)型別、元素的值(value)型別、記憶體分配器的鍵-值對型別、記憶體型別和記憶體池控制代碼,其中後三個引數屬於記憶體分配器的內容,可以省略,但此時封裝容器不再支援外部記憶體,將使用STL標準容器的記憶體分配器分配系統記憶體。在上述圖例中,元素的鍵型別為int,元素的值型別也為int,記憶體分配器的鍵-值對型別為std::pair<const int, int>,記憶體型別為MEM_TYPE,記憶體池控制代碼為空。

為了方便使用,建議使用using關鍵字為CMap容器取一個新的別名,如MapContainer。封裝後的CMap容器支援的操作包括(若無特別宣告,使用方法與STL標準容器中map的相應操作一致):

  • Operator overloads:"<";"=="。
  • Iterators:begin();end();cbegin();cend()。
  • Modifiers:insert ();clear()。
  • Capacity:size()。
  • Operations:count();find()。

CMap容器基於紅黑樹實現,不允許key重複,且元素根據元素的key自動排序。本容器內元素的key不能被修改,但是元素的value可以被修改,其插入、查詢操作的效率較高,時間複雜度均為O(log n),同時整個容器的記憶體消耗也較高。CMap容器的效能穩定,適用於需要快速處理一對一關係的資料場景。

和其它基於RB Tree的容器一樣,CMap容器也會對元素進行比較和排序,所以當CString等封裝容器以及其他自定義的資料型別作為CMap容器內元素的鍵(key)時,需要過載關於它們的比較運算子"<",給自定義的資料一個比較準則,同時本容器對內建的基本資料型別也有預設的比較準則std::less<T>。

2.8 CMultimap容器

// 使用示例
using MultiMapContainer = CMultimap<int, int, MyAlloc::CAllocator<std::pair<const int, int>, MEM_TYPE, nullptr>>;

CMultimap容器的使用方法同CMap容器的使用方法一致。為了方便使用,建議使用using關鍵字為CMultimap容器取一個新的別名,如MultiMapContainer。封裝後的CMultimap容器支援的操作包括(若無特別宣告,使用方法與STL標準容器中multimap的相應操作一致):

  • Operator overloads:"<";"=="。
  • Iterators:begin();end();cbegin();cend()。
  • Modifiers:insert ()。

CMultimap容器基於紅黑樹實現,元素是有序的,容器內可包含多個鍵(key)相同的元素,即允許一鍵(key)對應多值(value),但記憶體消耗高。本容器效能穩定,其插入操作的時間複雜度為O(log n),適用於需要快速處理一對多關係的資料場景。

CMultimap容器會對元素進行比較和排序,所以和CMap容器一樣,當自定義的資料型別作為本容器元素的鍵(key)時, 需要過載關於它們的比較運算子"<",給自定義的資料一個比較準則,同時CMultimap容器對內建的基本資料型別也有預設的比較準則std::less<T>。

2.9 CUnordered_map容器

// 使用示例,元素為內建基本資料型別
using UnordMapContainer = CUnordered_map<int, int, MyAlloc::CAllocator<std::pair<const int, int>, MEM_TYPE, nullptr> >;
// 使用示例,元素為自定義資料型別,如CString
using UnordMapContainer = CUnordered_map<StringContainer, int, MyAlloc::CAllocator<std::pair<const StringContainer, int>, MEM_TYPE, nullptr>, CString_Hash>;

當CUnordered_map容器內元素的鍵(key)為內建的基本資料型別時,本容器的使用方法同CMap容器的使用方法一致;當CUnordered_map容器內元素的鍵(key)為自定義的資料型別時,本容器還需要額外指定一個特例化的雜湊函式,如CString_Hash。

為了方便使用,建議使用using關鍵字為CUnordered_map容器取一個新的別名,如UnordMapContainer。封裝後的CUnordered_map容器支援的操作包括(若無特別宣告,使用方法與STL標準容器中unordered_map的相應操作一致):

  • Operator overloads:"<";"=="。
  • Iterators:begin();end();cbegin();cend()。
  • Modifiers:insert ();clear()。
  • Element lookup:count();find()。
  • Capacity:size();empty()。
  • Hash policy:reserve()。

CUnordered_map容器基於雜湊表實現,當雜湊對映位置的資料量在8以內時使用連結串列來儲存資料,當該位置的資料量大於8時則自動轉換為紅黑樹結構儲存資料,容器內元素的鍵(key)是唯一的,本容器的查詢速度快,其時間複雜度為O(1),但查詢效能不穩定,且記憶體消耗高。在對資料不要求有序的情況下,建議儘量使用CUnordered_map而不是CMap。

同CUnordered_set容器類似,當內建的基本資料型別作為CUnordered_map容器內元素的鍵(key)時,容器內部會使用預設的雜湊函式模板std::hash<T>來獲取雜湊值,而當CString等封裝容器以及其他自定義的資料型別作為本容器內元素的鍵(key)時,本容器則要求以函式物件類的方式,透過定義一個一元運算子operator()來特例化實現雜湊函式,如CString_Hash。

同CUnordered_set容器類似,當內建的基本資料型別作為CUnordered_map容器內元素的鍵(key)時,容器內部會使用預設的判等函式模板std::equal_to<T>來指定容器內的元素是否相等,當自定義的資料型別作為CUnordered_map容器內元素的鍵(key)時,容器要求在元素內部過載"=="運算子,使得std::equal_to<T>判等規則中使用的"=="運算子變得合法。除此之外,當自定義的資料型別作為本容器內元素的鍵(key)時,也可以完全捨棄std::equal_to<T>,使用函式物件類的方式自定義一個判等規則,類似於特例化雜湊函式的做法。

2.10 CUnordered_multimap容器

// 使用示例,元素為內建基本資料型別
using UnordMultiMapContainer = CUnordered_multimap<int, int, MyAlloc::CAllocator<std::pair<const int, int>, MEM_TYPE, nullptr>>;
// 使用示例,元素為自定義資料型別,如CString
using UnordMultiMapContainer = CUnordered_multimap<StringContainer, int, MyAlloc::CAllocator<std::pair<const StringContainer, int>, MEM_TYPE, nullptr>, CString_Hash>;

CUnordered_multimap容器的使用方法同CUnordered_map容器的使用方法一致。為了方便使用,建議使用using關鍵字為本容器取一個新的別名,如UnordMultiMapContainer。封裝後的CUnordered_multimap容器支援的操作包括(若無特別宣告,使用方法與STL標準容器中unordered_multimap的相應操作一致):

  • Operator overloads:"<";"=="。
  • Iterators:begin();end();cbegin();cend()。
  • Modifiers:insert ()。

CUnordered_multimap容器基於雜湊表結構實現,可以儲存多個鍵(key)相等的元素,且這些鍵相等的元素會被雜湊到同一個桶中儲存,但這也導致無法直接訪問元素。本容器適用於需要快速處理多對多關係的資料場景。

同CUnordered_map容器類似,當內建的基本資料型別作為CUnordered_multimap容器內元素的鍵(key)時,容器內部會使用預設的雜湊函式模板std::hash<T>來獲取雜湊值,而當CString等封裝容器以及其他自定義的資料型別作為本容器內元素的鍵(key)時,本容器則要求以函式物件類的方式,透過定義一個一元運算子operator()來特例化實現雜湊函式,如CString_Hash。

同CUnordered_map容器類似,當內建的基本資料型別作為CUnordered_multimap容器內元素的鍵(key)時,容器內部會使用預設的判等函式模板std::equal_to<T>來指定容器內的元素是否相等,當自定義的資料型別作為CUnordered_multimap容器內元素的鍵(key)時,容器要求在元素內部過載"=="運算子,使得std::equal_to<T>判等規則中使用的"=="運算子變得合法。除此之外,當自定義的資料型別作為本容器內元素的鍵(key)時,也可以完全捨棄std::equal_to<T>,使用函式物件類的方式自定義一個判等規則,類似於特例化雜湊函式的做法。

相關文章