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

SmartObject 类(类似 Objective-C 的内存管理)用于 C++(Smart Pointer 的替代品?)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.55/5 (8投票s)

2012 年 9 月 18 日

MIT

9分钟阅读

viewsIcon

37559

downloadIcon

389

本文介绍了 Objective-C 风格的 C++ 内存管理类 SmartObject。

目录 

  1. 引言
  2. 背景
  3. SmartObject 类的目的
  4. SmartObject 类的优缺点
  5. SmartObject 类实现 
    1. 构造函数
    2. 复制构造函数
    3. 赋值运算符 (= operator)
    4. 析构函数
    5. RetainObj
    6. ReleaseObj
  6. 调试代码详解
  7. 使用场景示例
    1. 将类声明为 SmartObject 类
    2. SmartObject 类的正常使用
    3. 引用管理示例
    4. 引用管理示例 2
    5. 引用管理示例 3
  8. 更多实际示例来源
  9. 结论
  10. 参考

简介

内存管理在 C++ 开发中始终是一个大问题。C++ 中有很多有用的内存管理类,如 Smart pointer、Auto pointer 等。我相信 Code Project 或其他地方已经介绍了许多好的想法和实践。我最近有机会花时间研究 Objective-C 开发,并想知道如果将 Objective-C 的内存管理方式移植到 C++ 会怎么样。所以,这是 Objective-C 风格内存管理在 C++ 中的实现。我  认为这是 C++ 开发的“ 最佳实践 ”,但我认为这是一个可以分享的有趣想法。

背景

阅读我撰写的文章 “How to create a Simple Lock Framework for C++ Synchronization” 将会很有帮助,因为 SmartObject 类中使用的同步机制就来自这篇文章。如果您想了解 Objective-C 内存管理,也可以阅读 “Objective-C Memory Management”

SmartObject 类的目的

我必须说,与智能指针或自动指针不同,SmartObject 类的使用方式不当可能会非常危险。SmartObject 类的目的仅仅是我对将 Objective-C 风格的内存管理移植到 C++ 的好奇心。我有时自己也会使用 SmartObject 类进行内存管理,因为它方便且简单,但这可能是我花时间开发 Objective-C 项目的原因,所以对您来说,易用性可能不一定成立。

SmartObject 类的优缺点

  •  优点 
    • 对于有 Objective-C 开发经验的开发者(或许对其他人也一样?),内存管理易于使用且简单。
    • 在 Debug 模式下易于跟踪引用持有者(如果使用得当)。
  • 缺点 
    • 如果使用不当,可能会非常危险。
    • 如果不熟悉 Objective-C 内存管理的概念,很容易混淆。

SmartObject 类实现

SmartObject 类非常简单,除了构造函数/析构函数/运算符重载外,它只包含两个方法:“RetainObj” 和 “ReleaseObj”。因此,SmartObject 类的结构如下:

  • protected
    • SmartObject
      • 构造函数和复制构造函数 
    • ~SmartObject
      • 析构函数 
  • public
    • operator= 
      • 复制运算符重载 
    • RetainObj 
      • 保留持有对象 
    • ReleaseObj 
      • 释放持有对象 
  • 私有的
    • 用于引用计数、锁对象等的变量  
下面是 SmartObject 类的骨架声明:
// SmartObject.h
 
class SmartObject
{
public:
   SmartObject & operator(const SmartObject&b);
   void RetainObj();
   void ReleaseObj();
protected:
   SmartObject();                      // constructor 
   SmartObject(const SmartObject &b);  // copy constructor 
   virtual ~SmartObject();             // destructor 
private: 
   int m_refCount;             // reference counter  
};  
  • 请注意,出于调试目的,SmartObject 的实际实现是在头文件中完成的。以上类声明是为了便于理解。 
  • 另请注意,为了演示,本节代码中已移除同步代码和调试代码。请下载上面的源代码以查看完整实现。 

构造函数

// SmartObject.h
 
class SmartObject
{ 
... 
protected:
   ... 
   SmartObject()
   {
      m_refCount=1; 
   }  
   ... 
};   

创建时,它将引用计数器初始化为 1。原因如下:

  • 使其能够与 Objective-C 内存管理中的 ReleaseObj 函数匹配。(alloc release 匹配) 
  • 避免在 Debug 模式下错误使用 delete 运算符。
    • 这将在后面的部分解释。 
构造函数还声明为 protected,因为我不希望此类自行创建,并且子类在其创建时应能够访问构造函数。 

复制构造函数

// SmartObject.h
 
class SmartObject
{ 
...
protected:
   ...
   SmartObject(const SmartObject&b)
   {
      m_refCount=1; 
   }
   ...  
};  

