C++ ——vector陣列筆記

戰爭熱誠發表於2024-01-21

   vector 是 C++ 標準庫中的一個動態陣列容器(Sequence Container),它可以自動管理記憶體大小,可以在執行時根據需要動態增長或縮小。它是一個非常常用且強大的容器,用於儲存一系列元素。可以簡單的認為,vector是一個能夠放任意型別的動態陣列。

  下面詳細介紹 vector 的使用方法,並提供相應的程式碼案例。

1,基本函式實現

1.1  建構函式

  建構函式是初始化一個陣列。vector本質是類别範本,可以儲存任何型別的資料。vector在宣告前需要加上資料型別,而vector則透過模板參量設定型別。

vector():建立一個空vector

vector(int nSize):建立一個vector,元素個數為nSize

vector(int nSize,const t& t):建立一個vector,元素個數為nSize,且值均為t

vector(const vector&):複製建構函式

vector(begin,end):複製[begin,end)區間內另一個陣列的元素到vector中

  示例:

vector<int> vec;  // 建立一個空陣列vec

vector<int> vec(1, 2, 3, 4, 5, 6);//vec中的內容為1, 2, 3, 4, 5, 6

vector<int> vec(4);  // 開闢四個空間,值預設為0

vector<int> vec(5, 4);  //建立5個值為4的陣列

vector<int> vec(a);//宣告並用a向量初始化vec向量

vector<int> vec(a.begin(), a.end()); // 將a的值從頭開始到尾複製

vector<int> vec(a.rbegin(), a.rend()); // 將a的值從尾到頭複製

int a[5]={1,2,3,4,5};
vector<int> vec(a,a+5);//將a陣列的元素用來初始化vector向量

vector<int> vec(&a[1],&a[4]);//將a[1]-a[4]範圍內的元素作為vec的初始值

 

1.2  增加函式

  即向vector中插入元素。其中emplace_back的效果和push_back一樣,都是尾部插入元素。二者的差別在於底部實現的機制不同;push_back是將這個元素複製或移動到容器中(如果是複製的話,事後會自行銷燬先前建立的這個元素);而emplace_back在實現時,則是直接在容器尾部建立這個元素,省去了複製或移動元素的過程,所以emplace_back的速度更快。

void push_back(const T& x):向量尾部增加一個元素X

iterator insert(iterator it,const T& x):向量中迭代器指向元素前增加一個元素x

iterator insert(iterator it,int n,const T& x):向量中迭代器指向元素前增加n個相同的元素x

iterator insert(iterator it,const_iterator first,const_iterator last):向量中迭代器指向元素前插入另一個相同型別向量的[first,last)間的資料

  示例:

//在vector的末尾插入新元素
vec.push_back(1);

//在迭代器的前面插入新元素
vector<int>::iterator it;
it=vec.begin();
vec.insert(it,5);//在第一個元素前面插入5

//在vector中加入3個1元素,同時清除掉以前的元素
vec.assign(3,1);//現在vector中只有3個1

  assgin修改vector,和insert操作類似,不過insert是從尾部插入,而assign則將整個vector改變。

 

1.3  刪除函式

  erase是透過迭代器刪除某個或某個範圍的元素,並返回下一個元素的迭代器。

iterator erase(iterator it):刪除向量中迭代器指向元素

iterator erase(iterator first,iterator last):刪除向量中[first,last)中元素

void pop_back():刪除向量中最後一個元素

void clear():清空向量中所有元素

  示例:

//刪除最後一個元素
vec.pop_back();

//刪除指定位置的元素
vec.erase(vec.begin());//刪除第一個位置的元素值

//清除所有元素
vec.clear();

//判斷該陣列是否為空
vec.empty();

  

1.4  遍歷函式

reference at(int pos):返回pos位置元素的引用

reference front():返回首元素的引用

reference back():返回尾元素的引用

iterator begin():返回向量頭指標,指向第一個元素

iterator end():返回向量尾指標,指向向量最後一個元素的下一個位置

