面向对象和C++基础—标准模板库(STL)篇

14. 标准模板库(STL)篇

  说实话,STL的一些东西已经在之前出现过很多次了,比如我们常用的vector,还有提到过的map、set等等,它们都是STL的组成部分,有这样一套库对于我们来说真的会方便很多,例如vector的自动增长能够让我们不用过分关注于内存的管理sort函数对于各种有迭代器类型的容器都可以完成排序,包括原生数组

(1). STL是什么?

  STL是Standard Template Library—标准模板库的缩写,是由Alexander Stepanov、Meng Lee和David R Musser在惠普实验室工作时所开发出来的,STL在1994年2月年才正式成为ANSI/ISO C++的一部分,可以说是挺晚的了。
  在C++中,它被主要组织为13个头文件:<algorithm>、<deque>、<functional>、<iterator>、<vector>、<list>、<map>、<memory>、<numeric>、<queue>、<set>、<stack>和<utility>,对于map、set还有对应的 <unordered_map>以及<unordered_set>

(2). STL包含了哪些东西?

  一般来说,STL包括了一系列的容器(容器适配器)、迭代器和算法,不过实际上STL还包括了下一篇的RAII部分会讲到的三种智能指针:unique_ptr、shared_ptr和weak_ptr,以及其它的例如std::functional、std::pair之类的东西。
  这一篇中会主要介绍STL中的各种容器(容器适配器)、迭代器和对应的一些算法。

(3). std::array与std::vector

  std::array和std::vector的使用方法如下:

std::vector<T> a;   // T为存储元素的类型
std::array<T, n> b; // T为存储元素的类型,n为数组的大小

  array的参数有T和n两个,也正是因此,std::array的数组才能在栈上完成分配。

#1.std::array的优势和基本使用

  首先有个问题,我们在C++中向函数传递一个原生数组的时候会发生什么?看看下面的例子:

#include <iostream>
using namespace std;
void check(int a[])
{
    cout << "sizeof(a) = " << sizeof(a) << endl;
}

int main()
{
    int a[100]{0};
    cout << "sizeof(a) = " << sizeof(a) << endl;
    check(a);
    return 0;
}

p84

  因为函数是传值的,所以传入的a退化成为了指针,sizeof(a)的值只有8了,而真正的数组sizeof(a)是400,原生数组没法传引用,那能不能用来解决一下这个问题呢?

#include <iostream>
using namespace std;
class Array
{
public:
    int a[100];
};

void check(const Array& arr)
{
    cout << "sizeof(arr.a) = " << sizeof(arr.a) << endl;
}

int main()
{
    Array arr;
    cout << "sizeof(arr.a) = " << sizeof(arr.a) << endl;
    check(arr);
    return 0;
}

p85

  好耶!问题解决了,我们可以用传对象引用的方式解决数组退化的问题,所以在C++的STL中,我们引入了std::array,它相当于一个原生数组,其内部的数组也是存储在栈区的,传入std::array可以保证数组不退化。
  下面是std::array的一些方法:

函数意义
at访问指定元素,同时进行越界检查
operator[]访问指定元素
front返回第一个元素
back返回最后一个元素
data返回底层数组
empty判断是否为空
size返回数组当前的元素数
max_size返回可容纳的最大元素数

  std::array支持迭代器,这意味着我们可以通过迭代器的方式遍历std::array对象,例如:

std::array<int, 30> a;
for (auto it = a.begin(); it != a.end(); it++) {
    ...
}
// 或者再进一步
for (auto& i : a) {
    ...
}

  迭代器类型重载了解引用运算符,意味着我们可以通过*it的方式访问迭代器对应的元素,看起来很像指针是吧?你可以把它理解为一个高级版本的指针,因为除了begin()还有rbegin(),它可以从最后一个元素向着第一个元素倒着遍历。

#2.std::vector的基本使用

  std::vector算是STL中最常用的容器之一了,因为其提供了可以动态变化的容量,我们在向其中插入元素的时候不再需要关注容量问题了,这可是件大好事。

  首先来说说vector对象的创建,vector的构造函数形式非常多,这里列几个常用的:

vector(); // 无参构造函数
vector(const vector&); // 拷贝构造函数
vector(vector&&); // 移动构造函数
vector(size_t count); // 构造时预先分配count个元素的空间
vector(std::initializer_list<T> init); // 通过std::initializer_list<T>构造
template<class InputIt>
vector(InputIt first, InputIt last); // 通过传入初始迭代器和末尾迭代器构造

  前面三种都好理解,我来说说后面三种。第四个传入count的构造函数会在构造的时候就默认分配好count个元素的位置,这样做可以保证构造完成后,前count个下标能够直接访问,而不会发生越界,因为vector的容量是动态的,当没有通过resize或push_back把元素个数加到对应个数之前,直接进行访问对应下标是可能抛出异常的,而直接在构造的时候分配好位置,能够使得vector的使用和原生数组类似。

  第五个构造函数中我们需要传入一个std::initializer_list<T>,也就是说我们可以在初始化的时候就插入一些元素,这个就和原生数组的int a[] = {1, 2, 3};类似,不过这里要注意了,因为有了这个构造函数,下面两行代码对于vector来说行为是不一致的

vector<int> v1{100}; // vector(std::initializer_list<T> init);
vector<int> v2(100); // vector(size_t count);

当vector类型为可以与int发生隐式类型转换的类型时,使用花括号进行初始化会被认为是使用了对应std::initializer_list<T>的构造函数

  最后一个构造函数中传入了两个迭代器,一个是起始位置,一个是结束位置,在这里你传入某个容器的begin()和end()可以,传入原生数组的头指针和一个尾指针也是可以的,例如:

vector<int> a{1, 2, 3};
vector<int> b(a.begin(), a.end()); // 迭代器

int c[]{3, 2, 1};
vector<int> d(c, c+3); // 指针

  vector这个容器有size和capacity两个属性,其中size是当前容器中真正已经存入的元素个数,而capacity是当前容器能够容纳的最大元素个数,一般当size的达到capacity时,vector就会自动完成扩容操作,我们可以通过size()和capacity()两个方法获得当前vector的对应属性的值。

vector<int> a;
cout << "a.size() = " << a.size() << ", a.capacity() = " << a.capacity() << endl;
for (int i = 0; i < 20; i++) {
    a.push_back(10);
    cout << "a.size() = " << a.size() << ", a.capacity() = " << a.capacity() << endl;
}

  你可以凭此探索一下你用的编译器中,vector的扩容规律是什么,MSVC中的是这样:
p86

  一般来说,vector会尽可能少的进行扩容操作,因为重新分配内存以及迁移数据是一个比较慢的过程。

  接下来是插入元素,首先如果我们要向vector的尾部插入元素,我们需要使用push_back()方法,例如:

vector<int> a;
a.push_back(10);

切记这个a在这里不能使用a[0] = 10;的方法进行元素插入,因为就如上面的图所示,如果使用无参构造函数完成vector的构造,vector默认是不分配内存的,这时候就相当于对一个空指针写入元素
  即便是分配了内存,如果要在尾部插入元素,依旧应该使用push_back,因为push_back会使得size增长,如果没有使用push_back的话,我们通过下标直接访问at()方法访问时可能会抛出异常。

  与插入元素相对应的则是删除最后一个元素,这里需要用到pop_back()方法,pop_back()能够使得size的数目-1,这里就不再演示了。
  下面是一些常用的函数:

函数意义
empty判断是否为空
reserve预留存储空间(改变capacity)
resize改变元素个数(改变size)
clear清空整个容器
insert插入元素
erase擦除元素
shrink_to_fit通过释放未使用的内存减少内存使用

insert函数的各种重载

// 在pos迭代器的位置插入一个值
iterator insert(const_iterator pos, const T& value); 

// 在pos迭代器的位置插入一个值(移动语义)
iterator insert(const_iterator pos, T&& value); 

// 在pos迭代器的位置插入count个值
iterator insert(const_iterator pos, size_t count, const T& value); 

// 在pos迭代器的位置插入从first到last的所有元素
template<class InputIt>
iterator insert(const_iterator pos, InputIt first, InputIt last); 

// 在pos迭代器的位置插入ilist中的所有值
iterator insert(const_iterator pos, std::initializer_list<T> ilist); 

erase函数的各种重载

// 擦除pos迭代器位置上的元素
iterator erase(const_iterator pos);

// 擦除从first迭代器到last迭代器之间的所有元素,左闭右开
iterator erase(const_iterator first, const_iterator last);

  vector同样支持迭代器,我们可以使用begin()和rbegin(),以及对应的end()和rend()实现遍历整个vector的操作,下面就不再举例了。