复制构造函数与构造函数遵循相同的理念,但它不复制输入 SmartObject 对象中的任何内容,因为每个对象都应有自己的引用计数器,并且不应被其他对象替换。如果未声明复制构造函数(将自动使用默认复制构造函数),则引用计数器将被输入 SmartObject 对象的引用计数器替换(这是我们不希望发生的)。

  • 请注意,此处未显示,但在同步代码中,它实际上会复制输入 SmartObject 对象的 LockPolicy,并根据 LockPolicy 创建自己的锁。

复制运算符 (= operator)

// SmartObject.h
 
class SmartObject
{
...
public:
   SmartObject &operator = (const SmartObject &b)
   {
      return *this; 
   } 
   ...
};   
  • 请注意,“operator=” 返回自身而不执行任何操作的原因是我不希望 SmartObject 对象被其他对象替换。如果“operator=”未实现,当调用复制运算符时,它将自动调用默认复制运算符,并且引用计数器的值将被其他对象的值替换。我之所以没有将其设为 private,是因为如果复制运算符是 private 的,当某个类 A 是 SmartObject 类的子类,并且类 A 的对象尝试从另一个对象复制时,将会导致编译器错误。 

析构函数

// SmartObject.h
 
class SmartObject
{ 
...
protected:
   ...
   ~SmartObject()
   {
      m_refCount--; 
      assert(m_refCount==0); 
   }
   ...    
};

SmartObject 类的理念(当然,这是 Objective-C 内存管理的理念)是使 new 运算符和 ReleaseObj 函数一一对应,并使 RetainObj ReleaseObj 函数一一对应。我还想使用 new delete 运算符进行正常的 C++ 内存管理,但为了提供一些安全性,我通过断言来限制 delete 运算符的使用,仅在引用计数为 1 时才允许。 

因此,析构函数将引用计数器减 1,并通过 assert 检查引用计数是否为 0。这可以防止如上所述的 delete 运算符的无效使用(因为 delete 运算符只能在引用计数为 0 时调用,否则会断言)。 

RetainObj

// SmartObject.h
 
class SmartObject
{ 
public:
   ...
   RetainObj()
   {
      m_refCount++; 
   }
   ...    
};     

如上所示,RetainObj 函数是一个简单的操作,它只是将引用计数器加 1。当调用它时,其理念是,一个 RetainObj 函数应该与一个 ReleaseObj 函数相匹配。 

ReleaseObj

// SmartObject.h
 
class SmartObject
{ 
public:
   ...
   Release()
   {
      m_refCount--;
      if(m_refCount==0)
      {
         m_refCount++;
         delete this;
         return; 
      } 
      assert(m_refCount>=0);
   }
   ...    
};   

对于一般情况,它很简单,只是将引用计数器减 1。但是,当引用计数器达到 0 时,对象必须被删除,因为这意味着没有其他对象引用该对象,所以它会调用 delete 运算符来删除自身。 

  • 请注意,此函数在引用计数为 0 时将引用计数器加 1 的原因是支持 delete 运算符,如上一节所述,因为它使用与 delete 运算符相同的析构函数(无论是来自 ReleaseObj 函数还是其他地方),为了满足要求(即 delete 运算符必须在引用计数为 1 时调用),在 ReleaseObj 中,它必须在调用 delete 运算符之前将引用计数设置为 1。  

调试代码详解

// Defines.h
 
...
#define WIDEN2(x) L ## x
#define WIDEN(x) WIDEN2(x)
#define __WFILE__ WIDEN(__FILE__)
#define __WFUNCTION__ WIDEN(__FUNCTION__)
#if defined(_UNICODE) || defined(UNICODE)
#define __TFILE__ __WFILE__
#define __TFUNCTION__ __WFUNCTION__
#else//defined(_UNICODE) || defined(UNICODE)
#define __TFILE__ __FILE__
#define __TFUNCTION__ __FUNCTION__
#endif//defined(_UNICODE) || defined(UNICODE)
... 

为了使 SmartObject 类能够跟踪和管理引用,它需要引用持有者的文件名和函数名。由于编译器支持 __FILE__ __FUNCTION__ 作为默认功能,我只是通过宽化来声明 __WFUNCTION__ __WFILE__ 以支持 Unicode 版本。为了使其支持通用目的(Unicode 和非 Unicode 版本),我声明 __TFILE__ __TFUNCTION__ 根据 UNICODE 定义声明,可以是 __FILE__ __FUNCTION__,或者 __WFILE__ __WFUNCTION__

// SmartObject.h
 
class SmartObject
{
public:
   ...
 
   void RetainObj(
#if defined(_DEBUG)
      TCHAR *fileName, TCHAR *funcName, unsigned int lineNum
#endif //defined(_DEBUG)
      )
   {
      ...
#if defined(_DEBUG)
      LOG_THIS_MSG(_T("%s::%s(%d) Retained Object : %d (Current Reference Count = %d)"),
               fileName,funcName,lineNum,this, this->m_refCount);
#endif //defined(_DEBUG)
   }
 