reverse_iterator rbegin():反向迭代器,指向最後一個元素

reverse_iterator rend():反向迭代器,指向第一個元素之前的位置

  遍歷陣列示例:

//向陣列一樣利用下標進行訪問
vector<int> a;
for(int i=0;i<a.size();i++){
     cout<<a[i];
}

//利用迭代器進行訪問
vector<int>::iterator it;
for(it=a.begin();it!=a.end();it++){
   cout<<*it;
}

  

1.5  vector容量和大小

  顧名思義,size表示當前有多少個元素,capacity是可容納的大小,因為vector是順序儲存的,那麼和陣列一樣,有一個初始容量,在vector裡就是capacity。capacity必然大於等於size,每次擴容時會改變,具體大小和vector底層實現機制有關。

  max_size 是可儲存的最大容量,和實際的編譯器,系統有關,使用的比較少。

  empty就是判斷vector是否為空,其實就是判斷size是否等於0。定義vector時設定了大小,resize修改大小等操作,vector都不為空,clear後,size=0,那麼empty判斷就為空。

bool empty() const:判斷向量是否為空,若為空,則向量中無元素

int size() const:返回向量中元素的個數

int capacity() const:返回當前向量所能容納的最大元素值

int max_size() const:返回最大可允許的vector元素數量值

  

2,容器特性和屬性

2.1  容器的特性

2.1.1,順序序列(Sequential Container)

  順序容器中的元素按照嚴格的線性順序排序。可以透過元素在序列中的位置訪問對應的元素。

  • std::vector 是C++標準庫中的一種順序序列容器,這意味著它按照元素的順序進行儲存和訪問。
  • 每個元素都有一個唯一的位置(索引),可以透過索引直接訪問,同時支援迭代器用於迴圈訪問。

2.1.2,動態陣列(Dynamic Array)

  支援對序列中的任意元素進行快速直接訪問,甚至可以透過指標算述進行該操作。提供了在序列末尾相對快速地新增/刪除元素的操作。

  • std::vector 是一個動態陣列容器,它在內部使用動態分配的陣列來儲存元素。
  • 它能夠動態調整陣列的大小,可以在執行時根據需要增加或減少元素的數量,而無需預先指定陣列的大小。

2.1.3,能夠感知記憶體分配器的(Allocator-aware)

  容器使用一個記憶體分配器物件來動態地處理它的儲存需求。

  • std::vector 是 allocator-aware 的,這意味著它能夠感知並與特定的記憶體分配器協同工作。
  • 使用者可以透過傳遞自定義的記憶體分配器(通常是一個模板引數),以實現對記憶體分配和釋放過程的控制。

   以下是一個簡單的示例,演示了 std::vector 的這三個特性:

#include <iostream>
#include <vector>

int main() {
    // 建立一個空的vector
    std::vector<int> myVector;

    // 新增元素
    myVector.push_back(10);
    myVector.push_back(20);
    myVector.push_back(30);

    // 遍歷並輸出元素
    for (const auto& num : myVector) {
        std::cout << num << " ";
    }

    return 0;
}

  在這個例子中,std::vector 被用作順序序列,儲存了動態陣列,並且它的記憶體管理是由C++標準庫的分配器機制處理的。

 

2.2  容器的屬性

  下面介紹一下 std::vector 的基本屬性,以及一些示例。

2.2.1 動態調整大小

   std::vector是一個動態大小的容器,可以在執行時根據需要自動調整大小。這意味著你可以隨時向vector中新增或刪除元素,而無需擔心陣列大小的固定性。它會自動處理記憶體管理。

  示例:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> myVector;  // 建立一個空的vector

    myVector.push_back(1);     // 新增元素
    myVector.push_back(2);
    myVector.push_back(3);

    std::cout << "Vector的大小: " << myVector.size() << std::endl;  // 輸出vector的大小

    return 0;
}

  