#3.std::vector<bool>和std::bitset

  在C++中,最小的内存单元是字节,在基础篇中我们提到过,C++的bool类型不是简单的宏定义,它采用一个字节来存储true和false两个值。
  不过说实话,一个字节还是太大了,因为true和false对应1和0,实际上只要一位,即1/8个字节即可,因此STL中的vector对于模板参数为bool的情况做了模板特化,vector<bool>中实现了用一个字节存储八个布尔值的需求,它的确是节省了空间,但因为每次对某个位置的bool值进行读写时都要进行位操作,最终的运行效率会比其他类型的vector慢很多,这是它的一大缺点。

  如果要更快地对由1和0组成的序列进行操作,可以考虑使用std::bitset,你可以自行上网查阅相关资料了解一下bitset的基本使用方法。

(4). std::map与std::unordered_map

#1.二者的主要区别和性能对比

  map和unordered_map是STL实现的映射表结构,我们可以通过以下标访问的形式直接访问到值,二者的操作基本完全相同,只是实现不同,map采用红黑树,而unordered_map采用哈希表的形式实现,使用红黑树实现的最大优势是:map中保持键值对有序,而unordered_map则是无序的
  二者在插入、查找和删除操作的的时间复杂度对比如下:

操作map时间复杂度unordered_map时间复杂度
插入元素 O ( log ⁡ n ) O(\log{n}) O(logn) O ( 1 ) O(1) O(1)
查找元素 O ( log ⁡ n ) O(\log{n}) O(logn) O ( 1 ) O(1) O(1)
删除元素 O ( log ⁡ n ) O(\log{n}) O(logn) O ( 1 ) O(1) O(1)

  在速度上unordered_map看似全面超越了map,但是在空间上,unordered_map占用的内存要远超map,上一篇中提到的赛博计算机2077这道题,我有两次提交分别使用了map和unordered_map完成,他们的内存占用对比如下;
p87

  上面1.012M是map版本,而13.730M是unordered_map版本,差距真的大的很夸张。

#2.基本使用

  首先,使用什么就要导入什么:

#include <map>
#include <unordered_map>

  创建map和unordered_map对象的方式如下:

map<KT, VT> m;
unordered_map<KT, VT> m1;

  其中KT为键类型,VT为值类型,我们可以在初始化的时候就加入几个键值对:

map<string, int> m{ {"A", 1}, {"B", 2} };
unordered_map<string, int> m{ {"C", 3}, {"D", 4} };

  在map和unordered_map中常被我们用于操作键值对的对象是std::pair<K, V> 对象,对于一个pair对象,我们可以这么得到键和值:

// 假设已经有了一个pair<int, int> p
cout << p.first << " : " << p.second << endl; 

first代表pair的第一个元素,second则是pair的第二个元素,在map和unordered_map容器中,first就代表键,second就代表值

  因此有了pair,我们也可以用构造pair对象的方式初始化:

map<string, int> m{ pair<string, int>{"A", 1}, pair<string, int>{"B", 2} };
unordered_map<string, int> m{ pair<string, int>{"C", 3}, pair<string, int>{"D", 4} };

  当然,除了直接调用pair类的构造函数,我们还可以用一些其他的函数构造pair对象,例如:

map<string, int> m{ make_pair("A", 1), make_pair("B", 2) };
unordered_map<string, int> m{ make_pair("C", 3), make_pair("D", 4) };

  如果我们要遍历整个map或unordered_map,我们只能通过迭代器的方式实现:

// 直接使用迭代器
for (auto it = m1.begin(); it != m1.end(); it++) {
    ... // it->first和it->second分别表示键和值
}

// 或使用基于范围的for循环
for (auto& i : m1) {
    ... // i.first和i.second分别表示键和值
}

// 或使用结构化绑定(C++17)
for (auto& [k, v] : m1) {
    ... // k和v分别表示键和值
}

  对于map这种有序容器,我们在定义的时候可以在模板参数中传入用于排序的仿函数,这样得到的容器中,就会依照排序规则,将键排序,例如:

#include <iostream>
#include <map>
using namespace std;

class Less
{
public:
    bool operator()(const int& a, const int& b) const
    {
        return a > b;
    }
};

