終於到了STL了,c++標準模板庫,這個纔是c++和c的最大區別,也是c++的終極boss。刷c++這個副本刷到這裏,也快結束了,這個這個副本的最大的一個boss。打敗了這個boss就可以獲得技能書了。(c++技能書)
8.1 STL理論基礎
8.1.1 STL概念
STL(standard Template library,標準模板庫),是惠普實驗室開發的一系列軟件統稱。現在只要出現在c++中,但是引入c++之前該技術已經存在很久了。
STL從廣義上分:容器(container)算法(algorithm)迭代器(iterator),容器和算法之間通過迭代器進行無縫連接。STL幾乎所有代碼都使用模板類和模板函數,這比傳統的由函數和類組成的庫提高了更好的代碼重用機會。
STL具有高可重用性,高性能,高移植性,跨平臺的優點:
- 高可重用性(幾乎所有代碼都是使用類模板和函數模板)
- 高性能(如map可以高效地從十萬條記錄裏面查出指定的記錄)
- 高移植性()
- 跨平臺
下面的容器介紹都是從這個網站學習過來的,要想看詳細信息,去這個網站學習學習。
http://c.biancheng.net/view/343.html
8.1.2 容器
容器(container)用於存放數據的類模板。可變長數組、鏈表、平衡二叉樹等數據結構在 STL 中都被實現爲容器。
程序員使用容器時,即將容器類模板實例化爲容器類時,會指明容器中存放的元素是什麼類型的。
容器中可以存放基本類型的變量,也可以存放對象。對象或基本類型的變量被插入容器中時,實際插入的是對象或變量的一個複製品。
順序容器
順序容器有以下三種:可變長動態數組 vector、雙端隊列 deque、雙向鏈表 list。
它們之所以被稱爲順序容器,是因爲元素在容器中的位置同元素的值無關,即容器不是排序的。將元素插入容器時,指定在什麼位置(尾部、頭部或中間某處)插入,元素就會位於什麼位置。
關聯容器
關聯容器有以下四種:set、multiset、map、multimap。關聯容器內的元素是排序的。插入元素時,容器會按一定的排序規則將元素放到適當的位置上,因此插入元素時不能指定位置。
#include <vector>
int main(int argc, char **argv)
{
//定義一個容器v
vector<int> v;
//往容器添加數據
v.push_back(10);
v.push_back(20);
v.push_back(30);
v.push_back(40);
//容器的操作方法,不僅僅只有一個,有很多個,可以看看我昨天轉載的博客
//裏面就有這些方
//https://blog.csdn.net/C1033177205/article/details/104120275
return 0;
}
8.1.3 迭代器
要訪問順序容器和關聯容器中的元素,需要通過“迭代器(iterator)”進行。迭代器是一個變量,相當於容器和操縱容器的算法之間的中介。迭代器可以指向容器中的某個元素,通過迭代器就可以讀寫它指向的元素。從這一點上看,迭代器和指針類似。
迭代器按照定義方式分成以下四種。
-
正向迭代器,定義方法如下:
容器類名::iterator 迭代器名; -
常量正向迭代器,定義方法如下:
容器類名::const_iterator 迭代器名; -
反向迭代器,定義方法如下:
容器類名::reverse_iterator 迭代器名; -
常量反向迭代器,定義方法如下:
容器類名::const_reverse_iterator 迭代器名;
迭代器的功能分類
不同容器的迭代器,其功能強弱有所不同。容器的迭代器的功能強弱,決定了該容器是否支持 STL 中的某種算法。例如,排序算法需要通過隨機訪問迭代器來訪問容器中的元素,因此有的容器就不支持排序算法。
常用的迭代器按功能強弱分爲輸入、輸出、正向、雙向、隨機訪問五種,這裏只介紹常用的三種。
-
正向迭代器。假設 p 是一個正向迭代器,則 p 支持以下操作:++p,p++,*p。此外,兩個正向迭代器可以互相賦值,還可以用==和!=運算符進行比較。
-
雙向迭代器。雙向迭代器具有正向迭代器的全部功能。除此之外,若 p 是一個雙向迭代器,則–p和p–都是有定義的。–p使得 p 朝和++p相反的方向移動。
-
隨機訪問迭代器。隨機訪問迭代器具有雙向迭代器的全部功能。若 p 是一個隨機訪問迭代器,i 是一個整型變量或常量,則 p 還支持以下操作:
p+=i:使得 p 往後移動 i 個元素。
p-=i:使得 p 往前移動 i 個元素。
p+i:返回 p 後面第 i 個元素的迭代器。
p-i:返回 p 前面第 i 個元素的迭代器。
p[i]:返回 p 後面第 i 個元素的引用。
此外,兩個隨機訪問迭代器 p1、p2 還可以用 <、>、<=、>= 運算符進行比較。p1<p2的含義是:p1 經過若干次(至少一次)++操作後,就會等於 p2。其他比較方式的含義與此類似。
對於兩個隨機訪問迭代器 p1、p2,表達式p2-p1也是有定義的,其返回值是 p2 所指向元素和 p1 所指向元素的序號之差(也可以說是 p2 和 p1 之間的元素個數減一)。
表1:不同容器的迭代器的功能
容器 | 迭代器功能 |
---|---|
vector | 隨機訪問 |
deque | 隨機訪問 |
list | 雙向 |
set / multiset | 雙向 |
map / multimap | 雙向 |
stack | 不支持迭代器 |
queue | 不支持迭代器 |
priority_queue | 不支持迭代器 |
下面寫個簡單的例子:
#include <vector>
int main(int argc, char **argv)
{
vector<int> v; //定義一個容器v
//往容器添加數據
v.push_back(10);
v.push_back(20);
v.push_back(30);
v.push_back(40);
//使用迭代器遍歷
//1.直接用迭代器遍歷
for(vector<int>::iterator it = v.begin(); it != v.end(); it++) {
cout << *it << " "; //*it 就代表着是迭代器裏面的內容,迭代器就像一個指針
}
cout << endl;
for(int i=0; i<v.size(); i++) {
cout << v[i] << " "; //可以像數組那樣遍歷
}
cout << endl;
//2.先使用stl函數for_each,這個函數功能等下講
return 0;
}
這裏我學習寫的遍歷就兩種把,其他的看卡上面的鏈接,很多內容的,還有迭代器的輔助函數,不能抄太多是吧,你們自己去看。
8.1.4 算法
STL 提供能在各種容器中通用的算法(大約有70種),如插入、刪除、查找、排序等。算法就是函數模板。算法通過迭代器來操縱容器中的元素。
許多算法操作的是容器上的一個區間(也可以是整個容器),因此需要兩個參數,一個是區間起點元素的迭代器,另一個是區間終點元素的後面一個元素的迭代器。例如,排序和查找算法都需要這兩個參數來指明待排序或待查找的區間。
這裏我自己找了個函數舉例:(終於不要擔心說抄襲了,哈哈哈)
//我在algorithm頭文件中找到一個比較簡單執行迭代器模板函數
//並且我很高興,可以看到這個模板,不過那些多餘的東西,我也看不懂,不過可以直接忽略
//_Frist _Last 是傳遞迭代器的,開始指針,和結束指針。
//_Func 傳遞的是操作函數,函數內部不做處理,需要傳遞一個回調函數處理
// FUNCTION TEMPLATE for_each
template <class _InIt, class _Fn>
_CONSTEXPR20 _Fn for_each(_InIt _First, _InIt _Last, _Fn _Func) { // perform function for each element [_First, _Last)
_Adl_verify_range(_First, _Last);
auto _UFirst = _Get_unwrapped(_First);
const auto _ULast = _Get_unwrapped(_Last);
for (; _UFirst != _ULast; ++_UFirst) {
_Func(*_UFirst);
}
return _Func;
}
下面是使用這個函數模板:
#include <vector>
#include <algorithm>
int main(int argc, char **argv)
{
vector<int> v; //定義一個容器v
//往容器添加數據
v.push_back(10);
v.push_back(20);
v.push_back(30);
v.push_back(40);
//使用迭代器遍歷
//1.直接用迭代器遍歷
for(vector<int>::iterator it = v.begin(); it != v.end(); it++) {
cout << *it << " "; //*it 就代表着是迭代器裏面的內容,迭代器就像一個指針
}
cout << endl;
for(int i=0; i<v.size(); i++) {
cout << v[i] << " "; //可以像數組那樣遍歷
}
cout << endl;
//2.先使用stl函數for_each,這個函數功能等下講
vector<int>::iterator i_begin = v.begin();
vector<int>::iterator i_end = v.end();
//之前忽略了一點,模板函數不需要填<>裏的參數,編譯器會根據參數,自己生產一個新的函數,然後提高匹配調用
for_each<>(i_begin, i_end, show);
for_each(i_begin, i_end, show);
return 0;
}
8.1.5 練習一下迭代器
1.容器存放person類的指針,並且遍歷出來
2.容器嵌套容器 一個容器作爲另一個容器的元素(這個沒寫完)
using namespace std;
class Person
{
public:
Person(char *name, int age)
{
//this->name = name;
strcpy(this->name, name);
this->age = age;
}
void show() {
cout << "名字" << name << "年齡" << age << endl;
}
char name[64];
int age;
private:
};
//家族類
class Family
{
public:
char name[60];
private:
};
/**
* @brief 創建test對象
* @param
* @retval
*/
#include <vector>
#include <algorithm>
int main(int argc, char **argv)
{
//這是存儲一個普通的類對象
vector<Person> v;
Person p1("mama", 40);
Person p2("me", 22);
Person p3("lisi", 22);
Person p4("zhaowu", 22);
v.push_back(p1);
v.push_back(p2);
v.push_back(p3);
v.push_back(p4);
for(vector<Person>::iterator it = v.begin(); it != v.end(); it++){
(*it).show();
}
//下面是存儲類對象的指針
vector<Person*> p_v;
Person *p_p1 = new Person("mama", 40);
Person *p_p2 = new Person("me", 22);
Person *p_p3 = new Person("lisi", 22);
Person *p_p4 = new Person("zhaowu", 22);
p_v.push_back(p_p1);
p_v.push_back(p_p2);
p_v.push_back(p_p3);
p_v.push_back(p_p4);
for(vector<Person*>::iterator it = p_v.begin(); it != p_v.end(); it++){
(*it)->show();
}
delete p_p1;
delete p_p2;
delete p_p3;
delete p_p4;
//容器嵌套容器 一個容器作爲另一個容器的元素
vector<vector<Person>> v3;
return 0;
}
8.2 string容器
8.2.1 string的特性
我們講的第一個容器是string,就是字符串的容器,我們在c語言中,描述字符串都是用字符數組或者char* 來表示字符串,下面來看看char *和string的比較:
- char *是一個指針,string是一個類
- string封裝了很多方法。(越用越爽的方法)
- 不用考慮內存釋放和越界。(都是string管理和維護)
8.2.2 string構造函數
#include <string>
int main(int argc, char **argv)
{
string s1;
string s2(10, 'a');
string s3("hello c++");
string s4(s3); //拷貝構造
cout << s1 << endl;
cout << s2 << endl;
cout << s3 << endl;
cout << s4 << endl;
return 0;
}
看代碼就懂,多說無益
8.2.3 string賦值語句
int main(int argc, char **argv)
{
string s1;
string s2(10, 'a');
s1 = "hello c++";
cout << s1 << endl;
s1 = s2;
cout << s1 << endl;
s1 = 'a';
cout << s1 << endl;
//成員方法
s1.assign("jj");
cout << s1 << endl;
//cout << s1 << endl;
// cout << s2 << endl;
// cout << s3 << endl;
// cout << s4 << endl;
return 0;
}
string把=操作符重載了,可以直接賦值。
8.2.4 string取值語句
#include <string>
int main(int argc, char **argv)
{
string s1;
string s2(10, 'a');
s1 = "hello c++";
for(int i=0; i<s1.size(); i++) {
cout << s1[i] << " ";
}
cout << endl;
for(int i=0; i<s1.size(); i++) {
cout << s1.at(i) << " ";
}
cout << endl;
//cout << s1 << endl;
// cout << s2 << endl;
// cout << s3 << endl;
// cout << s4 << endl;
return 0;
}
取值操作的時候,c++也把[]操作符重載了,所以可以直接用[]取值,at這個是函數方法取值,這兩個取值的區別就是:
[]取值 如果訪問越界了 就直接掛了
at方式取值,訪問越界,會拋異常out_of_range
8.2.5 string拼接
#include <string>
int main(int argc, char **argv)
{
string s1;
string s2(10, 'a');
s1 = "hello";
s1 += "c++";
cout << s1 << endl;
s1 += s2;
cout << s1 << endl;
s1.append("haha");
cout << s1 << endl;
string s3 = s1 + s2; //可以兩個字符串相加
cout << s3 << endl;
return 0;
}
結果:
下面是拼接的函數集:
8.2.6 string查找和替換
#include <string>
int main(int argc, char **argv)
{
string s1;
string s2(10, 'a');
s1 = "hello c++";
int pos = s1.find("c++");
cout << pos << endl;
return 0;
}
函數集:
8.2.7 string比較操作
不寫了偷懶。
8.2.8 string獲取子字符串
8.2.9 string插入和刪除
想不到一個string字符串這麼多函數,不過用法都挺簡單的,我們看下一容器。
8.3 vector容器
8.3.1 vector介紹
vector 是順序容器的一種。vector 是可變長的動態數組,支持隨機訪問迭代器,所有 STL 算法都能對 vector 進行操作。要使用 vector,需要包含頭文件 vector。
vector容器是單口容器,push_back pop_back都是在末端操作,這樣就減少容器內部數據的移動,提高效率。
vector動態增長的原理,當原來空間不足的時候,要重新申請一個比原來空間的大小X2的空間,然後把原來的數據拷貝到新的空間裏,釋放原理空間的內存,基本所有的動態數組都是這樣實現擴容。(Java的也是)
8.3.2 vector構造函數
void show(vector<int>& v) {
for(vector<int>::iterator it = v.begin(); it != v.end(); it++) {
cout << *it << " ";
}
cout << endl;
}
#include <vector>
int main(int argc, char **argv)
{
//vector構造函數
vector<int> v1; //默認構造
int arr[] = {10, 20, 30, 40};
vector<int> v2(arr, arr + sizeof(arr)/sizeof(arr[0]));
vector<int> v3(v2.begin(), v2.end());
vector<int> v4(v3);
show(v1);
show(v2);
show(v3);
show(v4);
return 0;
}
上面寫了幾種構造方法,結果:
下面是函數集:
8.3.3 vector常用賦值操作
void show(vector<int>& v) {
for(vector<int>::iterator it = v.begin(); it != v.end(); it++) {
cout << *it << " ";
}
cout << endl;
}
#include <vector>
int main(int argc, char **argv)
{
//vector賦值操作
vector<int> v1;
int arr[] = {10, 20, 30, 40};
vector<int> v2(arr, arr + sizeof(arr)/sizeof(arr[0]));
vector<int> v3;
int arr1[] = {100, 200, 300, 400};
vector<int> v4(arr1, arr1 + sizeof(arr1)/sizeof(arr1[0]));
//成員方法
v1.assign(v2.begin(), v2.end());
//重載=操作符
v3 = v2;
//v4和v1交換
v4.swap(v1);
show(v1);
show(v2);
show(v3);
show(v4);
return 0;
}
感覺都差不多,swap方式等下會有妙用,底層實現的交換,其實只是交換了兩個指向內存的指針而已,結果:
函數集:
8.3.4 vector大小操作
這個操作就不寫了,看看就懂了,哈哈哈。
8.3.5 vector數據存儲
這幾個跟string差不多,不寫了,vector也支持直接[]操作。
8.3.6 vector插入和刪除
void show(vector<int>& v) {
for(vector<int>::iterator it = v.begin(); it != v.end(); it++) {
cout << *it << " ";
}
cout << endl;
}
#include <vector>
int main(int argc, char **argv)
{
//vector賦值操作
vector<int> v1;
int arr[] = {10, 20, 30, 40};
vector<int> v2(arr, arr + sizeof(arr)/sizeof(arr[0]));
//添加操作
v2.push_back(55); //直接插到末尾
v2.insert(v2.begin(), 33); //這個按位置插入,這個這個位置是迭代器的指針
v2.insert(v2.end(), 77); //尾插
v2.insert(v2.begin() + 2, 99); //vector支持隨機訪問
//支持下標操作的,一般都支持隨機訪問
//也就是迭代器可以直接 +2 +3 -2 -4操作
show(v2);
//刪除操作
v2.pop_back(); //尾部刪除
v2.erase(v2.begin()); //指定位置刪除,這個是刪除頭
show(v2);
v2.erase(v2.begin() + 2, v2.end()); //刪除一個區域
show(v2);
v2.clear();
show(v2);
return 0;
}
上面代碼都把操作過了一遍,下面是運行的結果:
函數集:
8.3.7 巧用swap收縮vector空間
vector容器在我們添加元素的時候,是容量是不斷增長的,如果我們刪除元素之後,容量會不會減少呢?下面我們看看例子:
#include <vector>
int main(int argc, char **argv)
{
vector<int> v1;
//不斷的增長元素
for(int i=0; i<10000; i++) {
v1.push_back(i);
}
cout << "size = " << v1.size() << endl;
cout << "capacity = " << v1.capacity() << endl;
//突然減少元素
v1.resize(10);
cout << "size = " << v1.size() << endl;
cout << "capacity = " << v1.capacity() << endl;
return 0;
}
結果會怎麼樣呢?
很不幸的說,元素減少,容量不會減少,這樣的話,好像浪費了很多的空間,不過我們也有巧門,來減少容量。
//收縮空間
vector<int>(v1).swap(v1);
cout << "size = " << v1.size() << endl;
cout << "capacity = " << v1.capacity() << endl;
就加了這一句代碼,就可以成功的減少了空間,我們先來看結果:
是不是空間減少了,原理呢?
其實也挺簡單的,就是可能自己想不到,vector(v1).swap(v1);這句代碼,我們先分開
vector(v1) 這一部分是說明要申請一個vector的匿名對象,然後調用拷貝構造函數,用v1做模板,重新生成一個和v1一樣的匿名對象,然後這個匿名對象和真正的v1進行交換,也就是真正的v1的指針已經指向了匿名對象的那塊內存了,匿名對象的指針指向1萬多的空間的內存,然後匿名對象沒有被使用,就被釋放掉了,跟着匿名對象被釋放的是那1萬多的內存,這時候的v1的內存就是新開闢的那塊,所以用這種方法,確實很巧妙。
8.3.8 reserve申請空間
resize 和 reserve區別:
reserve是容器預留的空間,但在空間內不真正創建元素對象,所以在沒有添加新的對象之前,不能引用容器內的元素。
resize 是改變容器的大小,且在創建對象,調用這個函數之後,就可以引用容器內的對象了。
因爲在上面我們不斷的添加元素的時候,容器會因爲容量不足,進而不斷申請內存,申請完內存之後,又要拷貝,拷貝完了,又要釋放掉原來的內存,這樣一折騰,是不是感覺效率很低。所以如果我們知道了容器大概要存儲的元素個數,那麼我們就可以用reserve預留空間,而不用在添加的時候去折騰。 調用方法看上面的大小操作。