关联容器&观察者模式

本文详细介绍了C++ STL中的关联容器,包括set、multiset、map和multimap,它们的特点和使用方法,如插入、查找、删除等操作。此外,还探讨了观察者模式的概念,展示了如何在C++中实现观察者模式,包括注册事件、通知观察者等关键步骤。
摘要由CSDN通过智能技术生成

STL中的关联容器

在C++的STL中提供了两种关联容器,包含四个具体容器,分别是:
集合&多重集合
set
multiset
映射&多重映射
map
multimap

相对于顺序容器依靠元素所在容器的位置(下标)进行查找并顺序存储,关联容器是通过关键值(key)查找和存储元素,关联容器自带排序效果,底层是由红黑树实现的,顶部是最大的元素,可以认为是一个大根堆。下来我们分别用例子来介绍关联容器。

set&multiset

set 单重集合 不允许数据重复 使用std::find()查找数据,关联容器中自带这个功能
multiset 多重集合 允许数据重复
set和multiset的每个“节点”可以存放一个元素,这个元素的形式不限,可以写基本类型也可以写自定义类型,但是需要注意的是,集合知道基本类型的比较方式,每次在存放数据的时候都会进行比较操作,如果放入的是自定义类型,那么就需要对<符号进行重载,标记出实际的比较键,先看set的基本使用,之后会有一个朋友圈的例子来展示set存放自定义类型的适用场景。

int main()
{
	int arr[] = {1,468,48,5,3,4,6};
	int len = sizeof(arr)/sizeof(arr[0]);
	std::set<int>myset(arr,arr+len);

	//std::set<int>::iterator fit = myset.upper_bound(4);//> 返回大于4的第一个数字
	//std::set<int>::iterator fit = myset.lower_bound(4);//<= 返回小于等于4的第一个数字
	//在实现上来看,和std::find(myset.begin(), myset.end(), 12);相同,可以理解为是一个函数对象
	myset.insert(10);//插入也会按序排列
	myset.erase(myset.begin());//删除时即使给了begin也不一定删除放入数据的第一个元素,因为已经排过序了
	std::set<int>::iterator fit = myset.begin();
	//myset.insert(10);/这里会失效,因为set不支持重复元素放入

	while(fit != myset.end())
	{
		std::cout<<*fit<<" ";
		fit++;
	}
	std::cout<<std::endl;
	return 0;
}

set初始化时可以选择不带参数也可以像这样选择一个区间存放数据,当然,存放同类型的其他容器元素也可以用迭代器区间来初始化存放数据。set内部自带比较的效果,algorithm提供的sort是顺序排序,在这里不适用。

在insert插入数据中,set内部实现自动排列,我们无法确定插入后的元素到底存放在那个位置,同理删除的时候,如果传入的迭代器类型,即使删除begin()的元素,也不一定就是会将最小值/最大值删除。set在insert一个重复的key时会被拒绝,无法更新这个key。

upper_bound和lower_bound分别提供了第一个大于当前元素的值和小于等于当前元素的值,如果在第二个参数中传
greater<数据类型>()可以对这两种函数对象进行重载,将他们的作用取反,可以实现对输出进行控制。

也就是说如果upper_bound(begin,end,num,greater< type >)他的作用会变为寻找第一个小于num的值
lower_bound(begin,end,num,greater< type >)他的作用会变为寻找第一个大于等于num的值。

我在这里没有实现set_union()和set_intersection(),他们的作用是取容器的交集和并集,声明形式是:

set_union(a.begin,a.end,b.begin,b.end,insert_iterator<set< type>>(c,c.begin));//set_intersection接口和union相同。

前四个参数是需要进行交集/并集的容器的首尾迭代器,第五个是一个插入型迭代器,因为在set中将键看作为常量迭代器,而常量迭代器无法作为输出迭代子,无法修改迭代器指向的元素,这里需要使用输出迭代子insert_iterator或者inserter()函数对象。
给一个简单写法,因为需要填写五个参数,观察可知前四个的类型相似,这是我们可以使用宏函数来进行替换typedef也可以,但是没有必要。

#define ALL(x) x.begin(),x.end()
#define NET(x) insert_iterator< set< type>(x,x.begin())>
set_union(ALL(a),ALL(b),NET(c));//这样就可以更加清晰简单不易出错

下面是set存放自定义类型的例子:
FriendList