int main()
{
    map<int, int, Less> m1{ {1, 1}, { 2, 3 }, { 4, 9 }, { 8, 27 }};
    map<int, int> m2{ {1, 1}, { 2, 3 }, { 4, 9 }, { 8, 27 }};
    cout << "m1:" << endl;
    for (auto& [k, v] : m1) {
        cout << k << " : " << v << endl;
    }
    cout << "m2:" << endl;
    for (auto& [k, v] : m2) {
        cout << k << " : " << v << endl;
    }
    return 0;
}

  这里我们使用了结构化绑定的方式一次性获得键和值,这个在成员函数篇中已经提到过,这里就不再提了,我们来看看它输出的结果:
p88

  很好,传入了之后果然是按照我们的要求进行排序的。之后再来谈谈插入,map和unordered_map的键都是不能重复的,因此在插入时,如果键已经存在,则不会对map造成任何影响,下面我们使用insert方法插入键值对:

m1.insert({16, 81});
m1.insert(pair<int, int>{32, 243});
m1.insert(make_pair(64, 729));

  同样,这三种形式都是可以的,不过貌似通过中括号访问的方式要更加方便一点:

m1[1] = 3; // 当键已经存在的时候,可以直接进行修改
m1[128] = 2187; // 当键不存在时,可以直接插入一对键值对

  用这种看起来要比insert更方便呢,你说是吧?map还具有以下常用的函数:

函数意义
empty判断是否为空
size当前容纳的元素数
clear清空整个容器
erase擦除元素
count返回匹配特定键的元素数量,返回值只可能为0或1
find寻找带有特定键的元素,返回迭代器

  而unordered_map因为内部使用哈希表实现,它的函数和map在除了基本使用方法之外就有一些区别了,在这里我就不进行举例了,你可以自己查询cppreference中有关于unordered_map的内容。

(5). std::set与std::unordered_set

  set和unordered_set其实与map和unordered_map类似,set采用红黑树实现unordered_map采用哈希表实现,它们的定义、声明等和map、unordered_map是基本一致的:

set<T> s1;
unordered_set<T> s2;

  不过记得一件事,set和数学的集合概念基本一致,因此set中的每个元素都是唯一的,不过set因为是红黑树实现,所以有序,unordered_map相对于set更加符合集合的概念一点
  这里只有一个模板参数,毕竟只是一个值嘛,set因为有序,所以它同样可以传入一个仿函数,从而改变它的排序规则,两种set的遍历也是一样,不过这次遍历出来可以直接得到值了,例如:

#include <iostream>
#include <set>
#include <unordered_set>
using namespace std;

int main()
{
    set<int> s1{1, 3, 4, 2, 0, 2, 2};
    for (auto& i : s1) {
        cout << i << " ";
    }
    cout << endl;
    return 0;
}

p89

  就是这样,多余的元素会被去掉,然后我们通过基于范围的for循环可以直接遍历得到所有的值。set其它的更多方法,和map几乎完全一致,这里我就不再介绍了。

(6). std::deque

  deque是STL中的双端队列,即头尾均可入队、出队的队列,要使用双端队列,我们首先需要包含头文件:

#include <deque>

  首先是创建deque,这很简单:

deque<int> dq;
deque<int> dq1{1, 2, 3};

  我们依旧可以使用无参构造函数和std::initializer_list进行初始化,因为是双端队列,所以我们可以在头尾进行各种操作:

#include <iostream>
#include <deque>
using namespace std;

int main()
{
	deque<int> dq1{1, 2, 3, 4, 5, 6, 7};
	cout << dq1.back() << endl;
	dq1.pop_back();
	dq1.push_back(10);
	cout << dq1.back() << endl;
	
	cout << dq1.front() << endl;
	dq1.pop_front();
	dq1.push_front(4);
	cout << dq1.front() << endl;
	return 0;
}

p92

  就是这样,我们用push_back()和push_front()分别在尾和头插入元素,用pop_back()和pop_front()分别从尾和头出队,并且用front()和back()分别访问头和尾的当前元素

  deque支持迭代器,所以我们可以用下面的方式进行遍历:

for (auto it = dq1.begin(); it != dq1.end(); it++) {
    ... // 通过*it访问元素
}

for (auto& i : dq1) {
    ... // 基于范围的for循环,i就是元素引用
}

  deque还支持insert,erase操作,这个和vector等的操作是基本一致的,这里就不再赘述了。对了,deque甚至还支持下标访问以及at()访问,听起来好像和vector几乎无异啊,是吧?

(7). std::stack、std::queue和std::priority_queue