   void ReleaseObj(
#if defined(_DEBUG)
      TCHAR *fileName, TCHAR *funcName, unsigned int lineNum
#endif //defined(_DEBUG)
      )
   {
      ...
#if defined(_DEBUG)
      LOG_THIS_MSG(_T("%s::%s(%d) Released Object : %d (Current Reference Count = %d)"),
              fileName,funcName,lineNum,this, this->m_refCount);
#endif //defined(_DEBUG)
      ...
   }
 
   ...
protected:
 
   SmartObject(
#if defined(_DEBUG)
      TCHAR *fileName, TCHAR *funcName, unsigned int lineNum,
#endif //defined(_DEBUG)
      )
   {
      ...
#if defined(_DEBUG)
      LOG_THIS_MSG(_T("%s::%s(%d) Allocated Object : %d (Current Reference Count = %d)"),
              fileName,funcName,lineNum,this, this->m_refCount);
#endif //defined(_DEBUG)
      ...
   }
 
   SmartObject(
#if defined(_DEBUG)
      TCHAR *fileName, TCHAR *funcName, unsigned int lineNum,
#endif //defined(_DEBUG)
      const SmartObject& b)
   {
      ...
#if defined(_DEBUG)
      LOG_THIS_MSG(_T("%s::%s(%d) Allocated Object : %d (Current Reference Count = %d)"),
               fileName,funcName,lineNum,this, this->m_refCount);
#endif //defined(_DEBUG)
      ...
   }
 
   ...
};
 
#if defined(_DEBUG)
#define SmartObject(...) SmartObject(__TFILE__,__TFUNCTION__,__LINE__,__VA_ARGS__)
#define ReleaseObj() ReleaseObj(__TFILE__,__TFUNCTION__,__LINE__)
#define RetainObj() RetainObj(__TFILE__,__TFUNCTION__,__LINE__)
#endif//defined(_DEBUG)
 
...    

如上所示,如果处于 Debug 模式,RetainObjReleaseObjSmartObject 构造函数和 SmartObject 复制构造函数的参数会更改为:

( 
#if defined(_DEBUG)
      TCHAR *fileName, TCHAR *funcName, unsigned int lineNum,
#endif //defined(_DEBUG)   
...) 

这将允许上述函数从调用者接收文件名、函数名和行号。  

  • 请注意,析构函数不会被跟踪,因为它只需要捕获无效 delete 运算符使用的情况,而无效 delete 运算符的使用将在析构函数内的断言中捕获。 

然后,您可以通过打印调用每个函数(RetainObjReleaseObjSmartObject构造函数、SmartObject 复制构造函数)的文件名、函数名和行号来跟踪引用,如下所示:  

... 
#if defined(_DEBUG)
      LOG_THIS_MSG(_T("%s::%s(%d) Retained Object : %d (Current Reference Count = %d)"),
               fileName,funcName,lineNum,this, this->m_refCount);  
#endif //defined(_DEBUG)
...  
#if defined(_DEBUG)
      LOG_THIS_MSG(_T("%s::%s(%d) Released Object : %d (Current Reference Count = %d)"),
             fileName,funcName,lineNum,this, this->m_refCount);
#endif //defined(_DEBUG) 
...  
#if defined(_DEBUG)
      LOG_THIS_MSG(_T("%s::%s(%d) Allocated Object : %d (Current Reference Count = %d)"),
             fileName,funcName,lineNum,this, this->m_refCount);
#endif //defined(_DEBUG) 
...   

如果您下载了源代码文件,默认的 LOG_THIS_MSG_tprintf。但是,您可以根据自己的喜好将其更改为任何日志系统(例如 OutputDebugString)。  

此外,由于在 Debug 模式下为 RetainObjReleaseObjSmartObject 的每次调用输入 __TFILE____TFUNCTION____LINE__ 非常繁琐,通过声明以下定义,您可以使用 SmartObject ,就像在 Release 模式下一样,因为在 Debug 模式下,它会自动为您输入 __TFILE____TFUNCTION__ __LINE__

... 
#if defined(_DEBUG)
#define SmartObject(...) SmartObject(__TFILE__,__TFUNCTION__,__LINE__,__VA_ARGS__)
#define ReleaseObj() ReleaseObj(__TFILE__,__TFUNCTION__,__LINE__)
#define RetainObj() RetainObj(__TFILE__,__TFUNCTION__,__LINE__)
#endif//defined(_DEBUG)
...   
  • 请注意,这有一些限制,由于它们被声明为预处理器定义,因此无法在包含 SmartObject 类头的任何其他目的上声明 RetainObjReleaseObjSmartObject

使用场景示例

