使用 C++ 智能指针的技巧。
我想分享一下我在处理内存分配和内存泄漏问题方面的经验。
引言
C++ 以其效率而闻名。另一方面,它也有一些让开发者头疼的陷阱。如您所知,野指针和内存泄漏是我们最常遇到的问题。这些问题有时会让一些开发者感到绝望,使得使用 C++ 变得困难。其实,有一些方法可以避免这些问题。在这里,我想分享一下我处理这些问题的经验。
智能指针
大多数 C++ 开发者都了解智能指针。我们可以使用 Boost 库或 C++11 中的智能指针。有很多文章介绍智能指针,但很少有文章能清楚地说明如何正确使用它们。不幸的是,在使用智能指针时,有些观点需要被忽略。在某种程度上,这是正确的。不恰当地使用智能指针可能会带来不良后果,甚至导致内存泄漏。所以,让我们来谈谈使用智能指针的规则。
通常有三种智能指针:
- 作用域指针:作用域指针会在指针退出代码语句时自动释放对象。这很简单,我们几乎不会出错。所以,我们会更多地讨论它。
- 共享指针:对象直到没有共享指针引用它时才会被释放。这对于避免野指针非常有帮助。
- 弱指针:弱指针必须与共享指针结合使用。弱指针引用共享指针。弱指针可以转换为共享指针。但是,如果共享指针已经释放了对象,弱指针会自动释放引用。
用于使用共享指针的 Mgr 类
我们喜欢使用类似 xx_mgr
的类来管理对象。下面是一些以游戏服务器模型为例的代码:
class palyer_t
{
public:
int id();
};
typedef shared_ptr<palyer_t> palyer_ptr_t;
class player_mgr_t
{
public:
int add(palyer_ptr_t player_)
{
m_players[player_->id()] = player_;
return 0;
}
palyer_ptr_t get(int id_)
{
map<int, palyer_ptr_t>::iterator it = m_players.find(id_);
if (it != m_players.end())
{
return it->second;
}
return NULL;
}
int del(int id_)
{
m_players,erase(id_);
return 0;
}
protected:
map<int, palyer_ptr_t> m_players;
};
player_mgr_t
控制 player_t
的生命周期。我们必须确保只有 player_mgr_t
拥有 player_t
共享指针。我们可以使用一个 player_t
共享指针的临时变量,当函数退出时它会自动释放。所以,当玩家离线时,我们将其从 player_mgr_t
中删除,这样 player_t
对象就会被自动释放。这就是我们期望的。
用于使用共享指针的资源对象
我们都熟悉从文件或数据库读取内容,然后 new
一个对象或缓冲区来存放的情况。我最近在我的项目中解决了一个由 MySQL 查询引起的内存泄漏。mysql_fetch_result
的 MySQL API 返回一个需要释放的对象,即使它包含零行。例如:
struct db_data_t{
strint data;
};
typedef shared_ptr<db_data_t> db_data_ptr_t;
db_data_ptr_t load_data(const string& sql_){
//.....
return new db_data_t();
}
int process_1(db_data_ptr_t data_);
int process_2(db_data_ptr_t data_);
int process_3(db_data_ptr_t data_);
int process(db_data_ptr_t data_){
db_data_ptr_t data = load_data(sql);
if (process_1(data)){
return -1;
}
if (process_2(data)){
return -1;
}
if (process_3(data)){
return -1;
}
}
正如您所见,每当我们退出进程时,db_data
对象将被析构。这确保了 db_data_t
不会引起内存泄漏。
用于使用共享指针的属性对象
这种情况发生在两个对象之间存在所有者关系时。例如,player_t
可能拥有武器,也可能没有。一旦我们为 player_t
分配了一个武器对象,武器将不会被销毁,直到 player_t
被销毁。让我们看一个示例代码。
class weapon_t;
typedef shared_ptr<weapon_t> weapon_ptr_t;
class palyer_t
{
public:
int id();
weapon_ptr_t get_weapon() { return m_weapon; }
void set_weapon(weapon_ptr_t weapon_) { m_weapon = weapon_; }
protected:
weapon_ptr_t m_weapon;
};
用于使用弱指针的引用关系
我在上述情况之外还使用弱指针。以游戏服务器模型为例。怪物会攻击靠近它的玩家。所以 monster_t
类有一个 lock_target()
接口,它会被 AI 系统自动调用。如果目标玩家离线,我们应该将怪物的目标玩家设置为 null
。这很麻烦,也是我们经常出错的地方。而弱指针非常适合这种情况。
class monster_t{
public:
void set_target(shared_ptr<player_t> player_){
m_target_player = player_;
}
shared_ptr<player_t> get_target() { return m_target_player.lock(); }
protected:
weak_ptr<player_t> m_target_player;
};
如果真实的玩家离线,m_target_player
将自动设置为 null
。您还记得 mgr
对象的例子吗?如果我们结合起来,就可以轻松管理内存分配。
对象计数器
尽管我们使用智能指针,但我们很可能犯错误,导致程序内存持续增长。例如,在某些情况下,我们忘记在玩家离线时将 player_t
从 player_mgr_t
中删除,这并不会产生内存泄漏。另一个例子是,我们的程序有时内存增长的速度比其他时候快。但是,是哪个对象导致了这些内存分配呢?这通常无法在开发环境中测试。所以,我们需要随时了解对象的数量。通常,我们需要将对象数量的数据随着时间的推移转储到文件中。下面是一个简单的实现。
#include <stdio.h>
#include "base/fftype.h"
using namespace ff;
class foo_t: public ffobject_count_t<foo_t>
{
};
class dumy_t: public foo_t, ffobject_count_t<dumy_t>
{
};
int main(int argc, char* argv[])
{
foo_t foo;
dumy_t dumy;
map<string, long> data = singleton_t<obj_summary_t>::instance().get_all_obj_num();
printf("foo_t=%ld, dumy_t=%ld\n", data["foo_t"], data["dumy_t"]);
return 0;
}
输出
foo_t=2, dumy_t=1
我将上传实现代码的文件。我只发布了一些关键代码。首先,ffobject_count_t
会在构造期间自动增加对象的数量,并在析构期间自动减少数量。
class obj_counter_i
<pre>{
public:
obj_counter_i():m_ref_count(0){}
virtual ~ obj_counter_i(){}
void inc(int n) { (void)__sync_add_and_fetch(&m_ref_count, n); }
void dec(int n) { __sync_sub_and_fetch(&m_ref_count, n); }
long val() const{ return m_ref_count; }
virtual const string& get_name() { static string ret; return ret; }
protected:
volatile long m_ref_count;
};
template<typename T>
class obj_counter_t: public obj_counter_i
{
public:
obj_counter_t()
{
singleton_t<obj_summary_t>::instance().reg(this);
}
virtual const string& get_name() { return TYPE_NAME(T); }
};
template<typename T>
class ffobject_count_t
{
public:
ffobject_count_t()
{
singleton_t<obj_counter_t<T> >::instance().inc(1);
}
virtual ~ ffobject_count_t()
{
singleton_t<obj_counter_t<T> >::instance().dec(1);
}
};
如果我们及时将对象数量的数据转储到文件,我们将得到这样的文件。
obj,num,20120606-17:01:41
dumy,1111
foo,222
obj,num,20120606-18:01:41
dumy,11311
foo,2422
obj,num,20120606-19:01:41
dumy,41111
foo,24442
编写一个工具来分析数据很容易。例如,我们可以使用 Highcharts(JS 库)绘制线条。我已上传了我对该工具的实现。生成的图片如下。
摘要
- 我们应该谨慎处理 C++ 的内存分配。
- 智能指针可以帮助我们更轻松地处理内存分配。
- 在我看来,我们应该始终使用智能指针,但要正确使用。
- 我们应该知道何时使用共享指针,何时使用弱指针。如果您犯了错误,情况可能会比原始指针更糟。
- 对象计数器应该成为我们 C++ 程序基础设施的一部分。这有助于我们随着时间的推移分析内存分配。
- 更多代码可在 GitHub 上找到:https://github.com/fanchy/RedRabbit/blob/gh-pages/fflib/base/fftype.h