#1.为啥这仨放一起?

  因为这仨虽然也能存东西,但是跟前面和后面说的那些容器不同,在创建这三者的时候,我们可以选择实现它们的容器,也就是说,它们是基于已有容器实现的一种“二级容器”,因为其数据结构的通用性,对于各种有迭代器的容器,它们都可以正常使用,它们三个被称为容器适配器
  我们可以在这三个容器定义时,在模板参数中加入容器类型以完成容器的更替:

stack<int, vector<int>> s;
queue<int, stack<int>> q;
priority_queue<int, vector<int>> pq;

  后续的内容中我们不会修改这些容器适配器使用的默认容器。还有一件事,这三种容器适配器均不支持迭代器,我们无法通过迭代器的方式遍历

#2.std::stack

  stack就是栈,我们其实还挺常用栈的,栈是一种后进先出(LIFO)的数据结构,它有以下的一些基本使用方法:

#include <iostream>
#include <stack>
using namespace std;

int main()
{
    stack<int> s; // 创建一个stack,当然也可以通过std::initializer_list进行初始化
    s.push(1); // 调用push方法向栈顶插入一个元素
    for (int i = 0; i < 10; i++) {
        s.push(i);
    }
    cout << s.top() << endl; // 调用top方法返回当前栈顶元素
    s.pop(); // 调用pop方法弹出当前栈顶元素
    s.clear(); // 调用clear方法清空当前栈
    return 0;
}

  同时stack还有size()和empty()方法分别返回当前栈中元素的个数以及判断当前栈是否为空

#3.std::queue

  queue则是队列,它只能从队尾入队,从队首出队,和栈正好相反,它是一种先进先出(FIFO)的数据结构,首先还是需要包含头文件:

#include <queue>

  它有以下的一些使用方法:

#include <iostream>
#include <queue>
using namespace std;

int main()
{
    queue<int> q; // queue不支持用std::initializer_list初始化
    for (int i = 0; i < 10; i++) {
        q.push(i); // queue使用push()入队
    }
    cout << q.front() << ", "  // 通过front()查看队首元素 
        << q.back() << endl;   // 通过back()查看队尾元素
    q.pop(); // 使用pop()出队
    cout << q.front() << endl; // 这时队首已经改变
    return 0;
}

  其中展示了queue的基本操作,同样,它也有size()和empty()方法。

#4.std::priority_queue

  priority_queue是优先队列,有的时候我们也把它叫做堆,根据排序顺序,小的在上的叫做小根堆,大的在上的叫做大根堆,要使用priority_queue,我们要包含头文件:

#include <queue>

  这个不错,跟queue在一个头文件中,然后接下来看看它的基本用法:

#include <iostream>
#include <cstdlib>
#include <queue>
using namespace std;

int main()
{
    priority_queue<int> pq;
    for (int i = 5; i > 0; i--) {
        pq.push(i); // 使用push插入元素
    }
    cout << pq.top() << endl; // 通过top()查看当前根节点的值
    for (int i = 0; i < 15; i++) {
        pq.push(rand() % 100); // 向pq插入15个0~99之间的随机数
    }
    
    while (!pq.empty()) {
        cout << pq.top() << ","; // 输出根节点的值
        pq.pop(); // 弹出根节点的值
    }
    cout << endl;
    return 0;
}

  这里我们完成了一些基本操作,最后还利用优先队列完成了堆排序,看看结果:
p90

  看起来优先队列默认是大根堆啊,我们可以在初始化的时候修改一下排序规则:

class Less
{
public:
    bool operator()(const int& a, const int& b) const
    {
        return a > b;
    }
};

p91

  这样就好了,不过有一个事情要注意一下,priority_queue的less模板参数的位置在容器之后,因此我们指定less的时候要先加上容器的类型,例如这样:

priority_queue<int, vector<int>, Less> pq;

  priority_queue默认采用vector,因此这里我就直接写上vector了。

(8). std::list和std::forward_list

  list和forward_list是STL中双向链表单向链表的实现,list的使用和前面用到的各种容器几乎都是一致的,但是链表由于每个节点可能都不相邻,因此不提供下标访问(随机访问),如果要访问元素,只能通过迭代器访问,例如:

#include <iostream>
#include <list>
using namespace std;

int main()
{
    list<int> l{1, 2, 3, 4, 5, 6, 7};
    for (auto& i : l) {
        cout << i << " ";
    }
    cout << endl;
    return 0;
}

p93

  这里介绍一些list的特有方法,首先是merge(),merge()可以合并两个已排序链表,例如:

