SmartObject 类(类似 Objective-C 的内存管理)用于 C++(Smart Pointer 的替代品?)
本文介绍了 Objective-C 风格的 C++ 内存管理类 SmartObject。
目录
简介
内存管理在 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 模式,RetainObj
、ReleaseObj
、SmartObject
构造函数和 SmartObject
复制构造函数的参数会更改为:
(
#if defined(_DEBUG)
TCHAR *fileName, TCHAR *funcName, unsigned int lineNum,
#endif //defined(_DEBUG)
...)
这将允许上述函数从调用者接收文件名、函数名和行号。
- 请注意,析构函数不会被跟踪,因为它只需要捕获无效
delete
运算符使用的情况,而无效delete
运算符的使用将在析构函数内的断言中捕获。
然后,您可以通过打印调用每个函数(RetainObj
、ReleaseObj
、SmartObject
构造函数、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 模式下为 RetainObj
、ReleaseObj
、SmartObject
的每次调用输入 __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
类头的任何其他目的上声明RetainObj
、ReleaseObj
、SmartObject
。
使用场景示例
将类声明为 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 源代码中找到。
- epBaseServerObject.h
- epBaseServerSendObject.h
- epBaseClient.h
- epPacketParser.h
- epBaseServer.h
- epBaseServerWorker.h
- epPacket.h
结论
正如我在引言中所说,这 不是关于“C++ 内存管理最佳实践”的解释。但我认为这是一个值得思考的有趣话题。对我而言,这个 SmartObject 类不时地简化了我在开发 C++ 项目时的内存管理。如果有人像我一样觉得它易于使用和简单,那就太好了,即使有人觉得不是,我也认为它可能是一个有趣的思考点。希望您喜欢阅读这篇文章。
参考
- EpLibrary 2.0
- EpServerEngine
- “Objective-C 内存管理”
- Woong Gyu La 撰写的“如何为 C++ 同步创建简单的锁框架”
- Woong Gyu La 撰写的“EpServerEngine - 一个使用 C++ 和 Windows Winsock 的轻量级模板客户端-服务器框架”
历史
- 2013 年 8 月 22 日:- 根据 MIT 许可证重新分发
- 2012 年 9 月 21 日:- 更新了目录。
- 2012 年 9 月 17 日:- 提交了文章。