在C++中,适配器是一种设计模式,它用于将一个类的接口转换成另一个类的接口,以满足不兼容的接口之间进行交互的需求。适配器模式可以解决不同类之间接口不匹配的问题,使它们能够协同工作。
适配器模式通过引入一个中间层来实现接口的转换。这个中间层就是适配器,它封装了原始类(被适配者)的接口,并提供了新的接口给目标类使用。适配器模式使得目标类不需要直接依赖于原始类,而是通过适配器来间接访问原始类的功能。
适配器模式可以使得已有的类与其他类协同工作,提高代码的可复用性和灵活性。它常用于对已有类库的接口进行扩展或兼容性处理,以便在不改变原有代码的情况下实现接口的适配。
简单来说,适配器就是对已经存在的类的重新封装,更直接地说就是对已经存在的类的接口进行重新封装,得到我们想要的类及其需要的接口,来满足我们的需求。
C++STL标准库里面的stack、queue、优先级队列以及反向迭代器就是利用了适配器模式适配出来的。
如何利用适配器模拟实现C++STL里面的栈容器呢?通过前面的知识我们知道。栈是一种具有后进先出(LIFO)特性的数据结构,栈的底层可以是数组,也可以是链表,即数组栈和链式栈。
因为适配器就是对已有的容器的进行封装得来的,而我们C++的STL库里面刚好就有vector和list容器,所以我们当然就可以利用vector或者list模拟实现一个栈容器了,在这里我就用vector模拟实现一个数组栈。
namespace kb
{
//适配器需要传一个已有的容器作为模板参数
//容器的缺省参数是deque,deque是双端队列
//整体结构是一个链表结构,但是每一个节点
//存放的是一个数组的指针,并且指针是从中间
//节点开始存的,所以头插头删,尾插尾删效率
//都挺高,并且因为每个节点都是一个数组,也
//不用频繁地开空间,既避免了vector头插头删
//的效率低,也避免了list的频繁开空间,是一个
//不错的作为stack和list默认容器的缺省值
template <class T, class Container = deque<T>>
class stack
{
public:
//对于栈来说,push一般是调用数组的尾插实现的,虽然可以反过来设计,push调用
//头插,但是数组的头插效率太低,并且vector本身并没有提供头插的接口,所以push
// 用push_back,因为成员变量本身就是vector,所以直接调用vector的push_back
//函数即可
void push(const T& x)
{
_con.push_back(x);
}
//同样地,栈的pop就是从尾部删除一个数据即可,调用vector函数的pop_back接口
void pop()
{
_con.pop_back();
}
//栈顶元素就是数组的最后一个元素
T& top()
{
return _con.back();
}
const T& top() const
{
return _con.back();
}
//判空
bool empty() const
{
return _con.empty();
}
//计算有效数据个数
size_t size() const
{
return _con.size();
}
private:
//这个栈是借助传过来的容器实现其功能的,所以栈的成员变量就是
//传过来的容器实例化出来的对象
Container _con;
};
void test_stack(void)
{
//可以显式地传vector容器实现栈
stack<int,vector<int>> st;
st.push(1);
st.push(2);
st.push(3);
st.push(4);
st.push(5);
while (!st.empty())
{
cout << "top:" << st.top() << " size:" << st.size() << endl;
st.pop();
}
cout << endl;
}
}
那么如何利用适配器模拟实现C++STL里面的queue容器呢?通过前面的知识我们可以知道。队列是一种具有先进先出(FIFO)特性的数据结构,queue的底层一般默认都是用deque双端队列的,所以我们模拟实现的容器的缺省参数就用deque双端队列即可,双端队列是一种允许在两端进行高效插入和删除操作的线性数据结构。它通过使用一块连续的存储空间和一组指示头部和尾部位置的指针来实现。
namespace kb
{
template<class T, class Container = deque<T>>
class queue
{
public:
//队列的插入就是尾插,因为queue要符合先进先出的结构特性
void push(const T& x)
{
_con.push_back(x);
}
//pop就是头插,先进先出
void pop()
{
_con.pop_front();
}
//队头元素就是第一个元素
T& front()
{
return _con.front();
}
const T& front() const
{
return _con.front();
}
//队为元素就是最后一个元素
T& back()
{
return _con.back();
}
const T& back() const
{
return _con.back();
}
//队列元素个数
size_t size() const
{
return _con.size();
}
//判空
bool empty() const
{
return _con.empty();
}
private:
Container _con;
};
void test_queue(void)
{
queue<int> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
q.push(5);
while (!q.empty())
{
cout << "front:" << q.front() << " size:" << q.size() << endl;
q.pop();
}
cout << endl;
}
}
优先级队列(Priority Queue)是一种特殊的队列数据结构,它能够保证元素的出队顺序是基于优先级的,即具有最高优先级的元素先出队。
在C++中,std::priority_queue是标准库提供的实现优先级队列的容器适配器。它基于堆(Heap)数据结构来实现,常用的堆实现方式是二叉堆(Binary Heap)。
二叉堆是一种完全二叉树,其中每个节点的值都大于(或小于)其子节点的值(最大堆或最小堆)。在std::priority_queue中,默认使用最大堆来实现,即优先级最高的元素在队首。
需要注意的是,默认情况下,std::priority_queue使得较大的元素具有更高的优先级。如果需要改变比较方式,可以自定义仿函数(Functor)或使用lambda表达式作为std::priority_queue的模板参数。
因为priority_queue的底层数据结构是堆,而对于堆的操作是需要大量的随机访问数据的,向上调整,向下调整等等,所以默认的容器是vector。
既然是优先级队列,那么肯定是需要比较的,而容器内的数据类型又是不确定的,如果比较的时候写死了直接按数值大小比较,对于有些类型来说是不适合的,例如传来的参数是地址,那么显然是不能直接比较大小的,而是要解引用得到对应的有效值比较,所以这个时候仿函数就登场了,仿函数能够自己定制相关的比较规则对不同的类型完成比较,那么仿函数究竟是何方神圣,又是怎么做到以上操作的?
在C++中,仿函数(Functor)是一种行为类似函数的对象,可以像函数一样被调用,也可以像普通对象一样被复制、赋值和传递。
仿函数可以实现函数调用运算符(operator()),这使得它们可以像函数一样被调用,接受参数并返回结果。这种通过重载函数调用运算符的方式,使得对象像函数一样具备了可调用的特性。
如下就是一个仿函数的示例:
//带模板参数的仿函数类
template <class T>
struct Greater
{
bool operator()(const T& x1, const T& x2)
{
return x1 < x2;
}
};
int main()
{
//用仿函数类定义出一个仿函数类对象,该对象能够像函数一样调用
Greater<int> Gt;
cout << Gt(3, 5) << endl;
return 0;
}
但是对于某些特殊的数据是不能直接进行大于小于比较的,例如指针,我们想要比较内容并不是指针本身,而是要比较指针指向的内容,这时我们就需要修改仿函数的operator()的比较规则,按照我们想要的比较方式进行比较。
例如:
template <class T>
struct Greater
{
bool operator()(const T& x1, const T& x2)
{
//x1和x2是地址,需要解引用再比较大小
return *x1 < *x2;
}
};
int main()
{
Greater<int*> Gt;
int* b = new int(5);
int* a = new int(3);
cout << Gt(a, b) << endl;
return 0;
}
对于像日期类这样的自定义类型,我们也可以控制仿函数operator()里面的比较规则达到我们想要的比较结果。如此一来,把仿函数用到priority_queue队列里也是能够适配各种数据类型的比较了,只需要自己修改实现仿函数的比较规则,后续的代码里面只需要用仿函数对象完成比较即可。
#pragma once
namespace kb
{
//仿函数类模板
template<class T>
struct Less
{
//重载operator(),实例化出来的仿函数对象可以调用operator()
//完成对数据的比较逻辑,前提是元素要支持大于小于比较,如果不支持,
// 则需要我们自己重载元素比较的运算符,如果直接的大于小于比较不
//符合我们的需要,就需要修改比较规则,例如指针的直接比较不符合
// 我们的需求,就先解引用,再比较,总之,仿函数operator()的
//比较规则可以按照自己的需求定义,很灵活
bool operator()(const T& x1, const T& x2)
{
return x1 > x2;
}
};
//仿函数类模板
template<class T>
struct Greater
{
bool operator()(const T& x1, const T& x2)
{
return x1 < x2;
}
};
//对于不符合需求的比较,指针比较需要先解引用,再比较
//struct LessDate
//{
// bool operator()(const Date* x1, const Date* x2)
// {
// return *x1 > *x2;
// }
//};
//传递的第三个参数就是仿函数的类模板,C++库里面优先级队列的默认仿函数是Less,
//但是大的元素的优先级更高,恰恰和仿函数的less含义相反,所以这里是需要记忆的
template <class T,class Container=vector<T>,class Compare=Less<T>>
class priority_queue
{
private:
//向上调整(堆的知识)
void AdjustUp(int child)
{
//利用模板参数传过来的仿函数类模板实例化出仿函数对象
//可以像函数一样使用这个对象完成对两个数据的比较
Compare cmp;
int parent = (child - 1) / 2;
while (child > 0)
{
//cmp像函数一样使用,本质是仿函数对象(可调用对象)
if (cmp(_con[child], _con[parent]) > 0)
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) / 2;
}
else
{
break;
}
}
}
//向下调整(堆的知识)
void AdjustDown(int parent)
{
Compare cmp;
int child = parent * 2 + 1;
while (child < _con.size())
{
if (child + 1 < _con.size() && cmp(_con[child + 1], _con[child]) > 0)
{
child++;
}
if (cmp(_con[child], _con[parent]) > 0)
{
swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
public:
//需要注意的是,提供了这个用迭代器区间初始化的构造函数就必须要提供一个
//无参的构造函数作为默认构造函数,因为自己写了构造函数编译器就不会自动
//生成默认构造函数了
template<class InputIterator>
priority_queue(InputIterator first, InputIterator last)
{
//为了提高效率,可以先把数据全部尾插到容器中,容器可以看成是
//vector,支持[]访问元素,然后再对数组的元素进行建堆
while (first != last)
{
_con.push_back(*first);
++first;
}
//对数据的元素进行建堆,从最后一个非叶子节点开始往上做向下调整
for (int i = (size() - 1 - 1) / 2; i >= 0; i--)
{
AdjustDown(i);
}
}
//必须提供无参的构造函数
priority_queue()
{}
void push(const T& x)
{
//对于堆来说,插入数据就是先尾插到数组的尾部,
_con.push_back(x);
//再堆这个新插入的数据做一遍向上调整即可
AdjustUp(_con.size() - 1);
}
void pop()
{
//pop的目的是删除top的数,即删除第一个数
//先把第一个数和最后一个数交换
swap(_con[0], _con[_con.size() - 1]);
//删除最后一个数,本质就是删除了堆顶top的数
_con.pop_back();
//再对交换到堆顶的元素做一遍向下调整即可
AdjustDown(0);
}
const T& top()
{
//堆顶元素
return _con[0];
}
//判空
bool empty() const
{
return _con.empty();
}
//计算队列的数据个数
size_t size() const
{
return _con.size();
}
private:
//优先级队列默认的容器是vector
Container _con;
};
void test_PriorityQueue1(void)
{
std::vector<int> v = { 1,2,3,4,5 };
priority_queue<int,vector<int>,Greater<int>> pq(v.begin(), v.end());
/*pq.push(1);
pq.push(2);
pq.push(3);
pq.push(4);
pq.push(5);*/
while (!pq.empty())
{
cout << pq.top() << " ";
pq.pop();
}
cout << endl;
}
}
相信到现在为止迭代器我们是非常的熟悉了,它提供了一种统一的方式让我们访问容器的元素,而无需知道容器的底层实现。当然,反向迭代器顾名思义就是反着走的迭代器咯,正向迭代器的++就是反向迭代器的–,正向迭代器的–就是反向迭代器的++咯。能支持正向迭代器的一般都需要支持反向迭代器的,包括vector,list等等,所以反向迭代器也是一种适配器,即把其它容器的正向迭代器作为模板参数传过来,通过封装就能得到对应的反向迭代器了。
namespace kb
{
//为什么要传Ref和Ptr过来,因为*的返回值是类型的引用,
// ->的返回值是指针,跟正向迭代器的返回值是一样的
template <class Iterator,class Ref,class Ptr>
struct ReverseIterator
{
private:
//反向迭代器的成员变量就是一个容器的正向迭代器,本质就是对正向迭代器进行封装
Iterator _it;
typedef ReverseIterator<Iterator, Ref, Ptr> Self;
public:
//构造函数
ReverseIterator(const Iterator& it)
:_it(it)
{}
Ref operator*()
{
//曾经把 Iterator tmp(_it) 写成 Self tmp(_it) 造成死循环
//因为解引用是不能改变指针的指向的,而根据反向迭代器的设计规则
//需要先--再解引用,所以需要创建一个临时的迭代器,使其先--,再
//返回值解引用之后的值
Iterator tmp(_it);
return *(--tmp);
}
Ptr operator->()
{
//operator->就复用operator*()即可
return &(operator*());
}
Self& operator++()
{
//反向迭代器的++等于正向迭代器的--
--_it;
return *this;
}
Self& operator--()
{
//反向迭代器的--等于正向迭代器的++
++_it;
return *this;
}
bool operator!=(const Self& it)
{
return _it != it._it;
}
bool operator==(const Self& it)
{
return _it == it._it;
}
};
}
以上就是今天想要跟大家分享的内容,你学会了吗?如果感觉到有所收获,那么就点点赞点点关注呗,后续还会持续更新C++的相关内容哦,我们下期见啦!!!!!!!!