//set——一个实例 朋友圈
class Friend
{
private:
	std::string name;
	int age;
	friend std::ostream& operator<<(std::ostream&,const Friend&);
public:
	Friend(){}//在map容器中,由于先进行分配内存,此时看不到我们显示声明的构造函数,会调用默认构造函数分配空间
	//如果没有默认构造函数就会报错——没有合适的构造函数进行初始化
	Friend(std::string mname,int mage):name(mname),age(mage){}
	bool operator<(const Friend& rhs)const
	{
		return this->age < rhs.age;
	}
};
//使用map容器可以实现对好友的分类,比如1类好友 2类好友

class FriendList
{
private:
	std::set<Friend>myset;
public:
	void addFriend(Friend& fd)
	{
		myset.insert(fd);
	}
	void delFriend(Friend& fd)
	{
		myset.erase(fd);
	}
	void showFriend()
	{
		std::set<Friend>::iterator fit = myset.begin();
		while(fit != myset.end())
		{
			std::cout<<*fit<<" ";
			fit++;
		}
		std::cout<<std::endl;
	}
};
std::ostream& operator<<(std::ostream& out,const Friend& FD)
{
	out<<FD.name<<"==>";
	out<<FD.age;
	return out;
}
int main()
{
	FriendList FL;
	Friend f1("塔什干",16);//set不接受被比较的数据相同的情况,如果有需要,可以把set改为multiset
	Friend f2("吹雪",18);//对于<<重载是针对于set内部进行比较时需要的操作,因为是自定义类型放入set,所以必须重载<<
	FL.addFriend(f1);
	FL.addFriend(f2);
	FL.showFriend();
	return 0;
}

可以看出,当set存放自定义类型数据时,需要进行<运算符重载保证内部比较的实现,并且需要<<输出流重载来辅助输出数据,自定义的类型需要确定一个进行比较大小的键值,在重载<内部要体现出这点,本例子中使用的是年龄作为比较键。
multiset的例子:

typedef std::multiset<int> INTMS;
int main()
{
	const int size=16;
	int a[size]={17,11,29,89,73,53,61,37,41,29,3,47,31,59,5,2};
	INTMS intMultiset(a,a+size);	//用a来初始化INTMS容器实例
	std::ostream_iterator<int> output(std::cout, " ");
	//整型输出迭代子output,可通过cout输出用空格分隔的整数
	std::cout<<"这里原来有"<<intMultiset.count(17)//count计数器,计算容器内某元素个数
		<<"个数值17"<<std::endl;      //查找有几个关键字17
	intMultiset.insert(17);                   //插入一个重复的数17
	std::cout << "输入后这里有" << intMultiset.count(17) << "个数值17" << std::endl;
	INTMS::const_iterator result;//声明一个常量迭代器
	//const_iterator使程序可读INTMS的元素
	//但不让程序修改它的元素,result为INTMS的迭代子
	result=intMultiset.find(18);
	//找到则返回所在位置,设找到返回与调end()返回的同样值
	if (result == intMultiset.end()) 
		std::cout << "没找到值18" << std::endl;
	else 
		std::cout << "找到值18" << std::endl;
	std::cout<<"intMultiset容器中有"<<std::endl;
	std::copy(intMultiset.begin(), intMultiset.end(), output);//copy的重载
	//输出容器中全部元素
	std::cout << std::endl;

	return 0;
}

multiset可以让比较键值重复,这里的排序是稳定的。

map&multimap

还是以朋友圈举例,map 单重映射key_value 键值对集合 一个元素上三个域或者四个域–>左节点|K_V|右节点或者是|左节点|K|V|右节点,
map有三种插入方式
pari -->std::pair<int,Friend>mypair(1,FL1);mymap.insert(mypair);
value_type -->std::map<int,Friend>::value_type mytype(1,FL2); mymap(mytype);
前两种方法相当于已经建立好了K_V序列,这时存放是一个整体,所以无需提供默认构造函数
同时也可以将每个方法的两步合为一步,以生成临时对象的方式生成一个新的数据对,将这个临时数据对插入mymap后,临时对象生命周期结束,系统调用析构销毁,很正常也很合理,推荐使用这个方法。
mp[3] = 33;输出时需要从迭代器中获取 pair->first键值 pair->secand值的值
想要使用mp[3] = fi//fi是一个对象,这个表达式是由两个动作[] = 构成,第一步无法拿到fi的值,初始化也就只能使用默认初始化
所以此时fi必须放入默认构造函数
pair和value_type如果做出修改会被拒绝 mp[3] = fi修改会成功
Multimap 多重映射 允许一键的值重复,其他和map相同,只是multimap没有第三种[]插入方法。
map以朋友圈为例