#include <iostream>
#include <list>
using namespace std;

int main()
{
    list<int> l1{1, 3, 5, 7}, l2{0, 2, 4, 6, 11};
    l1.merge(l2);
    for (auto& i : l1) {
        cout << i << " ";
    }
    cout << endl;
    return 0;
}

p94

  这里就完成了两个有序链表的合并,我知道你可能比较叛逆,你说:“我就是要合并两个无序链表怎么了,它会怎么样? 好,我们来看看会怎么样:

    list<int> l1{1, 3, 5, 7}, l2{0, 2, 4, 3, 11};

  我们这里就简单地把l2的6改成3,看看运行结果:
p95

  这下好了,链表的merge方法里直接用断言检查了链表是否有序,如果无序,直接中断程序的运行,所以不要尝试合并无序的链表哦!

  第二个则是splice(),splice()可以把一个链表中的结点转移给另一个,它有以下重载:

void splice(const_iterator pos, list& other);
void splice(const_iterator pos, list&& other);
void splice(const_iterator pos, list& other, const_iterator it);
void splice(const_iterator pos, list&& other, const_iterator it);
void splice(const_iterator pos, list& other,
             const_iterator first, const_iterator last);
void splice(const_iterator pos, list&& other,
             const_iterator first, const_iterator last);

  第一和第二个可以把整个other转移过来,第三和第四个是从other的it起的所有结点转移过来,第五和第六个就是把other中从first到last之间的结点转移过来

  接下来是reverse(),reverse()方法可以实现把整个链表所有的元素倒置:

#include <iostream>
#include <list>
using namespace std;

int main()
{
    list<int> l1{1, 3, 5, 7};
    l1.reverse();
    for (auto& i : l1) {
        cout << i << " ";
    }
    cout << endl;
    return 0;
}

p96

  之后是sort(),之后我们要讲的algorithm中的标准算法sort不支持对于链表的排序,因此list本身有一个sort()方法可以用于排序,它的两个重载如下:

void sort();

template<class Compare>
void sort(Compare comp);

  第一个根据默认规则排序,第二个可以传入一个仿函数作为排序规则进行排序。

  最后则是unique()方法,这个方法能够从容器移除所有连续的重复元素,只留下相同元素中的第一个,例如:

#include <iostream>
#include <list>
#include <algorithm>
using namespace std;

int main()
{
    list<int> l1{1, 3, 3, 4, 5, 5, 7};
    l1.unique();
    for (auto& i : l1) {
        cout << i << " ";
    }
    cout << endl;
    return 0;
}

p97

  这样一来,我们就介绍完了list的各种新奇的方法,关于std::forward_list,我就不再细讲了,因为list的应用场景实际上已经覆盖了forward_list。

(9). 迭代器与基于范围的for循环

  有的时候你可能好奇,这个基于范围的for循环到底是怎么实现的,而且前面的一系列容器和容器适配器中,我们发现提供了迭代器的容器可以使用基于范围的for循环,而容器适配器没有提供,它就不可以用,基于范围的for循环和迭代器有什么关系呢?

  我自己最近半年内有在尝试自己实现一个STL,在实现Vector的时候,我在类中加入了这样两条语句:

using iterator = T*;
using const_iterator = const T*;

  并且在此基础上定义了:

const_iterator begin() const
{
    return arr; // arr是内部数组的指针
}

const_iterator end() const
{
    return arr + _size; // _size是已容纳元素的个数
}

iterator begin()
{
    return arr; // arr是内部数组的指针
}

iterator end()
{
    return arr + _size; // _size是已容纳元素的个数
}

  于是基于范围的for循环就神奇的可以在我自定义的STL上跑起来了,这也告诉我们一件事情,如果要让自己的类支持基于范围的for循环,就需要自己实现一个可以遍历整个容器的迭代器类型,你当然可以用指针对它进行一个包装,在实现了迭代器之后,你需要再定义好begin()和end()两个函数,这样一来基于范围的for循环就完全可以正常运行了。

  我的STL现在在GitHub上开源,项目叫做MySTL,由于本人代码水平一般,所以这个项目中可能有很多值得大幅改进的地方,这只是作为学习STL中的一个实验,如果大家有什么想法可以在仓库中以PR的形式进行提议,感谢大家的支持。

(10). 常用标准算法

  最后让我们来说说STL中常用的标准算法,首先我们需要包含头文件:

#include <algorithm>

  名字还挺直白,就叫算法。我们这个部分主要介绍sort和stable_sort、lower_bound、upper_bound和binary_search以及find,还有相当相当多的算法在algorithm中,碍于篇幅限制,我不会全部介绍,你可以参考cppreference中的algorithm页面

#1.sort和stable_sort

  sort是算法中最常用过的一个,sort的形式如下:

template< class RandomIt >
void sort( RandomIt first, RandomIt last );
template< class RandomIt, class Compare >
void sort( RandomIt first, RandomIt last, Compare comp );

  主要是这两种,第一种是传入需要排序的首和尾的迭代器,第二种则是在这个基础上在传入一个comp,这里看着好像comp只能传入仿函数,其实不是这样的,我们仍然可以像C语言中qsort的cmp一样,定义一个这样的函数:

bool comp(const T& a, const T& b)
{
    return ...;
}

  然后调用sort:

sort(a.begin(), a.end(), comp);

  sort也并不一定只能接受迭代器,我们往里面传入指针也是可以的,比如:

int a[]{0, 1, 2, 4, -1, 2, 0, 113, 23};
sort(a, a + 9);

  这样就行了,对于原生数组也是可以进行排序的,这里要注意两点,首先sort不是稳定的排序,对于两个相等的元素,在排序过后是否还能保持原来的相对位置不变,对于这个问题,algorithm中还提供了stable_sort方法解决这个问题。
  第二点则是对于内部存在动态内存分配的对象的数组的排序,我们应该采用sort而不是qsort进行排序,因为qsort进行的移动操作是如同memcpy一样简单的操作,对于这种对象的数组如果用qsort排序很有可能会严重干扰对象的内存管理状况,从而导致对象析构的时候出现重复析构等等问题,所以切勿使用qsort对于这一类的对象进行排序。

#2.lower_bound、upper_bound和binary_search

  这三个函数都是用于完成二分搜索的,其中lower_bound找到第一个不小于给定值的元素的迭代器,upper_bound找到第一个大于给定值的元素的迭代器,而binary_search则是确定给定元素是否存在。它们的原型如下:

// lower_bound
template< class ForwardIt, class T >
ForwardIt lower_bound( ForwardIt first, ForwardIt last, const T& value );

template< class ForwardIt, class T, class Compare >
ForwardIt lower_bound( ForwardIt first, ForwardIt last,
                       const T& value, Compare comp );

// upper_bound
template< class ForwardIt, class T >
ForwardIt upper_bound( ForwardIt first, ForwardIt last, const T& value );

template< class ForwardIt, class T, class Compare >
ForwardIt upper_bound( ForwardIt first, ForwardIt last,
                       const T& value, Compare comp );        

// binary_search
template< class ForwardIt, class T >
bool binary_search( ForwardIt first, ForwardIt last, const T& value );

template< class ForwardIt, class T, class Compare >
bool binary_search( ForwardIt first, ForwardIt last,
                    const T& value, Compare comp );

  它们三个的参数表完全一致,每个函数都允许我们指定一个比较规则,从而方便我们更好地利用二分查找找到对应的元素。但请特别注意,因为要使用二分查找,你用于查找的序列一定要满足默认(或在指定排序规则下)有序,否则不能完成二分查找的过程!

#3.find

  find则是直接找到某个元素的位置,它基于线性搜索实现,因此不要求被查找的序列有序,它的形式如下(包含一种可能的实现):

template<class InputIt, class T>
InputIt find(InputIt first, InputIt last, const T& value)
{
    for (; first != last; ++first)
        if (*first == value)
            return first
 
    return last;
}

  当找到元素时,返回对应的迭代器,否则就返回输入的末尾迭代器以表示没有找到

#4.关于STL

  以上介绍的三类算法,对于大部分具备随机访问迭代器的容器都是可以使用的,这便是STL的一个非常强大的优势,因为容器、迭代器和算法在设计时是分开设计的,因此它们之间才能工作的如此合拍

小结

  STL的内容非常非常之多,这一篇只是简要介绍了一些常用的容器以及对应的方法,以及一些常用的标准算法等等,这一篇的内容可以在阅读的时候多去尝试一下,这样能够明显提升熟练度,不过现在大部分IDE的代码提示功能都比较完善了,结合互联网,你应该是可以很快找到你所需要的函数的。
  下一篇我们会回到C++相较于C语言的一大重要更新—异常处理机制,敬请期待。

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值