C++ STL -- vector

XTG111發表於2024-04-19

本質

本質上就是一個陣列,儲存區域在記憶體中連續,但相比於靜態陣列,其可以在執行時動態調整大小(push_back,pop),無需手動管理記憶體

動態調整 -- 記憶體管理

vector中有兩個狀態資訊來維護記憶體管理:capacity和size。
capacity:表示當前分配到的記憶體空間大小
size:表示當前內部的元素數量
當size大於了capacity就需要重新分配

重新分配

當出現重新分配時,vector會被分配到一塊新的記憶體空間,然後將原有資料複製到新的記憶體空間,然後對原有資料空間進行釋放。這樣保持了連續性。

動態擴容

vector採用了指數增長的策略進行動態擴容,即當需要擴容時,vector將容量翻倍增長。這種擴容方式不僅避免了頻繁的記憶體分配,還確保了插入操作具有常數時間複雜度

簡易實現

透過編寫MyVector類實現類似std::vector的增、刪、獲取、遍歷四種功能

成員變數

主要是一個陣列指標、陣列大小以及陣列容量

    //動態陣列頭指標
    T* elements;
    //動態陣列大小
    size_t size;
    //動態陣列容量
    size_t capacity;

建構函式和解構函式

預設建構函式就是將頭指標置為空,大小和容量置為0

MyVector():elements(nullptr),capacity(0),size(0){}

解構函式採用delete清除已經分配的記憶體

~MyVector() {delete[] elements;}

複製建構函式,透過複製傳入的另一個類物件初始化改物件,首先為大小和容量進行賦值,然後利用new和std::copy對指標進行賦值

MyVector(const MyVector& v):size(v.size),capacity(v.capacity)
{
  elements = new T[capacity];
  std::copy(v.elements,v.elements+size,elements);
}

複製複製函式,透過過載=實現,需要先判斷兩者是否相等

MyVector &operator=(const MyVector& v)
{
  if(this != &v)
  {
    delete[] elements;
    size = v.size;
    capacity = v.capacity;
    elements = new T[capacity];
  std::copy(v.elements,v.gelements+size,elements);
  }
  return *this;
}

相關操作

reserve使用對陣列的擴容

void reserve(size_t newcapacity)
{
  if(newcapacity > capacity)
  {
    T* newelements = new T[newcapacity];
    std::copy(elements,elements+size,newelements);
    delets[] elements;
    capacity = newcapacity;
    elements = newelemenys;
  }
}

push_back函式實現在陣列尾部增加元素,判斷大小是否超過容量

void push_back(const T& value)
{
  if(size >= capacity)
  {
    reserve(capacity == 0 ? 1 : 2*capacity);
  }
  elements[size++] = value;
}

透過過載[]運算子實現對下標的索引

T& operator[](int index)
{
  if(index >= size)
  {
    throw std::out_of_range("invalid Index: out of range");
  }
  return elements[index];
}

insert透過傳入下標和數值實現元素的插入

void insert(int index, const T& value)
{
  if (index > size)
  {
    throw std::out_of_range("Index out of range");
  }
  else if(capacity == size) // 擴容
  {
    reserve(capacity == 0 ? 1:2*capacity);
  }
  //將index到末尾size的元素向後移動
  for(size_t i = size;i>index;i--)
  {
    elements[i] = elements[i-1];
  }
  elements[index] = value;
  size++;
}

begin()和end()是為了實現迭代器而設計的

    // 使用迭代器遍歷陣列的開始位置
T *begin()
{
  return elements;
}
  
// 使用迭代器遍歷陣列的結束位置
T *end()
{
  return elements + size;
}

pop_back()彈出陣列末尾元素只需要將陣列元素減一就可以實現了

void pop_back()
{
  size--;
}

vector相關

擴容過程

一般的擴容過程涉及到以下幾個步驟

  1. 分配一個更大的記憶體塊、通常是當前大小的兩倍
  2. 將當前所有元素移到新分配的記憶體塊中
  3. 銷燬舊元素,並釋放舊記憶體塊
  4. 插入新元素

push_back 和 emplace_back

其實現效果都是向vector的末尾新增元素
push_back會對給定物件進行複製或者移動構造,將元素新增導末尾,即會先透過建構函式將value給構造出來,然後再呼叫複製建構函式,新增到末尾
emplace_back則使用給定的引數直接在vector末尾構造一個元素,無需複製或者移動構造,只需要在末尾呼叫建構函式構造即可

減少vector的佔用空間

在C++ 11中引入了shrink_to_fit來請求移除未使用的容量,會嘗試將容量壓縮至size,打不保證一定壓縮

檢查vector是否為空

使用empty()方法,empty()方法是常數時間,因為一般透過檢測尾節點來實現判斷
而size()方法通常是遍歷整個陣列獲取長度

迭代器失效

如果對vector進行重新分配,那麼其所有指向元素的迭代器都會失效
如果在vector中間插入元素,那麼從該點到末尾的所有迭代器都會失效。
為了避免這些問題,最好使用remove() remove_if()方法搭配erase()方法來刪除元素

如果vector的元素型別為指標,需要考慮什麼問題

  1. 記憶體管理:在清除vector的頭指標之前,要保證每個元素分配的記憶體空間被清理掉
  2. 所有權和生命週期:在對vector元素訪問期間,要保證這些指標指向的物件是有效的。同時需要清楚的定義誰擁有這些物件和修改時機
  3. 異常控制:當建立和填充vector時需要需要一個機制來處理已經分配的記憶體,避免記憶體洩漏
  4. 智慧指標:一般建議使用智慧指標(unique_ptr或則shared_ptr)來當作指標型別,vector被銷燬後,會自動銷燬
  5. 避免懸垂指標:當指向的物件失效時,要確保沒有懸垂指標指向無效的記憶體地址,同樣vector被重新分配後,這些指標也會失效
  6. 深複製和淺複製:如果是指標型別,在複製vector時,需要考慮是複製指標+物件還是隻複製指標

相關文章