有的公司用C++做后台服务器的不用STL,理由一般是低效。效率可能主要是和动态内存分配有关。可以用一些手段,使用STL的时候让系统在运行的过程中内存保持稳定:不出现运行时释放系统内存的行为。
通过举例子说明用STL保持运行时的内存稳定。
Role
抽象一个游戏角色。
RoleMgr
用一个hash_map管理所有的游戏角色,一个角色进入游戏的时候,RoleMgr就拿出一个Role对象。RoleMgr可以预先分配好所有的Role对象(当然是未初始化的),也可以运行的过程不断地创建Role对象,Role对象的内存空间肯定是稳定的,就是不删除重复利用。
RoleMgr重复利用内存规则,这个很多人肯定都是这样用的,但看到很多项目都用自己实现一套对象管理机制实现这一规则。
实现一个只有默认行为的Role:
1 #include <iostream>
2 #include <ext/hash_map>
3 #include <ext/pool_allocator.h>
4
5 typedef uint32_t RoleID;
6
7 class Role
8 {
9 };
RoleMgr:
11 class RoleMgr
12 {
13 public:
14 Role* Create(const RoleID& roleID)
15 {
16 Role* role = m_roleAllocator.allocate(1);
20 m_roleAllocator.construct(role, Role());
21 // ::new (role) Role;
22 m_roleHash.insert(std::make_pair(roleID, role));
24
25 return role;
26 }
27
28 void Destroy(Role* role)
29 {
30 m_roleAllocator.deallocate(role, 1);
31 m_roleAllocator.destroy(role);
32 }
33 private:
34 typedef __gnu_cxx::__pool_alloc<Role> RoleAllocator;
35 typedef __gnu_cxx::hash_map<RoleID, Role*, __gnu_cxx::hash<RoleID>,
36 std::equal_to<RoleID>, __gnu_cxx::__pool_alloc<RoleID*> > RoleHash;
37 RoleAllocator m_roleAllocator;
39 RoleHash m_roleHash;
40 };
把错误处理相关的代码给去了,一般server程序错误处理代码和正常逻辑的代码接近1:1,这样可以更好地定位bug。
Create就是从Allocator中取一块buff,然后用placement new,把Role对象放到这块内存里,同时调用Role 构造函数 & 拷贝构造函数。
Destroy把Create分配出来的内存放回到Allocator中,同时调用Role的析构函数。后面会讲不能调用这个这个析构函数。
RoleAllocator维护Role的内存,注意__pool_alloc<Role>,是Role,不是Role*。
hash_map的RoleID(key), Role*(value)也通过__pool_alloc管理。
这样只要RoleMgr生命还存在(析构函数没被调用?),RoleAllocator和RoleMgr里面的__pool_alloc内存是稳定的,不会被释放。
接下来,我们继续完善例子。现在的游戏都有道具任务宠物之类的乱七八槽的东西,把一个简单的道具加入到例子里。
Item
道具的抽象。
ItemMgr
管理道具对象。
ItemMgr和RoleMgr是一样的,RoleID换成ItemID, Role换成Item。可以一个通用模板类,ObjMgr:
template <class ObjID, class Type> class ObjMgr;
Role有了一个道具管理器:
18 class Role
19 {
20 private:
21 ItemMgr m_itemMgr;
22 };
注意看RoleMgr::Destroy()
31 m_roleAllocator.destroy(role);
Role的析构被调用了,ItemMgr的析构也被调用了,ItemMgr里各种Allocator也会被析构,Allocator的内存就被释放,这个是不允许的。
我们要做的是把把 31行去掉,项目成员们都要知道哪些类是不会调用析构函数的。
总结一下使用这篇文章的方法写代码时一些约束:
1. 只能用gnu g++较新的版本;
2. 类的成员不好用指针的形式;
3. 需要内存稳定的类的析构函数不执行;
4. 用Allocator::allocate出来的对象注意完全初始化。