最简单的方法计算对象的实例数量

21
我想知道在执行某个特定点时分配了某些对象的确切数量。主要是为了寻找可能的内存泄漏(我主要使用RAII,几乎不使用new,但仍然可能会在添加新元素之前忘记清除向量中的.clear()或类似操作)。当然,我可以有一个
atomic<int> cntMyObject;

我在析构函数中减少对象实例计数,在构造函数中增加对象实例计数,在复制构造函数中也这样做(我希望我覆盖了所有内容 :))。但这对每个类来说都是硬编码的,并且在“Release”模式下禁用它并不简单。 所以,有没有一种简单而优雅的方法可以轻松禁用对象实例计数?


2
为什么不使用配置文件来查找内存泄漏?... - user405725
2
不是赋值运算符——它不会改变类型中现有对象的数量,只会更改其中一个对象的值。 - Steve Jessop
2
尽管我认为添加全局对象计数器的想法很有趣,但对于你消除内存错误的实际问题,我会说通过Valgrind运行程序是一个更可行的解决方案,而且还能提供更有意义的信息。 - Kerrek SB
你准备接受编译器特定的解决方案吗?我可以提供一个非侵入式的gcc解决方案,而且msvc也有类似的函数。 - Flexo
在这里使用分析器并不是答案。因为提问者提到忘记vector.clear()将始终是一个问题。否则,在Java程序中就不会有内存泄漏。这不是典型的C++泄漏,但它是一个重要的问题,需要完全不同的工具来解决,而不是像valgrind等工具。 - Lothar
显示剩余2条评论
9个回答

35

创建一个“计数对象”类,它在其构造函数和析构函数中进行适当的引用计数,然后从中派生您想要跟踪的对象。然后可以使用奇妙的重复模板模式来获取任何您希望跟踪的对象类型的不同计数。

// warning: pseudo code

template <class Obj>
class CountedObj
{
public:
   CountedObj() {++total_;}
   CountedObj(const CountedObj& obj) {++total_;}
   ~CountedObj() {--total_;}

   static size_t OustandingObjects() {return total_;}

private:
   static size_t total_;
};

class MyClass : private CountedObj<MyClass>
{};

4
在复制构造函数中,通常不需要进行相同对象检查,因为从自身复制构造一个对象通常是无效的,因此任何这样做的人都是在玩愚蠢的游戏。但这并不会造成任何损害。如果需要,请根据个人喜好添加原子性或锁,但提问者已经知道这一点了。 - Steve Jessop
我原本希望找到一些不需要永久性代码更改的解决方案,但这个方案也很好。 - NoSenseEtAl
4
在编写计数器线程时是没有意义的。您可以使用CountObj类,并将其作为要跟踪对象的类的基类,然后在实例化对象的线程上下文中,计数将自动递增。请注意,这里的"count"指的是对象的数量,而不是计数器的值。 - Ajeet Ganga
赋值运算符怎么办?赋值操作不应该也将 total_ 增加吗? - Flow
1
赋值通常不会增加对象的数量(通过复制构造函数进行的赋值会增加对象的数量)。 - Chad
显示剩余8条评论

12

你可以采用这种方法。

#ifdef DEBUG

class ObjectCount {
    static int count;
  protected:
    ObjectCount() {
        count++;
    }
  public:
    void static showCount() {
        cout << count;
    }
};

int ObjectCount::count = 0;


class Employee : public ObjectCount {
#else
class Employee {
#endif
  public:
    Employee(){}
    Employee(const Employee & emp) {

    }
};

DEBUG 模式下,调用 ObjectCount::showCount() 方法将返回创建的对象数量。


9
更好的做法是使用内存分析和泄漏检测工具,例如Valgrind或Rational Purify。
如果您无法使用这些工具并且想要实现自己的机制,则应该重载类的newdelete运算符,然后在其中实现内存诊断。
请参考这个 C++ FAQ答案,了解如何执行此操作以及需要注意什么。

FAQ条目是否完全忘记提到标准要求operator-new在失败的情况下包含调用new-handler的无限循环? - Kerrek SB
@Kerrek SB:无限循环? 我不确定我理解了,难道 new 在失败的情况下不应该只是抛出 std::bad_alloc 吗? - Alok Save
应该有某种无限循环,因为处理程序可能会安装更多的处理程序。如果您的新处理程序提供了内存,则会中断,否则就没有更多的新处理程序并且会抛出异常。 - Kerrek SB
嗯,也许它实际上不在标准中。3.7.4.1.3 只是说你“可以”调用新处理程序。Scott Meyers #49建议循环调用新的处理程序。 - Kerrek SB
@Kerrek SB:这是由标准规定的还是实现细节?如果它是标准规定的(我不知道,但如果是的话会很有趣),您可以在C++ Lounge与Sbi讨论,或在FAQ答案中添加评论。(如果你们有对话,请在这里添加一个链接,这样我也可以跟进,因为我处于不同的时区,现在已经过了午夜,所以不能留下来:() - Alok Save

3
这是一个类似工作示例:http://www.almostinfinite.com/memtrack.html(只需复制页面末尾的代码并将其放入Memtrack.h中,然后运行TrackListMemoryUsage()或其他函数以查看诊断信息)。
它会覆盖operator new,并进行一些奥秘的宏操作,使其在分配每个对象时“标记”信息,从而使它能够计算对象实例数和使用的内存大小。但并不完美,他们使用的宏在某些情况下会出错。如果您决定尝试此功能,请确保在任何标准头文件之后包含它。

2

如果不了解您的代码和要求,我认为有两个合理的选择:

a) 使用 boost::shared_ptr。它内置了您建议的原子引用计数并处理内存管理(因此您实际上永远不需要关心计数)。 它的引用计数可以通过 use_count() 成员获得。

b) 如果像a)一样的含义,例如处理指针和在各处使用 shared_ptrs 或可能的性能开销对您不可接受,我建议简单地使用可用的内存泄漏检测工具(例如 Valgrind,见上文),这将在程序退出时报告您的未释放对象。而且没有必要使用侵入式辅助类来跟踪对象计数(无论如何仅限于调试),它们只会混乱您的代码,我个人认为。