int main()
{
	std::map<int,Friend>mp;//第二个参数也可以是容器,没问题
	Friend f1("塔什干",16);
	Friend f2("吹雪",16);//在set中,需要比较的值不能重复
	Friend f3("约翰斯顿",16);
	Friend f4("白雪",16);
	Friend f5("深雪",16);
	//在set例子中就是年龄不能重复,局限性很大
	mp.insert(std::pair<int,Friend>(1,f1));//第一种插入方式
	mp.insert(std::pair<int,Friend>(2,f2));
	std::map<int,Friend>::value_type mymap(2,f4);
	mp.insert(mymap);
	//第一种第二种方法尝试修改已有的key中的数据会被拒绝
	//mp.insert(std::map<int,Friend>::value_type(2,f5));
	mp[2] = f5;//第三种方法修改成功
	//第二种插入方式,每个容器都有自己的value_type接口,可以这样做
	mp[3] = f3;//第三种插入方式,这个表达式是由两个动作[] = 构成,第一步无法拿到f3的值,初始化也就只能使用默认初始化
	//也就是说只有使用第三种情况是在map构造函数时完成,所以需要value自己的默认构造函数
	//map源码中一定有[] = 两个的operator重载
	//前两种方法都是已经创建好了map对象,所以不需要value的默认构造函数
	//为了一致性,在value类中都加上默认构造函数即可
	std::map<int, Friend>::iterator it = mp.begin();
	while (it != mp.end())
	{
		std::cout << it->first << " ==> ";
		std::cout << it->second << std::endl;
		it++;
	}
	return 0;
}

在这里插入图片描述
可见,只有第三种方法尝试修改已存在的键值内部数据是成功的。
以上例子中,可以实现1类好友,2类好友类似的实现,当然,键值也可以是string类型,这样更加直观,排序时会按字典排序将键值升序排列,并且map支持下标访问,通过键值来访问。
问题来了,一类好友难道只能放一个好友?明显不符合实际,这样我们就要引入multimap了。

int main()
{
	std::multimap<int,Friend>mp;//第二个参数也可以是容器,没问题
	Friend f1("塔什干",16);
	Friend f2("吹雪",16);//在set中,需要比较的值不能重复
	Friend f3("约翰斯顿",16);
	Friend f4("白雪",16);
	Friend f5("深雪",16);
	//在set例子中就是年龄不能重复,局限性很大
	mp.insert(std::pair<int,Friend>(1,f1));//第一种插入方式
	mp.insert(std::pair<int,Friend>(2,f2));
	std::map<int,Friend>::value_type mymap(2,f4);
	mp.insert(mymap);
	//第二种插入方式,每个容器都有自己的value_type接口,可以这样做
	mp.insert(std::map<int,Friend>::value_type(2,f5));
	mp.insert(std::pair<int,Friend>(3,f3));
	//mp[3] = f3;//multimap没有[]重载,只能使用前两中方式操作
	std::map<int, Friend>::iterator it = mp.begin();
	while (it != mp.end())
	{
		std::cout << it->first << " ==> ";
		std::cout << it->second << std::endl;
		it++;
	}
	return 0;
}

在这里插入图片描述
关于multimap的注意事项在注释中已有注明,大家在阅读代码的时候可以参看。遍历的方法使用迭代器,这样和其他容器遍历并无差异。

观察者模式

何为观察者模式?

观察者
	查看事件是否有无发生/事件有无到来并通知感兴趣的监听者进行处理
	内部需要注册事件
	观察者内部实现需要有 事件--》监听者 多对多的关系,知晓接收到的事件是哪一个监听者感兴趣
监听者
	处理事件
	map<int,std::vector<Listener*>> 可以构成一对一的关系,也就是一个事件对应一个对其感兴趣的监听者队列
	每个监听者对同一个感兴趣的事件的处理方法不尽相同,所以这里要用动多态,调用虚函数,必须使用指针
	只有指针指向虚函数才可以实现动多态。
	通知事件