2.2.2 快速隨機訪問

  std::vector支援隨機訪問,即透過索引直接訪問元素。這使得在訪問和修改元素時效率非常高。 由於元素的連續儲存(底層實現是陣列),std::vector 支援快速的隨機訪問。你可以透過索引直接訪問向量中的元素。

  示例:

#include <iostream>
#include <vector>

int main() {
    std::vector<std::string> names = {"Alice", "Bob", "Charlie"};

    // 隨機訪問元素
    std::cout << "第二個元素是: " << names[1] << std::endl;

    return 0;
}

  

2.2.3 動態記憶體管理

  std::vector負責管理其內部的動態記憶體,因此你無需手動分配或釋放記憶體。當元素被新增或刪除時,vector會自動處理記憶體的分配和釋放。當向量的大小超過其當前容量時,會動態分配更多的記憶體以容納更多的元素。這可以確保在新增元素時不會頻繁重新分配記憶體。

#include <iostream>
#include <vector>

int main() {
    std::vector<double> prices;

    // 新增元素
    prices.push_back(10.5);
    prices.push_back(20.0);
    prices.push_back(15.75);

    // 遍歷元素並輸出
    for (const auto& price : prices) {
        std::cout << "價格: " << price << std::endl;
    }

    return 0;
}

  

  總結: std::vector是C++中一個強大且靈活的容器,它提供了動態大小、隨機訪問、動態記憶體管理和豐富的功能。透過合理利用std::vector,你可以更輕鬆地處理動態資料集合,而無需手動管理記憶體或擔心固定陣列大小的限制。除此之外,還有下面屬性:

  • 1,連續記憶體儲存:std::vector中的元素在記憶體中是連續儲存的,這意味著可以透過指標算術直接訪問元素,並且對於許多演算法來說,這種儲存方式是高效的,有助於提高訪問效率。
  • 2,尾部插入和刪除: 向量支援在尾部進行快速的元素插入和刪除操作。
  • 3,快速尾部操作: 由於元素的尾部插入和刪除是快速的,std::vector 適用於需要在序列的末尾執行大量操作的場景。
  • 4,STL 演算法和迭代器支援: std::vector 與 C++ 標準模板庫(STL)的其他部分無縫整合,可以使用演算法和迭代器對向量進行操作。你可以使用迭代器來遍歷 std::vector 中的元素。

 

2.3  如何獲取vector的記憶體

  在C++中,std::vector 是一個封裝了動態陣列的容器,它自動處理記憶體的分配和釋放。要獲取 std::vector 的底層記憶體地址,你可以使用 data() 成員函式,它返回指向容器第一個元素的指標。

  以下是一個示例:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> myVector = {1, 2, 3, 4, 5};

    // 獲取底層記憶體地址
    int* ptr = myVector.data();

    // 輸出每個元素及其記憶體地址
    for (size_t i = 0; i < myVector.size(); ++i) {
        std::cout << "元素 " << myVector[i] << " 的記憶體地址: " << &myVector[i] << std::endl;
    }

    // 輸出整個vector的底層記憶體地址
    std::cout << "Vector的底層記憶體地址: " << ptr << std::endl;

    return 0;
}

  請注意,透過 data() 獲取的指標指向的是 vector 中第一個元素的地址。如果 vector 是空的,data() 返回的是一個合法但未定義的指標,因此在使用之前應確保 vector 不為空。

  注意:一般情況下,直接使用 data() 獲取 vector 的底層記憶體並進行操作可能不是一個良好的實踐,因為 std::vector 提供了許多高階的函式和方法,可以更安全和方便地訪問元素。直接訪問記憶體可能會引發未定義的行為,特別是在修改元素時。

 

2.3.1  疑問:獲取記憶體使用 myVector.data() 和 myVector[0]的區別是什麼?

  myVector.data()myVector[0] 返回的是不同型別的指標,並且有不同的用途。

myVector.data()

  • myVector.data() 返回指向 vector 第一個元素的指標,是一個原始指標(T* 型別,其中 T 是 vector 中元素的型別)。
  • 這個指標允許你直接訪問 vector 的底層記憶體,但是需要小心使用,因為它沒有邊界檢查和安全保障。
  • 在對 data() 返回的指標進行操作時,務必確保 vector 不為空。
