特此声明:本文由我学习完侯捷老师的C++ STL课程后将多篇博客和资料整合而来,主要借鉴的博客地址:
- 侯捷 C++ STL标准库和泛型编程【C++学习笔记】 超详细 万字笔记总结 笔记合集_候捷c++的stl笔记-CSDN博客
- 侯捷C++八部曲笔记(二、STL标准库和泛型编程)_侯捷stl-CSDN博客
- 【STL和泛型编程-侯捷】—学习笔记(上)_后置++不能用两次-CSDN博客
【STL和泛型编程-侯捷】—学习笔记(下)-CSDN博客本来打算自己总结笔记的,但是由于已经有前辈做的更好,又由于自己实在没有额外的时间,就匆匆将几篇博客和一些资料整理而来,特此声明以表感谢!!
1 STL概述
STL —— Standard Template Library,标准模板库
C++ Standard LIbrary,C++标准库中包含STL(即STL+一些小东西)
1.1 头文件名称
- C++标准库的 header files 不带
.h
,例如:#include<vector>
- 新式 C header files 不带
.h
,例如:#include<cstdio>
- 老式 C header files 带
.h
仍然可用,例如:#include<stdio.h>
新式 header 内的组件封装于 namespace std
老式 header 内的组件不封装于 namespace std
1.2 STL基础介绍
STL六大部件:容器(Containers)、分配器(Allocators)、算法(Algorithms)、迭代器(Iterators)、仿函数(Functors)、适配器(Adapters)
- 容器:放数据
- 分配器:是来支持容器将数据放到内存里
- 算法:是一个个函数来处理存放在容器里的数据
- 迭代器:就是来支持算法操作容器的
- 仿函数:作用类似函数,例如相加相减等等
- 适配器:有三种,分别将容器,迭代器,仿函数来进行一个转换
实例:
- 首先是创建一个 container(vector)
- allocator 来帮助 container 来分配内存(一般会忽略不写)
- 用一个 Algorithm 来操作数据(count_if 是数出满足条件的个数)
- iterator 就是一个泛化的指针,来告诉 Algorithm 要处理哪里的数据
- 用一个 functor 来判断数据(less 其有两个参数传入,第一个 < 第二个就为真)
- 先用一个 function adapter(bind2nd)绑定了第二个参数为 40;再用一个 function adapter(not1)来对整个判断结果进行否定
判断条件 predicate 为:not1(bind2nd(less<int>(), 40))
—— 表示 >= 40 数为真
前闭后开:【 ),基本所有容器都有 begin()
end()
,但 begin 是指向的容器的第一个元素,而 end 是指向的容器最后一个元素的 **下一个
例子:遍历容器
...
Container<T> c;
Container<T>::iterator i = c.begin();
for (; i != c.end(); ++i)
{
...
}
//但在C++11中可以用新语法简写
...
Container<T> c;
for (auto elem : c)
{
...
}
1.3 typename
在模板参数的关键字使用中与 class
是一样的
在类型前面加上 typename
:
template <typename T>
class MyTemplateClass {
public:
typedef typename T::NestedType NestedType;
};
template <typename T>
void MyTemplateFunction() {
typename T::SomeType variable;
// ...
}
在这个例子中,typename
用于告诉编译器 T::NestedType
和 T::SomeType
是类型名称而不是成员变量
typename
是一个用于明确指定符号是一个类型的关键字,以帮助编译器正确解析代码并避免歧义,如果不使用 typename
,编译器可能会认为符号是一个值而不是类型,导致编译错误。
2 OOP vs. GP
-
OOP —— Object-Oriented programming 面向对象编程
- 将数据和操作关联到一起
- OOP是把数据和方法放在一个类里面
例如容器 List,其自带了一个
sort()
,因为链表的存储空间不是连续的,Iterator 不能实现加减操作,所以不能使用全局的::sort()
-
GP —— Generic Programming 泛式编程
-
-GP是把数据和方法分开(将数据和操作分开),这么做有什么好处呢?容器和算法可以闭门造车,通过迭代器产生关联;
-
容器和算法的团队就可以各自闭门造车,其间通过 Iterator 联通即可
-
算法通过 Iterator 确定操作范围,并通过 Iterator 取用容器的元素
-
所有的算法,其内的最终涉及元素的操作都是比大小
-
-
指定比较方法时,什么时候用仿函数,什么时候用函数呢?
使用容器指定比较方法时用仿函数,使用算法指定比较方法时使用函数
-
所有算法,在操作容器的元素的时候,无非就是比较大小。
3 容器
3.1 容器结构分类
分类:序列式容器 Sequence Container,关联式容器 Associative Container
-
序列式容器:按照放入的次序进行排列
- Array 数组,固定大小
- Vector 向量,会自动扩充大小
- Deque 双向队列,双向都可以扩充
- List 链表,双向链表
- Forward-List 链表,单向链表
-
关联式容器:有 key 和 value,适合快速的查找
STL中实现使用红黑树(高度平衡二叉树)和哈希表
-
Set,key 就是 value,元素不可重复
-
Map,key 和 value 是分开的,元素不可重复
-
Multi~,元素是可以重复的
-
Unordered~,HashTable Separate Chaining
-
其中 Array,Forward-List,Unordered~ 都是C++11的
3.2 序列式容器
3.2.1 array
chatGPT介绍
C++ 标准模板库(STL)中的 std::array
是一个用于管理定长数组的容器。与传统的C风格数组相比,std::array
提供了更多的功能和更好的安全性,同时仍保持了数组的高效性。下面详细介绍 std::array
的特点、使用方法及其常用成员函数。
std::array
的特点
- 定长数组:
std::array
是一种定长容器,一旦创建,大小不可更改。因此在编译时必须确定其大小。 - 在栈上分配内存:与
std::vector
不同,std::array
在栈上分配内存,这使得它的性能与 C 风格数组相似,但提供了更多的功能。 - 类型安全:
std::array
提供类型安全,可以防止数组越界等常见错误。 - 与STL兼容:
std::array
支持 STL 提供的算法和迭代器,因此可以与其他 STL 容器无缝协作。
std::array
的定义
std::array
的定义如下:
std::array<Type, N> arr;
Type
是数组中元素的类型。N
是数组的大小。
示例代码
#include <iostream>
#include <array>
int main() {
// 创建一个包含5个整数的 std::array
std::array<int, 5> arr = {1, 2, 3, 4, 5};
// 访问元素
std::cout << "First element: " << arr[0] << std::endl;
// 修改元素
arr[0] = 10;
// 使用范围for循环遍历元素
for (int elem : arr) {
std::cout << elem << " ";
}
std::cout << std::endl;
return 0;
}
常用成员函数
std::array
提供了一些常用的成员函数,用于操作和访问数组元素:
-
at(size_t index)
:返回给定索引处的元素,并执行边界检查。int val = arr.at(2); // 如果index超出范围,将抛出std::out_of_range异常
-
operator[]
:返回给定索引处的元素,不进行边界检查。int val = arr[2];
-
size()
:返回数组的大小。size_t size = arr.size();
-
front()
和back()
:分别返回数组中的第一个和最后一个元素。int first = arr.front(); int last = arr.back();
-
data()
:返回指向底层数组的
测试
#include <array>
#include <iostream>
#include <ctime>
#include <cstdlib> //qsort, bsearch, NULL
void test_array() {
cout << "\n test_array().......... \n";
// 创建一个包含long型元素的array容器,ASIZE为数组的大小
array<long, ASIZE> c;
// 记录开始时间
clock_t timeStart = clock();
// 填充数组 c 中的元素,使用 rand() 生成随机数
for (long i = 0; i < ASIZE; ++i) {
c[i] = rand();
}
// 输出填充数组所花费的毫秒数
cout << "milli-seconds : " << (clock() - timeStart) << endl;
// 输出数组的大小、第一个元素、最后一个元素、起始地址
cout << "array.size()= " << c.size() << endl;
cout << "array.front()= " << c.front() << endl;
cout << "array.back()= " << c.back() << endl;
cout << "array.data()= " << c.data() << endl;
// 获取目标值
long target = get_a_target_long();
// 记录开始时间
timeStart = clock();
// 使用标准库的 qsort 函数(快排)对数组 c 进行排序
::qsort(c.data(), ASIZE, sizeof(long), compareLongs);
// 使用标准库的 bsearch 函数(二分查找)在排序后的数组中搜索目标值
long* pItem = (long*)::bsearch(&target, c.data(), ASIZE, sizeof(long), compareLongs);
// 输出排序和搜索所花费的毫秒数
cout << "qsort()+bsearch(), milli-seconds : " << (clock() - timeStart) << endl;
// 如果找到目标值,输出该值;否则输出未找到消息
if (pItem != NULL)
cout << "found, " << *pItem << endl;
else
cout << "not found! " << endl;
}
运行结果:
随机数据填充容器:47ms;排序和搜索:187ms
深度探索
C++TR1下(比较简单):
template <typename _Tp, std::size_t _Nm>
struct array
{
typedef _Tp value_type;
typedef _Tp* pointer;
typedef value_type* iterator; // 迭代器为_Tp*
value_type _M_instance[_Nm ? _Nm : 1]; // 如果_Nm为0,就分配一个空间
iterator begin() { return iterator(&_M_instance[0]); }
iterator end() { return iterator(&_M_instance[_Nm]); }
...
};
GCC4.9下(复杂且无益处):
// GCC4.9通过多个typedef以下面的逻辑创建的array里的data
typedef int T[100]; // T即类型int[100]
T c; // 与int c[100]一样
3.2.2 vector
chatGPT介绍
C++ 标准模板库(STL)中的 std::vector
是一个动态数组容器,它可以根据需要自动调整其大小。std::vector
是一种常用的数据结构,因为它结合了数组的高效性和动态管理内存的能力。下面详细介绍 std::vector
的特点、使用方法、常用成员函数及其背后的机制。
std::vector
的特点
-
动态大小:
std::vector
可以根据需求动态调整其大小,这意味着你可以在运行时添加或删除元素,而不需要关心内存管理细节。 -
随机访问:与普通数组一样,
std::vector
支持常量时间的随机访问,可以通过索引直接访问任意位置的元素。 -
连续内存:
std::vector
在内存中存储元素是连续的,因此它与 C 风格数组兼容,可以通过指针操作向量中的元素。 -
自动内存管理:
std::vector
会自动处理内存的分配和释放,用户不需要手动管理内存,这降低了内存泄漏和其他内存管理错误的风险。 -
支持STL算法:
std::vector
支持 STL 中的各种算法,如排序、查找、复制等,并提供了与其他 STL 容器无缝协作的能力。
std::vector
的使用
std::vector
的使用非常简单。可以通过以下方式定义一个 std::vector
:
std::vector<int> vec; // 创建一个存储 int 类型的空向量
std::vector<int> vec(10); // 创建一个存储10个元素的 int 类型向量,每个元素默认初始化为 0
std::vector<int> vec(10, 5); // 创建一个存储10个元素的 int 类型向量,每个元素初始化为 5
std::vector<int> vec = {1, 2, 3}; // 使用初始化列表创建并初始化一个向量
常用成员函数
-
元素访问
at(size_type pos)
:返回指定位置的元素,并进行范围检查。operator[](size_type pos)
:返回指定位置的元素,不进行范围检查。front()
:返回第一个元素。back()
:返回最后一个元素。data()
:返回指向第一个元素的指针(用于与 C 风格数组兼容)。
-
容量相关
size()
:返回当前向量中的元素个数。capacity()
:返回当前分配的能够容纳的元素个数。empty()
:判断向量是否为空。reserve(size_type new_cap)
:预留至少能够容纳new_cap
个元素的空间,避免频繁重新分配内存。shrink_to_fit()
:请求减少容量以适应当前大小,可能会减少内存占用。
-
修改元素
push_back(const T& value)
:在向量末尾添加一个元素。emplace_back(Args&&... args)
:在向量末尾原地构造一个元素(更高效)。pop_back()
:移除向量末尾的元素。insert(iterator pos, const T& value)
:在指定位置插入一个元素。erase(iterator pos)
:移除指定位置的元素。clear()
:清空向量中的所有元素。resize(size_type count)
:调整向量的大小,如果新大小大于当前大小,使用默认值填充新的元素。
-
迭代器
begin()
:返回指向第一个元素的迭代器。end()
:返回指向末尾的迭代器(不指向任何元素)。rbegin()
和rend()
:返回反向迭代器,分别指向末尾和起始位置。
std::vector
的底层机制
-
自动增长:
std::vector
的关键特点是其动态增长机制。当vector
需要更多空间时,它会分配一个比当前容量更大的新内存块,通常是当前容量的两倍,然后将旧数据复制到新内存中。这种增长策略在性能和内存使用之间取得了平衡。 -
内存管理:为了减少内存分配的开销,
std::vector
通常会分配比当前所需更大的内存空间,这就是capacity()
通常大于size()
的原因。shrink_to_fit()
可以用来释放多余的内存。 -
效率:由于
std::vector
存储元素是连续的,其访问时间是 O(1),与普通数组相同。然而,由于动态调整大小和元素插入/删除的开销,某些操作的性能可能不如静态数组。
使用 std::vector
的注意事项
-
内存增长:频繁的内存重新分配可能会影响性能,因此如果你能预估需要的元素数量,使用
reserve()
预分配空间可以提高性能。 -
内存碎片:在插入、删除或扩展向量时,可能会导致内存碎片化,特别是在大型应用
测试
#include <vector>
#include <stdexcept>
#include <string>
#include <cstdlib> //abort()
#include <cstdio> //snprintf()
#include <iostream>
#include <ctime>
#include <algorithm> //sort()
// 测试函数,接受一个引用类型的长整型参数
void test_vector(long& value)
{
cout << "\ntest_vector().......... \n";
vector<string> c; // 创建一个字符串类型的向量
char buf[10];
clock_t timeStart = clock(); // 记录开始时间
for(long i=0; i< value; ++i) // 循环插入随机生成的字符串
{
try {
snprintf(buf, 10, "%d", rand()); // 将随机整数转换为字符串
c.push_back(string(buf)); // 将字符串添加到向量中
} // 这里是处理异常,如内存不够
catch(exception& p) {
cout << "i=" << i << " " << p.what() << endl;
// 输出出现异常的信息以及对应的索引值
// 曾經最高 i=58389486 then std::bad_alloc
abort(); // 异常处理后中止程序
}
}
cout << "milli-seconds : " << (clock()-timeStart) << endl; // 输出填充向量花费时间
cout << "vector.max_size()= " << c.max_size() << endl; // 输出向量的最大容量
cout << "vector.size()= " << c.size() << endl; // 输出向量的实际大小
cout << "vector.front()= " << c.front() << endl; // 输出向量的首元素
cout << "vector.back()= " << c.back() << endl; // 输出向量的末尾元素
cout << "vector.data()= " << c.data() << endl; // 输出向量地址
cout << "vector.capacity()= " << c.capacity() << endl << endl; // 输出向量的容量
// 直接find来查找————次序查找
string target = get_a_target_string(); // 获取一个目标字符串
{
timeStart = clock(); // 记录开始时间
auto pItem = find(c.begin(), c.end(), target); // 在向量中查找目标字符串
cout << "std::find(), milli-seconds : " << (clock()-timeStart) << endl;
if (pItem != c.end())
cout << "found, " << *pItem << endl << endl; // 输出找到的目标字符串
else
cout << "not found! " << endl << endl; // 输出未找到目标字符串
}
// 先排序再二分法查找
{
timeStart = clock(); // 记录开始时间
sort(c.begin(), c.end()); // 对向量中的字符串进行排序
cout << "sort(), milli-seconds : " << (clock()-timeStart) << endl;
timeStart = clock();
string* pItem = (string*)::bsearch(&target, (c.data()),
c.size(), sizeof(string), compareStrings);
cout << "bsearch(), milli-seconds : " << (clock()-timeStart) << endl;
if (pItem != NULL)
cout << "found, " << *pItem << endl << endl; // 输出在排序后向量中找到的目标字符串
else
cout << "not found! " << endl << endl; // 输出在排序后向量中未找到目标字符串
}
c.clear(); // 清空向量中的数据
test_moveable(vector<MyString>(),vector<MyStrNoMove>(), value); // 调用另一个函数进行测试
}
这是 array 在后面插入元素,其中若空间 capacity 不够,其会进行两倍扩充——即空间不够时会将原来的空间 *2
- (1)每次Push.back申请的空间以两倍增长,会申请预留空间;size是大小,capacity是申请的内存空间大小;是另外找一个两倍大的空间
- (2)try和catch抓取异常
- (3)find是算法,是模板函数;所有算法都是全局模板函数;
c.push_back(string(buf));
运行结果:
随机数据填充容器:3063ms;直接搜索:0ms(运气很好);排序后二分查找:2765ms
深度探索
GCC2.9下:
一共3个指针:start
,finish
,end_of_storage
所以 sizeof(vector<int>)
是12
template <class T, class Alloc = alloc>
class vector
{
public:
typedef T value_type;
typedef value_type* iterator; // 迭代器就是T*
typedef value_type& reference;
typedef size_t size_type;
protected:
iterator start;
iterator finish;
iterator end_of_storage;
public:
iterator begin() { return start; }
iterator end() { return finish; }
size_type size() const { return size_type(end() - begin()); }
size_type capacity() const { return size_type(end_of_storage - begin()); }
bool empty() const { return begin() == end(); }
reference operator[](size_type n) { return *(begin() + n); }
// 所有连续储存的容器都有[]的重载
reference front() { return *begin(); }
reference back() { return *(end() - 1); }
}
vector 每次成长会大量调用元素的拷贝构造函数和析构函数,是一个大成本
void push_back(const T& x)
{
if (finish != end_of_storage) // 还有备用空间
{
construct(finish, x); // 全局函数
++finish;
}
else // 无备用空间
insert_aux(end(), x);
}
template <class T, class Alloc>
void vector<T, Alloc>::insert_aux(iterator position, const T& x){
if (finish != end_of_storage){ // insert_aux还会被其他函数调用所以还有检查
// 在‘备用空间起始处’构建一个元素以vector最后一个元素为初值
// insert_aux也可能被insert调用,元素插入位置不定
construct(finish, *(finish - 1));
++finish;
T x_copy = x;
copy_backward(position, finish - 2, finish - 1);
*position = x_copy;
}
else{
const size_type old_size = size();
const size_type len = old_size != 0 ? 2 * old_size : 1;
// 原大小为0,则分配1;否则,分配原大小的2倍
iterator new_start = data_allocator::allocate(len);
iterator new_finish = new_start;
try{
// 拷贝安插点前的原内容
new_finish = uninitialized_copy(start, position, new_start);
construct(new_finish, x);
++new_finish;
// 拷贝安插点后的原内容
new_finish = uninitialized_copy(position, finish, new_finish);
}
catch (...){
destroy(new_start, new_finish);
data_allocator::deallocate(new_start, len);
throw;
}
// 解构并释放原vector
destroy(begin(), end());
deallocate();
// 调整迭代器,指向新vector
start = new_start;
finish = new_finish;
end_of_storage = new_start + len;
}
GCC4.9下变得复杂:
且迭代器也变得乱七八糟,舍近求远,何必如此!!
3.2.3 list
chatGPT介绍
C++ 标准模板库(STL)中的 std::list
是一种双向链表容器,专为需要频繁插入和删除操作的场景设计。std::list
与 std::vector
不同,它不支持随机访问,但在插入和删除元素时具有更高的效率。下面详细介绍 std::list
的特点、使用方法、常用成员函数以及底层机制。
std::list
的特点
-
双向链表:
std::list
是一种双向链表(Doubly Linked List),这意味着每个元素都有指向前一个和后一个元素的指针。你可以从任一方向遍历链表。 -
高效插入和删除:由于链表的结构,
std::list
可以在常量时间内进行插入和删除操作,特别是在已知位置的情况下,这使得它在频繁修改数据的场景中非常高效。 -
不连续的内存存储:
std::list
的元素并不需要在内存中连续存放,这与std::vector
不同。每个元素都独立存在,并通过指针连接到前后元素。 -
不支持随机访问:由于
std::list
的链表结构,不能通过索引直接访问元素,因此随机访问的时间复杂度为 O(n)。 -
双向迭代器:
std::list
提供双向迭代器,可以向前或向后遍历,但不支持随机访问迭代。
std::list
的使用
使用 std::list
创建一个双向链表非常简单,语法如下:
std::list<int> lst; // 创建一个存储 int 类型元素的空链表
std::list<int> lst(5); // 创建一个包含 5 个元素的 int 类型链表,每个元素默认初始化为 0
std::list<int> lst(5, 100); // 创建一个包含 5 个元素的 int 类型链表,每个元素初始化为 100
std::list<int> lst = {1, 2, 3}; // 使用初始化列表创建并初始化一个链表
常用成员函数
-
元素访问
front()
:返回链表中的第一个元素。back()
:返回链表中的最后一个元素。
-
容量相关
empty()
:判断链表是否为空。size()
:返回链表中元素的数量。max_size()
:返回链表可以容纳的最大元素数量。
-
修改元素
push_front(const T& value)
:在链表的前端插入一个元素。push_back(const T& value)
:在链表的末尾插入一个元素。pop_front()
:移除链表前端的元素。pop_back()
:移除链表末尾的元素。insert(iterator pos, const T& value)
:在指定位置前插入一个元素,返回新元素的迭代器。erase(iterator pos)
:移除指定位置的元素,返回下一个元素的迭代器。clear()
:移除链表中的所有元素。resize(size_type count)
:调整链表的大小。如果新大小大于当前大小,使用默认值填充新的元素。emplace_front(Args&&... args)
:在链表的前端原地构造一个元素。emplace_back(Args&&... args)
:在链表的末尾原地构造一个元素。
-
操作链表
splice(iterator pos, list& other)
:将other
链表的内容插入到当前链表中的pos
位置处。remove(const T& value)
:移除所有与value
相等的元素。remove_if(Predicate pred)
:移除所有满足谓词pred
的元素。reverse()
:反转链表中的元素顺序。unique()
:移除链表中连续重复的元素,只保留一个。sort()
:对链表中的元素进行排序。
-
迭代器
begin()
:返回指向链表中第一个元素的迭代器。end()
:返回指向链表末尾(不指向任何元素)的迭代器。rbegin()
和rend()
:返回反向迭代器,分别指向末尾和起始位置。
std::list
的底层机制
-
链表节点:每个元素存储在一个节点中,节点包含数据和指向前后元素的指针。插入或删除元素只需要更新相邻节点的指针,因此操作非常高效。
-
内存管理:由于
std::list
是动态分配的链表,每个元素都是独立分配的,这意味着插入和删除操作不会导致大量的内存重新分配,也不会像std::vector
那样需要移动大量元素。 -
效率与局限性:
- 插入和删除:在链表的任意位置插入或删除元素的时间复杂度为 O(1)。
- 随机访问:由于需要从头遍历链表以找到指定位置,随机访问的时间复杂度为 O(n),这也是
std::list
的主要局限性。
使用 std::list
的注意事项
-
适用场景:
std::list
非常适合需要频繁插入或删除操作的场景,尤其是在操作涉及到非末尾位置时。如果主要需求是随机访问,则应考虑使用std::vector
或std::deque
。 -
内存开销:由于链表需要存储指向前后节点的指针,因此
std::list
的内存开销比std::vector
要大。此外,每个元素的动态分配也会带来一些额外的性能开销。 -
操作效率:虽然插入和删除效率高,但频繁的指针操作可能导致性能问题,特别是在硬件缓存利用率较低的情况下。
总之,std::list
是一种在特定情况下非常有用的容器,适用于需要高效插入和删除的场景,但在需要快速随机访问的场合应谨慎使用。
测试
// 同理
void test_list(long& value)
{
...
list<string> c; // 创建一个字符串列表
char buf[10]; // 字符串缓冲区
...
string target = get_a_target_string(); // 获取目标字符串
timeStart = clock();
auto pItem = find(c.begin(), c.end(), target); // 在列表中查找目标字符串
cout << "std::find(),milli-seconds : " << (clock()-timeStart) << endl; // 输出查找时间
...
timeStart = clock();
c.sort(); // 对列表进行排序
cout << "c.sort(), milli-seconds : " << (clock()-timeStart) << endl; // 输出排序时间
c.clear(); // 清空
}
注意:
c.sort();
是容器自带的排序函数,如果容器自带肯定是要比全局的排序函数好的list 同样也是用
c.push_back(string(buf));
往里添加元素的
运行结果:
随机数据填充容器:3265ms;直接搜索:16ms;排序:2312ms(所用时间)
深度探索
GCC2.9中
// list class
template <class T, class Alloc = alloc>
class list
{
protected:
typedef __list_node<T> list_node;
public:
typedef list_node* link_type;
typedef __list_iterator<T, T&, T*> iterator; // 迭代器,每一个容器都会 typedef
// 只传一个参数就行了 不理想
protected:
link_type node; // 一个 __list_node<T> 的指针
...
};
// 节点 class
template <class T>
struct __list_node
{
typedef void* void_pointer; // 每次用还要转换类型 不理想
void_pointer prev;
void_pointer next;
T data;
};
除了 array,vector 这样是连续存储的容器,其他容器的 iterator 都是智能指针,其有大量的操作符重载 —— 模拟指针
基本上所有的 iterator 都有下面_5_个 typedef 和一大堆操作符重载
// iterator class
template <class T, class Ref, class Ptr>
struct __list_iterator
{
typedef __list_iterator<T, T&, T*> self;
typedef bidirectional_iterator_tag iterator_category; // (1)双向迭代器
typedef T value_type; // (2)迭代器所指对象的类型
typedef Ptr pointer; // (3)迭代器所指对象的指针类型
typedef Ref reference; // (4)迭代器所指对象的引用类型
typedef __list_node<T>* link_type;
typedef ptrdiff_t difference_type; // (5)两个迭代器之间的距离类型
link_type node; // iterator本体,一个指向__list_node<T>的指针
reference operator*() const { return (*node).data; }
pointer operator->() const { return &(operator*()); }
self& operator++() // ++i
{
node = (link_type)((*node).next); // 移到下一个节点
return *this;
}
self operator++(int) // i++ 为了区分加上了一个参数其实无用
{
self tmp = *this;
++*this;
return tmp;
}
...
};
注意:self operator++(int){...}
的 self tmp = *this;
中,由于先调用了 =
唤起了 copy ctor 用以创建 tmp 并以 *this
为初值,所以不会唤起 operator*
—— *this
已经被解释为 ctor 的参数
下面的 ++*this;
同理
与 int 类似:iterator 可以连续前++,但不能连续后++
所以前++是返回引用,后++返回值
因为要符合前闭后开原则,所以在 list 尾端加上了一个空白节点
GCC4.9中做出了改进:
- 迭代器模板参数从三个 --> 只有一个
- 节点 class 中的前后指针类型从
void*
-->_LIst_node_base*
在GCC4.9中 sizeof(list<int>)
是 8
在GCC2.9中 sizeof(list<int>)
是 4
3.2.4 forward_list
chatGPT介绍
C++ 标准模板库(STL)中的 std::forward_list
是一种单向链表容器。与 std::list
不同,std::forward_list
仅支持单向遍历,因此它的内存占用更小,适用于需要简单链表结构且内存效率至关重要的场景。下面详细介绍 std::forward_list
的特点、使用方法、常用成员函数及其底层机制。
std::forward_list
的特点
-
单向链表:
std::forward_list
是单向链表(Singly Linked List),每个元素只包含一个指向下一个元素的指针,无法向前遍历。 -
低内存开销:由于每个节点只存储一个指向下一个节点的指针,相比
std::list
,std::forward_list
的内存开销更低。 -
不支持逆向遍历:因为是单向链表,
std::forward_list
不支持逆向遍历和双向迭代器。 -
高效插入和删除:与
std::list
类似,std::forward_list
在已知位置进行插入和删除操作非常高效,时间复杂度为 O(1)。 -
不支持随机访问:与
std::list
一样,std::forward_list
不支持通过索引随机访问元素,访问特定元素需要从头遍历,时间复杂度为 O(n)。
std::forward_list
的使用
std::forward_list
的使用相对简单。你可以通过以下方式定义一个 std::forward_list
:
std::forward_list<int> flist; // 创建一个存储 int 类型元素的空链表
std::forward_list<int> flist(5); // 创建一个包含 5 个元素的 int 类型链表,每个元素默认初始化为 0
std::forward_list<int> flist(5, 100); // 创建一个包含 5 个元素的 int 类型链表,每个元素初始化为 100
std::forward_list<int> flist = {1, 2, 3}; // 使用初始化列表创建并初始化一个链表
常用成员函数
-
元素访问
front()
:返回链表中的第一个元素。
-
容量相关
empty()
:判断链表是否为空。max_size()
:返回链表可以容纳的最大元素数量。
-
修改元素
push_front(const T& value)
:在链表的前端插入一个元素。pop_front()
:移除链表前端的元素。insert_after(iterator pos, const T& value)
:在指定位置之后插入一个元素,返回新插入元素的迭代器。erase_after(iterator pos)
:移除指定位置之后的元素,返回下一个元素的迭代器。emplace_after(iterator pos, Args&&... args)
:在指定位置之后原地构造一个元素。splice_after(iterator pos, forward_list& other)
:将other
链表的内容插入到当前链表中的pos
位置之后。remove(const T& value)
:移除所有与value
相等的元素。remove_if(Predicate pred)
:移除所有满足谓词pred
的元素。reverse()
:反转链表中的元素顺序。unique()
:移除链表中连续重复的元素,只保留一个。sort()
:对链表中的元素进行排序。
-
迭代器
begin()
:返回指向链表中第一个元素的迭代器。end()
:返回指向链表末尾(不指向任何元素)的迭代器。before_begin()
:返回指向第一个元素前的迭代器(仅适用于插入操作)。cbegin()
和cend()
:返回常量迭代器,分别指向起始和末尾。cbefore_begin()
:返回常量迭代器,指向第一个元素前的迭代器。
std::forward_list
的底层机制
-
链表节点:
std::forward_list
的每个节点只包含数据和指向下一个节点的指针,因此插入和删除操作只需更新相邻节点的指针,操作时间复杂度为 O(1)。 -
内存管理:由于
std::forward_list
只维护一个指针的开销,内存使用更为紧凑。这使得它在某些需要严格控制内存的场景中比std::list
更具优势。 -
效率:
- 插入和删除:在链表的任意位置插入或删除元素的时间复杂度为 O(1)。
- 随机访问:由于
std::forward_list
只能单向遍历,访问特定元素需要线性时间,时间复杂度为 O(n)。
使用 std::forward_list
的注意事项
-
适用场景:
std::forward_list
适用于内存资源有限或不需要双向遍历的场景。如果需要频繁插入或删除元素,且这些操作大多集中在链表的头部,那么std::forward_list
是一个很好的选择。 -
不适合的场景:当你需要双向遍历或快速随机访问时,不应选择
std::forward_list
。在这些场景下,std::vector
或std::list
更为合适。 -
操作细节:由于
std::forward_list
是单向链表,许多std::list
中的操作在std::forward_list
中需要使用不同的方法,例如使用before_begin()
迭代器来处理插入和删除操作。
总之,std::forward_list
是一种轻量级的链表容器,适合需要高效、低内存开销的单向链表操作的场景。它在某些特定应用中提供了比 std::list
更优的性能和内存效率。
测试
// 同理
void test_forward_list(long& value)
{
...
forward_list<string> c; // 创建一个前向列表
char buf[10]; // 字符串缓冲区
...
string target = get_a_target_string(); // 获取目标字符串
timeStart = clock();
auto pItem = find(c.begin(), c.end(), target); // 在前向列表中查找目标字符串
cout << "std::find(),milli-seconds : " << (clock()-timeStart) << endl; // 输出查找时间
...
timeStart = clock();
c.sort(); // 进行排序
cout << "c.sort(), milli-seconds : " << (clock()-timeStart) << endl; // 输出排序时间
c.clear(); // 清空
}
注意:forward_list 只有
c.push_front();
且没有forward_list.back()
forward_list.size()
运行结果:
随机数据填充容器:3204ms;直接搜索:15ms;排序:2656ms
- 单向链表,只提供头插法(从前面往里放)。尾插效率太低,计算size效率太低,寻找back效率太低,不提供。
- ::find()全局函数,函数模板
深度探索
与 list 相似,略
3.2.6 deque
chatGPT介绍
C++ 标准模板库(STL)中的 std::deque
(双端队列)是一种通用的、动态的序列容器,支持在序列的两端进行快速的插入和删除操作。std::deque
是 “double-ended queue” 的缩写,它提供了类似 std::vector
的功能,同时增加了对两端的高效操作能力。下面详细介绍 std::deque
的特点、使用方法、常用成员函数及其底层机制。
std::deque
的特点
-
双端操作:
std::deque
支持在容器的前端和后端进行 O(1) 时间复杂度的插入和删除操作,这使得它非常适合需要频繁在两端进行操作的场景。 -
随机访问:
std::deque
像std::vector
一样支持常量时间的随机访问,可以通过索引直接访问任意位置的元素。 -
动态大小:
std::deque
能根据需要动态调整大小,插入和删除操作会自动处理内存的分配和释放。 -
连续内存块:
std::deque
的实现通常由多个连续的内存块组成,因此虽然支持随机访问,但其内存布局与std::vector
不同。std::vector
是一个单一的连续内存块,而std::deque
是多个块的组合。 -
两端均衡:与
std::vector
不同,std::deque
在两端的插入和删除操作都非常高效,这使得它在处理需要在两端进行操作的数据时比std::vector
更具优势。
std::deque
的使用
std::deque
的使用方法与 std::vector
类似,可以通过以下方式定义和初始化一个 std::deque
:
std::deque<int> deq; // 创建一个存储 int 类型元素的空双端队列
std::deque<int> deq(5); // 创建一个包含 5 个元素的 int 类型双端队列,每个元素默认初始化为 0
std::deque<int> deq(5, 100); // 创建一个包含 5 个元素的 int 类型双端队列,每个元素初始化为 100
std::deque<int> deq = {1, 2, 3}; // 使用初始化列表创建并初始化一个双端队列
常用成员函数
-
元素访问
at(size_type pos)
:返回指定位置的元素,并进行范围检查。operator[](size_type pos)
:返回指定位置的元素,不进行范围检查。front()
:返回双端队列中的第一个元素。back()
:返回双端队列中的最后一个元素。
-
容量相关
empty()
:判断双端队列是否为空。size()
:返回双端队列中的元素数量。max_size()
:返回双端队列可以容纳的最大元素数量。shrink_to_fit()
:请求减少容量以适应当前大小,可能会减少内存占用。
-
修改元素
push_front(const T& value)
:在双端队列的前端插入一个元素。push_back(const T& value)
:在双端队列的末尾插入一个元素。pop_front()
:移除双端队列前端的元素。pop_back()
:移除双端队列末尾的元素。insert(iterator pos, const T& value)
:在指定位置前插入一个元素。erase(iterator pos)
:移除指定位置的元素。clear()
:移除双端队列中的所有元素。resize(size_type count)
:调整双端队列的大小。如果新大小大于当前大小,使用默认值填充新的元素。emplace_front(Args&&... args)
:在双端队列的前端原地构造一个元素。emplace_back(Args&&... args)
:在双端队列的末尾原地构造一个元素。
-
迭代器
begin()
:返回指向双端队列中第一个元素的迭代器。end()
:返回指向双端队列末尾(不指向任何元素)的迭代器。rbegin()
和rend()
:返回反向迭代器,分别指向末尾和起始位置。
std::deque
的底层机制
-
分段内存布局:
std::deque
的实现通常由多个内存块(segments)组成,这些块被组织成一个数组。这样可以在两端进行高效的插入和删除操作,而不需要像std::vector
那样频繁地重新分配和移动内存。 -
两端操作的高效性:
std::deque
在内存布局上预留了前后两端的空间,插入和删除操作可以直接在这些预留空间中进行,不需要移动其他元素,因此非常高效。 -
效率与局限性:
- 插入和删除:
std::deque
在两端的插入和删除操作都为 O(1) 时间复杂度,而在中间位置插入和删除的时间复杂度为 O(n)。 - 随机访问:由于
std::deque
的分段内存布局,虽然支持常量时间的随机访问,但访问操作可能比std::vector
略慢,特别是在处理大数据集时。
- 插入和删除:
使用 std::deque
的注意事项
-
适用场景:
std::deque
非常适合需要在两端进行频繁插入和删除的场景,例如实现队列或双端队列(deque)结构。它在同时需要快速的插入/删除操作和随机访问的场景中也表现优异。 -
不适合的场景:如果你的应用程序主要依赖中间位置的插入和删除操作,或者你只需要在一端进行操作,
std::deque
可能不是最佳选择。此时,std::vector
或std::list
可能更适合。 -
内存管理:由于
std::deque
使用分段内存布局,其内存使用效率通常比std::vector
更好,特别是在处理大量数据时。此外,std::deque
不会像std::vector
那样频繁地重新分配整个数组,因此在某些情况下内存碎片可能更少。
总结
std::deque
是一个功能强大的容器,提供了在两端进行高效插入和删除的能力,同时保留了随机访问的特性。它在某些特定应用场景中表现非常出色,例如需要实现队列或双端队列的场景。如果你的应用程序需要在两端频繁操作数据,同时还需要支持快速的随机访问,那么 std::deque
是一个非常合适的选择。
-
类似vector,两边都能扩充,逻辑上是连续的,但是物理上是分段连续的。有一个map来存放这些段。当一个buffer使用完的之后,会有一个新的buffer,这个buffer的地址放在map中。这个buffer一次分配多大呢?这会影响到这个容器的效率~
-
deque还需要指定buffer size,也就是每一个buffer容纳的元素个数,默认是0,就会做相应的操作来存放默认数量的元素,但是肯定不会是让一个buffer存放0个元素的。迭代器类型用的是随机存取(也就是连续的)是deque类做的伪装。迭代器做了模拟连续空间的操作!
测试
类似vector,两边都能扩充,实际上是分段连续的
其是通过 map(是一个vector,但在扩充时会 copy 到中间)里的指针指向各个 buffer,buffer 里再存数据,每个 buffer 的大小一致,每次扩充都是扩充一个指针指向一个新的 buffer
void test_deque(long& value)
{
...
deque<string> c; // 创建一个双端队列
char buf[10]; // 字符串缓冲区
...
string target = get_a_target_string(); // 获取目标字符串
timeStart = clock();
auto pItem = find(c.begin(), c.end(), target); // 在队列中查找目标字符串
cout << "std::find(),milli-seconds : " << (clock()-timeStart) << endl; // 输出查找时间
...
timeStart = clock();
sort(c.begin(), c.end()); // 对队列进行排序
cout << "sort(),milli-seconds : " << (clock()-timeStart) << endl; // 输出排序时间
c.clear(); // 清空队列
}
运行结果:
随机数据填充容器:2704ms;直接搜索:15ms;排序:3110ms
下面的 stack 和 queue 内部都是一个 deque,所以技术上这两个可以看作容器适配器 Container Adapter
深度探索
GCC2.9下
template <class T, class Alloc = alloc, size_t BufSiz = 0>
class deque
{
public:
typedef T value_type;
typedef __deque_iterator<T, T&, T*, BufSiz> iterator;
typedef size_t size_type;
typedef T* pointer;
protected:
typedef pointer* map_pointer; // T** 指向指针的指针
protected:
iterator start;
iterator finish;
map_pointer map;
size_type map_size;
// 两个迭代器:16*2,一个指针:4,一个size_t:4,一共40字节
public:
iterator begin() { return start; }
iterator end() { return finish; }
size_type size() const { return finish - start; }
...
};
注意:第三个模板参数
size_t BufSiz = 0
有一个函数:如果不为0,则 buffer size 就是传入的数据
如果为0,表示预设值,那么
如果
sz = sizeof(value_type)
< 512,传回512/sz
如果sz = sizeof(value_type)
>= 512,传回1
迭代器四个指针,cur
指向当前元素,first
指向当前 buffer 的第一个元素,last
指向当前 buffer 的最后一个元素的下一个,node
指向当前 buffer 在 map(控制中心)的指针
// deque迭代器
template <class T, class Ref, class Ptr, size_t BufSiz>
struct __deque_iterator
{
typedef random_access_iterator_tag iterator_category; // (1)
typedef T value_type; // (2)
typedef Ptr pointer; // (3)
typedef Ref reference; // (4)
typedef size_t size_type;
typedef ptrdiff_t difference_type; // (5)
typedef T** map_pointer;
typedef __deque_iterator self;
T* cur;
T* first;
T* last;
map_pointer node; // 指向指针的指针
// 四个指针,一共16字节
...
};
deque 中的 insert 函数:
iterator insert(iterator position, const T& x)
{
if (position.cur == start.cur) // 插入点在deque最前端
{ // 交给push_front
push_front(x);
return start;
}
else if (position.cur == finish.cur) // 插入点在deque最尾端
{ // 交给push_front
push_back(x);
iterator tmp = finish;
--tmp;
return tmp;
}
else // 在中间插入
{
return insert_aux(position, x);
}
}
iterator insert_aux(iterator pos, const T& x)
{
difference_type index = pos - start; // 安插点前元素个数
value_type x_copy = x;
if (index < size() / 2) // 安插点前的元素少————搬前面的
{
push_front(front());
...
copy(front2, pos1, front1); // 搬元素
}
else // 安插点后的元素少————搬后面的
{
push_back(back());
...
copy_backward(pos, back2, back1);
}
*pos = x_copy; // 安插点设新值
return pos;
}
deque 模拟连续空间(deque iterator 的功能):
-
-
:两个位置之间的距离——前闭后开的元素个数两个位置之间的距离 = buffer_size * 两个位置之间 buffer 的数量 + 末尾位置到 buffer 前端的长度 + 起始位置到 buffer 末尾的长度
-
++
/--
:注:下面带参数的是后++(i++) -
+=
/+
:self& operator+=(difference_type n) { difference_type offset = n + (cur - first); if (offset >= 0 && offset < difference_type(buffer_size())) // 若+了之后在缓冲区大小范围内 cur += n; // 直接移动迭代器 n 步 else { difference_type node_offset = offset > 0 ? offset / difference_type(buffer_size()) : -difference_type((-offset - 1) / buffer_size()) - 1; // 计算偏移的节点数,offset > 0判断是为了之后的-=/- // 这里(-offset - 1)后除buffer_size()再-1是为了offset==buffer_size()的情况 set_node(node + node_offset); // 调整节点,使迭代器指向正确的节点 cur = first + (offset - node_offset * difference_type(buffer_size())); // 调整迭代器位置 } return *this; } self operator+(difference_type n) const { self tmp = *this; // 复制当前迭代器 return tmp += n; // 返回向前移动 n 步后的迭代器副本 }
-
-=
/-
:// -就等于+负的 self& operator-=(difference_type n) { return *this += -n; } self operator-(difference_type n) const { self tmp = *this; return tmp -= n; }
-
[]
:reference operator[](difference_type n) const { return *(*this + n); }
GCC4.9下:其实没必要这样
G2.91 允许指派 buffer_size
G4.53 不允许了
3.2.7 stack queque
chatGPT介绍
1.stack
C++ 标准模板库(STL)中的 std::stack
是一个容器适配器,用于实现后进先出(LIFO)的数据结构。std::stack
使得你可以使用一个底层容器(如 std::deque
、std::vector
或 std::list
)来实现栈的功能,提供了栈的基本操作接口,如推入、弹出、访问栈顶元素等。
std::stack
的特点
-
后进先出(LIFO):
std::stack
遵循后进先出的原则,即最后插入的元素最先被访问或移除。 -
底层容器:
std::stack
是一个容器适配器,它内部使用一个底层容器(通常是std::deque
,但也可以是std::vector
或std::list
)来存储数据。默认情况下,std::deque
被用作底层容器。 -
接口简单:
std::stack
提供了一些基本的栈操作,如推入元素、弹出元素和访问栈顶元素,但不提供随机访问或迭代器。 -
不支持迭代器:由于栈的特性,
std::stack
不支持迭代器,因此无法遍历栈中的元素。
std::stack
的使用
创建和使用 std::stack
非常简单,下面是一些常见的用法示例:
#include <iostream>
#include <stack>
#include <deque> // 可以指定底层容器
int main() {
// 使用 std::deque 作为底层容器(默认)
std::stack<int> s;
// 推入元素
s.push(1);
s.push(2);
s.push(3);
// 访问栈顶元素
std::cout << "Top element: " << s.top() << std::endl; // 输出 3
// 弹出元素
s.pop();
std::cout << "New top element after pop: " << s.top() << std::endl; // 输出 2
// 判断栈是否为空
if (s.empty()) {
std::cout << "Stack is empty." << std::endl;
} else {
std::cout << "Stack size: " << s.size() << std::endl; // 输出 2
}
return 0;
}
常用成员函数
-
元素操作
push(const T& value)
:将元素value
压入栈顶。pop()
:移除栈顶的元素。注意,pop()
不会返回被移除的元素,只有栈顶元素被删除。top()
:返回栈顶的元素,但不移除它。
-
容量相关
empty()
:检查栈是否为空。返回true
如果栈为空,false
否则。size()
:返回栈中元素的数量。
-
底层容器操作
c
:底层容器的访问接口(一般情况下不建议直接使用)。c
是一个 public 成员,表示底层容器。
std::stack
的底层机制
-
底层容器:
std::stack
的底层容器可以是std::deque
(默认)、std::vector
或std::list
。可以通过模板参数指定底层容器,例如:std::stack<int, std::vector<int>> s;
这将使用
std::vector
作为底层容器。 -
操作效率:
- 推入和弹出:这些操作的时间复杂度是 O(1),因为它们仅涉及到底层容器的前端操作。
- 访问栈顶:
top()
操作的时间复杂度也是 O(1),因为它只需访问底层容器的最后一个元素。
-
不支持迭代器:由于栈的特性,它不支持迭代器。因此,无法直接遍历栈中的元素。只能通过
top()
和pop()
操作来访问和修改栈中的元素。
使用 std::stack
的注意事项
-
适用场景:
std::stack
适用于需要后进先出行为的场景,如处理函数调用、递归操作、表达式求值等。它提供了简单而直观的栈操作。 -
底层容器选择:选择合适的底层容器可以影响栈的性能。例如,
std::deque
适合于频繁的插入和删除操作,而std::vector
可能在底层实现上更高效,但在栈的两端插入和删除的性能较差。 -
没有迭代器:由于栈的设计原则,它不支持迭代器。如果你需要遍历元素,考虑使用其他 STL 容器,如
std::vector
或std::list
。 -
内存管理:
std::stack
使用的底层容器会自动管理内存,因此你无需担心内存分配和释放问题。
总结
std::stack
是一个简单而强大的容器适配器,提供了后进先出的数据结构。它基于底层容器实现,支持高效的栈操作,如推入、弹出和访问栈顶元素。尽管它不支持迭代器和随机访问,但在处理需要后进先出特性的应用场景时,它是一个非常有用的工具。选择合适的底层容器和理解其操作特性可以帮助你更好地利用 std::stack
。
2.queue
C++ 标准模板库(STL)中的 std::queue
是一个容器适配器,提供了先进先出(FIFO)的数据结构。std::queue
是 “queue” 的缩写,它实现了基本的队列操作,允许在队列的一端插入元素,在另一端删除元素。这个适配器通常使用 std::deque
作为底层容器,但也可以使用其他容器(如 std::list
)来实现队列。
std::queue
的特点
-
先进先出(FIFO):
std::queue
遵循先进先出的原则,即最先插入的元素最早被访问或移除。 -
底层容器:
std::queue
是一个容器适配器,默认使用std::deque
作为底层容器,也可以指定其他容器(如std::list
)来实现队列。 -
简单接口:
std::queue
提供了基本的队列操作,如入队、出队和访问队列的前端和后端元素,但不提供随机访问或迭代器功能。 -
不支持迭代器:由于队列的特性,
std::queue
不支持迭代器,因此无法遍历队列中的元素。
std::queue
的使用
创建和使用 std::queue
是非常简单的。下面是一些常见的用法示例:
#include <iostream>
#include <queue>
#include <deque> // 可以指定底层容器
int main() {
// 使用 std::deque 作为底层容器(默认)
std::queue<int> q;
// 入队操作
q.push(1);
q.push(2);
q.push(3);
// 访问队列前端元素
std::cout << "Front element: " << q.front() << std::endl; // 输出 1
// 出队操作
q.pop();
std::cout << "New front element after pop: " << q.front() << std::endl; // 输出 2
// 判断队列是否为空
if (q.empty()) {
std::cout << "Queue is empty." << std::endl;
} else {
std::cout << "Queue size: " << q.size() << std::endl; // 输出 2
}
return 0;
}
常用成员函数
-
元素操作
push(const T& value)
:将元素value
添加到队列的末尾。pop()
:移除队列前端的元素。注意,pop()
不会返回被移除的元素。front()
:返回队列前端的元素,但不移除它。back()
:返回队列末尾的元素,但不移除它。
-
容量相关
empty()
:检查队列是否为空。返回true
如果队列为空,false
否则。size()
:返回队列中元素的数量。
-
底层容器操作
c
:底层容器的访问接口(一般情况下不建议直接使用)。c
是一个 public 成员,表示底层容器。
std::queue
的底层机制
-
底层容器:
std::queue
默认使用std::deque
作为底层容器,但也可以使用std::list
或其他容器。底层容器的选择会影响队列的性能和特性。例如:std::queue<int, std::list<int>> q; // 使用 std::list 作为底层容器
-
操作效率:
- 入队和出队:这些操作的时间复杂度是 O(1),因为它们仅涉及到底层容器的前端和末尾操作。
- 访问队列前端和末尾:
front()
和back()
操作的时间复杂度也是 O(1)。
-
不支持迭代器:由于队列的特性,它不支持迭代器。因此,无法直接遍历队列中的元素。只能通过
front()
和pop()
操作来访问和修改队列中的元素。
使用 std::queue
的注意事项
-
适用场景:
std::queue
适用于需要先进先出(FIFO)行为的场景,如任务调度、缓冲区管理、广度优先搜索等。它提供了简单而直观的队列操作。 -
底层容器选择:选择合适的底层容器可以影响队列的性能。例如,
std::deque
适合于频繁的插入和删除操作,而std::list
也适合于频繁的插入和删除,但可能会增加一些内存开销。 -
没有迭代器:由于队列的设计原则,它不支持迭代器。如果你需要遍历元素,考虑使用其他 STL 容器,如
std::vector
或std::list
。 -
内存管理:
std::queue
使用的底层容器会自动管理内存,因此你无需担心内存分配和释放问题。
总结
std::queue
是一个简单而强大的容器适配器,提供了先进先出的数据结构。它基于底层容器实现,支持高效的入队、出队和访问队列前端及末尾元素。虽然不支持迭代器和随机访问,但在需要先进先出特性的应用场景中,它是一个非常有用的工具。选择合适的底层容器和理解其操作特性可以帮助你更好地利用 std::queue
。
测试
stack:
queue:
stack,queue 是通过
push()
和pop()
来放取元素的,且无_iterator_ 的操作
深度探索
stack 和 queue 内部默认用 deque 来实现,所以有时候不会将这两个认为容器而是一个适配器
-
底层函数可以使用 list 和 deque(deque默认更快)
-
queue 不能用 vector,stack 可以用 vector
-
set,map 都不能用
用时编译器可以通过的,但在具体使用函数时,若遇到底层容器没有这个函数时,就会报错
// queue
template<class T, class Sequence = deque<T>>
class queue
{
...
protected:
Sequence c; // 底层容器
public:
// 都是通过底层容器来实现
bool empty() const { return c.empty(); }
size_type size() const { return c.size(); }
reference front() { return c.front(); }
const_reference front() const { return c.front(); }
reference back() { return c.back(); }
const_reference back() const { return c.back(); }
void push(const value_type& x) { c.push_back(x); }
void pop() { c.pop_front(); }
};
// stack
template<class T, class Sequence = deque<T>>
class stack
{
...
protected:
Sequence c; // 底层容器
public:
// 都是通过底层容器来实现
bool empty() const { return c.empty(); }
size_type size() const { return c.size(); }
reference top() { return c.back(); }
const_reference top() const { return c.back(); }
void push(const value_type& x) { c.push_back(x); }
void pop() { c.pop_back(); }
};
stack,queue 都不允许遍历,也不提供 iterator
3.3 关联式容器
3.3.0 RB-Tree
chatGPT介绍
在 C++ 标准模板库(STL)中,红黑树(RB-Tree)是一种自平衡的二叉搜索树(Binary Search Tree,BST),用于实现有序的关联容器,如 std::map
和 std::set
。红黑树确保了树的高度保持对数级别,从而提供了高效的插入、删除和查找操作。下面详细介绍红黑树的特点、性质、操作以及在 STL 中的应用。
红黑树的特点
-
自平衡:红黑树是一种自平衡的二叉搜索树,它通过对树的节点进行着色和重排,确保树的高度在对数级别,从而保证了基本操作的时间复杂度为 O(log n)。
-
节点颜色:每个节点都有一个颜色属性(红色或黑色),这些颜色属性帮助维持树的平衡。
-
属性和约束:
- 节点着色:每个节点要么是红色,要么是黑色。
- 根节点:根节点是黑色。
- 红色节点的子节点:红色节点的两个子节点必须是黑色(即红色节点不能有红色子节点)。
- 黑色节点路径:从任何节点到其每个叶子节点的路径上,必须包含相同数量的黑色节点(称为黑高)。
- 空节点(Nil 节点):所有空节点(叶子节点的子节点)被视为黑色,并且没有实际存储数据。
红黑树的操作
-
插入操作:插入新节点时,首先将节点插入到适当的位置,然后调整树的结构和节点的颜色以保持红黑树的性质。这包括:
- 将新节点着色为红色。
- 根据父节点和叔叔节点的颜色进行调整(可能需要旋转和重新着色)。
- 如果需要,进行旋转(左旋或右旋)以维持树的平衡。
-
删除操作:删除节点时,可能需要处理几个复杂情况,以确保树的性质保持不变。这包括:
- 删除节点并替换它。
- 调整树的结构和颜色,可能需要进行旋转和重新着色,以保持平衡。
-
查找操作:查找操作类似于二叉搜索树的查找操作,时间复杂度为 O(log n)。
-
旋转操作:
- 左旋(Left Rotation):将当前节点的右子树旋转到当前节点的位置,使当前节点成为左子树的子节点。
- 右旋(Right Rotation):将当前节点的左子树旋转到当前节点的位置,使当前节点成为右子树的子节点。
红黑树的 STL 应用
在 C++ STL 中,std::map
和 std::set
是基于红黑树实现的容器。它们利用红黑树的自平衡性质来提供高效的查找、插入和删除操作。
-
std::set
:一个有序的集合容器,所有元素都是唯一的。底层使用红黑树存储元素,元素按照升序排列。#include <set> #include <iostream> int main() { std::set<int> s; s.insert(3); s.insert(1); s.insert(2); for (int x : s) { std::cout << x << " "; } // 输出:1 2 3 return 0; }
-
std::map
:一个有序的关联容器,存储键值对,每个键是唯一的。底层使用红黑树,按键的升序排列。#include <map> #include <iostream> int main() { std::map<int, std::string> m; m[1] = "one"; m[2] = "two"; m[3] = "three"; for (const auto& pair : m) { std::cout << pair.first << " => " << pair.second << std::endl; } // 输出: // 1 => one // 2 => two // 3 => three return 0; }
红黑树的复杂度
- 查找操作:O(log n),因为树的高度是对数级别的。
- 插入操作:O(log n),由于需要调整树的平衡。
- 删除操作:O(log n),因为删除操作可能需要调整树的平衡。
- 迭代操作:由于树的有序性,迭代操作通常是 O(n),需要遍历所有节点。
总结
红黑树是一种重要的自平衡二叉搜索树,用于 STL 中的 std::map
和 std::set
等容器。它通过节点着色和旋转操作来维持树的平衡,确保了高效的查找、插入和删除操作。红黑树的特性和操作使其在许多需要有序集合的应用场景中表现优异。
红黑树(Red-Black Tree)是一种自平衡的二叉搜索树 BST(AVL 是另一种)
rb-tree 提供遍历操作和 iterators,按_中序遍历_遍历,便可以得到排序状态
不能用 iterator 去改变元素的 key(其有严谨的排列规则)
rb-tree 提供两种 insertion 操作:
insert_unique()
和insert_equal()
,前者表示 key 独一无二,后者表示 key 可重复
GCC2.9下:
template<class Key, // key的类型
class Value, // Value里包含key和date
class KeyOfValue, // 从Value中取出key的仿函数
class Compare, // 比较key大小的仿函数
class Alloc = alloc>
class rb_tree
{
protected:
typedef __rb_tree_node<Value> rb_tree_node;
...
public:
typedef rb_tree_node* link_type;
...
protected:
size_type node_count; // rb-tree节点数量,大小4
link_type header; // 头指针,大小4
Compare Key_compare; // key比大小的仿函数,大小1
// sizeof: 9 ——> 12(填充到4的倍数)
...
};
GCC4.9下:
_M_color 是 “枚举”(Enumeration)
3.3.1 set / multiset
chatGPT介绍
C++ 标准模板库(STL)中的 std::set
和 std::multiset
都是基于红黑树实现的容器,提供了有序集合的功能。虽然它们有许多相似之处,但也有一些重要的区别。下面详细介绍这两个容器的特点、操作、使用方法以及它们之间的主要区别。
std::set
特点
-
有序集合:
std::set
是一个有序集合容器,元素按照升序(默认)排列。你可以自定义排序规则。 -
唯一元素:
std::set
中的所有元素都是唯一的。如果尝试插入一个已经存在的元素,插入操作会失败。 -
基于红黑树:底层使用红黑树实现,因此提供了对数级别的查找、插入和删除操作的时间复杂度。
-
不支持重复元素:如果插入的元素已经存在,则
std::set
不会插入新的元素。 -
不支持直接访问:
std::set
不支持随机访问,无法通过索引访问元素,只能使用迭代器。
常用操作
-
插入元素:
insert
方法用于插入新元素。如果元素已经存在,插入操作将不会修改容器。std::set<int> s; s.insert(1); s.insert(2); s.insert(2); // 插入失败,因为 2 已经存在
-
查找元素:
find
方法用于查找元素,返回指向该元素的迭代器,如果元素不存在则返回end()
。auto it = s.find(2); if (it != s.end()) { std::cout << "Found: " << *it << std::endl; }
-
删除元素:
erase
方法用于删除指定元素或范围的元素。s.erase(2); // 删除元素 2
-
访问元素:使用迭代器访问元素。
for (auto it = s.begin(); it != s.end(); ++it) { std::cout << *it << " "; }
-
检查容器状态:使用
empty()
和size()
来检查容器是否为空以及容器的大小。if (s.empty()) { std::cout << "Set is empty." << std::endl; } std::cout << "Size: " << s.size() << std::endl;
std::multiset
特点
-
有序集合:
std::multiset
也是一个有序集合容器,元素按照升序(默认)排列,支持自定义排序规则。 -
允许重复元素:
std::multiset
允许容器中包含重复的元素。多个相同的元素可以被插入到容器中。 -
基于红黑树:与
std::set
相同,std::multiset
底层使用红黑树实现。 -
元素访问:
std::multiset
同样不支持随机访问,但可以通过迭代器进行遍历。
常用操作
-
插入元素:
insert
方法用于插入新元素,允许重复元素。std::multiset<int> ms; ms.insert(1); ms.insert(2); ms.insert(2); // 允许插入重复元素
-
查找元素:
find
方法用于查找一个元素,返回指向该元素的迭代器。如果有多个相同的元素,find
只返回第一个找到的元素。auto it = ms.find(2); if (it != ms.end()) { std::cout << "Found: " << *it << std::endl; }
-
删除元素:
erase
方法用于删除指定元素。删除操作只会删除某一个元素的实例。ms.erase(2); // 删除所有值为 2 的元素
-
访问元素:使用迭代器访问所有元素。
for (auto it = ms.begin(); it != ms.end(); ++it) { std::cout << *it << " "; }
-
检查容器状态:使用
empty()
和size()
来检查容器状态。if (ms.empty()) { std::cout << "Multiset is empty." << std::endl; } std::cout << "Size: " << ms.size() << std::endl;
set和multiset的主要区别
-
元素唯一性:
std::set
:不允许重复元素,所有元素都是唯一的。std::multiset
:允许重复元素,多个相同的元素可以存在于容器中。
-
插入操作:
std::set
:插入操作失败时不会插入新元素(如果元素已经存在)。std::multiset
:即使元素已存在,仍然可以插入多个相同的元素。
-
删除操作:
std::set
:删除操作会删除指定的唯一元素。std::multiset
:删除操作会删除指定值的所有实例,或者可以通过迭代器删除单个实例。
总结
std::set
和std::multiset
都是基于红黑树实现的有序容器,提供了高效的元素查找、插入和删除操作。std::set
不允许重复元素,而std::multiset
允许重复元素。- 这两个容器提供了有序的存储和自动排序,支持高效的操作和遍历,但不支持随机访问或直接通过索引访问元素。
选择 std::set
还是 std::multiset
主要取决于是否需要处理重复元素。如果需要一个不允许重复元素的集合,使用 std::set
;如果允许重复元素,则使用 std::multiset
。
测试
void test_multiset(long& value)
{
cout << "\ntest_multiset().......... \n";
multiset<string> c; // 创建一个multiset
char buf[10];
clock_t timeStart = clock(); // 记录起始时间
for(long i=0; i< value; ++i) // 添加元素到multiset中
{
try {
snprintf(buf, 10, "%d", rand()); // 将随机数转换为字符串格式
c.insert(string(buf)); // 将字符串插入multiset中
}
catch(exception& p) { // 捕获可能的异常
cout << "i=" << i << " " << p.what() << endl; // 输出异常信息
abort(); // 终止程序
}
}
cout << "毫秒数 : " << (clock()-timeStart) << endl; // 输出时间差,计算插入时间
cout << "multiset.size()= " << c.size() << endl; // 输出multiset大小
cout << "multiset.max_size()= " << c.max_size() << endl; // 输出multiset的最大容量
string target = get_a_target_string();
{
timeStart = clock();
auto pItem = find(c.begin(), c.end(), target); // 在multiset中使用 std::find(...) 查找目标字符串
cout << "std::find(),毫秒数 : " << (clock()-timeStart) << endl;
...
}
{
timeStart = clock();
auto pItem = c.find(target); // 在multiset中使用 c.find(...) 查找目标字符串
cout << "c.find(),毫秒数 : " << (clock()-timeStart) << endl;
...
}
c.clear(); // 清空multiset
}
安插元素是使用
insert()
,其位置由红黑树决定
容器自己有
c.find()
,其会比全局的::find()
快
运行结果:
随机数据填充容器:6609ms(其在填充的时候就进行排序了);直接搜索 ::find()
:203ms;c.find()
:0ms
深度探索
以 rb-tree 为底层结构,因此有——元素自动排序,key 与 value 和一
set / multiset 提供遍历操作和 iterators,按_中序遍历_遍历,便可以得到排序状态
禁止用 iterator 去改变元素的值(其有严谨的排列规则)
set的key 独一无二,其
insert()
操作用的 rb-tree 的:insert_unique()
multiset 的 key 可以重复,其
insert()
操作用的 rb-tree 的:insert_equal()
GCC2.9下:
// set
template <class Key, class Compare = less<Key>, class Alloc = alloc>
class set
{
public:
typedef Key key_type;
typedef Key value_type;
typedef Compare key_compare;
typedef Compare value_compare;
private:
typedef rb_tree<key_type, value_type, identity<value_type>,
key_compare, Alloc> rep_type;
rep_type t; // 采用红黑树作为底层机制
public:
typedef typename rep_type::const_iterator iterator;
// 注意:这里是const_iterator,所以不能用iterator改元素
...
};
3.3.2 map / multimap
chatGPT介绍
在 C++ 标准模板库(STL)中,std::map
和 std::multimap
是两种基于红黑树实现的关联容器。它们都用于存储键值对(key-value pairs),但在处理键的唯一性和重复性方面有所不同。下面详细介绍这两种容器的特点、操作、使用方法以及它们之间的主要区别。
std::map
特点
-
有序的键值对:
std::map
是一个有序的关联容器,存储的键值对按照键的升序(默认)排列。可以自定义排序规则。 -
唯一键:
std::map
中的所有键都是唯一的。如果尝试插入一个已经存在的键,插入操作会失败,容器中的键值对不会被覆盖。 -
基于红黑树:底层使用红黑树实现,因此提供了对数级别的查找、插入和删除操作的时间复杂度。
-
不支持重复键:
std::map
不允许有重复的键。每个键只能对应一个值。 -
不支持直接访问:
std::map
不支持随机访问,无法通过索引访问元素,只能使用迭代器。
常用操作
-
插入键值对:
insert
方法用于插入新键值对。如果键已经存在,插入操作将不会修改容器。std::map<int, std::string> m; m.insert({1, "one"}); m.insert({2, "two"}); m.insert({2, "deux"}); // 插入失败,因为键 2 已经存在
-
查找键:
find
方法用于查找一个键,返回指向该键值对的迭代器。如果键不存在,则返回end()
。auto it = m.find(2); if (it != m.end()) { std::cout << "Found: " << it->second << std::endl; // 输出 "two" }
-
删除键值对:
erase
方法用于删除指定键的键值对。m.erase(2); // 删除键 2 对应的键值对
-
访问键值对:可以通过迭代器访问所有键值对。
for (const auto& pair : m) { std::cout << pair.first << " => " << pair.second << std::endl; }
-
检查容器状态:使用
empty()
和size()
来检查容器状态。if (m.empty()) { std::cout << "Map is empty." << std::endl; } std::cout << "Size: " << m.size() << std::endl;
std::multimap
特点
-
有序的键值对:
std::multimap
是一个有序的关联容器,存储的键值对按照键的升序(默认)排列,支持自定义排序规则。 -
允许重复键:
std::multimap
允许多个键值对具有相同的键。这意味着同一个键可以对应多个值。 -
基于红黑树:与
std::map
相同,std::multimap
底层使用红黑树实现。 -
支持重复键:
std::multimap
允许键重复,因此可以插入多个具有相同键的键值对。 -
不支持直接访问:
std::multimap
不支持随机访问,但可以使用迭代器进行遍历。
常用操作
-
插入键值对:
insert
方法用于插入新键值对,可以插入具有相同键的多个值。std::multimap<int, std::string> mm; mm.insert({1, "one"}); mm.insert({2, "two"}); mm.insert({2, "deux"}); // 允许插入重复的键
-
查找键:
find
方法用于查找一个键,返回指向第一个具有该键的键值对的迭代器。如果键不存在,则返回end()
。auto it = mm.find(2); if (it != mm.end()) { std::cout << "Found: " << it->second << std::endl; // 输出 "two"(第一个匹配的值) }
-
删除键值对:
erase
方法用于删除指定键的所有键值对,或者通过迭代器删除单个键值对。mm.erase(2); // 删除所有键为 2 的键值对
-
访问键值对:可以通过迭代器访问所有键值对。
for (const auto& pair : mm) { std::cout << pair.first << " => " << pair.second << std::endl; }
-
检查容器状态:使用
empty()
和size()
来检查容器状态。if (mm.empty()) { std::cout << "Multimap is empty." << std::endl; } std::cout << "Size: " << mm.size() << std::endl;
map
和 multimap
的主要区别
-
键的唯一性:
std::map
:不允许重复键,每个键只能对应一个值。std::multimap
:允许重复键,多个相同的键可以对应不同的值。
-
插入操作:
std::map
:插入操作失败时不会插入新键值对(如果键已经存在)。std::multimap
:允许插入具有相同键的多个键值对。
-
删除操作:
std::map
:删除操作会删除指定的唯一键及其对应的值。std::multimap
:删除操作会删除指定键的所有键值对,或者通过迭代器删除单个键值对。
-
查找操作:
std::map
:find
方法查找特定的键,返回第一个匹配的键值对。std::multimap
:find
方法查找特定的键,返回第一个匹配的键值对,但可以有多个匹配项。可以使用equal_range
方法查找所有具有相同键的键值对。
总结
std::map
和std::multimap
都是基于红黑树实现的有序关联容器,用于存储键值对。std::map
不允许重复的键,而std::multimap
允许键重复。- 这两种容器都提供了高效的查找、插入和删除操作,并按键排序,但不支持随机访问。
- 选择使用
std::map
还是std::multimap
取决于是否需要处理重复的键值对。如果需要唯一键,则使用std::map
;如果需要处理多个具有相同键的值,则使用std::multimap
。
测试
void test_multimap(long& value)
{
...
multimap<long, string> c; // 创建一个multimap,key 为 long 类型,value 为 string 类型
char buf[10];
clock_t timeStart = clock(); // 记录起始时间
for(long i=0; i< value; ++i) // 添加元素到multimap中
{
try {
snprintf(buf, 10, "%d", rand()); // 将随机数转换为字符串格式并复制到缓冲区
// multimap 不可使用 [] 做 insertion
c.insert(pair<long, string>(i, buf)); // 将元素插入multimap中
}
catch(exception& p) { // 捕获可能的异常
cout << "i=" << i << " " << p.what() << endl; // 输出异常信息
abort(); // 终止程序
}
}
cout << "毫秒数 : " << (clock()-timeStart) << endl; // 输出时间差,计算插入时间
cout << "multimap.size()= " << c.size() << endl; // 输出multimap大小
cout << "multimap.max_size()= " << c.max_size() << endl; // 输出multimap的最大容量
long target = get_a_target_long();
timeStart = clock();
auto pItem = c.find(target); // 在multimap中查找目标 key
cout << "c.find(),毫秒数 : " << (clock()-timeStart) << endl;
if (pItem != c.end())
cout << "找到,value=" << (*pItem).second << endl; // 如果找到,输出找到的值
else
cout << "未找到!" << endl; // 如果未找到,输出未找到的信息
c.clear(); // 清空multimap
}
c.insert(pair<long, string>(i, buf));
中 key 是从1~1000000,value 是随机取的,将其组合为 pair 插入
运行结果:
随机数据填充容器:4812ms(其在填充的时候就进行排序了);c.find()
:0ms
深度探索
以 rb-tree 为底层结构,因此有——元素自动排序
map/ multimap 提供遍历操作和 iterators,按_中序遍历_遍历,便可以得到排序状态
不能用 iterator 去改变元素的key(其有严谨的排列规则),但可以用 iterator 去改变元素的 data
因此 map / multimap 将 user 指定的 key_type 设定成
const
map的key 独一无二,其
insert()
操作用的 rb-tree 的:insert_unique()
multimap 的 key 可以重复,其
insert()
操作用的 rb-tree 的:insert_equal()
GCC2.9下:
template <class Key, // key的类型
class T, // data的类型
class Compare = less<Key>,
class Alloc = alloc>
class map
{
public:
typedef Key key_type;
typedef T data_type;
typedef T mapped_type;
typedef pair<const Key, T> value_type;
// 注意:这里是const Key ———— 防止改key
typedef Compare key_compare;
private:
typedef rb_tree<key_type, value_type, select1st<value_type>, key_compare, Alloc> rep_type;
rep_type t; // 采用红黑树作为底层机制
public:
typedef typename rep_type::iterator iterator;
...
};
map 的插入元素有特殊写法:
c[i] = string(buf)
,其中i
就是 key;multimap没有map 的
[]
功能:访问元素: 如果指定的键存在于映射中,
map[key]
将返回与该键关联的 data;如果键不存在,map[key]
将自动创建一个新的键值对,key 为指定的 key,data 为默认 data,并返回这个默认 data
3.3.3 HashTable
chatGPT介绍
在 C++ 标准库中,哈希表(Hash Table)是一种用于实现高效查找、插入和删除操作的数据结构。虽然 C++ 标准库没有一个名为 HashTable
的直接容器,但哈希表的概念在 std::unordered_map
和 std::unordered_set
中得到了实现。下面详细介绍哈希表的工作原理、其在 STL 中的实现以及一些相关操作。
哈希表的基本概念
1. 哈希函数(Hash Function)
哈希函数是将键(key)映射到哈希表中的桶(bucket)位置的函数。一个好的哈希函数可以将键均匀地分布在哈希表中,以减少冲突。
- 作用:计算键的哈希值,将其映射到哈希表中的桶。
- 要求:哈希函数应尽量避免哈希冲突,即不同的键应映射到不同的桶。
2. 桶(Bucket)
桶是哈希表中存储元素的容器。哈希表通过哈希函数计算键的哈希值,将键值对放置到相应的桶中。
- 作用:组织和存储键值对。
- 实现:通常每个桶内部是一个链表、平衡树或者另一个哈希表。
3. 哈希冲突(Hash Collision)
哈希冲突发生在不同的键被映射到相同的桶中。解决冲突的常用方法包括链表法(链式哈希)和开放定址法(如线性探测)。
- 链式哈希:在每个桶内维护一个链表(或其他结构),所有映射到同一桶的元素都存储在这个链表中。
- 开放定址法:当发生冲突时,通过探测其他桶来寻找空位置。
4. 负载因子(Load Factor)
负载因子是哈希表中元素数量与桶数量的比值。负载因子过高会导致性能下降,因为冲突增多。
- 作用:影响哈希表的性能。
- 调整:通过重新哈希(rehashing)来增加桶的数量,降低负载因子。
unordered_map和
unordered_set`
这两个 STL 容器基于哈希表实现,提供了高效的查找、插入和删除操作。
std::unordered_map
-
定义:存储键值对(key-value pairs),每个键对应一个值。
-
特点:
- 唯一键:每个键在容器中必须唯一。
- 平均时间复杂度:查找、插入和删除操作的平均时间复杂度为 O(1)。
- 哈希函数:键值对通过哈希函数映射到桶中。
- 不支持有序操作:元素的顺序是不确定的。
-
常用操作:
- 插入:
insert
方法用于添加新的键值对。std::unordered_map<int, std::string> umap; umap.insert({1, "one"}); umap.insert({2, "two"});
- 查找:
find
方法用于查找键,返回对应的迭代器。auto it = umap.find(1); if (it != umap.end()) { std::cout << "Found: " << it->second << std::endl; }
- 删除:
erase
方法用于删除指定的键。umap.erase(1);
- 访问:使用迭代器遍历容器。
for (const auto& pair : umap) { std::cout << pair.first << " => " << pair.second << std::endl; }
- 调整桶数量:
rehash
方法可以调整哈希表的桶数量。umap.rehash(20); // 调整到至少 20 个桶
- 插入:
std::unordered_set
-
定义:存储唯一的元素,不存储与元素关联的值。
-
特点:
- 唯一元素:容器中不允许重复的元素。
- 平均时间复杂度:查找、插入和删除操作的平均时间复杂度为 O(1)。
- 哈希函数:元素通过哈希函数映射到桶中。
- 不支持有序操作:元素的顺序是不确定的。
-
常用操作:
- 插入:
insert
方法用于添加新的元素。std::unordered_set<int> uset; uset.insert(1); uset.insert(2);
- 查找:
find
方法用于查找元素,返回对应的迭代器。auto it = uset.find(1); if (it != uset.end()) { std::cout << "Found: " << *it << std::endl; }
- 删除:
erase
方法用于删除指定的元素。uset.erase(1);
- 访问:使用迭代器遍历容器。
for (const auto& elem : uset) { std::cout << elem << " "; }
- 调整桶数量:
rehash
方法可以调整哈希表的桶数量。uset.rehash(20); // 调整到至少 20 个桶
- 插入:
哈希表的优点和缺点
优点
- 高效查找:提供平均 O(1) 的查找时间复杂度。
- 快速插入和删除:插入和删除操作的平均时间复杂度为 O(1)。
- 动态大小调整:哈希表可以根据负载因子的变化动态调整桶的数量(rehashing)。
缺点
- 无序:元素的顺序是不确定的,不支持按顺序遍历。
- 哈希冲突:需要处理哈希冲突,可能影响性能。
- 内存使用:为了维持哈希表的效率,可能会使用更多的内存。
总结
std::unordered_map
和std::unordered_set
是 C++ STL 中的哈希表实现,提供了高效的查找、插入和删除操作。std::unordered_map
存储键值对,要求键唯一;std::unordered_set
存储唯一元素。- 哈希表的性能依赖于哈希函数的质量和负载因子的管理。
- 哈希表不支持有序操作,元素的顺序是不确定的,但它们在需要快速查找和插入的应用场景中非常有用。
测试
-
元素的位置 = key % bucket大小
-
bucket vector 的大小为质数
-
当元素个数大于 bucket 的总数时,bucket vector 扩充并重新打散放在新计算的 bucket 中(rehashing 很花时间)—— bucket 一定比元素多
在扩充时,按 vector 扩充为2倍大小,但会选择靠进这个数的一个质数做新的大小
GCC2.9下:
template <class Value, // Value里包含key和date
class Key, // key的类型
class HashFcn, // hash函数
class ExtractKey, // 从Value中取出key的方法
class EqualKey, // 判断key相等的函数
class Alloc>
class hashtable
{
public:
typedef HashFcn hasher;
typedef EqualKey key_equal; // 判断key相等的函数
typedef size_t size_type;
private:
// 3个函数对象,大小一共3(应该是0,因为一些因素)
hasher hash;
key_equal equals;
ExtractKey get_key;
typedef __hashtable_node<Value> node;
vector<node*, Alloc> buckets; // vector里3个指针,大小12
size_type num_elements; // 大小4
// 一共19 ——> 20(调整为4的倍数)
public:
size_type bucket_count() const { return buckets.size(); }
};
Hash函数:
偏特化写不同类型的 hash 函数,下图都是数值类型,直接返回就可以
下图对 c 风格的字符串做了处理(也可以自己设计),来生成 hash code
注意:老版本STL没有提供现成的 string 类型的 hash 函数
3.3.4 unordered容器
ChatGPT介绍
在C++标准模板库(STL)中,unordered
容器是基于哈希表实现的一类容器,包括以下四种主要类型:
std::unordered_set
std::unordered_multiset
std::unordered_map
std::unordered_multimap
这些容器的共同特点是使用哈希表存储数据,因此它们能够提供常数时间复杂度的查找、插入和删除操作(平均情况下)。不过,这些容器中的元素是无序的,因为它们的顺序由哈希函数决定。
unordered
容器的通用特点
- 无序存储:所有
unordered
容器中的元素或键值对的顺序都是不确定的,因为它们是基于哈希函数分配到不同的桶中的。 - 哈希函数:这些容器的性能很大程度上依赖于哈希函数的质量。一个好的哈希函数能够减少冲突,提高容器的性能。
- 负载因子与 rehashing:容器会根据负载因子的变化自动调整桶的数量,以确保查找、插入和删除操作的效率。
使用场景
- 快速查找:当需要快速查找元素或键值对时,
unordered
容器非常适合。 - 无序数据:当不关心元素的顺序,只关注存储和查找效率时,使用
unordered
容器更为合适。 - 键值对存储:
unordered_map
和unordered_multimap
适用于需要根据键管理和访问值的场景,如字典或关联数组。
总结
C++ STL 中的 unordered
容器提供了基于哈希表的高效数据管理方式,适用于需要快速查找和管理无序数据的场景。unordered_set
和 unordered_multiset
处理唯一和重复的元素集合,而 unordered_map
和 unordered_multimap
则用于管理唯一和重复的键值对。通过使用合适的哈希函数和适当管理负载因子,这些容器可以显著提升程序的性能。
测试
void test_unordered_multiset(long& value)
{
cout << "\ntest_unordered_multiset().......... \n";
unordered_multiset<string> c; // 创建一个 unordered_multiset
char buf[10];
clock_t timeStart = clock(); // 记录起始时间
for(long i=0; i< value; ++i) // 添加元素到 unordered_multiset 中
{
try {
snprintf(buf, 10, "%d", rand()); // 将随机数转换为字符串格式
c.insert(string(buf)); // 将字符串插入 unordered_multiset 中
}
catch(exception& p) { // 捕获可能的异常
cout << "i=" << i << " " << p.what() << endl; // 输出异常信息
abort(); // 终止程序
}
}
cout << "毫秒数 : " << (clock()-timeStart) << endl; // 输出时间差,计算插入时间
cout << "unordered_multiset.size()= " << c.size() << endl; // 输出 unordered_multiset 大小
cout << "unordered_multiset.max_size()= " << c.max_size() << endl; // 输出 unordered_multiset 的最大容量
cout << "unordered_multiset.bucket_count()= " << c.bucket_count() << endl; // 输出 unordered_multiset 的桶数量
cout << "unordered_multiset.load_factor()= " << c.load_factor() << endl; // 输出 unordered_multiset 的负载因子
cout << "unordered_multiset.max_load_factor()= " << c.max_load_factor() << endl; // 输出 unordered_multiset 的最大负载因子
cout << "unordered_multiset.max_bucket_count()= " << c.max_bucket_count() << endl; // 输出 unordered_multiset 的最大桶数量
for (unsigned i=0; i< 20; ++i) {
cout << "bucket #" << i << " has " << c.bucket_size(i) << " elements.\n"; // 输出前20个桶中的元素数量
}
string target = get_a_target_string();
{
timeStart = clock();
auto pItem = find(c.begin(), c.end(), target); // 在 unordered_multiset 中使用 std::find(...) 查找目标字符串
cout << "std::find(),毫秒数 : " << (clock()-timeStart) << endl;
if (pItem != c.end())
cout << "found, " << *pItem << endl; // 如果找到,输出找到的元素
else
cout << "not found! " << endl; // 如果未找到,输出未找到的信息
}
{
timeStart = clock();
auto pItem = c.find(target); // 在 unordered_multiset 中使用 c.find(...) 查找目标字符串
cout << "c.find(),毫秒数 : " << (clock()-timeStart) << endl;
if (pItem != c.end())
cout << "found, " << *pItem << endl; // 如果找到,输出找到的元素
else
cout << "not found! " << endl; // 如果未找到,输出未找到的信息
}
c.clear(); // 清空unordered_multiset
}
运行结果:
随机数据填充容器:4406ms;直接搜索 ::find()
:109ms;c.find()
:0ms;前二十个 bucket 中只有一个有24个元素
深度探索
4 分配器
4.0 分配器介绍 ChatGPT
C++ 中的 分配器(Allocator) 是用于抽象和管理内存分配与释放的机制,主要用于标准模板库(STL)容器。分配器的设计允许开发者自定义内存管理策略,从而优化性能、满足特殊需求或实现特定功能(如内存池、共享内存等)。本文将详细介绍 C++ 分配器的概念、作用、自定义分配器的实现以及在 STL 中的应用。
1. 分配器的概念
分配器 是一个模板类,用于定义对象的内存分配和释放方式。在 STL 中,所有容器都接受一个分配器作为模板参数,默认使用 std::allocator
。通过自定义分配器,开发者可以控制容器如何管理内存。
分配器的主要功能:
- 内存分配:为对象分配原始的未构造的内存。
- 对象构造:在已分配的内存上构造对象。
- 对象销毁:调用对象的析构函数,销毁对象。
- 内存释放:释放先前分配的内存。
2. 标准分配器 std::allocator
std::allocator
是 C++ 标准库提供的默认分配器,实现了最基本的内存分配和对象管理功能。其定义位于头文件 <memory>
中。
主要成员函数:
-
allocate
:分配未构造的内存。pointer allocate(size_type n);
-
deallocate
:释放先前分配的内存。void deallocate(pointer p, size_type n);
-
construct
:在已分配的内存上构造对象。(C++17 之前)void construct(pointer p, const T& val);
-
destroy
:调用对象的析构函数。(C++17 之前)void destroy(pointer p);
注意:从 C++17 开始,
construct
和destroy
被移除了,建议使用std::allocator_traits
或者直接使用std::uninitialized_fill
等算法。
3. 自定义分配器
自定义分配器允许开发者控制内存管理策略。例如,可以实现一个内存池分配器,以减少频繁的内存分配和释放带来的开销。
实现步骤:
-
继承或实现分配器接口:可以继承自
std::allocator
,或者直接实现所需的成员函数。 -
定义类型别名:如
value_type
、pointer
、size_type
等。 -
实现必要的成员函数:如
allocate
、deallocate
等。
示例:简单的内存池分配器
以下是一个基本的内存池分配器的示例,实现了固定大小的内存块的分配和释放。
#include <memory>
#include <cstddef>
#include <list>
template <typename T>
class PoolAllocator {
public:
using value_type = T;
PoolAllocator() = default;
~PoolAllocator() {
for (auto& block : blocks_) {
::operator delete(block);
}
}
T* allocate(std::size_t n) {
if (n != 1) {
throw std::bad_alloc();
}
if (!free_list_) {
expandPool();
}
T* ptr = free_list_;
free_list_ = free_list_->next;
return reinterpret_cast<T*>(ptr);
}
void deallocate(T* p, std::size_t n) {
if (p == nullptr || n != 1) return;
auto node = reinterpret_cast<FreeNode*>(p);
node->next = free_list_;
free_list_ = node;
}
template <typename U, typename... Args>
void construct(U* p, Args&&... args) {
::new ((void*)p) U(std::forward<Args>(args)...);
}
template <typename U>
void destroy(U* p) {
p->~U();
}
private:
struct FreeNode {
FreeNode* next;
};
void expandPool() {
std::size_t size = sizeof(T) > sizeof(FreeNode) ? sizeof(T) : sizeof(FreeNode);
FreeNode* block = reinterpret_cast<FreeNode*>(::operator new(size));
block->next = free_list_;
free_list_ = block;
blocks_.push_back(block);
}
FreeNode* free_list_ = nullptr;
std::list<FreeNode*> blocks_;
};
解释:
-
allocate
:每次只允许分配一个对象的内存。如果空闲列表为空,则调用expandPool
扩展内存池。 -
deallocate
:将释放的内存块加入到空闲列表中,供后续分配使用。 -
construct
/destroy
:用于对象的构造和销毁。
使用示例:
#include <vector>
#include <iostream>
int main() {
std::vector<int, PoolAllocator<int>> vec;
for (int i = 0; i < 10; ++i) {
vec.push_back(i);
}
for (const auto& val : vec) {
std::cout << val << " ";
}
return 0;
}
4. 分配器与容器
STL 中的容器都接受一个分配器作为模板参数,默认使用 std::allocator
。通过提供自定义分配器,可以改变容器的内存管理方式。
示例:为 std::vector
提供自定义分配器
#include <vector>
#include <iostream>
// 假设已经定义了 PoolAllocator<T>
int main() {
std::vector<int, PoolAllocator<int>> vec;
vec.reserve(100); // 预留空间
for (int i = 0; i < 100; ++i) {
vec.push_back(i);
}
for (const auto& val : vec) {
std::cout << val << " ";
}
return 0;
}
在上述示例中,std::vector
使用了自定义的 PoolAllocator
,从而在插入元素时使用内存池进行内存管理。
5. std::allocator_traits
从 C++11 开始,引入了 std::allocator_traits
,用于统一和简化分配器的实现。它为分配器提供了默认实现和辅助功能,建议在自定义分配器中使用。
使用示例
修改之前的 PoolAllocator
,使其使用 std::allocator_traits
:
#include <memory>
#include <cstddef>
#include <list>
template <typename T>
class PoolAllocator {
public:
using value_type = T;
PoolAllocator() = default;
~PoolAllocator() {
for (auto& block : blocks_) {
::operator delete(block);
}
}
T* allocate(std::size_t n) {
if (n != 1) {
throw std::bad_alloc();
}
if (!free_list_) {
expandPool();
}
T* ptr = reinterpret_cast<T*>(free_list_);
free_list_ = free_list_->next;
return ptr;
}
void deallocate(T* p, std::size_t n) {
if (p == nullptr || n != 1) return;
auto node = reinterpret_cast<FreeNode*>(p);
node->next = free_list_;
free_list_ = node;
}
private:
struct FreeNode {
FreeNode* next;
};
void expandPool() {
std::size_t size = sizeof(T) > sizeof(FreeNode) ? sizeof(T) : sizeof(FreeNode);
FreeNode* block = reinterpret_cast<FreeNode*>(::operator new(size));
block->next = free_list_;
free_list_ = block;
blocks_.push_back(block);
}
FreeNode* free_list_ = nullptr;
std::list<FreeNode*> blocks_;
};
在使用时,容器会通过 std::allocator_traits
来调用分配器的相应方法,如构造和销毁对象。
6. 分配器的应用场景
-
性能优化:通过自定义分配器,可以减少内存碎片,提高分配和释放的效率,尤其是在频繁进行小对象分配的场景下。
-
内存池:预先分配一大块内存,按需分配给对象,避免频繁的系统调用。
-
共享内存:在多进程场景下,通过分配器将对象放置在共享内存中,实现跨进程的数据共享。
-
自定义内存策略:如实时系统中,需要严格控制内存分配的时间和方式。
7. 注意事项
-
兼容性:确保自定义分配器满足分配器的要求,尤其是在不同的容器和算法中正确工作。
-
异常安全:在分配和释放内存时,要考虑异常安全,避免内存泄漏。
-
线程安全:如果在多线程环境中使用,需要确保分配器的线程安全性。
8. 总结
分配器是 C++ 中强大的内存管理工具,通过自定义分配器,开发者可以针对特定的应用场景优化内存分配策略。理解分配器的工作原理和正确使用方法,对于编写高性能和高可靠性的代码至关重要。
4.1 测试
分配器都是与容器共同使用的,一般分配器参数用默认值即可
list<string, allocator<string>> c1;
不建议直接用分配器分配空间,因为其需要在释放内存时也要指明大小
int* p;
p = allocator<int>().allocate(512, (int*)0); // 临时变量调用函数
allocator<int>().deallocate(p,512); // 释放时需要指明之前申请的大小
4.2 源码解析
VC6下:allocator 中有 allocate
,deallocate
其分别用函数 ::operator new
和 ::operator delete
来调用 c 中的 malloc 和 free
pointer allocate(size_type _N, const void*){...} // 后面一个参数只是用来指明类型的
void deallocate(void _FARQ *_P, size_type){...}
这里经过包装还是调用的 malloc 和 free,其执行效率变慢;且如果申请的空间比较小,会有较大比例的额外开销(cookie,调试模式所需空间等等)
GCC2.9 下:其容器都是调用的名叫 alloc 的分配器
其从0到15有一共16个链表,分别代表8字节到16*8字节,例如 #0 的位置用 malloc 要一大块内存,然后做切割,切成一块一块的8字节空间不带cookie,用单向链表穿起来;当要申请6字节的大小的空间时,其就会到 #0 中占用一块 —— 节省空间
在 GCC4.9 中各个容器又用回了 allocator,而上面的 alloc 变成了
__poll_alloc
5 迭代器
5.0 迭代器介绍 chatGPT
C++ 中的 迭代器(Iterator) 是用于遍历容器元素的一种抽象工具,类似于指针,提供了一种统一的方式来访问和操作 STL 容器中的元素。迭代器是 STL 的核心组件之一,与算法、容器紧密结合,使得容器与算法之间的分离成为可能。
以下将详细介绍 C++ 中的迭代器,包括其分类、特性、常见操作、自定义迭代器等。
1. 迭代器的概念
迭代器 是一个对象,允许用户逐一访问容器中的元素,而不需要了解底层数据结构。C++ STL 中的大多数容器都提供了迭代器,例如 std::vector
、std::list
、std::map
等。
迭代器的作用类似于指针,可以使用 *
操作符解引用获得当前元素,使用 ++
、--
操作符进行迭代(移动到下一个或上一个元素)。
2. 迭代器的分类
C++ STL 中的迭代器分为五类,按照它们的功能和性能特性进行划分:
2.1 输入迭代器(Input Iterator)
-
特点:只读访问容器中的元素,支持单向遍历。
-
操作:解引用(
*
)、前置/后置递增(++
)。 -
使用场景:适用于只需要从容器中读取元素的算法,如
std::find
。示例:
std::istream_iterator<int> input_it(std::cin); std::istream_iterator<int> end; while (input_it != end) { std::cout << *input_it << " "; ++input_it; }
2.2 输出迭代器(Output Iterator)
-
特点:只能写入元素,不能读取,支持单向遍历。
-
操作:解引用赋值(
*
)、前置/后置递增(++
)。 -
使用场景:适用于将结果输出到容器或流的算法,如
std::copy
。示例:
std::ostream_iterator<int> output_it(std::cout, " "); *output_it = 1; // 输出到标准输出 ++output_it;
2.3 前向迭代器(Forward Iterator)
-
特点:支持只读或读写访问,支持单向遍历,允许多次遍历相同的元素。
-
操作:解引用(
*
)、前置/后置递增(++
)。 -
使用场景:适用于需要读写访问的算法,如
std::replace
。示例:
std::forward_list<int> flist = {1, 2, 3}; auto it = flist.begin(); while (it != flist.end()) { std::cout << *it << " "; ++it; }
2.4 双向迭代器(Bidirectional Iterator)
-
特点:支持双向遍历,既可以向前遍历,也可以向后遍历。
-
操作:解引用(
*
)、前置/后置递增(++
)、前置/后置递减(--
)。 -
使用场景:适用于需要双向遍历的算法,如
std::reverse
。示例:
std::list<int> lst = {1, 2, 3}; auto it = lst.rbegin(); // 反向迭代器 while (it != lst.rend()) { std::cout << *it << " "; ++it; }
2.5 随机访问迭代器(Random Access Iterator)
-
特点:支持随机访问,可以直接跳转到容器中的任意元素。是功能最强的迭代器类型。
-
操作:解引用(
*
)、前置/后置递增(++
)、前置/后置递减(--
)、随机访问(it + n
、it[n]
、it - n
)。 -
使用场景:适用于需要高效随机访问的算法,如
std::sort
。示例:
std::vector<int> vec = {1, 2, 3}; auto it = vec.begin(); it += 2; // 移动到第三个元素 std::cout << *it << std::endl; // 输出 3
3. 迭代器的常见操作
-
begin()
/end()
:返回指向容器第一个元素和最后一个元素之后的迭代器。std::vector<int> vec = {1, 2, 3}; auto it = vec.begin();
-
解引用:通过
*it
访问当前迭代器指向的元素。std::cout << *it << std::endl;
-
递增:
++it
前置递增,it++
后置递增,移动到下一个元素。++it;
-
递减(仅适用于双向和随机访问迭代器):
--it;
-
随机访问(仅适用于随机访问迭代器):
it += 2; // 移动到第三个元素
-
比较:可以使用
==
、!=
来比较两个迭代器是否指向同一位置。随机访问迭代器还支持<
、>
、<=
、>=
等比较操作。
4. 反向迭代器
C++ 提供了反向迭代器,用于反向遍历容器。反向迭代器通过调用 rbegin()
和 rend()
获得。
示例:
std::vector<int> vec = {1, 2, 3};
auto rit = vec.rbegin(); // 反向迭代器指向最后一个元素
while (rit != vec.rend()) {
std::cout << *rit << " ";
++rit;
}
在上述代码中,vec.rbegin()
返回指向最后一个元素的反向迭代器,vec.rend()
则返回指向第一个元素之前位置的反向迭代器。
5. 自定义迭代器
C++ 允许用户定义自己的迭代器,通过实现迭代器接口的相关函数,可以将自定义数据结构与 STL 算法结合使用。
自定义迭代器的步骤:
- 继承
std::iterator
或定义必要的类型别名,如value_type
、pointer
、reference
等。 - 实现
operator*
用于解引用。 - 实现
operator++
、operator--
(根据迭代器类型,选择实现前置或后置版本)。 - 实现比较操作符
==
、!=
用于迭代器比较。
示例:简单的自定义迭代器
template<typename T>
class MyIterator {
public:
using value_type = T;
using pointer = T*;
using reference = T&;
using difference_type = std::ptrdiff_t;
using iterator_category = std::forward_iterator_tag;
MyIterator(pointer ptr) : ptr_(ptr) {}
reference operator*() const { return *ptr_; }
pointer operator->() { return ptr_; }
// 前置递增
MyIterator& operator++() {
++ptr_;
return *this;
}
// 后置递增
MyIterator operator++(int) {
MyIterator tmp = *this;
++(*this);
return tmp;
}
friend bool operator==(const MyIterator& a, const MyIterator& b) {
return a.ptr_ == b.ptr_;
}
friend bool operator!=(const MyIterator& a, const MyIterator& b) {
return a.ptr_ != b.ptr_;
}
private:
pointer ptr_;
};
// 使用自定义迭代器
int main() {
int arr[] = {1, 2, 3};
MyIterator<int> begin(arr);
MyIterator<int> end(arr + 3);
for (auto it = begin; it != end; ++it) {
std::cout << *it << " ";
}
return 0;
}
6. 迭代器适配器
C++ STL 提供了多种迭代器适配器,用于在现有的迭代器基础上扩展功能:
std::reverse_iterator
:反向迭代器
,用于反向遍历容器。
-
std::insert_iterator
:用于在容器中插入元素的迭代器适配器。std::back_inserter
、std::front_inserter
也是常见的插入迭代器,分别用于从容器的末尾和开头插入元素。 -
std::istream_iterator
和std::ostream_iterator
:分别用于从输入流读取数据和向输出流写入数据的迭代器适配器。
示例:反向迭代器适配器
std::vector<int> vec = {1, 2, 3, 4, 5};
std::reverse_iterator<std::vector<int>::iterator> rit = vec.rbegin();
while (rit != vec.rend()) {
std::cout << *rit << " "; // 输出 5 4 3 2 1
++rit;
}
示例:插入迭代器适配器
std::vector<int> vec = {1, 2, 3};
std::vector<int> vec2;
std::copy(vec.begin(), vec.end(), std::back_inserter(vec2)); // 将 vec 的元素复制到 vec2 中
示例:流迭代器适配器
std::vector<int> vec = {1, 2, 3};
std::copy(vec.begin(), vec.end(), std::ostream_iterator<int>(std::cout, " ")); // 输出 1 2 3
7. 迭代器的安全性问题
C++ 中的迭代器操作有时会引发安全性问题,主要包括:
-
迭代器失效:当容器的结构发生变化时,如插入、删除、重分配等操作,迭代器可能会变得无效。
- 例如,在
std::vector
中插入或删除元素后,指向原始元素的迭代器可能不再有效。 - 使用
std::list
或std::forward_list
等链表容器时,插入和删除操作通常不会使迭代器失效。
- 例如,在
-
访问非法位置:如果迭代器指向了容器的
end()
或rend()
,对其解引用可能会导致未定义行为。
避免迭代器失效的建议
- 了解容器特性:掌握不同容器在进行插入、删除操作时对迭代器的影响。例如,在
std::vector
中,避免在迭代时进行插入或删除操作。 - 迭代时慎用增删操作:在遍历容器时,尽量避免增删操作。若需要修改容器,考虑使用
remove_if
、erase
结合使用,或通过手动管理迭代器来避免失效。 - 检查迭代器有效性:在使用迭代器时,务必检查其是否指向有效位置。
8. 总结
迭代器是 C++ 标准模板库中非常强大的工具,它为容器提供了统一的访问接口,使得算法与容器分离成为可能。理解并正确使用迭代器是掌握 C++ STL 的关键:
- 迭代器类型:从最简单的输入、输出迭代器,到前向、双向和随机访问迭代器,不同类型的迭代器提供了不同的功能和性能特性。
- 常见操作:迭代器支持解引用、递增、递减、随机访问和比较操作。
- 适配器:迭代器适配器如
reverse_iterator
、insert_iterator
、istream_iterator
和ostream_iterator
扩展了迭代器的功能。 - 安全性问题:迭代器失效和非法访问是常见的安全性问题,必须谨慎处理。
通过合理地选择和使用迭代器,开发者可以编写出更为高效、简洁且易维护的 C++ 程序。
5.1 迭代器的设计准则
Iterator 必须提供5种 associated type(说明自己的特性的)来供算法来识别,以便算法正确地使用 Iterator
template <class T, class Ref, class Ptr>
struct __list_iterator
{
...
typedef bidirectional_iterator_tag iterator_category; // (1)迭代器类别:双向迭代器
typedef T value_type; // (2)迭代器所指对象的类型
typedef Ptr pointer; // (3)迭代器所指对象的指针类型
typedef Ref reference; // (4)迭代器所指对象的引用类型
typedef ptrdiff_t difference_type; // (5)两个迭代器之间的距离类型
// iter1-iter2 时,要保证数据类型以存储任何两个迭代器对象间的距离
...
}
// 迭代器回答
// | Λ
// | |
// | |
// V |
// 算法直接提问
template <typename I>
inline void algorithm(I first, I last)
{
...
I::iterator_category
I::pointer
I::reference
I::value_type
I::difference_type
...
}
但当 Iterator 并不是 class 时,例如指针本身,就不能 typedef
了 —— 这时就要设计一个 Iterator Traits
Traits:用于定义类型特征的信息,从而在编译时根据类型的不同进行不同的操作或处理 —— 类似一个萃取机(针对不同类型做不同操作:偏特化)
// I是class iterator进
template <class I>
struct Iterator_traits
{
typedef typename I::iterator_category iterator_category;
typedef typename I::value_type value_type;
typedef typename I::difference_type difference_type;
typedef typename I::pointer pointer;
typedef typename I::reference reference;
// typename用于告诉编译器,接下来的标识符是一个类型名,而不是一个变量名或其他名称
// I::iterator_category 是一个类型名
// iterator_category是这个迭代器类型内部的一个嵌套类型(typedef ...)
};
// I是指向T的指针进
template <class T>
struct Iterator_traits<T*>
{
typedef random_access_iterator_tag iterator_category;
typedef T value_type;
typedef ptrdiff_t difference_type;
typedef T* pointer;
typedef T& reference;
};
// I是指向T的常量指针进
template <class T>
struct Iterator_traits<const T*>
{
typedef random_access_iterator_tag iterator_category;
typedef T value_type; // 注意是T而不是const T
// 按理说是const T,但声明一个不能被赋值的变量无用
// 所以value_type不应加上const
typedef ptrdiff_t difference_type;
typedef const T* pointer;
typedef const T& reference;
};
除了 Iterator Traits,还有很多其他 Traits
5.2 迭代器的分类
迭代器的分类对算法的效率有很大的影响
- 输入迭代器 input_iterator_tag:istream迭代器
- 输出迭代器 output_iterator_tag:ostream迭代器
- 单向迭代器 forward_iterator_tag:forward_list,hash类容器
- 双向迭代器 bidirectional_iterator_tag: list、红黑树容器
- 随机存取迭代器 random_access_iterator_tag:array、vector、deque
用有继承关系的class实现:
- 方便迭代器类型作为参数进行传递,如果是整数的是不方便的
- 有些算法的实现没有实现所有类型的迭代器类别,就要用继承关系去找父迭代器类别
struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag : public input_iterator_tag {};
struct bidirectional_iterator_tag : public forward_iterator_tag {};
struct random_access_iterator_tag : public bidirectional_iterator_tag {};
算法 distance 将会按照迭代器的类别进行不同的操作以提升效率
- 如果迭代器可以跳,直接
last - first
即可 - 如果迭代器不能跳,就只能一步一步走来计数
两者的效率差别很大
但如果迭代器类别是
farward_iterator_tag
或者bidirectional_iterator_tag
,该算法没有针对这种类型迭代器实现,就可以用继承关系来使用父类的实现(继承关系——“is a” 子类是一种父类,当然可以用父类的实现)
算法 copy 将经过很多判断筛选来找到最高效率的实现
其中用到了 Iterator Traits 和 Type Traits 来进行筛选
has trivial op=() 是指的有不重要的拷贝赋值函数(例如复数用的自带的拷贝赋值函数)
注意:由于 output_iterator_tag(例如 ostream_iterator)是 write-only,无法用
*
来读取内容,所以在设计时就需要再写个专属版本
在源码中,算法都是模板函数,接受所有的 iterator,但一些算法只能用特定的 iterator,所以其会在模板参数的名称上进行暗示:
6 算法
- (1)算法看不到容器,只能看到迭代器,通过迭代器去处理容器;根据迭代器的类型,判断怎么提供哪种最优的方式。
- (2)Algorithm看不见容器,对其一无所知,所以,需要的信息都必须从迭代器中获得,因此迭代器必须可以回到算法的提问,才能搭配算法的所有操作;
6.0 算法介绍 ChatGPT
C++标准模板库(STL)中的**算法(Algorithms)**是用于操作容器元素的一组通用函数。它们提供了广泛的功能,包括排序、搜索、修改、计算等。STL的算法设计以迭代器为核心,使得它们能够应用于几乎任何容器类型,从而实现了容器与算法的分离。
1. 算法的分类
STL 中的算法大致可以分为以下几类:
- 非修改算法:不改变容器中的元素。
- 修改算法:会修改容器中的元素。
- 排序算法:对容器中的元素进行排序。
- 排序相关算法:与排序相关但不直接排序的算法。
- 数值算法:专门用于数值计算的算法。
2. 非修改算法
非修改算法不会改变容器中元素的内容。这类算法通常用于搜索、计数、查找最值等操作。
-
for_each
:对范围内的每个元素执行给定的操作。std::vector<int> vec = {1, 2, 3}; std::for_each(vec.begin(), vec.end(), [](int &n) { n *= 2; });
-
find
:在范围内查找与指定值匹配的第一个元素。auto it = std::find(vec.begin(), vec.end(), 2);
-
count
:计算范围内满足特定条件的元素个数。int count = std::count(vec.begin(), vec.end(), 2);
-
all_of
/any_of
/none_of
:检查范围内的元素是否全部、任意或没有满足特定条件。bool all_positive = std::all_of(vec.begin(), vec.end(), [](int n) { return n > 0; });
-
equal
:判断两个范围内的元素是否相等。bool is_equal = std::equal(vec.begin(), vec.end(), vec2.begin());
3. 修改算法
修改算法会改变容器中元素的内容,如拷贝、移动、替换、移除等操作。
-
copy
:将一个范围内的元素复制到另一个范围中。std::vector<int> vec2(3); std::copy(vec.begin(), vec.end(), vec2.begin());
-
move
:将一个范围内的元素移动到另一个范围中(不复制,而是“移动”资源)。std::vector<int> vec2 = std::move(vec);
-
swap
:交换两个容器的内容。std::swap(vec, vec2);
-
replace
:将范围内满足特定条件的元素替换为新值。std::replace(vec.begin(), vec.end(), 2, 4);
-
remove
:移除范围内满足特定条件的元素,注意该算法并不改变容器的大小。auto it = std::remove(vec.begin(), vec.end(), 2); vec.erase(it, vec.end()); // 实际移除元素
4. 排序算法
STL提供了多种排序算法,用于对容器内的元素进行排序。
-
sort
:对范围内的元素进行升序排序。std::sort(vec.begin(), vec.end());
-
partial_sort
:部分排序,将范围的前n
个元素按顺序排列,其余部分无序。std::partial_sort(vec.begin(), vec.begin() + 3, vec.end());
-
nth_element
:重排范围内的元素,使得第n
个元素处于它在排序后的正确位置。std::nth_element(vec.begin(), vec.begin() + 2, vec.end());
-
stable_sort
:稳定排序算法,排序后相等元素的相对顺序保持不变。std::stable_sort(vec.begin(), vec.end());
5. 排序相关算法
这些算法与排序密切相关,但不直接进行排序。
-
binary_search
:在已排序范围内查找元素,返回是否找到。bool found = std::binary_search(vec.begin(), vec.end(), 3);
-
lower_bound
/upper_bound
:在已排序范围内,查找第一个不小于/大于指定值的位置。auto lb = std::lower_bound(vec.begin(), vec.end(), 3); auto ub = std::upper_bound(vec.begin(), vec.end(), 3);
-
equal_range
:在已排序范围内查找与指定值相等的元素范围。auto range = std::equal_range(vec.begin(), vec.end(), 3);
-
merge
:将两个已排序的范围合并为一个新的已排序范围。std::vector<int> merged(vec.size() + vec2.size()); std::merge(vec.begin(), vec.end(), vec2.begin(), vec2.end(), merged.begin());
6. 数值算法
这些算法用于数值计算,主要包含在 <numeric>
头文件中。
-
accumulate
:计算范围内元素的累积和。int sum = std::accumulate(vec.begin(), vec.end(), 0);
-
inner_product
:计算两个范围内元素的内积。int product = std::inner_product(vec.begin(), vec.end(), vec2.begin(), 0);
-
adjacent_difference
:计算相邻元素的差,并将结果保存到一个新范围。std::vector<int> diff(vec.size()); std::adjacent_difference(vec.begin(), vec.end(), diff.begin());
-
partial_sum
:计算前缀和,并将结果保存到一个新范围。std::vector<int> sum(vec.size()); std::partial_sum(vec.begin(), vec.end(), sum.begin());
7. 其它常见算法
-
max_element
/min_element
:查找范围内的最大值或最小值。auto max_it = std::max_element(vec.begin(), vec.end()); auto min_it = std::min_element(vec.begin(), vec.end());
-
unique
:删除范围内相邻的重复元素。auto it = std::unique(vec.begin(), vec.end()); vec.erase(it, vec.end());
-
reverse
:反转范围内的元素。std::reverse(vec.begin(), vec.end());
-
rotate
:将范围内的元素旋转,使第一个元素成为新范围的第一个元素。std::rotate(vec.begin(), vec.begin() + 1, vec.end());
-
next_permutation
/prev_permutation
:生成范围内元素的下一个/上一个排列。std::next_permutation(vec.begin(), vec.end());
8. STL 算法与迭代器的关系
STL 中的算法大多以迭代器为参数,而非直接操作容器。这种设计使得算法具有极强的泛型性,可以应用于任何支持迭代器的容器。
例如,std::sort
接受两个随机访问迭代器作为参数,这意味着它可以对 std::vector
、std::deque
等容器进行排序,因为这些容器的迭代器支持随机访问。
9. 总结
C++ STL 中的算法提供了强大而灵活的工具集,涵盖了各种常见的操作需求。掌握这些算法的使用技巧不仅可以提高代码的效率,还能增强代码的可读性和维护性。通过理解算法的分类和各自的功能,可以更加有效地利用 STL 来解决复杂的编程问题。
关键点总结:
- 泛型性:STL算法可以与任何支持迭代器的容器配合使用。
- 效率:大多数算法都是高效实现的,如
std::sort
是一种快速排序的实现。 - 灵活性:通过组合不同的算法和迭代器,几乎可以完成所有常见的数据操作需求。
算法的标准样式:需要传进去两个指针
6.1 算法源码
6.1.1 accumulate
两个版本:
-
元素累加到 init 上
template <class InputIterator, class T> T accumulate(InputIterator first, InputIterator last, T init) { for (; first != last; ++first) init = init + *first; // 累加到init return init; }
-
元素累运算到 init 上
template <class InputIterator, class T, class BinaryOperation> T accumulate(InputIterator first, InputIterator last, T init, BinaryOperation binary_op) { for (; first != last; ++first) init = binary_op(init, *first); // 累运算到init上 return init; }
这里可以用任意的二元操作(可以是函数,也可以是仿函数)
测试:
#include <iostream> // std::cout
#include <functional> // std::minus
#include <numeric> // std::accumulate
// 函数
int myfunc (int x, int y) {return x+2*y;}
// 仿函数
struct myclass {
int operator()(int x, int y) {return x+3*y;}
} myobj;
void test_accumulate()
{
cout << "\ntest_accumulate().......... \n";
int init = 100;
int nums[] = {10,20,30};
cout << "using default accumulate: ";
cout << accumulate(nums,nums+3,init); //160
cout << '\n';
cout << "using functional's minus: ";
cout << accumulate(nums, nums+3, init, minus<int>()); //40
cout << '\n';
cout << "using custom function: ";
cout << accumulate(nums, nums+3, init, myfunc); //220
cout << '\n';
cout << "using custom object: ";
cout << accumulate(nums, nums+3, init, myobj); //280
cout << '\n';
}
6.1.2 for_each
让范围里的所有元素都依次做同一件事情
Function 可以是函数也可以是仿函数
template <class InputIterator, class Function>
Function for_each(InputIterator first, InputIterator last, Function f)
{
for (; first != last; ++first) {
f(*first);
}
return f;
}
与C++11中的 range-based for statement 差不多
6.1.3 replace…
-
replace
:范围内的所有等于 old_value 的,都被 new_value 取代template <class ForwardIterator, class T> void replace(ForwardIterator first, ForwardIterator last, const T& old_value, const T& new_value) { for (; first != last; ++first) { if (*first == old_value) *first = new_value; } }
-
replace_if
:范围内所有满足pred()
为 true 的元素都被 new_value 取代template <class ForwardIterator,class Predicate, class T> void replace_if(ForwardIterator first, ForwardIterator last, Predicate pred, const T& new_value) { for (; first != last; ++first) { if (pred(*first)) *first = new_value; } }
-
replace_copy
:范围内的元素全部 copy 到新地方,其中所有等于 old_value 的,都被替代为 new_valuetemplate <class InputIterator, class OutputIterator, class T> OutputIterator replace_copy(InputIterator first, InputIterator last, OutputIterator result, const T& old_value, const T& new_value) { for (; first != last; ++first, ++result) { *result = (*first == old_value) ? new_value : *first; } return result; }
6.1.4 count…
-
count
:在范围中计数值等于 value 的个数template <class InputIterator, class T> typename iterator_traits<InputIterator>::difference_type // 返回类型 count (InputIterator first, InputIterator last, const T& value) { typename iterator_traits<InputIterator>::difference_type n = 0; for (; first != last; ++first) { if (*first == value) ++n; } return n; }
-
count_if
:在范围中计数满足条件pred()
的个数template <class InputIterator, class Predicate> typename iterator_traits<InputIterator>::difference_type // 返回类型 count_if (InputIterator first, InputIterator last, Predicate pred) { typename iterator_traits<InputIterator>::difference_type n = 0; for (; first != last; ++first) { if (pred(*first)) ++n; } return n; }
- 容器不带成员函数
count()
:array,vector,forward_list,deque- 容器自带成员函数
count()
:set / multiset,map / multimap,unordered_set / unordered_multiset,unordered_map / unorderd_multimap —— 所有关联式容器
6.1 5 find…
-
find
:在范围内找到值等于 value 的元素template <class InputIterator, class T> InputIterator find(InputIterator first, InputIterator last, const T& value) { while (first != last && *first != value) ++first; return first; }
-
find_if
:在范围内找到满足pred()
的元素template <class InputIterator, class Predicate> InputIterator find_if(InputIterator first, InputIterator last, Predicate pred) { while (first != last && !pred(*first)) ++first; return first; }
都是循序查找,效率低
- 容器不带成员函数
find()
:array,vector,forward_list,deque- 容器自带成员函数
find()
:set / multiset,map / multimap,unordered_set / unordered_multiset,unordered_map / unorderd_multimap —— 所有关联式容器
6.1.6 sort
源码复杂
测试:
// 函数
bool myfunc (int i,int j) { return (i<j); }
//仿函数
struct myclass {
bool operator() (int i,int j) { return (i<j);}
} myobj;
// 定义向量
int myints[] = {32,71,12,45,26,80,53,33};
vector<int> myvec(myints, myints+8); // 32 71 12 45 26 80 53 33
// 用默认的比较(operator <)
sort(myvec.begin(), myvec.begin()+4); //(12 32 45 71)26 80 53 33
// 用自己的函数作比较
sort(myvec.begin()+4, myvec.end(), myfunc); // 12 32 45 71(26 33 53 80)
// 用自己的仿函数作比较
sort(myvec.begin(), myvec.end(), myobj); //(12 26 32 33 45 53 71 80)
// 用反向迭代器 reverse iterator 和默认的比较(operator <)
sort(myvec.rbegin(), myvec.rend()); // 80 71 53 45 33 32 26 12
// 用显式默认比较(operator <)
sort(myvec.begin(), myvec.end(), less<int>()); // 12 26 32 33 45 53 71 80
// 使用另一个比较标准(operator >)
sort(myvec.begin(), myvec.end(), greater<int>()); // 80 71 53 45 33 32 26 12
- 容器不带成员函数
sort()
:array,vector,deque,所有关联式容器(本身就排好序了)- 容器自带成员函数
sort()
:list,forward_list(只能用自带)
reverse iterator:
其中用的是 reverse_iterator —— iterator adapter
6.1.7 binary_search
二分查找是否存在目标元素(并不给予位置),使用前必须先排序;其主要使用 lower_bound()
来找到能放入 val 的最低位置,再判断该元素是否存在
template <class ForwardIterator, class T>
bool binary_search(ForwardIterator first, ForwardIterator last, const T& value)
{
first = lower_bound(first, last, value);
return (first != last && !(value < *first));
// first == last 就是序列中所有元素都小于value
// first == last 时,*first是没有值的,所以需要先检查
// value < *first 就是序列中没有等于value的
}
lower_bound()
:用于在有序序列中查找第一个大于等于该值的元素(包括目标值本身),并返回一个指向该位置的迭代器
- 如果目标值在序列中多次出现,返回第一个出现的位置
- 如果目标值在序列中不存在,它将返回指向比目标值大的第一个元素位置,或者返回
last
upper_bound()
:用于在有序序列中查找第一个大于该值的元素(不包括目标值本身),并返回一个指向该位置的迭代器
- 如果目标值在序列中多次出现,返回第一个大于目标值的位置
- 如果目标值在序列中不存在,它将返回与
lower_bound()
一样的位置一样是前闭后开的原则,且他们都用的是二分查找的方法
7 仿函数
- 仿函数—只服务于算法,使用仿函数作为特有的参数指定特定操作
仿函数专门为算法服务,设计成一个函数/仿函数是为了能传入算法
-
仿函数必须重载()操作符;
-
仿函数可以被修改的条件就是继承合适的基类,用于回答问题;仿函数适配器要问问题,仿函数回答问题;
-
仿函数就是一个class重载了()运算法,称为函数对象;是一个对象,但像一个函数;
STL中的每个仿函数都继承了 binary_function
/ unary_function
—— 融入到STL中
STL规定每个 Adaptable Function(之后可以改造的函数)都应该继承其中一个(因为之后 Function Adapter 将会提问)
// 一个操作数的操作,例如“!”
template <class Arg, class Result>
struct unary_function
{
typedef Arg argument_type;
typedef Result result_type;
};
// 两个操作数的操作,例如“+”
template <class Arg1, class Arg2, class Result>
struct binary_function
{
typedef Arg1 first_argument_type;
typedef Arg2 second_argument_type;
typedef Result result_type;
};
// 理论大小都是0,实际上可能是1(如果有人继承,那就一定是0)
防函数是我们自己可能会写的,所以自己写的时候,如果想要融入STL,就要继承上面的两个之一
7.0 仿函数介绍 ChatGPT
在C++标准模板库(STL)中,仿函数(Functor),也称为函数对象,是一个行为类似函数的对象。仿函数通过重载函数调用运算符 ()
,使对象可以像函数一样被调用。仿函数在STL中广泛用于算法和容器的自定义操作,因为它们不仅可以像普通函数一样调用,还可以携带状态和数据,从而提供更强的灵活性和功能性。
1. 仿函数的基本概念
仿函数是一个类或结构体的对象,该类或结构体重载了函数调用运算符 ()
。当我们创建一个仿函数的实例并使用 ()
调用它时,实际上是在调用该对象的 operator()
方法。
示例:简单的仿函数
#include <iostream>
struct Square {
int operator()(int x) const {
return x * x;
}
};
int main() {
Square square;
std::cout << square(5) << std::endl; // 输出 25
return 0;
}
在这个例子中,Square
是一个仿函数,它的 operator()
方法计算输入整数的平方。
2. 仿函数与普通函数的对比
普通函数是编译时无法携带状态的,而仿函数可以通过成员变量存储状态,从而在调用时使用这些状态。
示例:携带状态的仿函数
#include <iostream>
struct Adder {
int base;
Adder(int b) : base(b) {}
int operator()(int x) const {
return base + x;
}
};
int main() {
Adder add5(5);
std::cout << add5(10) << std::endl; // 输出 15
return 0;
}
在这个例子中,Adder
仿函数通过 base
成员变量携带了一个状态 5
,并在每次调用时将其与输入值相加。
3. STL中的仿函数
STL 中的许多算法和容器都可以使用仿函数。例如,std::sort
可以通过仿函数自定义排序规则,std::for_each
可以使用仿函数执行特定操作。
示例:使用仿函数进行排序
#include <algorithm>
#include <iostream>
#include <vector>
struct Greater {
bool operator()(int a, int b) const {
return a > b;
}
};
int main() {
std::vector<int> vec = {1, 5, 3, 2, 4};
std::sort(vec.begin(), vec.end(), Greater());
for (int v : vec) {
std::cout << v << " "; // 输出 5 4 3 2 1
}
return 0;
}
在这个例子中,Greater
仿函数用于指定排序时应使用大于关系,从而实现降序排序。
4. 标准库中的仿函数
C++ 标准库(尤其是在 <functional>
头文件中)提供了许多常用的仿函数。它们大多是用于常见操作的模板类。
-
算术仿函数:
std::plus
、std::minus
、std::multiplies
、std::divides
、std::modulus
、std::negate
。例如,
std::plus<int>()(3, 4)
返回 7,相当于3 + 4
。 -
关系仿函数:
std::equal_to
、std::not_equal_to
、std::greater
、std::less
、std::greater_equal
、std::less_equal
。例如,
std::greater<int>()(4, 3)
返回true
。 -
逻辑仿函数:
std::logical_and
、std::logical_or
、std::logical_not
。例如,
std::logical_and<bool>()(true, false)
返回false
。 -
位运算仿函数:
std::bit_and
、std::bit_or
、std::bit_xor
、std::bit_not
。
5. 绑定器和适配器
C++ 还提供了一些工具,可以将仿函数、函数指针或普通函数进行“绑定”或“适配”,从而改变其行为或简化调用。
-
std::bind
:可以将仿函数的某些参数绑定为固定值,从而创建新的仿函数。#include <iostream> #include <functional> int add(int a, int b) { return a + b; } int main() { auto add5 = std::bind(add, 5, std::placeholders::_1); // 将第一个参数固定为 5 std::cout << add5(3) << std::endl; // 输出 8 return 0; }
-
std::function
:是一个通用的函数包装器,可以保存任何可调用对象(包括仿函数、函数指针、lambda 表达式等)。#include <iostream> #include <functional> int main() { std::function<int(int, int)> func = [](int a, int b) { return a * b; }; std::cout << func(3, 4) << std::endl; // 输出 12 return 0; }
6. Lambda表达式与仿函数
在现代C++中,lambda表达式(匿名函数)提供了一种更简洁的方式来创建临时仿函数。Lambda 表达式常用于算法中进行自定义操作,替代传统的仿函数类。
示例:使用lambda替代仿函数
#include <algorithm>
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 5, 3, 2, 4};
std::sort(vec.begin(), vec.end(), [](int a, int b) { return a > b; });
for (int v : vec) {
std::cout << v << " "; // 输出 5 4 3 2 1
}
return 0;
}
在这个例子中,lambda表达式 [](int a, int b) { return a > b; }
实现了与前面 Greater
仿函数相同的功能,但代码更简洁。
7. 自定义仿函数的用途
仿函数在STL中有很多应用场景:
- 自定义排序规则:通过仿函数定制排序算法中的排序逻辑。
- 自定义操作:在
std::for_each
等算法中,通过仿函数执行复杂的操作。 - 状态管理:通过仿函数的成员变量在算法中保存状态信息。
8. 总结
仿函数是C++中一个强大且灵活的特性,它结合了对象的状态和函数的行为,使得在STL中使用自定义操作变得更加方便。通过理解仿函数的概念和应用,程序员可以编写出更灵活、更高效的代码。
关键点总结:
- 仿函数是对象化的函数:它们通过重载
operator()
来实现函数调用的行为。 - 标准库提供了常用的仿函数:如算术、关系、逻辑、位运算等仿函数。
- 仿函数可以携带状态:这使得它们比普通函数更加灵活。
- Lambda 表达式是现代 C++ 中更简洁的仿函数实现方式。
8 适配器
- 适配器 Adapter 只是一个小变化,比如改个接口,函数名称等等
- 其出现在三个地方:仿函数适配器,迭代器适配器,容器适配器
- 可以使用继承 / 复合的两种方式实现,STL中都用复合
其思想就是将该记的东西记起来,以便日后使用
8.0 适配器介绍 ChatGPT
在C++中,适配器(adapter)是一种设计模式的实现,用于解决接口不兼容的问题。适配器模式的主要目的是将一个类的接口转换成客户端所期望的另一种接口。这种模式通常用于将现有代码中的接口转换为符合新需求的接口,从而使得原本不兼容的接口能够协同工作。
在C++标准库中,适配器主要有以下几种应用:
1. 容器适配器(Container Adapters)
C++标准库提供了三种容器适配器,它们通过改变底层容器的接口来提供不同的数据结构。
-
std::stack
:后进先出(LIFO)的数据结构。它是基于其他容器(如std::deque
或std::list
)实现的。适配器提供了push
、pop
和top
方法,但隐藏了底层容器的具体实现细节。#include <stack> #include <iostream> int main() { std::stack<int> s; s.push(1); s.push(2); std::cout << s.top() << std::endl; // 输出 2 s.pop(); std::cout << s.top() << std::endl; // 输出 1 return 0; }
-
std::queue
:先进先出(FIFO)的数据结构。它同样基于其他容器(如std::deque
)实现。提供了push
、pop
和front
、back
等方法。#include <queue> #include <iostream> int main() { std::queue<int> q; q.push(1); q.push(2); std::cout << q.front() << std::endl; // 输出 1 q.pop(); std::cout << q.front() << std::endl; // 输出 2 return 0; }
-
std::priority_queue
:优先队列,是一种按照优先级排序的队列。它通过堆(std::vector
或其他容器)来实现,可以通过自定义比较函数来决定优先级。#include <queue> #include <vector> #include <iostream> int main() { std::priority_queue<int> pq; pq.push(1); pq.push(3); pq.push(2); std::cout << pq.top() << std::endl; // 输出 3 pq.pop(); std::cout << pq.top() << std::endl; // 输出 2 return 0; }
2. 迭代器适配器(Iterator Adapters)
C++标准库中的迭代器适配器提供了一种方式来转换和组合不同的迭代器类型。
-
std::reverse_iterator
:使得迭代器可以以相反的顺序进行迭代。#include <vector> #include <iostream> #include <iterator> int main() { std::vector<int> v = {1, 2, 3, 4, 5}; std::reverse_iterator<std::vector<int>::iterator> rit(v.end()); std::reverse_iterator<std::vector<int>::iterator> rend(v.begin()); while (rit != rend) { std::cout << *rit << ' '; ++rit; } return 0; }
-
std::istream_iterator
和std::ostream_iterator
:用于将流与容器的输入输出操作适配起来。#include <iostream> #include <iterator> #include <vector> int main() { std::vector<int> v; std::copy(std::istream_iterator<int>(std::cin), std::istream_iterator<int>(), std::back_inserter(v)); std::copy(v.begin(), v.end(), std::ostream_iterator<int>(std::cout, " ")); return 0; }
3. 函数适配器(Function Adapters)
函数适配器用于适配函数对象或可调用对象的接口。
-
std::bind
:绑定函数对象的参数。#include <iostream> #include <functional> void print_sum(int a, int b) { std::cout << a + b << std::endl; } int main() { auto bound_func = std::bind(print_sum, 5, std::placeholders::_1); bound_func(10); // 输出 15 return 0; }
-
std::function
:用于存储和调用任何可调用对象,如函数指针、函数对象、lambda表达式等。#include <iostream> #include <functional> int main() { std::function<void(int)> func = [](int x) { std::cout << x << std::endl; }; func(5); // 输出 5 return 0; }
总结
在C++中,适配器模式通过提供一个适配层,使得不同接口之间可以进行兼容。无论是容器适配器、迭代器适配器还是函数适配器,它们都使得C++的标准库更加灵活和强大,能够满足各种复杂的编程需求。
8.1 容器适配器
stack,queue 都是属于 deque 的 Adapter
比如 stack 中将 deque 的 push_back
改名为 push
8.2 函数适配器
8.2.1 binder2nd
binder2nd —— 绑定第二参数
// 数范围内所有小于40的元素个数
cout << count_if(vi.begin(), vi.end(),
bind2nd(less<int>(), 40));
// 辅助函数bind2nd,使用方便
// 编译器自动推动op的类型(函数模板)
template <class Operation, class T>
inline binder2nd<Operation> bind2nd(const Operation& op, const T& x)
{
typedef typename Operation::second_argument_type arg2_type;
// 调用ctor生成一个binder2nd临时对象并返回
return binder2nd<Operation>(op, arg2_type(x));
}
// binder2nd适配器:将二元函数对象转换为一元函数对象
template <class Operation>
class binder2nd
: public unary_function<typename Operation::first_argument_type,
typename Operation::result_type>
// 可能binder2nd也要被改造,要回答问题
{
protected:
Operation op; // 内部成员,记录op和第二实参
typename Operation::second_argument_type value;
public:
binder2nd(const Operation& x,
const typename Operation::second_argument_type& y)
: op(x), value(y) {} // ctor,将op和第二实参记录下来
typename Operation::result_type
operator()(const typename Operation::first_argument_type& x) const
{
return op(x, value); // 实际调用op,第二实参为value
}
};
当然还有:binder1st —— 绑定第一参数
新型适配器:bind
,代替了 bind1st
,bind2nd
,binder1st
,binder2nd
8.2.2 not1
not1 —— 否定
// 数范围内所有大于等于40的元素个数
cout << count_if(vi.begin(), vi.end(),
not1(bind2nd(less<int>(), 40)));
8.2.3 bind
C++11提供的 Adapter,其可以绑定:
- functions
- function objects
- member functions
- data members
测试函数 / 对象
// functions
double my_divide(double x, double y)
{
return x/y;
}
// function objects 测试与functions同理
// divides<double> my_divide;
struct MyPair
{
// data members
double a, b;
// member functions
double multiply()
{
return a*b;
}
};
占位符 placeholders:
using namespace std::placeholders;
提供了
_1
,_2
,_3
,·······下面的的
_1
指的是被绑函数中的第一个参数
-
binding functions / function objects 测试
-
单纯将两个整数
10
,2
绑定到my_divide
auto fn_five = bind(my_divide, 10, 2); cout << fn_five() << endl; // 5.0
-
用
_1
占据第一参数,第二参数绑定2,即x/2
auto fn_half = bind(my_divide, _1, 2); cout << fn_half(10) << endl; // 5.0
-
用
_1
占据第一参数,_2
占据第二参数,即y/x
auto fn_invert = bind(my_divide, _2, _1); cout << fn_invert(10, 2) << endl; // 0.2
-
给
bind
指定了一个模板参数int
,将my_divide
的返回类型变为int
,即int(x/y)
auto fn_rounding = bind<int>(my_divide, _1, _2); cout << fn_rounding(10, 3) << endl; // 3
-
-
binding member functions / data members 测试
MyPair ten_two {10, 2};
用C++11的新语法定义一个实例-
绑定 member functions,由于成员函数有
this
,所以_1
就相当于this
,即x.multiply()
auto bound_memfn = bind(&MyPair::multiply, _1); cout << bound_memfn(ten_two) << endl; // 20
-
绑定 data members,绑定是谁的数据
把实例
ten_two
绑定到a
,即ten_two.a
auto bound_memdata = bind(&MyPair::a, ten_two); cout << bound_memdata() << endl; // 10
用占位符绑定,即
x.a
auto bound_member_data2 = bind(&MyPair::b, _1); cout << bound_member_data2(ten_two) << endl;
-
8.3 迭代器适配器
8.3.1 reverse_iterator
注意:对逆向迭代器取值,就是取其所指正向迭代器的前一个位置
template <class Iterator>
class reverse_iterator
{
protected:
Iterator current;
public:
// 五个associated types与对应的正向迭代器相同
typedef Iterator iterator_type; // 代表正向迭代器
typedef reverse_iterator<Iterator> self; // 代表逆向迭代器
public:
explicit reverse_iterator(iterator_type x) : current(x) {}
reverse_iterator(const self& x) : current(x.current) {}
iterator_type base() const { return current; } // 取出正向迭代器
// 对逆向迭代器取值,就是取其所指正向迭代器的前一个位置
reference operator*() const
{ Iterator tmp = current; return *--tmp; }
pointer operator->() const { return &(operator*()); } // 同上
// 前进变后退,后退变前进
self& operator++()
{ --current; return *this; }
self& operator--()
{ ++current; return *this; }
self operator+(difference_type n)const
{ return self(current-n); }
self operator-(difference_type n)const
{ return self(current+n); }
};
8.3.2 inserter
对于 copy(InputIterator first, InputIterator last, OutputIterator result)
,其会不管 OutputIterator
后是否有充裕空间,对 result
开始依次赋值
但如果使用 inserter
,就会有如下用 copy
实现的插入的效果
list<int> foo, bar;
for (int i = 1; i <= 5; i++)
{
foo.push_back(i);
bar.push_back(i*10);
}
list<int>::iterator it = foo.begin();
advance(it, 3);
copy(bar.begin(), bar.end(), inserter(foo, it));
注:其是 output_iterator_tag
其实现原理核心就是 —— 对 =
的操作符重载
insert_iterator<Container>&
operator=(const typename Container::value_type& val)
{
// 关键:转调用insert()
iter = container->insert(iter, val);
++iter; // 使其一直随target贴身移动
return *this;
}
8.4 X适配器
8.4.1 ostream_iterator
其会将 copy
变为一个输出工具,分隔符是 ,
vector<int> vec = { 1,2,3,4,5,6,7,8,9,10 };
ostream_iterator<int> out_it(cout, ",");
copy(vec.begin(), vec.end(), out_it); // 1,2,3,4,5,6,7,8,9,10,
其核心依然是操作符重载,这样就相当于 cout<<*first;
cout<<",";
basic_ostream<charT,traits>* out_stream;
const charT* delim;
...
ostream_iterator<T, charT, traits>& operator=(const T& value)
{
*out_stream << value;
if(delim!=0) *out_stream << delim; // 分隔符delimiter
return *this;
}
ostream_iterator<T,charT,traits>& operator*(){return *this;}
ostream_iterator<T,charT,traits>& operator++(){return *this;}
...
其中 out_stream
存的 cout
,delim
存的 ,
8.4.2 istream_iterator
例一:
在创建 iit
的时候就已经把所有的键盘输入读进去了,之后就是一个一个取出来赋值给 value 的操作
double value1, value2;
istream_iterator<double> eos; // end of stream iterator
istream_iterator<double> iit(cin); // 相当于cin>>value
if(iit != eos)
value1 = *iit; // 相当于return value
iit++; // 迭代器不断++,就是不断地读内容
if(iit != eos)
value2 = *iit;
例二:
从 cin
读 data,插入到目的容器
istream_iterator<double> eos; // end of stream iterator
istream_iterator<double> iit(cin);
copy(iit, eos, inserter(c,c.begin()));
原理依旧是大量的**操作符重载 **—— 就可以改变原函数的作用
basic_istream<charT, traits>* in_stream;
T value;
...
istream_iterator():in_stream(0){} // eos
istream_iterator(istream_type& s):in_stream(&s){++*this;} // 进++
istream_iterator<T,charT,traits,Distance>& operator++()
{
if(in_stream && !(*in_stream >> value)) // 开始读了
in_stream = 0;
return *this;
}
const T& operator*() const { return value; }
...
9 STL周围
9.1 万用Hash Function
Hash Function的常规写法:其中 hash_val
就是万用Hash Function
class CustumerHash
{
public:
size_t operator()(const Customer& c) const
{ return hash_val(c.fname(), c.lname(), c.no()); }
};
还可以直接用函数实现,或者写一个
hash
的特化版本
原理:
通过三个函数重载实现从给入数据中逐一提取来不断改变 seed
// 第一个函数 首先进入该函数
template <typename... Types>
inline size_t hash_val(const Type&... args)
{
size_t seed = 0; // 设置初始seed
hash_val(seed, args...); // 进入第二个函数
return seed; // seed就是最后的HashCode
}
// 第二个函数 该函数中逐一提取一个参数
template <typename T, typename... Types>
inline void hash_val(size_t& seed, const T& val, const Types&... args)
{
hash_combine(seed, val); // 逐一取val,改变seed
hash_val(seed, args...); // 递归调用自己,直到取完进入第三个函数
}
// 第三个函数
template <typename T>
inline void hash_val(size_t& seed, const T& val)
{
hash_combine(seed, val); // 取最后一个val,改变seed
}
// 改变seed的函数
template <typename T>
inline void hash_combine(size_t& seed, const T& val)
{
// 乱七八糟的运算,越乱越好
seed ^= hash<T>()(val) + 0x9e3779b9 + (seed<<6) + (seed>>2);
}
C++11中 variadic templates:
从传入的内容(任意个数,任意元素类型)分为一个和其他,递归再分为一个和其他······
0x9e3779b9:是黄金比例!
9.2 Tuple
可以将一些东西组合在一起
9.2.1 用例
-
创建
tuple
tuple<string, int, int, complex<double>> t; tuple<int, float, string> t1(41, 6.3, "nico"); auto t2 = make_tuple(22, 44, "stacy");
-
输出
tuple
// 输出t1中的第一个 cout << get<0>(t1) << endl; // 41 cout << t << endl; // 在VS2022上并没有<<的重载
-
运算
t1 = t2; if(t1 < t2) // 以特定的方式进行的比较 { ... }
-
绑定解包
tuple<int, float, string> t3(77, 1.1, "more light"); int i; float f; string s; tie(i, f, s) = t3; // i == 77, f == 1.1, s == "more light"
-
// tuple里有多少类型 tuple_size< tuple<int, float, string> >::value; // 3 // 取tuple里面的类型,前面一堆代表float tuple_element<1, TupleType>::type fl = 1.0; // float fl = 1.0;
9.2.2 原理
依然是使用 variadic templates,通过递归继承,不断从 ...
中提取内容
// 空的tuple
template <> class tuple<> {}; // 直到取完
// tuple主体
template <typename Head, typename... Tail>
class tuple<Head, Tail...>
: private tuple<Tail...> // 递归继承
{
typedef tuple<Tail...> inherited;
public:
tuple() {}
tuple(Head v, Tail... vtail)
: m_head(v), inherited(vtail...) {}
...
protected:
Head m_head; // 每次取出的元素
};
👈🏻不断的继承就可以实现不同类型的组合了
其余函数:
...
{
public:
...
Head head() { return m_head; }
inherited& tail() { return *this; } // 通过转型获得Tail部分
...
};
一般不这么用
9.3 type traits
泛化模板类,包括五种比较重要的typedef
默认构造函数重要吗?
拷贝构造函数重要嘛?
拷贝赋值构造函数重要嘛?
析构函数重要嘛?
是不是旧格式(struct,只有数据,没有方法)?
默认的回答都是重要的!
比如说对于int的ttype traits,五个问题的回答都不重要。一般是算法会对traits进行提问。
实用性不高。
9.3.1 用例
GCC2.9中:
默认的 __type_traits
进行了一系列泛化的设定(trivial 是不重要的意思)
struct __true_type {};
struct __false_type {};
template <class type>
struct __type_traits
{
typedef __true_type this_dummy_member_must_be_first;
typedef __false_type has_trivial_default_constructor;
typedef __false_type has_trivial_copy_constructor;
typedef __false_type has_trivial_assignment_operator;
typedef __false_type has_trivial_destructor;
typedef __false_type is_POD_type; // Plain Old Data 类似C的struct
};
还会通过特化来实现针对不同类型的设定,例
template <> struct __type_traits<int>
{
typedef __true_type has_trivial_default_constructor;
typedef __true_type has_trivial_copy_constructor;
typedef __true_type has_trivial_assignment_operator;
typedef __true_type has_trivial_destructor;
typedef __true_type is_POD_type;
};
C++11中:
有了很多个 type traits,可以回答更多问题
测试:
cout << is_void<T>::value << endl;
cout << is_integral<T>::value << endl;
cout << is_floating_point<T>::value << endl;
cout << is_array<T>::value << endl;
...
不论是什么类型都可以自动检测它的 traits,非常厉害!(里面有虚函数——就能自动检测出它有多态性)
9.3.2 原理
模板的作用
例 is_integral
依然是采用的一种问答的方式实现的
template <typename _Tp>
struct is_integral
:public __is_intagral_helper<typename remove_cv<_Tp>::type>::type
{ };
首先 remove_cv
(const
和 volatile
)
// 通过偏特化实现remove const
template <typename _Tp>
struct remove_const
{ typedef _Tp type };
template <typename _Tp>
struct remove_const<_Tp const>
{ typedef _Tp type };
// remove volatile 同理
再通过 __is_intagral_helper
进行问答
// 通过偏特化实现
template <typename>
struct __is_integral_helper
:public false_type { };
template <>
struct __is_integral_helper<bool>
:public true_type { };
template <>
struct __is_integral_helper<int>
:public true_type { };
template <>
struct __is_integral_helper<long>
:public true_type { };
...
其他深入 class 内部的一些 traits 比如是否有虚函数,是否是一个类,是否是POD等等,其实现可能都与编译器有关
9.4 move
moveable class 中有:
// move ctor
MyString(MyString&& str) noexcept // 用&&与普通版本区别开
: _data(str._data), _len(str._len)
{
str._len = 0;
str._data = NULL; // 避免析构函数释放资源
}
// move assignment
MyString& operator=(MyString&& str) noexcept
{
if (this != &str)
{
_len = str._len;
_data = str._data;
str._len = 0;
str._data = NULL; // 避免析构函数释放资源
}
return *this;
}
// dtor
virtual ~MyString()
{
if(_data) delete _data; // 一定要检查
}
MyString C11(C1); // ctor
MyString C12(move(C1)); // move ctor
是浅拷贝,并且把之前的指向去除了
对于 vector 这样的容器,其用 move 就只是 swap 了三根指针,非常快!
move 之后原来的东西不能再使用,比如拿数据插入容器,用临时对象,编译器看到就会自动使用 move 版本的
MyString C11(C1);
时,创建了一个实例 C11,编译器就不知道是否能用 move,就需要自己MyString C12(move(C1));
使用 move,但注意之后一定不能用原来的C1
&&
(右值引用)这是C++11引入的特性,右值引用用于处理临时对象或将资源所有权转移给其他对象,以提高性能和资源管理
moveable元素对各种容器的速度效能影响
-
moveable指的是move构造、move赋值
-
move():是一种浅层拷贝,当用a初始化b后,a不再需要时,最好是初始化完成后就将a析构,使用move最优。
-
如果说,我们用a初始化了b后,仍要对a进行操作,用这种浅层复制的方法就不合适了。所以C++引入了移动构造函数,专门处理这种,用a初始化b后,就将a析构的情况。这种操作的好处是:将a对象的内容复制一份到b中之后,b直接使用a的内存空间,这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷。
-
移动构造函数实现是:调用拷贝构造函数,但是会将原来的对象中的成员变量置0!这样就不会调用原对象的析构函数了!如下图加深的部分,而且用的是引用的引用&&!&&是右值引用,右值有一个很重要的性质:只能绑定到一个将要销毁的对象
-
move的使用场景是:原来的对象不再使用。
-
调用移动构造函数方法,显示调用move:
classObj_1(std::move(classObj_2))
感谢您的关注!!