【9】C++進階系列(泛型設計以及STL標準模板庫)
1、泛型程式設計基本概念
泛型程式設計:編寫不依賴與具體資料型別的程式,將演算法從特定的資料結構中抽象出來,成為通用的。C++的模板為泛型程式設計定義了關鍵的基礎。
兩個術語:概念,模型
概念:用來界定具備一定功能的資料型別,例如:將“可以比較大小的所有資料型別(有比較運算子)”這一概念記為Comparable;將“具有共有的複製建構函式並可以用‘=’賦值的資料型別”這一概念記為Assignable;將“可以比較大小,具有共有的複製建構函式並可以用‘=’賦值的所有資料型別”這個概念記為Sortable。
對於兩個不同的概念A和B,如果概念A所需的所有功能也是概念B所需求的功能,那麼就說概念B是概念A的子概念。例如:Sortable既是Comparable的子概念也是Assignable的子概念。
模型(model):符合一個概念的資料型別稱為該概念的模型,例如:int型別是Comparable概念的模型。靜態陣列型別不是Assignable概念的模型(無法用‘=’給整個靜態陣列賦值)。
用概念做模板引數名:很多STL的實現程式碼就是用概念來命名模板引數的。
為概念賦予一個名稱,並使用該名稱作為模板引數名。例如:表示insertionSort這樣一個函式模板的原型。
template <class Sortable>
void insertionSort(Sortable a[],int n);
2、STL簡介
標準模板庫(Standard Template Library,簡稱STL)定義了一套概念體系,為泛型程式設計提供了邏輯基礎
STL中的各個類模板、函式模板的引數都是用這個體系中的概念來規定的。使用STL的模板時,型別引數既可以是c++標準庫中已有的型別,也可以是自定義的型別——只要這些型別是所要求概念的模板。
STL的基本元件:
容器(containaer)、迭代器(iterator)、函式物件(function object)、演算法(algorithms)
他們之間的關係:
1、Iterator(迭代器)是演算法和容器的橋樑,將迭代器作為演算法的引數,通過迭代器來訪問容器而不是將容器直接作為演算法的引數。
2、將函式物件作為演算法的引數而不是將函式執行的運算作為演算法的一部分。
3、使用STL中提供的或者自定義的迭代器和函式物件,配合STL的演算法,可以組合各種各樣的功能。
為了讓演算法更通用,可以適合不同的容器,它就將迭代器作為了演算法的引數。另外有些功能是由函式物件來定製的,比如說將一個比較大小的函式傳給排序演算法作為函式物件傳過去,演算法就可以通過傳遞過來的比較大小的功能實現不同的排序功能(如升序、降序)。
容器(container):容納、包含一組元素的物件
基本容器類模板:
順序容器:array(陣列)、vector(向量)、deque(雙端佇列)、forward_list(單鏈表),list(列表)
(有序)關聯容器:set(集合)、multiset(多重集合)、map(對映)、multimap(多重對映)
無序關聯容器:unordered_set(無序集合)、unordered_multiset(無序多重集合)、unordered_map(無序對映)、unordered_multimap(無序多重對映)
容器介面卡:stack(棧)、queue(佇列)、priority_queue(優先佇列)
在使用容器和介面卡是還需要包含相應的標頭檔案。
迭代器(iterator):實質是一個泛型指標
提供了順序訪問容器中每個元素的方法;
可以使用“++”運算子來獲得指向下一個元素的迭代器。(自增運算)
可以用"*"運算子訪問迭代器所指向的元素,如果元素型別是類或者結構體,還可以用“->”運算子直接訪問該元素的一個成員。
有些迭代器還支援通過“--”運算子獲得指向上一個元素的迭代器;(自減運算)
迭代器是泛化的指標:指標也具有同樣的特性,因此指標本身就是一種迭代器。
使用獨立於STL容器的迭代器,需要包含標頭檔案<iterator>
函式物件(function object):行為類似於函式的物件,我麼可以像呼叫函式一樣去使用這個物件。就像泛化的函式
一個行為類似函式的物件,對他可以像呼叫函式一樣呼叫。
函式物件是泛化的額函式:任何普通的函式和任何過載了“()”運算子的類的物件都可以作為函式物件使用。
使用STL的函式物件,需要包含標頭檔案<functional>
演算法(algorithms):
可以廣泛用於不同的物件和內建的資料型別。
STL包括70多個演算法:不如,排序演算法、消除演算法、計數演算法、比較演算法、變換演算法、置換演算法和容器管理等。
使用STL的演算法,需要包含標頭檔案<algorithm>
例子:從標準輸入讀入幾個整數,存入向量容器,輸出他們的相反數。
#include<iostream>
#include<vector>
#include<iterator>
#include<algorithm>
#include<functional>
using namespace std;
/*transform的一種典型實現
template <class InputIterator,class OutputIterator,class UnaryFunction>
OutputIterator transform(InputIterator first, InputIterator last, OputputIterator result, UnaryFunction op) {
for (;first!=last;++first; ++result)
{
*result = op(*first);
}
return result;
}
*/
int main() {
const int N = 5;
vector<int> s(N);
for (int i = 0; i < N; i++) {
cin >> s[i];
}
transform(s.begin(), s.end(), ostream_iterator<int>(cout, " "), negate<int>());
cout << endl;
return 0;
}
迭代器(iterator):
迭代器是演算法和容器之間的橋樑
迭代器用作訪問容器中的元素
演算法不直接操作容器中的資料,而是通過迭代器實現間接操作。
演算法和容器獨立
增加新的演算法,無序影響容器的實現
增加新的容器,原有的演算法也能實現。
輸入流、輸出流迭代器:
輸入流迭代器:istream_iterator<T>——以輸入流(如cin)為引數構造,可用*(p++)獲得下一個輸入的元素。
輸出流迭代器:ostream_iterator<T>——構造是需要提供輸出流(如cout),可用(*p++)=x將x輸出到輸出流
二者都屬於介面卡,介面卡是用來為已有物件提供新的介面的物件,輸入流介面卡和輸出流介面卡為流物件提供了迭代器的介面。
例子:輸入輸出(求平方)
#include<iterator>
#include<algorithm>
#include<iostream>
using namespace std;
double square(double x) {
return x * x;
}
int main() {
transform(istream_iterator<double>(cin), istream_iterator<double>(), ostream_iterator<double>(cout, "\t"), square);
cout << endl;
return 0;
}
迭代器區間:兩個迭代器表示一個迭代區間:[p1,p2)
STL演算法常以迭代器的區間作為輸入,傳遞輸入資料。合法的區間:p1經過n次(n>0)自增操作後p1==p2,區間包含p1但不包含p2.
例子:排序
#include<iterator>
#include<algorithm>
#include<iostream>
#include<vector>
using namespace std;
/*
double square(double x) {
return x * x;
}
int main() {
transform(istream_iterator<double>(cin), istream_iterator<double>(), ostream_iterator<double>(cout, "\t"), square);
cout << endl;
return 0;
}
*/
//將輸入的n個T型別資料排序並,將結果通過輸出迭代器result輸出
template<class T,class InputIterator,class OutputIterator>
void mySort(InputIterator first, InputIterator last,OutputIterator result) {
vector <T> s;
for (; first != last; ++first) {
s.push_back(*first);
}
sort(s.begin(), s.end());
copy(s.begin(), s.end(), result);//排序後的資料使用copy函式送到result迭代器指示的輸出位置,將s序列通過輸出迭代器輸出。
}
int main() {
double a[5] = { 1.2,2.4,0.8,3.3,3.2 };
mySort <double>(a, a + 5, ostream_iterator<double>(cout, " "));
cout << endl;
mySort <int>(istream_iterator<int>(cin), istream_iterator<int>(), ostream_iterator<int>(cout, " "));
cout << endl;
return 0;
}//直接執行沒有輸出,需要結合ctrl+z,才會看見輸出。ctrl+zwei windows預設標準輸出流結束符
迭代器的輔助函式:advance(p,n)對p執行n次自增操作。distance(first,last)計算兩個迭代器first和last的舉例,即對first執行多少次“++”操作後才能使得first==last。
3、容器的基本功能和分類
容器時用來容納、包含一族元素或者元素集合的物件,基於容器中元素的組織方式分類:順序容器,關聯容器。按照與容器所關聯的迭代器型別分類:可逆容器、隨機訪問容器。
容器的通用功能:
用預設建構函式構造空容器
支援關係運算符:==,!=,<, <= , >, >=;
begin(),end():獲得容器首、尾迭代器
clear():將容器清空
empty():判斷容器是否為空
size():得到容器的元素個數
s1.swap(s2):將s1和s2兩容器內容交換
相關資料型別:(S表示容器型別)
S::iterator:指向容器元素的迭代器型別
S::const_iterator:常迭代器型別
對可逆容器的訪問:
STL為每個可逆容器都提供了逆向迭代器,逆向迭代器可以通過下面的成員函式得到:rbegin()指向容器為的逆向迭代器,rend()指向容器首的逆向迭代器。
逆向迭代器的型別名的表示方式如下:S::reverse_iterator:逆向迭代器型別,S::const_reverse_iterator逆向常迭代器型別。
隨機訪問容器:
支援對容器的元素進行隨機訪問,s[n]:獲得容器的第n個元素。
4、順序容器的基本功能:
STL中的順序容器:向量(vector)、雙端佇列(deque)、列表(list)、單向連結串列(forward_list)、陣列(array),他們在邏輯上看作是一個長度可擴充套件的陣列。
元素線性排列,可以隨時在指定位置插入元素和刪除元素。必須符合Assignable這一概念(即具有共有的複製建構函式並可以用“=”賦值);array物件的大小固定,forward_list有特殊的新增和刪除的操作。
順序容器的介面(不包含單項鍊別和陣列):
建構函式,
複製函式assign,
插入函式insert,push_front(值對list和deque),push_back,emplace,emplace_front。
刪除函式:erase,clear,pop_front(只對list和deque),,pop_back,emplace_back。
首尾元素直接訪問:front,back
改變大小:resize
#include <iostream>
#include <deque>
#include <list>
#include <iterator>
using namespace std;
template<class T>
void printContainer(const char* msg, const T& s) {
cout << msg << ":";
copy(s.begin(), s.end(), ostream_iterator<int>(cout, " "));
cout << endl;
}
int main() {
deque <int> s;
for (int i = 0; i < 10; i++)
{
int x;
cin >> x;
s.push_front(x);
}
printContainer("deque at first", s);
//用s容器的內容的逆序構造列表容器l
list<int> l(s.rbegin(), s.rend());
printContainer("list at first", l);
//將列表容器l的每相鄰兩個元素順序顛倒
list<int>::iterator iter = l.begin();
while (iter!=l.end())
{
int v = *iter;
iter = l.erase(iter);
l.insert(++iter, v);
}
printContainer("list at last", l);
s.assign(l.begin(), l.end());
printContainer("deque at last", s);
return 0;
}
5、順序容器的特點
向量vector:
特點:一個可擴充套件的動態陣列;隨機訪問、在尾部插入或者刪除元素快;在中間或頭部插入或者刪除元素慢。
向量的容量capacity:實際分配的空間大小。
s.capacity():返回當前容量;
s.reserve():若容量小於n,則對s進行擴充套件,是其容量至少為n。
雙端佇列deque:
特點:
在雙端插入或者刪除元素快,在中間插入或者刪除元素慢。隨機訪問比較快,但比向量容器慢。
列表list:
特點:
在任意位置插入元素和刪除元素都很快,不支援隨機訪問。
接合(splice)操作:拼接
s1.splace(p,s2,q1,q2):將s2中[q1,q2)移動到s1中p所指向的元素之前。
單向連結串列(forward_list):
單向連結串列每個結點只有指向下個結點的指標,沒有簡單的方法可以獲得結點的前驅結點;未定義insert、emplace和erase操作,而定義了insert_after、emplace_after和erase_after操作,其引數與list的insert、emplace和erase相同,但並不是插入或者刪除迭代器p1所指的元素,而是對p1所指的元素之後的結點進行操作。
不支援size操作
陣列array:
array提供了對內建陣列的封裝,提供了更安全,更方便的額使用陣列的方式
array的物件的大小是固定的,定義時除了需要置頂元素型別,還需要指定容器大小,不能動態的改變容器的大小。
順序容器的選擇:
STL所提供的順序容器各有所長也各有所短。我們在編寫程式時應當根據我們對容器所需要執行的操作來選擇哪一種容器。
如果需要執行大量的隨機訪問,而且當擴充套件容器時只需要向容器的尾部加入新的元素,就應當選擇向量容器vector;
如果需要少量的隨機訪問操作,需要在容器的兩端插入或者刪除元素,則應當選擇雙端佇列容器deque;
如果不需要對容器進行隨機訪問,但是需要在中間位置插入或者刪除元素,就應當選擇列表容器list或者forward_list;
如果需要陣列,array相對於內建陣列型別而言,是一種安全、更容易使用的陣列型別。
6、順序容器的插入迭代器和介面卡
順序容器的插入迭代器:
用於向容器頭部、尾部或中間指定位置插入元素的迭代器。
包括前插迭代器(front_inserter)、後插迭代器(back_inserter)、和任意位置插入迭代器(inserter)
例:list<int> s;back_inserter iter(s);*(iter++)=5;//通過iter把5插入s的末尾。
順序容器的介面卡:以順序容器為基礎構建一些常用的資料結構,是對順序容器的封裝
棧(stack):最先壓入的元素最後被彈出
佇列(queue):最先壓入的元素最先被彈出
優先順序佇列(priority_queue):最“大”的元素最先被彈出。“大”的意思是可以在使用的時候來定製。
棧和佇列模板:
棧模板:template <class T,class Sequence=deque<T>>class stack;
佇列模板:template<class T,class FrontInsertionSequence=deque<class T>>class queue;
棧可以用任何一種順序容器作為基礎容器,而佇列只允許用前插順序容器(雙端佇列或者列表)
棧和佇列共同支援的操作:
s1 op s2:op可以是==,!=,>,>=,<,<=之一,它會對兩個容器介面卡之間的元素按字典序進行比較。
s.size():返回s元素的個數
s.empty():返回s是否為空
s.push(t):將元素t壓入s中
s.pop():將一個元素從s中彈出,對於棧來時,每次彈出的都是最後一個被壓入的元素,而對於對量,每次彈出的都是最先被壓入的元素。
不支援迭代器,因為他們不允許對任意元素進行訪問。
棧和佇列不同的操作:
棧的操作:s.top()返回棧頂元素的引用
佇列操作:s.front()或得對頭元素的引用。s.back()獲得隊尾元素的引用。
例子:用棧實現單詞的反向輸出效果
#include <iostream>
#include <iterator>
#include<stack>
#include<string>
using namespace std;
int main() {
stack<char> s;
string str;
cin >> str;
for (string::iterator iter=str.begin(); iter!= str.end();++iter)
{
s.push(*iter);
}
while (!s.empty())
{
cout << s.top();
s.pop();
}
cout << endl;
return 0;
}
優先順序佇列:
優先順序佇列也像棧和佇列一樣支援元素的壓入和彈出。但元素的順序與元素的大小有關,每次彈出的總是容器中最“大”的一個元素。template<class T,class Sequence=vector<T>>class priority_queue;
優先順序佇列的基礎容器必須是支援隨機訪問的順序容器。
支援棧和佇列的size,empty,push,pop幾個成員函式,用法與棧和佇列相同。
優先順序佇列並不支援比較操作。
與棧類似,優先順序佇列提供了一個top函式,可以獲得下一個即將被彈出的元素(即最“大”的元素)的引用。
例子:優先順序佇列模擬細胞分裂
#include<iostream>
#include<time.h>
#include<queue>
#include<concurrent_priority_queue.h>
using namespace std;
const int SPLIT_TIME_MIN = 500;//細胞分裂最短時間
const int SPLIT_TIME_MAX = 2000;//細胞分裂最大時間
class Cell;
priority_queue<Cell> cellQueue;
class Cell
{//細胞類
public:
Cell(int birth) :id(count++) {//birth為細胞的誕生時間
//初始化,確定分裂時間
time = birth + (rand() % (SPLIT_TIME_MAX - SCHAR_MIN)) + SPLIT_TIME_MIN;
}
int getId() const { return id; }//得到細胞編號
int getSplitTime() const { return time; }//得到細胞分裂時間
bool operator <(const Cell&c)const {//定義“<”
return time > c.time;
}
void split() const {//細胞分裂
Cell child1(time), child2(time);
cout << time << "c:Cell #" << id << " split to #" << child1.getId() << " and " << child2.getId() << endl;
cellQueue.push(child1);//將第一個自細胞壓入優先順序佇列
cellQueue.push(child2);//將第二個自細胞壓入優先順序佇列
}
private:
static int count;//細胞總數
int id;//當前編號
int time;//細胞分裂時間
};
int Cell::count = 0;
int main() {
srand(static_cast<unsigned>(time(0)));
int t;//模擬時間長度
cout << "Simulation time:";
cin >> t;
cellQueue.push(Cell(0));//將第一個細胞壓入有優先順序佇列
while (cellQueue.top().getSplitTime() <= t)
{
cellQueue.top().split();//模擬下一個細胞分裂
cellQueue.pop();//將剛剛分裂的細胞彈出
}
return 0;
}
7、關聯容器
關聯容器的介面和特點:
特點:
每個關聯容器都有一個鍵(key)
可以根據鍵高效的查詢元素
介面:
插入:insert
刪除:erase
查詢:find
定界:lower_bound,upper_bound,equal_range
計數:count
8、集合(set)
集合用來儲存一組無重複的元素。由於集合的元素本身是有序的,可以高效的查詢指定元素,也可以方便地得到指定大小範圍的元素在容器中所處的空間。
例子:輸入遺傳實數,將重複的曲調,去最大和最小者的中值,分別輸出小於等於此中值和大於此中值的實數。
#include<set>
#include<iterator>
#include<utility>
#include<iostream>
using namespace std;
int main() {
set<double> s;
while (true)
{
double v;
cin >> v;
if (v==0)
{
break;
}
pair<set<double>::iterator, bool> r = s.insert(v);
if (!r.second)
{
cout << v << "is duplicated" << endl;
}
}
set<double>::iterator iter1 = s.begin();
set<double>::iterator iter2 = s.end();
double medium = (*iter1 + *(--iter2)) / 2;
cout << " <=medium:";
copy(s.begin(), s.upper_bound(medium), ostream_iterator<double>(cout, " "));
cout << endl;
cout << " >=medium:";
copy(s.lower_bound(medium), s.end(), ostream_iterator<double>(cout, " "));
cout << endl;
return 0;
}