1
我的方法会将泄漏计数输出到Debug Output(通过我们代码库中实现的DebugPrint函数,用你自己的函数替换该调用...)
#include <typeinfo> 
#include <string.h>
class CountedObjImpl
{
public:
        CountedObjImpl(const char* className) : mClassName(className) {}
        ~CountedObjImpl()
        {
                DebugPrint(_T("**##** Leakage count for %hs: %Iu\n"), mClassName.c_str(), mInstanceCount);
        }
        size_t& GetCounter() 
        {
                return mInstanceCount;
        }

private:
        size_t mInstanceCount = 0;
        std::string mClassName;
};

template <class Obj>
class CountedObj
{
public:
        CountedObj() { GetCounter()++; }
        CountedObj(const CountedObj& obj) { GetCounter()++; }
        ~CountedObj() { GetCounter()--; }

        static size_t OustandingObjects() { return GetCounter(); }

private:
        size_t& GetCounter()
        {
                static CountedObjImpl mCountedObjImpl(typeid(Obj).name());
                return mCountedObjImpl.GetCounter();
        }
};

使用示例:

class PostLoadInfoPostLoadCB : public PostLoadCallback, private CountedObj<PostLoadInfoPostLoadCB>

1
我们曾经使用一个带有内部计数器的基类的解决方案,并从中派生,但现在我们将其全部改为了boost::shared_ptr,它保留了引用计数并为您清理内存。Boost智能指针系列非常有用:boost smart pointers

1

一些答案中讨论了将计数器添加到单个类中。但是,这需要选择要计数的类并以某种方式修改它们。以下假设您正在添加此类计数器以查找保留比预期更多某些类的对象的错误。

简要回顾已经提到的一些事情:对于真正的内存泄漏,当然有valgrind:memcheck和泄漏消毒剂。但是,对于其他没有真正泄漏的情况,它们无法帮助(未清除的向量、从未访问键的映射条目、shared_ptr的循环等)。

但是,由于这没有被提到:在valgrind工具套件中还有massif,它可以为您提供有关所有分配的内存块及其分配位置的信息。但是,假设valgrind:massif对您也不可行,并且您确实想要实例计数。

如果你愿意尝试一些hackish的解决方案,而且上述选项都不起作用,为了偶尔进行bug调试,你可以考虑以下方法:现在,堆上的许多对象实际上由智能指针持有。这可以是标准库中的智能指针类,也可以是您使用的相应辅助库的智能指针类。然后,技巧就在于(以shared_ptr为例):通过修改shared_ptr实现,即向shared_ptr类添加实例计数器,可以一次获取许多类的实例计数器。然后,对于某个类Foo,属于shared_ptr<Foo>的计数器将为您提供类Foo的实例数量的指示。

当然,它并不像直接将计数器添加到各自的类中那样精确(仅由裸指针引用的实例不计入),但可能对您的情况足够准确。当然,这与永久更改智能指针类无关-只是在调试bug时。至少,智能指针实现不太复杂,因此对它们进行修补很简单。


-1

这种方法比这里的其他解决方案简单得多。

为计数制作一个变量,并使其静态。在构造函数中将该变量增加1,在析构函数中将其减少1。

确保初始化变量(不能在标题中初始化,因为它是静态的)。

.h

// Pseudo code warning
class MyObject
{
   MyObject();
   ~MyObject();
   static int totalObjects;
}

.cpp

int MyObject::totalObjects = 0;

MyObject::MyObject()
{
   ++totalObjects;
}

MyObject::~MyObject()
{
   --totalObjects;
}

每次创建新实例时,构造函数都会被调用,并且totalObjects自动增加1。

你还必须对复制构造函数进行仪器化。构造函数和析构函数必须在头文件中声明。如果你修复了这个问题,那么它本质上就与被接受的答案相同。 - HolyBlackCat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接