每个事件发生时,可能会有多个观察者感兴趣,同时一个观察者也可能会对多个事件感兴趣,不同的观察者处理同一个事件的行为不尽相同,所以有必要注意到这点。
下面写一个观察者模式的例子:
监听者可以有许多,他们基本类型相同,但是对于响应事件是不同的,此时可以考虑纯虚函数来实现
派生类实现虚函数,根据他们关注的事件进行操作
观察者只需要一个,存储着事件和与之感兴趣的监听者队列,自然需要在观察者上有相应的注册事件函数。
在注册事件时分为两个情况

  1. 事件已存在,此时只需要对本事件的监听者队列push新的监听者即可
  2. 事件不存在,此时需要申请一个监听者队列,本例子中监听者队列使用的是vector,申请号队列后将已有的一个监听者push到队列中即可

接下来,如何将观察者发现的事件通知观察者呢?这时需要一个接口,传入事件,在事件队列查找是否有此事件,如果没有则不作处理/给出错误提示,如果找到了事件,则需要开始遍历此事件的监听者队列,对迭代器解引用(本身存入的元素就是const Listener*)所以解引用后还是一个指针,再调用监听者基类的虚函数,此时会在虚表查找对应的虚函数地址,因为监听者队列里都是对此事件感兴趣的监听者,所以default就没有“用武之地”,但是为了安全还是得加上。

/*
	listener1     1  2
	listener2     2  3
	listener3     1  3
*/
class Listener//监听者们
{
public:
	Listener(std::string name):mname(name){}
	virtual void handlemessage(int Message)const = 0;
	//自身添加const修饰是保证常对象可以调用常方法
protected:
	std::string mname;
};

class Listener1:public Listener
{
public:
	Listener1(std::string name):Listener(name){}
	virtual void handlemessage(int message)const
	{
		switch(message)
		{
		case 1:
			std::cout<<mname<<" focus on "<<std::endl;break;
		case 2:
			std::cout<<mname<<" focus on "<<std::endl;break;
		default:
			std::cout<<mname<<" not focus on "<<std::endl;break;
		}
	}
};

class Listener2:public Listener
{
	public:
	Listener2(std::string name):Listener(name){}
	virtual void handlemessage(int message)const
	{
		switch(message)
		{
		case 2:
			std::cout<<mname<<" focus on "<<std::endl;break;
		case 3:
			std::cout<<mname<<" focus on "<<std::endl;break;
		default:
			std::cout<<mname<<" not focus on "<<std::endl;break;
		}
	}
};

class Listener3:public Listener
{
	public:
	Listener3(std::string name):Listener(name){}
	virtual void handlemessage(int message)const
	{
		switch(message)
		{
		case 1:
			std::cout<<mname<<" focus on "<<std::endl;break;
		case 3:
			std::cout<<mname<<" focus on "<<std::endl;break;
		default:
			std::cout<<mname<<" not focus on "<<std::endl;break;
		}
	}
};

class Observe//观察者
{
private:
	std::map<int,std::vector<const Listener*>> mmp;
public:
	void registermessage(int message,const Listener* p1)//注册事件
	{
		std::map<int,std::vector<const Listener*>>::iterator fit = mmp.find(message);
		if(fit != mmp.end())
		{
			fit->second.push_back(p1);
		}
		else
		{
			std::vector<const Listener*>vec;
			vec.push_back(p1);
			std::pair<int,std::vector<const Listener*>>mypair(message,vec);//第二个参数传递的是vector容器而不是元素
			mmp.insert(mypair);
		}
	}
	void nofity(int message)
	{
	auto fit = mmp.find(message);//auto == std::map<int,std::vector<const Listener*>>::iterator
		if(fit != mmp.end())
		{
			auto it = fit->second.begin();//auto == std::vector<const Listener*>::iterator
			while(it != fit->second.end())
			{
				(*it)->handlemessage(message);
				//使用hand接口通知事件
				it++;//遍历Listener集合
			}
		}
	}
};

int main()
{
	Listener1 l1("listener1");
	Listener2 l2("listener2");
	Listener3 l3("listener3");

	Observe ob;
	ob.registermessage(1, &l1);
	ob.registermessage(2, &l1);
	ob.registermessage(2, &l2);
	ob.registermessage(3, &l2);
	ob.registermessage(1, &l3);
	ob.registermessage(3, &l3);

	ob.nofity(1);
	return 0;
}

在本例子中,有些迭代器的名字过于长,写起来也很不方便,在返回时接受此类型数据时,可以使用auto关键字来自动识别接收,但是使用过多也会让代码可读性下降,如果能记住的话,最好原样写,或者使用typedef声明一个好记的别名也是可以的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值