int* ptr = myVector.data();
// 對 ptr 進行直接記憶體操作

  

myVector[0]

  • myVector[0] 是 vector 的運算子過載,它返回 vector 中第一個元素的引用。
  • 這個引用允許你直接訪問 vector 的第一個元素,並且透過引用操作進行修改。它提供了邊界檢查,確保你訪問的是有效的元素。
int& ref = myVector[0];
// 對 ref 進行直接操作,修改會影響 vector 中的元素

  

選擇使用的場景:

  • 如果你需要直接訪問 vector 中第一個元素的底層記憶體,並且要進行原始指標的操作,可以使用 myVector.data()
  • 如果你只是想獲取第一個元素的值,並且可能會進行修改,使用 myVector[0] 更直觀且更安全,因為它提供了引用而不是裸指標,並且有邊界檢查。

 

 3,C++ vector 底層實現機制

  這裡參考的是:https://c.biancheng.net/view/6901.html

  STL 眾多容器中,vector是最常用的容器之一,其底層所採用的資料結構非常簡單,就只是一段連續的線性記憶體空間。

3.1  vector原始碼分析

  透過分析vector容器的原始碼就可以看到,他就是使用三個迭代器來表示:

//_Alloc 表示記憶體分配器,此引數幾乎不需要我們關心
template <class _Ty, class _Alloc = allocator<_Ty>>
class vector{
    ...
protected:
    pointer _Myfirst;
    pointer _Mylast;
    pointer _Myend;
};

  其中,_Myfirst 指向的是 vector 容器物件的起始位元組位置;_Mylast 指向當前最後一個元素的末尾位元組;_myend 指向整個 vector 容器所佔用記憶體空間的末尾位元組。
  下圖演示了以上這 3 個迭代器分別指向的位置。

   如上圖所示,透過這 3 個迭代器,就可以表示出一個已容納 2 個元素,容量為 5 的 vector 容器。

  在此基礎上,將 3 個迭代器兩兩結合,還可以表達不同的含義,例如:

    • _Myfirst 和 _Mylast 可以用來表示 vector 容器中目前已被使用的記憶體空間;
    • _Mylast 和 _Myend 可以用來表示 vector 容器目前空閒的記憶體空間;
    • _Myfirst 和 _Myend 可以用表示 vector 容器的容量。
    • 對於空的 vector 容器,由於沒有任何元素的空間分配,因此 _Myfirst、_Mylast 和 _Myend 均為 null。

  透過靈活運用這 3 個迭代器,vector 容器可以輕鬆的實現諸如首尾標識、大小、容器、空容器判斷等幾乎所有的功能,比如:

template <class _Ty, class _Alloc = allocator<_Ty>>
class vector{
public:
    iterator begin() {return _Myfirst;}
    iterator end() {return _Mylast;}
    size_type size() const {return size_type(end() - begin());}
    size_type capacity() const {return size_type(_Myend - begin());}
    bool empty() const {return begin() == end();}
    reference operator[] (size_type n) {return *(begin() + n);}
    reference front() { return *begin();}
    reference back() {return *(end()-1);}
    ...
};

  

3.2  vector 擴大容量的本質

  上面有說到果,vector作為容器有著動態陣列的功能,當加入的資料大於vector容量(capacity)時會自動擴容,系統會自動申請一片更大的空間,把原來的資料複製過去,釋放原來的記憶體空間。

  另外需要指明的是,當 vector 的大小和容量相等(size==capacity)也就是滿載時,如果再向其新增元素,那麼 vector 就需要擴容。vector 容器擴容的過程需要經歷以下 3 步:

  1. 完全棄用現有的記憶體空間,重新申請更大的記憶體空間;
  2. 將舊記憶體空間中的資料,按原有順序移動到新的記憶體空間中;
  3. 最後將舊的記憶體空間釋放。

  這也就解釋了,為什麼 vector 容器在進行擴容後,與其相關的指標、引用以及迭代器可能會失效的原因

  由此可見,vector 擴容是非常耗時的。為了降低再次分配記憶體空間時的成本,每次擴容時 vector 都會申請比使用者需求量更多的記憶體空間(這也就是 vector 容量的由來,即 capacity>=size),以便後期使用。vector 容器擴容時,不同的編譯器申請更多記憶體空間的量是不同的。以 VS 為例,它會擴容現有容器容量的 50%。

 