将类声明为 SmartObject 类

// TestClass.h
 
#include "SmartObject.h" 
class TestClass: public SmartObject
{ 
public:
   TestClass():  SmartObject()
 
   {
      //Initialization 
      m_myVal=new int();
      *m_myVal=1; 
   } 
   TestClass  (const TestClass& b): SmartObject(b)
   {
      //Initialization by copying
      m_myVal = new int();
      *m_myVal = *(b.m_myVal); 
   } 
   TestClass & operator=(const TestClass& b)
   {
      if(this!=&b)
      {  
         *m_myVal=*(b.m_myVal);
          SmartObject::operator=(b); 
      }  
      return *this;   
   }
   virtual ~TestClass()
   {
      if(m_myVal)  
         delete m_myVal;  
   }  
   void DoSomething() 
   { 
      *m_myVal=*m_myVal+1; 
   }  
private: 
   int *m_myVal;  
};   

您可以通过像继承其他类一样继承 SmartObject 类,轻松地将一个类声明为 SmartObject 类。  

SmartObject 类的正常使用

... 
TestClass testClass;
testClass.DoSomething();
...   

要使用 TestClass 对象,您可以像平常在 C++ 中一样实例化该类并使用它。 

TestClass *testClass = new SomeClass(); // reference count = 1
...
delete testClass ;                   // deletion (Only allowed when reference count is 1)      

同样,如上例所示,它 可以像原始 C++ 内存管理一样使用,即匹配 new delete 运算符。  

引用管理示例

void SomeFunc(TestClass*sClass)
{ 
   sClass->RetainObj();  // reference count =  2
   ...
   sClass->ReleaseObj(); // reference count =  1
}
... 
void SomeOtherFunc()
{ 
   TestClass *testObj= new TestClass (); // reference count = 1
   SomeFunc(testObj);
   testObj ->ReleaseObj();               // reference count = 0, and auto-released
}    

当将 SmartClass 对象传递给函数(此处为 SomeFunc)时,如果接收函数通过调用 RetainObj 来保留该对象,则引用计数会增加,并且在完成后应通过调用 ReleaseObj 来释放引用。 

  • 请注意,在上述示例中,如果 SomeFunc 是一个线程函数,并且 SomeFunc SomeOtherFunc 之后调用 ReleaseObj 函数,“自动释放”将在 SomeFunc 调用 ReleaseObj 时发生,而不是在 SomeOtherFunc 函数中。 

引用管理示例 2

SomeClass *someClass = new SomeClass(); // reference count = 1
someClass->RetainObj();                 // reference count = 2
...
someClass->ReleaseObj();                // reference count = 1
delete someClass;                       // deletion (Only allowed when reference count is 1) 

SmartObject 要求 RetainObj ReleaseObj 一一对应。我通常建议将 new 运算符和 ReleaseObj 进行匹配,就像匹配 RetainObj ReleaseObj 一样,但它仍然允许您匹配 new delete 运算符,并在两者之间使用 RetainObj ReleaseObj,但有一个限制。  

  • 要使用 delete 运算符,对象的引用计数必须为 1。(引用计数 == 1 意味着自创建 SmartObject 以来没有其他引用对象。) 

引用管理示例 3

void SomeFunc(SomeClass *sClass)
{
   sClass->RetainObj();  // reference count = 2 
   ... 
   sClass->ReleaseObj(); // reference count = 1 
} 
... 
TestClass *testClass = new TestClass(); // reference count = 1
testClass->RetainObj();                 // reference count = 2 
testClass->ReleaseObj();                // reference count = 1
SomeFunc(testClass); 
testClass->ReleaseObj();                // reference count = 0, and auto-release   

只要您匹配 ReleaseObj 函数,无论您通过调用 RetainObj 函数多少次来保留对象,都没关系。当引用计数 == 1 时调用 ReleaseObj 函数,对象将被“自动释放”。 

更多实际示例来源

更多实际示例可以在 EpServerEngine 源代码中找到。 

(请参阅 “EpServerEngine - A lightweight Template Server-Client Framework using C++ and Windows Winsock” 文章了解更多详情。)   

结论

正如我在引言中所说,这 不是关于“C++ 内存管理最佳实践”的解释。但我认为这是一个值得思考的有趣话题。对我而言,这个 SmartObject 类不时地简化了我在开发 C++ 项目时的内存管理。如果有人像我一样觉得它易于使用和简单,那就太好了,即使有人觉得不是,我也认为它可能是一个有趣的思考点。希望您喜欢阅读这篇文章。 

参考

历史

  • 2013 年 8 月 22 日:- 根据 MIT 许可证重新分发 
  • 2012 年 9 月 21 日:- 更新了目录。 
  • 2012 年 9 月 17 日:- 提交了文章。 
© . All rights reserved.