C++标准库的容器分为序列容器和关联容器。

序列容器简单的有vectorlistdeque,复杂的还有配接器stackqueuepriority_queue

关联容器简单的有setmap,复杂的有multisetmultimap,这都是基于RB-tree的,基于hashtable的也有hash-sethash-maphash-multisethash-multimap。

工作中很多容器并不常用,常用的无非这几个vectorlistsetmapqueue。下面就简单总结一下这几个简单容器的适用需求(游戏功能逻辑方面的需求)。

 

1.vector

使用频率超高。通常我们使用数组是因为有明确的上限。而使用vector是因为有可能扩展。

有明确上限的,比如月签到系统。只需要一个31长度的数组,因为一个月最多31天。

没有明确上限的,比如成就系统。成就系统里的杀怪成就,一开始需求可能是10,100,1000,也就是杀怪10个,100个,1000个可以获得奖励。但是上线之后觉得数值不合适,需要多一个2000的条目。这种就非常适合用vector。

更多的情况其实是用数组和vector都行的。这种情况就需要编程者自己斟酌了。

看一个例子,等级礼包。如图:

 

多个容器应用的分布 容器个数_多个容器应用的分布

我们先枚举出奖励的的各种状态。



enum RoleLevelRewardState
{
    RLR_INVALID = 0,            //不可领取
    RLR_CAN_GET,                //可领取
    RLR_HAS_GET,                //已领取
};



我们可以规定一个上限。这个上限是奖励的条目上限。如果策划需求改动了条目数量,比如加了一个100级的等级奖励,那我们就需要改动MAX_ROLE_REWARD这个值。

当然一般情况下,最好是扩展,而不改动已经存在的。因为要兼顾老玩家,比如老玩家已经100级了,前面领取过50,70,90级的奖励。但是这时策划需要增加一个60级的奖励…这就比较乱。最好和策划商量兼容老玩家。

如果是增加一个120级的奖励,这自然就比较简单。



static const int MAX_ROLE_REWARD = 3;



定义我们的数组:



int m_role_level_reward[MAX_ROLE_REWARD];



如果用vector,就是这样写:



std::vector<int> m_role_level_reward;



数组在构造函数里的初始化,或者重置清空通常是这样写:



memset(m_role_level_reward, 0, sizeof(m_role_level_reward));



vector初始化和清空就简单些:



m_role_level_reward.clear();



边界问题是我们需要重视的,在引用数组或vector元素时,都用首先判断边界。



bool UpdateRoleRewardState(int index)
{
    if (index < 0 || index >= MAX_ROLE_REWARD) return false;
        
    m_role_reward[index] = 1; //改变内容
    return true;     
}



更多的不同在于循环时:



for (int i = 0; i < MAX_ROLE_REWARD; ++i) // 数组



for (int i = 0; i < (int)m_role_level_reward.size() && i < MAX_ROLE_REWARD; ++i) // vector



(int)强转是因为.size()返回的是size_t类型,其实判断i < size已经足够,还要加上MAX_ROLE_REWARD是保证需求的数量限制和异常出现,较为稳妥。

当然vector用迭代器遍历更为标准。但下标遍历也是可以的。

 

2.list

有很多情况,需求可以使用list,也可以使用vector。其实只需要记得,如果需求需要随意在某个位置插入随意删除某个位置的记录,改变整体顺序的就用list,其他都可以用vector。

list是个循环的双向链表,vector是连续空间。所以list可以随意在任何位置插入,随意删除某个位置的记录,而vector不行。vector的insert和erase代价都很大,会使得迭代器失效。 

一个典型的例子,记录玩家杀死大boss的记录表。

多个容器应用的分布 容器个数_关联容器_02

首先它的数量肯定不确定的,所以数组不适合。而且要求只记录最近的50条,新的记录显示在前面。这就非常适合用list了。

因为我们肯定会有一个逻辑是记录数大于50的时候,删掉最后1条记录。再把新的记录加到前面。



static const MAX_KILL_RECORD_NUM = 50;
if ((int)m_list.size() > MAX_KILL_RECORD_NUM)
{
    m_list.pop_back();
    //m_list.push_front();  //插入的数据结构略过
}



list可以任意的insert,erase,pop,push都是代价很小的。

我们要遍历list,就不能像vector一样了。因为vector有[]操作,list没有。所以list需要用迭代器遍历。

 

3.map

map也是很常用的容器。与vector,list差别很大,它是一个关联容器。所谓关联容器就是有key和value对应。底层是红黑树(RB-Tree)实现的。

通常的使用场景是记录的数据有一个唯一的key作为索引的标志。比如结婚系统,夫妻的相关数据。

夫妻有关的数据结构比如是:



struct MarriageInfo
{
    char man_name[64];
    char woman_name[64];
    int man_role_id;
    int woman_role_id;
    int love_value; //恩爱值  
};



存在一个map里,key是夫妻俩的role_id组合。比如A的role_id是1000,B的role_id是1011,那么两人共同的夫妻数据key就是1000_1011,一个字符串类型。



std::map<string, MarriageInfo> m_marriage_data;



这个数据里有个唯一的key与其value对应。这种类型的数据通常用map合适。但其实你仔细想想,用vector也能实现需要的功能。那样的话数据结构里加一项类型key的项就行。取决于编程者的风格。



struct MarriageInfo
{
    string marriage_key;
    char man_name[64];
    char woman_name[64];
    int man_role_id;
    int woman_role_id;
    int love_value; //恩爱值  
};
std::vector<MarriageInfo> m_marriage_data;



 map在引用元素之前可以用key做find操作,来判断。



bool UpdateMarriageData(string key)
{
    std::map<string, MarriageInfo>::iterator iter = m_marriage_data.find(key);
    if (iter == m_marriage_data.end()) return false;
    iter->second.love_value = 100;
    return true;
}



插入一个map元素,通常可以make_pair一下,然后insert。也有一种简单的写法,就是直接[]引用。



bool InsertMarriageData(string key, MarriageInfo marriage_info)
{
    std::map<string, MarriageInfo>::iterator iter = m_marriage_data.find(key);
    if (iter == m_marriage_data.end()) return false;
    m_marriage_data[key] = marriage_info;
    return true;
}



 

4.set

set是其实是一种简单的map,它的value隐藏了,只显示key出来。set适用比较简单的需求。

比如记录玩家获得的物品奖励,已经获得过的物品不能再获得,物品id作为key。



std::set<ItemId> m_reward_record;



那么set和map的用法类似,是简化的map。取决于需求的复杂度。

 

5.queue

队列queue。queue其实是一种适配器。底层是deque实现,屏蔽了deque的部分功能,封装成一个队列。队列是一个后进先出的结构。

有一类需求适合,就是匹配需求。就拿吃鸡来说。首先进入的是一个等待副本。先进来的玩家进入排队。满100个人,队列就出去100个人,进入另外一个副本。

匹配实际是一个复杂的功能需求,取决于匹配的条件是什么。吃鸡的需求可能是满100个,送走100个。这100个人有什么等级限制啊,经验限制啊,小队限制啊,都是复杂的。

另外匹配还是一个轮询过程。轮询的优化策略也有讲究,不再展开。

但是毫无疑问queue队列很符合这类的需求。