65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C++ 智能指针的技巧。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.65/5 (9投票s)

2013年10月7日

CPOL

4分钟阅读

viewsIcon

25645

downloadIcon

297

我想分享一下我在处理内存分配和内存泄漏问题方面的经验。

引言

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_tplayer_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
© . All rights reserved.