【C++】STL之list學習
list學習總結
- 一. list是什麼?
- 二.list的使用(常見使用介面)
- 1.list的常見建構函式
- 2.list常見的iterator
- 3.list的size和empty介面
- 4.list獲取資料元素(front&&back)
- 5.list的增刪改查介面
- 6.list的迭代器失效問題
- 三.list的模擬實現
一. list是什麼?
1.list概述
list
是可以在常數時間範圍
內在任意位置進行插入和刪除
的序列式容器
,並且該容器可以前後雙向迭代
。list
的底層是雙向連結串列
結構,雙向連結串列中每個元素儲存在互不相關的獨立節點
中,在節點中通過指標
指向前一個
後一個
元素。list
與forward_list
非常相似,最主要的不同在於forward_list
是單鏈表
,只能單向迭代
。
2.list相對其他容器的優缺點
- 與其他的
序列式容器
相比(array,vector,deque)
,list
通常在任意位置
進行插入、移除
元素的執行效率更好,一般在常數時間
內。 list和forward_list
最大的缺陷
是不支援任意位置的隨機訪問
。比如:要訪問list
的第6個
元素,必須從已知的位置(比如頭部或者尾部)
迭代到該位置,在這段位置上迭代需要線性的時間
開銷;list
還需要一些額外的空間
,以儲存每個節點的相關聯資訊(指向前一個結點或後一個結點的指標域)
list
相對於vector
還有一個好處就是空間的利用率比較高。每刪除或者插入
一個元素,就配置或釋放
一個空間。
3.list的資料結構
SGI
版本的list
不僅僅是一個雙向連結串列
,而且還是一個環狀雙向連結串列
,也就是一條雙向迴圈連結串列
。
注:本圖擷取自《STL原始碼剖析一書》
二.list的使用(常見使用介面)
1.list的常見建構函式
list()
:構造空的list
list (size_type n, const value_type& val = value_type())
:構造一個含有n
個val
值的list
list (const list& x)
:拷貝建構函式list (InputIterator first, InputIterator last)
:迭代器區間構造
void test1()
{
list<int> l1;//空構造
list<int> l2(4, 10);//構造4個值為10的結點
list<int> l3(l2);//拷貝構造
list<int> l4(l2.begin(), l2.end());//使用l2的迭代器區間[begin,end)構造
//C++11語法糖遍歷
for (auto& e : l4)
{
cout << e << " ";
}
cout << endl;
int arr[] = { 1, 2, 3, 4 };//使用陣列為迭代器區間構造
list<int> l5(arr, arr + (sizeof(arr) / sizeof(int)));
//迭代器遍歷
list<int>::iterator it = l5.begin();
while (it != l5.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
2.list常見的iterator
2.1 常見介面
begin()
:返回第一個元素的迭代器end()
:返回最後一個元素下一個位置的迭代器rbegin()
:返回第一個位置的反向迭代器,既end() - 1
rend()
:返回最後一個元素下一個位置的reverse_iterator
,即end()
位置cbegin() (C++11)
:返回第一個元素的const_iterator
cend() (C++11)
:返回最後一個元素下一個位置的const_iterator
2.2 迭代器在list的位置
begin與end
為正向
迭代器,對迭代器執行++
操作,迭代器向後
移動rbegin()與rend()
為反向
迭代器,對迭代器執行++
操作,迭代器向前
移動cbegin與cend
為const
的正向
迭代器,與begin和end
不同的是:該迭代器指向節點中的元素值不能修改
crbegin與crend
為cons
t的反向
迭代器,與rbegin和rend
不同的是:該迭代器指向節點中的元素值不能修改
2.3 iterator的使用
void test2()
{
int arr[] = { 1, 2, 3, 4 };
list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
//正向迭代器 1 2 3 4
list<int>::iterator it = l1.begin();
while (it != l1.end())
{
//*it *= 2;可以修改
cout << *it << " ";
++it;
}
cout << endl;
//正向const迭代器 1 2 3 4(但是*it不能修改)
list<int>::const_iterator cit = l1.cbegin();
while (cit != l1.cend())
{
//*cit *= 2; 不能修改
cout << *cit << " ";
++cit;
}
cout << endl;
//反向迭代器 4 3 2 1
list<int>::reverse_iterator rit = l1.rbegin();
while (rit != l1.rend())
{
//*rit *= 2; 可以修改
cout << *rit << " ";
++rit;
}
cout << endl;
//反向const迭代器 4 3 2 1
list<int>::const_reverse_iterator crit = l1.crbegin();
while (crit != l1.crend())
{
//*crit *= 2; 不可以修改
cout << *crit << " ";
++crit;
}
cout << endl;
}
3.list的size和empty介面
bool empty() const
:檢測list是否為空,是返回true,否則返回falsesize_t size() const
:返回list中有效節點的個數
void test3()
{
int arr[] = { 1, 2, 3, 4 };
list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
int size = l1.size();
//輸出l1的大小
cout << size << endl;
//判斷l1是否為空,不為空遍歷列印
if (l1.empty())
{
cout << "list為空" << endl;
}
else
{
for (auto e : l1)
{
cout << e << " ";
}
cout << " ";
}
}
4.list獲取資料元素(front&&back)
reference front()
:返回list第一個結點值的引用const_reference front() const
:返回list的第一個節點中值的const引用reference back()
:返回list的最後一個節點中值的引用const_reference back() const
:返回list的最後一個節點中值的const引用
void test4()
{
int arr[] = { 1, 2, 3, 4 };
list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
l1.front() = 10;//將l1中的第一個資料改為10
const int& x = l1.back();//獲取l1中最後一個結點值的const引用
//x = 10;const引用不可改變
cout << x << endl;
}
5.list的增刪改查介面
5.1 介面說明
-
void push_front (const value_type& val)
:在list第一個結點前插入值為 val的結點 -
void pop_front()
:刪除list中第一個結點 -
void push_back (const value_type& val)
:在list最後一個結點後插入值為val的結點 -
void pop_back()
:刪除list中最後一個結點 -
iterator insert (iterator position, const value_type& val)
:在list position 位置中插 入值為val的結點 -
void insert (iterator position, size_type n, const value_type& val)
:在position位置插入值為val的n個結點 -
void insert (iterator position, InputIterator first, InputIterator last)
:在list position位置插入 [first, last)區間中元素 -
iterator erase (iterator position)
:刪除position位置上的結點 -
iterator erase (iterator first, iterator last)
: 刪除list中[first, last)區間中的元素 -
void swap (list& x)
:交換兩條list中的結點 -
void resize (size_type n, value_type val = value_type())
:將list中有效元素個數改變 到n個,多出的元素用val 填充 -
void clear()
:清空list中的有效元素
下邊3個介面為C++11新特性的介面:
template <class... Args> void emplace_front (Args&&... args)
:在list中的第一個結點前根據引數構造新的結點template <class... Args> void emplace_back (Args&&... args)
:在list最後一個結點後根據引數直接構造新的結點template <class... Args> iterator emplace( const_iterator position, Args&&... args)
:在list的任意位置根據引數直接構造新的結點
/*測試emplace_back、emplace_front、emplace */
class Date {
public:
Date(int year = 1900, int month = 1, int day = 1)
: _year(year)
, _month(month)
, _day(day)
{
cout << "Date(int, int, int):" << this << endl;
}
Date(const Date& d)
: _year(d._year)
, _month(d._month)
, _day(d._day)
{
cout << "Date(const Date&):" << this << endl;
}
private:
int _year;
int _month;
int _day;
};
//push_back尾插:先構造好元素,然後將元素拷貝到節點中,插入時先調建構函式,再調拷貝建構函式
//emplace_back尾插:先構造節點,然後呼叫建構函式在節點中直接構造物件
//emplace_back比push_back更高效,少了一次拷貝建構函式的呼叫
void test8()
{
list<Date> l;
Date d(2018, 10, 20);
l.push_back(d); //構造--拷貝構造
l.emplace_back(2018, 10, 21); //插入時直接構造
l.emplace_front(2018, 10, 19); //插入時直接構造
}
5.2 測試功能
/*測試push_front、push_back、pop_front、pop_back*/
void test5()
{
int arr[] = { 1, 2, 3, 4 };
list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
l1.push_front(0);
l1.push_back(5);
PrintList(l1);//0 1 2 3 4 5
l1.pop_front();
l1.pop_back();
PrintList(l1);//1 2 3 4
}
/*測試insert、erase*/
void test6()
{
int arr[] = { 1, 2, 3, 4 };
list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
//獲得值為3的迭代器
list<int>::iterator pos = find(l1.begin(), l1.end(), 3);
//在pos前插入值為0的結點
l1.insert(pos, 0);
PrintList(l1);//1 2 0 3 4
//在pos前插入5個值為8的結點
l1.insert(pos, 2, 8);
PrintList(l1);//1 2 0 8 8 3 4
//在pos前插入[v.begin(), v.end)區間中的結點
vector<int> v(2, 7);
l1.insert(pos, v.begin(), v.end());
PrintList(l1);//1 2 0 8 8 7 7 3 4
//刪除pos位置的元素
l1.erase(pos);
PrintList(l1);//1 2 0 8 8 7 7 4
//刪除迭代器區間的值
l1.erase(l1.begin() , l1.end());
PrintList(l1);//list為空
}
/* 測試resize、swap、clear */
void test7()
{
int arr[] = { 1, 2, 3, 4 };
list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
PrintList(l1);//1 2 3 4
//將l1中元素個數增加到10個,多出的元素用預設值填充
//如果list中放置的是內建型別,預設值為0
//如果list中放置自定義型別元素,呼叫預設建構函式
l1.resize(6);
PrintList(l1);//1 2 3 4 0 0
//將l1中的元素增加到8個,多出的元素用8來填充
l1.resize(8, 8);
PrintList(l1);//1 2 3 4 0 0 8 8
//將l1中的元素減少到5個
l1.resize(5);
PrintList(l1);//1 2 3 4 0
// 用vector中的元素來構造list
vector<int> v{ 4, 5, 6 };
list<int> l2(v.begin(), v.end());
PrintList(l2);//4 5 6
//交換l1和l2的元素
l1.swap(l2);
PrintList(l1);//4 5 6
PrintList(l2);//1 2 3 4 0
// 將l2中的元素清空
l2.clear();
cout << l2.size() << endl;//0
PrintList(l2);//空
}
6.list的迭代器失效問題
vector的insert和erase
迭代器都會失效,vector在insert
時會出現擴容
的問題,所以使用迭代器時可能會出現野指標問題,vector在erase
時由於vs的檢查機制,雖然將pos
迭代器位置的資料刪除後,會將後邊的元素移動到前邊,不會存在野指標,但是vs編譯器
會檢查出來,這個迭代器已經失效
。相對而說,list
的insert
不會失效,因為list不存在擴容
的問題,但是erase
也會存在失效,失效的只是指向被刪除節點的迭代器,其他迭代器不會受到影響。例如:(1-2-3,刪除2之後,1-3,迭代器依舊指向2,但是2已經被刪除了,所以會出現類似野指標的問題)
void test9()
{
int arr[] = { 1, 2, 3, 4 };
list<int> l1(arr, arr + (sizeof(arr) / sizeof(int)));
list<int>::iterator it = l1.begin();
while (it != l1.end())
{
// erase()函式執行後,it所指向的節點已被刪除
//因此it無效,在下一次使用it時,必須先給其賦值
//l1.erase(it);
//++it;這樣做編譯器報錯迭代器失效
/*可以這麼修改*/
//it = l1.erase(it);//erase刪除之後,返回下一個位置的迭代器
l1.erase(it++);//也可以這麼寫,相當於it = l1.erase(it);
}
}
三.list的模擬實現
1.list的結點類
list本身
和list的結點
是兩個不同的結構,要實現list
,必須先要實現它的結點類
。要實現的是一個雙向帶頭迴圈連結串列
,所以結點設計必須包括三個域,既prev指標域(指向前一個幾點)、next指標域(指向後一個結點)、data(資料域)
。
template<class T>
//為什麼不用struct而用class?
//如果不使用訪問限定符使用struct,struct預設訪問限定符為public
//如果使用訪問此限定符使用class,class預設訪問限定符為class
struct ListNode
{
//建構函式(T()相當於是一個匿名物件)
ListNode(const T& data = T())
:_data(data)
,_next(nullptr)
,_prev(nullptr)
{}
//不需要過載解構函式,直接使用預設生成的析構即可,因為成員變數只是3個指標
T _data; //資料域
ListNode<T>* _next;//指向前一個結點的指標
ListNode<T>* _prev;//指向回一個結點的指標
};
2.list的迭代器
list
不能像vector
一樣以一個原生指標
作為迭代器,因為它的結點不能保證在儲存空間中連續存在
。list
迭代器必須要能夠指向list的結點
,並且有能力進行正確的遞增、遞減、取值、成員取用
等操作。遞增操作時指向下一個結點
,遞減操作時指向下一個操作
,取值時取的是結點的資料值
,成員取用的是結點的成員
。所以我們應該講上述的成員和操作封裝在一個類
中。我們可以將原生態指標進行封裝
,因迭代器的使用形式與指標完全相同
,因此,在自定義的類中必須實現以下方法:
- 指標可以
解引用
,迭代器的類中必須過載operator*()
- 指標可以
++
向後移動,迭代器類中必須過載operator++()
與operator++(int)
和operator--()和operator--(int)
- 迭代器需要進行是否相等的比較,因此還需要過載
operator==()與operator!=()
/**
* 模擬實現list的迭代器類
*/
template<class T>
struct ListIterator
{
typedef ListNode<T> Node;
typedef ListIterator<T> iterator;
//構造
ListIterator(Node* node)
:_node(node)
{}
//拷貝構造
ListIterator(const iterator& i)
:_node(i._node)
{}
/**
* 過載迭代器的解引用、++、!=、==、--等
*/
// *it
T& operator*()
{
return _node -> _data;
}
// ++it
iterator operator++()
{
_node = _node -> _next;
return *this;
}
// it++
iterator operator++(int)
{
iterator tmp(*this);
_node = _node -> _next;
return tmp;
}
// --it
iterator operator--()
{
_node = _node -> _prev;
return *this;
}
// it--
iterator operator--(int)
{
iterator tmp(*this);
_node = _node -> _prev;
return tmp;
}
// it1 != it2
bool operator!=(const iterator& it)
{
return _node != it._node;
}
// it1 == it2
bool operator==(const iterator& it)
{
return _node == it._node;
}
Node* _node;
};
3.list建構函式
list
的常見建構函式
在上邊的已經提及,可以根據其功能模擬實現它:
//迭代器定義為和庫一樣的名字可以支援語法糖
typedef ListIterator<T><