养成良好的C++编程习惯之内存管理的应用详解
本文导语: 开篇导读 虽然本系列文章定位为科普读物,但本座相信它们不但适合新手们学习借鉴,同时也能引发老鸟们的反思与共鸣。欢迎大家提出宝贵的意见和反馈 ^_^ 在开篇讲述本章主要内容之前,本座首先用...
开篇导读
虽然本系列文章定位为科普读物,但本座相信它们不但适合新手们学习借鉴,同时也能引发老鸟们的反思与共鸣。欢迎大家提出宝贵的意见和反馈 ^_^
在开篇讲述本章主要内容之前,本座首先用小小篇幅论述一下一种良好的工作习惯 —— 积累、提炼与求精。在工作和学习的过程中,不断把学到的知识通过有效的方式积累起来,形成自己的知识库,随着知识量的扩大,就会得到从量变到质变的提升。另外还要不断地对知识进行提炼,随着自己知识面的扩大以及水平的提升,你肯定会发现原有知识库存在着一些片面、局限、笨拙甚至错误。这时,就需要你有精益求精精的态度和毅力对知识库进行优化整理。
也许以上这些各位都曾想过去实施,也明白其中的道理,但是自己就是给自己各种堂而皇之的借口不花时间去做。这样说吧,技术之路不好走,这个行业有两项基本要求:1、对软件开发工作本身有很大兴趣;2、耐得住寂寞。两者缺一不可,否则还是趁年轻早点转行吧,要不转做软件行业的销售、产品或者管理也行,总之就不要做开发 ^_^
--------------------------------------------------------------------------------
内存管理相关问题
一提起 C/C++ 的内存管理,大部分人脑海里都立刻涌出 new / delete / malloc / free 等几个恐怖的单词吧?的确,C/C++ 的手工内存管理是它们区别于其他语言的一大特点,也像一道屏障立在那些想从其它语言转向 C/C++ 的人士身前。由此也引起各大论坛对 “C++ 人气低落”和“是否应该引入垃圾回收机制”等相关话题的剧烈争论。本座一直无视这些争论,其实并非本座不关心 C++ 的发展与命运,相反,本座十分关心。虽然从现在的眼光看来,无论是 C++ 身上有多少硬伤,C++ 委员会的大爷们和 C++ 编译器厂商的大佬们如何扯猫尾。毕竟最爱就是最爱,残缺美也是美,不解释。本座之所以不关心这些争论,原因是因为看透了,一门语言就像一种人生,是有生命周期的,没落只是快慢的问题,旧事物总会被新事物取代,这是客观规律不可避免。秦始皇最终不也是没找到长生不老的仙丹么?只要曾经发光发热过,在还有价值的时候能为大众所用就已经无憾了。本座在此还要申明一种态度:本座并不排斥任何语言,相反,本座对新语言的诞生非常感兴趣。会去了解它们的特点,看看它们能帮助解决哪方面的问题。正如这几年,由于工作需要,本座用得最多的是 Java 和一些动态语言(它们的确能解决很多问题),而 C/C++ 却没再用了。
嗯,扯远了,我们还是回到正题吧。说起 C/C++ 的内存管理似乎令人望而生畏,满屏的 new / delete / malloc / free,OutPut 窗口无尽的 Memory Leak 警告,程序诡异的 0X00000004 指针异常,仿佛回到那一年我们一起哭过的日子,你 Hold 得住吗?其实,现实并没有你想的那么糟糕。只要你付出一点点,花一点点心思,没错!就一点点而已 —— 用 C++ 类封装内存访问,就会解决你大部分的烦恼,让你受益终身。以 Windows 程序为例,主要有以下几种内存管理方式:
•虚拟内存(Virtual Memory)
•默认堆和私有堆(Process Heap & Private Heap)
•内存映射文件(File Mapping)
•进程堆栈(Heap,其实就是用 malloc() 或 默认的 new 操作符在 Process Heap 里一小块一小块地割肉 ^_^)
•栈(Stack,内存由调用者或被调用者自动管理)
今天我们的主题是封装,至于每种内存模型的概念和 API 的使用方式在这里就不讲了,Google 一下就知道。其实用 C++ 封装上述前 4 种内存访问的原理都差不多,就是在构造函数或其他操作函数中分配内存,然后再在析构函数中确保内存被正确释放。虚拟内存、默认堆和私有堆的操作方式相似,这里就不一一展示了,有兴趣的朋友可以参考本座前几天发表的那篇无人问津的文章:《C++ 封装私有堆(Private Heap)》,哎!下面对内存映射文件的封装也只稍作介绍、我们主要讨论的是使用频率最高的 malloc() 和 new 的封装。
--------------------------------------------------------------------------------
内存映射文件
下面的代码把 File Mapping 句柄以及从 File Mapping 映射出来的内存分别封装到 CFileMapping 和 CShareMemory 中,可以直接使用 CShareMemory 可以创建一个 File Mapping 以及映射 File Mapping 的内存。
class CFileMapping
{
public:
CFileMapping(
LPCTSTR lpszName,
DWORD dwMaximumSizeLow,
DWORD dwMaximumSizeHigh = 0,
HANDLE hFile = INVALID_HANDLE_VALUE,
DWORD flProtect = PAGE_READWRITE,
LPSECURITY_ATTRIBUTES lpFileMappingAttributes = NULL
)
{
m_hMap = ::CreateFileMapping (
hFile,
lpFileMappingAttributes,
flProtect,
dwMaximumSizeHigh,
dwMaximumSizeLow,
lpszName
);
ASSERT(IsValid());
}
~CFileMapping()
{
if(IsValid())
VERIFY(::CloseHandle(m_hMap));
}
LPVOID ViewMap (
DWORD dwNumberOfBytesToMap,
DWORD dwFileOffsetLow,
DWORD dwFileOffsetHigh = 0,
DWORD dwDesiredAccess = FILE_MAP_ALL_ACCESS
)
{
return ::MapViewOfFile (
m_hMap,
dwDesiredAccess,
dwFileOffsetHigh,
dwFileOffsetLow,
dwNumberOfBytesToMap
);
}
BOOL UnViewMap(LPCVOID lpBaseAddress)
{
return ::UnmapViewOfFile(lpBaseAddress);
}
operator HANDLE () {return m_hMap;}
BOOL IsValid () {return m_hMap != NULL;}
private:
HANDLE m_hMap;
DECLARE_PRIVATE_COPY_CONSTRUCTOR(CFileMapping)
};
class CShareMemory
{
public:
CShareMemory(DWORD dwSize, LPCTSTR lpszName = NULL)
: m_fm(lpszName, dwSize)
{
ASSERT(dwSize > 0);
}
~CShareMemory()
{
for(set::const_iterator it = m_set.begin(); it != m_set.end(); ++it)
{
LPVOID pV = (LPVOID)*it;
ASSERT(pV);
m_fm.UnViewMap(pV);
}
m_set.clear();
}
LPVOID Alloc(DWORD dwNumberOfBytesToMap, DWORD dwFileOffsetLow)
{
LPVOID pV = m_fm.ViewMap(dwNumberOfBytesToMap, dwFileOffsetLow);
if(pV) m_set.insert((ULONG_PTR)pV);
ASSERT(pV);
return pV;
}
BOOL Free(LPCVOID lpBaseAddress)
{
ASSERT(lpBaseAddress);
set::iterator it = m_set.find((ULONG_PTR)lpBaseAddress);
if(it != m_set.end())
m_set.erase(it);
return m_fm.UnViewMap(lpBaseAddress);
}
private:
CFileMapping m_fm;
set m_set;
DECLARE_PRIVATE_COPY_CONSTRUCTOR(CShareMemory)
};
细心的朋友一定会发觉其实这样封装是有缺点的:首先,CShareMemory 只能做内存共享,不能映射到真实文件(hFile 永远为 INVALID_HANDLE_VALUE);第二,可以对 CShareMemory 的 Alloc() 和 Free() 方法进一步封装,利用封装类的析构函数自动调用 Free(),这样就可以完全消除 “set m_set” 这个属性了;第三,CFileMapping 也可以把文件句柄一起封装进来,这样,从 CreateFile() 到 CreateFileMapping() 都受控了。这个不完美的封装就权当反面教材吧 ^_^
--------------------------------------------------------------------------------
malloc() 系列函数
很多人都建议,在 C++ 中尽量用 new 操作符取代 malloc(),因为 new 类型安全,自动调用构造函数和析构函数等等。关于这点本座略有异议,在某些情形下 malloc() 其实比 new 更好使,效率方面我们可以不计较(几乎所有编译器的 new 操作符都用 malloc() 分配内存),从事过偏底层开发的人都清楚,我们避免不了处理 row data(如:socket 的收发缓冲区等)数据,这类数据是非常适合使用 malloc() 的,用 new 分配的内存还要停顿下来想想到底是用 delete、delete[]、::delete、::delete[] 中的哪个释放,malloc() 分配的内存想都不用想,free() 包打天下,何况人家有 realloc() 可以方便地重新调整内存,你有没有 “renew” 呢?总之一句话,malloc() 的确是有存在的必要,就看接下来我们如何封装它了,请看代码:
// T : 数据类型(内置类型或结构体)
// MAX_CACHE_SIZE : 预申请内存的最大数目,以 sizeof(T) 为单位,如果该值设置合理,对于
// 需要动态递增缓冲区的 buffer 来说能大大提高效率
template
class CBufferPtrT
{
public:
explicit CBufferPtrT(size_t size = 0, bool zero = false) {Reset(); Malloc(size, zero);}
explicit CBufferPtrT(const T* pch, size_t size) {Reset(); Copy(pch, size);}
// 拷贝构造函数要分两种情形
CBufferPtrT(const CBufferPtrT& other) {Reset(); Copy(other);}
template CBufferPtrT(const CBufferPtrT& other) {Reset(); Copy(other);}
~CBufferPtrT() {Free();}
T* Malloc(size_t size = 1, bool zero = false)
{
Free();
return Alloc(size, zero, false);
}
T* Realloc(size_t size, bool zero = false)
{
return Alloc(size, zero, true);
}
void Free()
{
if(m_pch)
{
free(m_pch);
Reset();
}
}
template CBufferPtrT& Copy(const CBufferPtrT& other)
{
if((void*)&other != (void*)this)
Copy(other.Ptr(), other.Size());
return *this;
}
CBufferPtrT& Copy(const T* pch, size_t size)
{
Malloc(size);
if(m_pch)
memcpy(m_pch, pch, size * sizeof(T));
return *this;
}
// 动态扩大 buffer
template CBufferPtrT& Cat(const CBufferPtrT& other)
{
if((void*)&other != (void*)this)
Cat(other.Ptr(), other.Size());
return *this;
}
// 动态扩大 buffer
CBufferPtrT& Cat(const T* pch, size_t size = 1)
{
size_t pre_size = m_size;
Realloc(m_size + size);
if(m_pch)
memcpy(m_pch + pre_size, pch, size * sizeof(T));
return *this;
}
template bool Equal(const CBufferPtrT& other) const
{
if((void*)&other == (void*)this)
return true;
else if(m_size != other.Size())
return false;
else if(m_size == 0)
return true;
else
return (memcmp(m_pch, other.Ptr(), m_size * sizeof(T)) == 0);
}
bool Equal(T* pch) const
{
if(m_pch == pch)
return true;
else if(!m_pch || !pch)
return false;
else
return (memcmp(m_pch, pch, m_size * sizeof(T)) == 0);
}
T* Ptr() {return m_pch;}
const T* Ptr() const {return m_pch;}
T& Get(int i) {return *(m_pch + i);}
const T& Get(int i) const {return *(m_pch + i);}
size_t Size() const {return m_size;}
bool IsValid() const {return m_pch != 0;}
// 啊哈,竟然是类型安全的
operator T* () {return Ptr();}
operator const T* () const {return Ptr();}
// 哇塞,竟然还支持索引访问
T& operator [] (int i) {return Get(i);}
const T& operator [] (int i) const {return Get(i);}
bool operator == (T* pv) const {return Equal(pv);}
template bool operator == (const CBufferPtrT& other) {return Equal(other);}
// 赋值操作符要分两种情形
CBufferPtrT& operator = (const CBufferPtrT& other) {return Copy(other);}
template CBufferPtrT& operator = (const CBufferPtrT& other) {return Copy(other);}
private:
void Reset() {m_pch = 0; m_size = 0; m_capacity = 0;}
size_t GetAllocSize(size_t size) {return max(size, min(size * 2, m_size + MAX_CACHE_SIZE));}
T* Alloc(size_t size, bool zero = false, bool is_realloc = false)
{
if(size >= 0 && size != m_size)
{
size_t rsize = GetAllocSize(size);
if(size > m_capacity || rsize < m_size)
{
m_pch = is_realloc ?
(T*)realloc(m_pch, rsize * sizeof(T)) :
(T*)malloc(rsize * sizeof(T)) ;
if(m_pch || rsize == 0)
{
m_size = size;
m_capacity = rsize;
}
else
Reset();
}
else
m_size = size;
}
if(zero && m_pch)
memset(m_pch, 0, m_size * sizeof(T));
return m_pch;
}
private:
T* m_pch;
size_t m_size;
size_t m_capacity;
};
// 常用 buffer 类型的 typedef
typedef CBufferPtrT CCharBufferPtr;
typedef CBufferPtrT CWCharBufferPtr;
typedef CBufferPtrT CByteBufferPtr;
typedef CByteBufferPtr CBufferPtr;
#ifdef _UNICODE
typedef CWCharBufferPtr CTCharBufferPtr;
#else
typedef CCharBufferPtr CTCharBufferPtr;
#endif
嗯。这里要解释一下为何需要两个拷贝构造函数和赋值操作符重载,首先,编译器为不同的模板参数生成不同的类,也就是说:CBufferPtrT 和 CBufferPtrT 被看作是不同的类,另外,C++ 编译器为每个类提供了提供了拷贝构造函数和赋值操作符重载的默认实现(浅拷贝)。因此,上述的第一组拷贝构造函数和赋值操作符重载是改写编译器的默认实现,第二组拷贝构造函数和赋值操作符重载是处理其它类到本类的转换。
本座对这个封装灰常满意(唯一美中不足的就是 cnblogs 的编辑器太坑爹了,把代码弄乱 ^_^),它并非只是一个普通的 malloc() 封装,完全能可以把它看作是一种“支持索引访问的类型安全的动态缓冲区”。如果把它放在一个 socket 通信类中作为成员属性,充当跨越多个线程和多个方法访问的接收缓冲区和发送缓冲区的角色就最适合不过了(当然要自己做同步了)。大家可以调试一下下面的测试例子,了解一下它的用法:
测试用例
int _tmain(int argc, _TCHAR* argv[])
{
CBufferPtr buffer;
unsigned char c1 = 'X';
unsigned char pc1[] = "123";
unsigned char pc2[] = "abc";
buffer.Cat(&c1);
buffer.Cat(pc1, 3);
buffer.Cat(pc2, 3);
CBufferPtrT buffer2 = buffer;
buffer2.Cat(buffer);
buffer2.Realloc(0);
unsigned char* pc = buffer;
const unsigned char& c = buffer[5];
buffer[5] = 'O';
short i1 = 0x7FFF;
short pi0[] = {9,9,9};
short pi1[] = {1,2,3};
short pi2[] = {4,5,6};
short pi3[] = {8,8,8};
CBufferPtrT bufferS(pi0, 3);
bufferS.Cat(&i1);
bufferS.Cat(pi1, 3);
bufferS.Cat(pi2, 3);
bufferS.Cat(pi3, 3);
CBufferPtrT bufferS2;
bufferS2.Malloc(4);
bufferS2 = bufferS;
bufferS2.Realloc(30);
CBufferPtrT bufferI(5, true);
for(size_t i = 0; i < bufferI.Size(); i++)
bufferI[i] = i *10;
bufferI.Malloc();
bufferI[0] = 123;
// 下面这行编译不通过,正好说明这个类是类型安全的
// bufferI = bufferS;
return 0;
}
--------------------------------------------------------------------------------
new & delete
一说到 new 的封装大家立马想到的就是智能指针吧!没错,就是智能指针。但 STL 提供的 auto_ptr 缺陷很多,首先使用起来不方便,竟然连这种写法都不支持:“std::auto_ptr pi = new int;”,天理何在啊!更可恨的是不支持数组指针(需要 delete[]),另外如果某些类重载了 new 操作符的话使用它也有很多问题的,还有其它的很多缺点(我忘记了 ^_^)。不过,C++0x 似乎对智能指针作了重大改进,已经有支持引用计数的智能指针了,但不知是否解决数组指针和区分 delete 与 ::delete 的问题(本座没实测,要是您知道麻烦告诉一声 ^_^)。无论如何,下面代码列出的智能指针支持区分 delete / delete[] / ::delete / ::delete[]。算是 auto_ptr 的改良(也没有使用引用计数),文章篇幅太长了,测试用例就不发了,各位看官自行尝试吧:
/************************************************************************/
/* smart_ptr 单实体或数组智能指针 */
/************************************************************************/
template
struct simple_deleter
{
static void delete_ptr(_Ty* pv) {delete pv;}
};
template
struct global_simple_deleter
{
static void delete_ptr(_Ty* pv) {::delete pv;}
};
template
struct array_deleter
{
static void delete_ptr(_Ty* pv) {delete[] pv;}
};
template
struct global_array_deleter
{
static void delete_ptr(_Ty* pv) {::delete[] pv;}
};
template
class smart_ptr
{
public:
smart_ptr(_Ty* _Ptr = 0) : _Myptr(_Ptr) {}
smart_ptr(smart_ptr& _Right) : _Myptr(_Right.release()) {}
~smart_ptr()
{
reset();
}
smart_ptr& reset(_Ty* _Ptr = 0)
{
if (_Ptr != _Myptr)
{
if(_Myptr)
_Deleter::delete_ptr(_Myptr);
_Myptr = _Ptr;
}
return *this;
}
smart_ptr& reset(smart_ptr& _Right)
{
if (this != &_Right)
reset(_Right.release());
return *this;
}
_Ty* release()
{
_Ty* _Ptr = _Myptr;
_Myptr = 0;
return _Ptr;
}
smart_ptr& operator = (_Ty* _Ptr) {return reset(_Ptr);}
smart_ptr& operator = (smart_ptr& _Right) {return reset(_Right);}
bool is_valid () const {return _Myptr != 0;}
_Ty& operator * () const {return *_Myptr;}
_Ty* get () const {return _Myptr;}
_Ty* operator -> () const {return _Myptr;}
operator _Ty* () const {return _Myptr;}
private:
template smart_ptr (const smart_ptr&);
template smart_ptr& reset (const smart_ptr&);
template smart_ptr& operator = (const smart_ptr&);
template smart_ptr (const smart_ptr&);
template smart_ptr& reset (const smart_ptr&);
template smart_ptr& operator = (const smart_ptr&);
protected:
_Ty* _Myptr;
};
/************************************************************************/
/* smart_simple_ptr 单实体智能指针 */
/************************************************************************/
template
class smart_simple_ptr : public smart_ptr
{
public:
smart_simple_ptr(_Ty* _Ptr = 0) : smart_ptr(_Ptr) {}
smart_simple_ptr(smart_simple_ptr& _Right) : smart_ptr(_Right) {}
smart_simple_ptr(smart_ptr& _Right) : smart_ptr(_Right) {}
smart_simple_ptr& operator = (smart_ptr& _Right)
{return (smart_simple_ptr&)__super::operator = (_Right);}
smart_simple_ptr& operator = (smart_simple_ptr& _Right)
{return (smart_simple_ptr&)__super::operator = (_Right);}
smart_simple_ptr& operator = (_Ty* _Ptr)
{return (smart_simple_ptr&)__super::operator = (_Ptr);}
private:
template smart_simple_ptr (const smart_ptr&);
template smart_simple_ptr& operator = (const smart_ptr&);
template smart_simple_ptr (const smart_simple_ptr&);
template smart_simple_ptr& operator = (const smart_simple_ptr&);
};
/************************************************************************/
/* smart_gd_simple_ptr 单实体智能指针 (使用全局 delete) */
/************************************************************************/
template
class smart_gd_simple_ptr : public smart_ptr
{
public:
smart_gd_simple_ptr(_Ty* _Ptr = 0) : smart_ptr(_Ptr) {}
smart_gd_simple_ptr(smart_gd_simple_ptr& _Right) : smart_ptr(_Right) {}
smart_gd_simple_ptr(smart_ptr& _Right) : smart_ptr(_Right) {}
smart_gd_simple_ptr& operator = (smart_ptr& _Right)
{return (smart_gd_simple_ptr&)__super::operator = (_Right);}
smart_gd_simple_ptr& operator = (smart_gd_simple_ptr& _Right)
{return (smart_gd_simple_ptr&)__super::operator = (_Right);}
smart_gd_simple_ptr& operator = (_Ty* _Ptr)
{return (smart_gd_simple_ptr&)__super::operator = (_Ptr);}
private:
template smart_gd_simple_ptr (const smart_ptr&);
template smart_gd_simple_ptr& operator = (const smart_ptr&);
template smart_gd_simple_ptr (const smart_gd_simple_ptr&);
template smart_gd_simple_ptr& operator = (const smart_gd_simple_ptr&);
};
/************************************************************************/
/* smart_array_ptr 数组智能指针 */
/************************************************************************/
template
class smart_array_ptr : public smart_ptr
{
public:
smart_array_ptr(_Ty* _Ptr = 0) : smart_ptr(_Ptr) {}
smart_array_ptr(smart_simple_ptr& _Right) : smart_ptr(_Right) {}
smart_array_ptr(smart_ptr& _Right) : smart_ptr(_Right) {}
smart_array_ptr& operator = (smart_ptr& _Right)
{return (smart_array_ptr&)__super::operator = (_Right);}
smart_array_ptr& operator = (smart_array_ptr& _Right)
{return (smart_array_ptr&)__super::operator = (_Right);}
smart_array_ptr& operator = (_Ty* _Ptr)
{return (smart_array_ptr&)__super::operator = (_Ptr);}
private:
template smart_array_ptr (const smart_ptr&);
template smart_array_ptr& operator = (const smart_ptr&);
template smart_array_ptr (const smart_array_ptr&);
template smart_array_ptr& operator = (const smart_array_ptr&);
};
/************************************************************************/
/* smart_gd_array_ptr 数组智能指针 (使用全局 delete) */
/************************************************************************/
template
class smart_gd_array_ptr : public smart_ptr
{
public:
smart_gd_array_ptr(_Ty* _Ptr = 0) : smart_ptr(_Ptr) {}
smart_gd_array_ptr(smart_gd_array_ptr& _Right) : smart_ptr(_Right) {}
smart_gd_array_ptr(smart_ptr& _Right) : smart_ptr(_Right) {}
smart_gd_array_ptr& operator = (smart_ptr& _Right)
{return (smart_gd_array_ptr&)__super::operator = (_Right);}
smart_gd_array_ptr& operator = (smart_gd_array_ptr& _Right)
{return (smart_gd_array_ptr&)__super::operator = (_Right);}
smart_gd_array_ptr& operator = (_Ty* _Ptr)
{return (smart_gd_array_ptr&)__super::operator = (_Ptr);}
private:
template smart_gd_array_ptr (const smart_ptr&);
template smart_gd_array_ptr& operator = (const smart_ptr&);
template smart_gd_array_ptr (const smart_gd_array_ptr&);
template smart_gd_array_ptr& operator = (const smart_gd_array_ptr&);
};
--------------------------------------------------------------------------------
后记
• 对于内存管理,其实还有一种情形还没讲的,就是如何优雅地管理 vetor、list、map 这类容器中的指针,这个话题留到以后讨论 STL 时再详细阐述吧。
•在本座的代码中基本上看不到 free / delere 这类单词(new 则是有的 —— 给智能指针赋值的时候 ^_^),就本座的经验而言,封装如果利用得当确实能减少很多麻烦,使代码更清晰,有条理,降低错误发生几率。
•当然了,封装并不是万能,它不能解决所有问题,关键是靠个人的专注与细心。
•本座码字提出自己的观点,旨在抛砖引玉,激发大家思考如何培养良好的编程习惯,不是权威,更不能尽信。最实在的知识应该来自个人最直接的体验。
您可能感兴趣的文章:
本站(WWW.)站内文章除注明原创外,均为转载、整理或搜集自网络。欢迎任何形式的转载,转载请注明出处。