STL和泛型编程
- 一.STL六大部件
- "前开后闭"区间
- 二.容器
- (1)顺序容器
- 1.array
- 源码剖析
- 2.vector
- 源码剖析
- vector的迭代器
- 3.list
- 源码剖析
- 迭代器的设计规则
- 关于重载++操作符
- 关于重载->和*操作符
- 4.forward_list
- 源码剖析
- 5.deque
- 源码剖析
- 底层数据结构
- 操作实现
- deque的设计
- deque的迭代器
- deque的插入
- deque模拟连续空间
- (2)容器适配器
- 1.stack
- 2.queue
- queue和stack源码剖析
- (3)关联式容器
- 1.mutiset
- 2.multimap
- (4)哈希结构
- 1.unordered_multiset
- 2.unordered_multimap
- (5)元素不可以重复
- 1.set
- 2.map
- 3.unordered_set
- 4.unordered_map
- 三.OOP VS GP
- 四.分配器
- 五. 红黑树
- 源码剖析
- 测试
- 以红黑树为底层的容器
- set、multiset
- 源码剖析![image-20240321190148264](https://img-blog.csdnimg.cn/img_convert/6e5f5ae6ce382c7873fef6af04324458.png)
- map、multimap
- 源码剖析
- map独有的[]
- 六. 哈希表
- 源码剖析
- 测试
- unordered容器
- 七. 算法
- 迭代器的分类
- 验证容器的迭代器种类
- 迭代器分类对算法的影响
- 算法源代码剖析
- 1.accumulate
- 2.for_each
- 3.replace,replace_if,replace_copy
- 4.count,count_if
- 5.find,find_if
- 6.sort
- 7.binary_search
- 八. 仿函数(函数对象)
- STL仿函数编写条件
- 九. 适配器
- 1.binder2nd
- 2.not1
- 3.bind
- 八. 仿函数(函数对象)
- STL仿函数编写条件
- 九. 适配器
- 1.binder2nd
- 2.not1
- 3.bind
以STL为目标探讨泛型编程。
一.STL六大部件
算法通过迭代器来处理容器中的数据。
容器使用分配器来管理它们的内存。分配器将内存分配和释放的实现细节封装起来,使得容器可以在运行时根据需要动态地分配内存。
适配器允许在现有的容器或迭代器之上实现不同的行为或提供新的接口,从而更加方便地操作数据。
仿函数通常用于算法、容器和其他组件中,以提供各种功能和灵活性。它们可以作为算法的参数传递,也可以用于自定义容器的排序、查找和比较操作等。使用仿函数可以在编译时将函数调用行为封装到对象中,从而提供更灵活和通用的功能。算法通常通过仿函数来完成比较、排序和其他操作,这使得算法可以适用于各种数据类型和需求。
"前开后闭"区间
所有容器满足:"前开后闭"区间
二.容器
-
array无法扩充。
-
vector尾可以扩充。
-
deque头尾都可以扩充。
set和map一般是红黑树实现,unordered_set和unordered_map一般是hash表实现(分离链接法)。
(1)顺序容器
1.array
源码剖析
2.vector
源码剖析
几乎所有vector的实现都是当vector需要扩充的时候,进行两倍成长。会造成大量的拷贝构造和析构。
vector的迭代器
3.list
源码剖析
list本身就是一个指针(link_type),link_type是一个指针(list_node*)指向了一个_list_node结构体。
list是非连续空间,因此iterator不能是指针(只有vector和array的iterator是指针,其余都是结构体)。
迭代器的设计规则
迭代器必须定义五种类型,以便于能回答算法的问题。
1.迭代器的类型。关于前向、双向、随机等。
2.两个迭代器间的距离。
3.迭代器指向的元素的类型。
iterator_traits<Iterator>::value_type
:迭代器指向的元素的类型。iterator_traits<Iterator>::difference_type
:两个迭代器之间的差值类型。这通常用于表示两个迭代器之间的距离。iterator_traits<Iterator>::iterator_category
:迭代器的类型分类,如std::input_iterator_tag
、std::forward_iterator_tag
、std::bidirectional_iterator_tag
或std::random_access_iterator_tag
。这用于区分迭代器的不同能力。iterator_traits<Iterator>::pointer
和iterator_traits<Iterator>::reference
:这两个类型通常分别定义为value_type*
和value_type&
,但某些迭代器类型可能会提供特殊的指针或引用类型。
为了应付迭代器有指针的形式,需要一个中间层traits。traits中使用了模板的偏特化,区分当参数是指针或类。
关于重载++操作符
重载后置++
- 先调用拷贝构造函数将当前的对象复制给tmp
- 然后对当前对象进行前置++操作
- 最后返回tmp
关于返回值
- 因为标准库中后置++不可以连做两次,因此这里值传递返回。前置++可以连做两次,返回引用。
关于重载->和*操作符
list的指针类型修改为指向自己的,相比于指向void更精确。
4.forward_list
只提供push_front插入,且不提供size()。
源码剖析
5.deque
源码剖析
在C++标准库中,deque
(双端队列)的实现是优化了从序列两端插入和删除操作的容器。与vector
相比,它不保证所有元素都在连续的内存地址中,但仍然能够提供对任何元素的直接(随机访问)访问。这是通过一个复杂的内部数据结构实现的,旨在兼顾两端操作的高效性和随机访问的能力。
底层数据结构
deque
的实现通常采用一种“分块数组”(或称为“段数组”)的策略。具体来说:
- 多个固定大小的数组块:
deque
由多个数组块组成,每个块能够存储固定数量的元素。这些数组块的大小通常是实现定义的,可以根据元素的类型和容器的大小动态调整以优化性能。 - 映射器(Map):
deque
内部使用一个中心数据结构(通常是一个数组或动态数组),来维护对这些块的引用(指针或索引)。这个中心结构被称为映射器,它按序记录了所有块的位置,使得可以快速定位到任何一个元素所在的块。
操作实现
- 随机访问:通过映射器,
deque
可以计算出任何元素的位置,即首先定位到正确的块,然后在该块内进行索引。这使得随机访问的时间复杂度为O(1),与vector
相同。 - 在两端插入和删除:由于
deque
的设计,它可以在头部或尾部添加或移除元素而无需移动其他元素。如果在一端的块已满且需要添加更多元素时,deque
将分配一个新的块并更新映射器。这种操作通常比vector
的动态扩容(涉及复制现有元素到新的内存地址)更高效。 - 动态扩容:当
deque
增长到超出当前映射器容量时,它需要一个更大的映射器来存储更多的块引用。此时,deque
将分配一个新的更大的映射器,复制旧映射器中的引用到新映射器中,然后释放旧映射器。
deque的设计
deque的迭代器
deque的插入
deque模拟连续空间
(2)容器适配器
不提供iterator,因为一定是一端进,一端出。提供iterator会导致可以从任意地方进出容器,破坏一致性。
1.stack
2.queue
queue和stack源码剖析
(3)关联式容器
1.mutiset
key就是value,value就是key。
2.multimap
(4)哈希结构
1.unordered_multiset
2.unordered_multimap
(5)元素不可以重复
1.set
2.map
3.unordered_set
4.unordered_map
三.OOP VS GP
因为在模板sort的内部,对迭代器进行了跳跃式操作,只有随机访问迭代器可以这么操作。
而list的迭代器是双向迭代器,只支持++和–。
优点:
四.分配器
C++标准库在很多地方采用特殊对象处理内存的分配和规划,这样的对象称为分配器(allocator)。allocator表现出一种特殊内存模型,被当成一种用来把内存需求转换为内存低级调用的抽象层。
C++标准库中链表模板的完整参数定义是:
template<class T, class Alloc=allocator<T>>
class list;
当省略最后的模板参数时,容器将采用标准中预定义的分配器std::allocator<T>
。该分配器调用new/delete操作符申请和释放内存,足以满足大多数需求。而当需要定制容器中的内存操作时,可以按照标准中的分配器规范,将内存操作封装在一个新的分配器类模板中并传入容器,比如:
std::list<my_data, my_allocator<my_data>> custom_list;
标准库中可以接受分配器的容器模板包括:
- 序列型容器
vector
、deque
、list
、forward_list
- 集合容器
set
、multiset
、map
、multimap
- 散列表容器
unordered_set
、unordered_multiset
、unordered_map
、unordered_multimap
具体来说:
-
分配器是一个类(
class Alloc
),而不是函数或者模板 -
分配器是和具体类型相关的,因为默认构造器
std::allocator<T>
随容器保存类型的不同而不同。 -
可以想象,分配器的行为应该类似new/delete操作符,而不是malloc/free(前者需要知道空间所存放的类型而后者只关心空间的大小)。
-
分配器内至少需要有两个成员函数分别响应容器的申请以及释放内存的请求。
-
分配器的首要作用是申请与释放内存。通过两个成员函数
allocate
和deallocate
来实现。无论其定义如何,两成员函数必须能够实现如下用法:
a.allocate(n); // 申请能保存n个value_type的数据, 如果申请空间失败,抛出异常std::bad_alloc
a.allocate(n, p);
a.deallocate(p, n); //要释放的空间指针p和空间大小n
五. 红黑树
源码剖析
测试
以红黑树为底层的容器
set、multiset
源码剖析
map、multimap
源码剖析
map独有的[]
六. 哈希表
空间足够的时候,假如有2的32次方元素变化,可以用4G的空间来存储。但是这是不可能的。
空间不够的时候,假如有2的32次方元素变化,因此需要取模将元素放入对应的格子中。但是会发生碰撞,因此用链表将碰撞的元素串起来。
如果某个格子串起来的元素的个数大于格子的总数,说明格子空间不够了,因此重新划分格子数目。
源码剖析
测试
对数值来说,传进去的数值就是编号。
对字符来说,选一种方法能尽可能打乱。
unordered容器
七. 算法
- 算法的所有操作都依赖迭代器。
迭代器的分类
随机访问迭代器:vector、array、deque
双向访问迭代器:list、set、multisest、map、multimap
单向访问迭代器:forward_list
验证容器的迭代器种类
迭代器分类对算法的影响
distance算法针对不同的迭代器使用不同的方法计算距离。
返回类型就是difference_type。
算法源代码剖析
1.accumulate
2.for_each
3.replace,replace_if,replace_copy
replace
函数用于将容器中的某个值替换为另一个值。
函数原型:
template<class ForwardIt, class T, class U>
void replace(ForwardIt first, ForwardIt last, const T& old_value, const U& new_value);
first, last
:指定要搜索的容器范围。old_value
:要被替换的值。new_value
:替换后的新值。
示例:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 2, 4, 2, 5};
std::replace(vec.begin(), vec.end(), 2, 9);
for (int x : vec) {
std::cout << x << ' ';
}
// 输出:1 9 3 9 4 9 5
return 0;
}
在这个例子中,我们将 vec
中所有的 2
替换为 9
。
replace_if
函数用于根据谓词(即一个返回布尔值的函数或函数对象)的条件来替换容器中的元素。
函数原型:
template<class ForwardIt, class UnaryPredicate, class T>
void replace_if(ForwardIt first, ForwardIt last, UnaryPredicate p, const T& new_value);
first, last
:指定要搜索的容器范围。p
:一个一元谓词,用于测试每个元素是否应被替换。new_value
:替换后的新值。
示例:
#include <iostream>
#include <vector>
#include <algorithm>
bool is_even(int n) {
return n % 2 == 0;
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6};
std::replace_if(vec.begin(), vec.end(), is_even, 0);
for (int x : vec) {
std::cout << x << ' ';
}
// 输出:1 0 3 0 5 0
return 0;
}
在这个例子中,我们使用 is_even
谓词来检查 vec
中的每个元素是否为偶数,并将所有偶数替换为 0
。
replace_copy
函数与 replace
类似,但它不会修改原始容器,而是将替换后的结果复制到另一个容器中。
函数原型:
template<class InputIt, class OutputIt, class T, class U>
OutputIt replace_copy(InputIt first, InputIt last, OutputIt d_first, const T& old_value, const U& new_value);
first, last
:指定要搜索的输入容器范围。d_first
:指向输出容器的开始迭代器。old_value
:要被替换的值。new_value
:替换后的新值。
示例:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 2, 4, 2, 5};
std::vector<int> result;
result.resize(vec.size()); // 预先分配足够的空间
std::replace_copy(vec.begin(), vec.end(), result.begin(), 2, 9);
for (int x : result) {
std::cout << x << ' ';
}
// 输出:1 9 3 9 4 9 5
// 注意:原始 vec 容器没有被修改
return 0;
}
在这个例子中,我们将 vec
中所有的元素复制到result
容器中,如果遇到值为2的元素,将其改为9复制到result
中。
4.count,count_if
count
函数用于统计容器中等于某个特定值的元素数量。
函数原型:
template<class InputIt, class T>
size_t count(InputIt first, InputIt last, const T& value);
first, last
:指定要搜索的容器范围。value
:要搜索的值。
示例:
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
std::vector<int> vec = {1, 2, 3, 2, 4, 2, 5};
size_t count_of_two = std::count(vec.begin(), vec.end(), 2);
std::cout << "Number of 2 in vec: " << count_of_two << std::endl;
// 输出:Number of 2 in vec: 3
return 0;
}
count_if
函数用于统计容器中满足某个谓词(即一个返回布尔值的函数或函数对象)的元素数量。
函数原型:
template<class InputIt, class UnaryPredicate>
size_t count_if(InputIt first, InputIt last, UnaryPredicate p);
first, last
:指定要搜索的容器范围。p
:一个一元谓词,用于测试每个元素是否应被计入统计。
示例:
#include <iostream>
#include <vector>
#include <algorithm>
bool is_even(int n) {
return n % 2 == 0;
}
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5, 6};
size_t count_of_evens = std::count_if(vec.begin(), vec.end(), is_even);
std::cout << "Number of even numbers in vec: " << count_of_evens << std::endl;
// 输出:Number of even numbers in vec: 3
return 0;
}
在这个例子中,我们定义了一个 is_even
谓词来检查一个整数是否为偶数,并使用 count_if
统计 vec
中偶数的数量,结果是 3
。
5.find,find_if
6.sort
7.binary_search
八. 仿函数(函数对象)
STL仿函数编写条件
unary_function:一个参数
binary_function:两个参数
九. 适配器
1.binder2nd
注意区分小括号是直接调用函数还是创建函数对象。
2.not1
3.bind
7411967)]
八. 仿函数(函数对象)
[外链图片转存中…(img-FKy1J6zp-1711347411967)]
[外链图片转存中…(img-K8lnpskn-1711347411967)]
[外链图片转存中…(img-4fNkLvv0-1711347411967)]
STL仿函数编写条件
[外链图片转存中…(img-LQs5p3Y0-1711347411967)]
unary_function:一个参数
binary_function:两个参数
九. 适配器
[外链图片转存中…(img-ssSnydJm-1711347411968)]
[外链图片转存中…(img-Wan0qRXc-1711347411968)]
1.binder2nd
[外链图片转存中…(img-Zzkjq2ht-1711347411968)]
注意区分小括号是直接调用函数还是创建函数对象。
2.not1
[外链图片转存中…(img-eVhrrKOu-1711347411968)]
3.bind
[外链图片转存中…(img-XHB9sF1W-1711347411968)]
[外链图片转存中…(img-hDiOBKs8-1711347411968)]
[外链图片转存中…(img-8kZRm5Ds-1711347411968)]
[外链图片转存中…(img-PgOUiWZA-1711347411968)]
[外链图片转存中…(img-CM2boWpx-1711347411968)]