4,使用示例

  std::vector 是一個非常有用的標準庫容器,它提供了動態陣列的功能,可以根據需要自動調整大小。以下是一些關於 std::vector 的基本示例:

4.1:引入標頭檔案

  首先,使用的話需要引入標頭檔案 <vector>

#include <vector>

  

4.2:建立 vector 物件

  直接使用vector 模板類來建立一個 vector 物件。可以建立儲存特定型別元素的 vector,格式為: vector<資料型別> 名字。例如:

vector<int> myVector; // 建立一個儲存整數的 vector,名字為myVector

vector<char> myVector; // 建立一個儲存字元的 vector,名字為myVector

vector<string> myVector; // 建立一個儲存字串的 vector,名字為myVector
……

  

4.3:建立 vector並新增元素

  使用push_back()函式將元素新增到vector的末尾,預設且只能新增到末尾。

  程式碼如下:

#include <iostream>
#include <vector>

int main() {
    // 建立一個空的 vector
    std::vector<int> numbers;

    // 向 vector 中新增元素
    numbers.push_back(1);
    numbers.push_back(2);
    numbers.push_back(3);

    // 訪問 vector 中的元素
    std::cout << "Vector elements: ";
    for (int i = 0; i < numbers.size(); ++i) {
        std::cout << numbers[i] << " ";
    }
    std::cout << std::endl;

    return 0;
}

  列印結果如下:

Vector elements: 1 2 3

  

4.4:使用範圍迴圈遍歷 vector

  程式碼如下:

#include <iostream>
#include <vector>

int main() {
    // 建立一個 vector,並初始化
     std::vector<std::string> fruits = {"Apple", "Banana", "Orange"};

    // 也可以向 vector 中新增元素
    fruits.push_back("pear");

    // 使用範圍迴圈遍歷 vector
    std::cout << "Fruits: ";
    for (const auto& fruit : fruits) {
        std::cout << fruit << " ";
    }
    std::cout << std::endl;

    return 0;
}

  結果如下:

Fruits: Apple Banana Orange pear 

  

4.5:插入和刪除元素

#include <iostream>
#include <vector>

int main() {
    // 建立一個 vector,並初始化
    std::vector<double> prices = {10.5, 20.3, 15.0};

    // 直接在尾部插入元素
    prices.push_back(88);

    // 在指定位置插入元素
    prices.insert(prices.begin() + 1, 25.8);

    // 刪除指定位置的元素
    prices.erase(prices.begin() + 2);

    // 列印 vector 元素
    std::cout << "Prices: ";
    for (const auto& price : prices) {
        std::cout << price << " ";
    }
    std::cout << std::endl;

    return 0;
}

  結果:

Prices: 10.5 25.8 15 88 

  

4.6:使用迭代器訪問陣列

  除了直接使用for迴圈遍歷訪問陣列之外,我們也可以利用迭代器將容器中陣列輸出。

  首先宣告一個迭代器,vector<int>::iterator it;  來訪問vector容器,目的是遍歷或者指向vector容器的元素。

#include <iostream>
#include <vector>

int main() {
    // 建立一個 vector,並初始化
    std::vector<int> numbers = {1, 2, 3, 4, 5};

    // 使用迭代器訪問 vector 中的元素
    std::cout << "Vector elements: ";
    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << " ";
    }
    std::cout << std::endl;

    return 0;
}

  結果如下:

Vector elements: 1 2 3 4 5 

  

相關文章