COM组件笔记(二)
一、内存资源的何时释放?
在主函数的最后面执行delete pA确实是一个可行的办法。但却不是好办法。因为这样子最终是释放了pA的内存资源,不过却不是“及时”(在pA所指的组件不用时)地释放内存资源。
如果一个程序,所有的资源在不用时都没有及时释放,这个程序在运行中所占用的内存将是巨大的。如何解决这个问题呢?这就需要引用计数技术。
二、引用计数的原理
- 引用计数技术就是用来管理对象生命期的一种技术。
- 对象O可能同时被外界A,外界B,外界C引用。也就是说外界A,外界B,外界C可能都在使用对象O。
- 每次当对象被外界引用时,计数器就自增1。
- 每次当外界不用对象时,计数器就自减1。
- 在计数值为零时,对象本身执行delete this,销毁自己的资源。
- 引用计数使得对象通过计数能够知道何时对象不再被使用,然后及时地删除自身所占的内存资源。在第一节当中的实现的IUnknown接口的AddRef与Release就是引用计数的实现方法。
以下是自己基于上一节代码,添加引用计数后的Demo3:

1 #define _CRT_SECURE_NO_WARNINGS 2 #include <iostream> 3 #include <Unknwn.h> 4 using namespace std; 5 6 // {4E22FE4C-FC61-4616-B13E-7AC19186000D} 7 static const IID IID_IX = 8 { 0x4e22fe4c, 0xfc61, 0x4616, { 0xb1, 0x3e, 0x7a, 0xc1, 0x91, 0x86, 0x0, 0xd } }; 9 10 // {26C7D646-9C25-4C88-AAC5-215FC0569CD5} 11 static const IID IID_IY = 12 { 0x26c7d646, 0x9c25, 0x4c88, { 0xaa, 0xc5, 0x21, 0x5f, 0xc0, 0x56, 0x9c, 0xd5 } }; 13 14 15 // 接口IX 16 interface IX : public IUnknown 17 { 18 virtual void Fx1() = 0; 19 virtual void Fx2() = 0; 20 }; 21 22 // 接口IY 23 interface IY : public IUnknown 24 { 25 virtual void Fy1() = 0; 26 virtual void Fy2() = 0; 27 }; 28 29 // 组件CA 30 class CA : public IX, public IY { 31 //构造和析构 32 public: 33 CA() { 34 m_lCount = 0; 35 36 // 构造时,需要自增引用计数 37 AddRef(); 38 } 39 40 virtual ~CA() { 41 cout << "我被释放了!" << endl; 42 } 43 44 public: 45 virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void** ppv) { 46 if (iid == IID_IUnknown) { 47 // 如果未知的话,我们一般来说返回先继承的那个接口 48 *ppv = static_cast<IX*>(this); 49 } 50 else if (iid == IID_IX) { 51 // 返回IX接口 52 *ppv = static_cast<IX*>(this); 53 } 54 else if (iid == IID_IY) { 55 // 返回IY接口 56 *ppv = static_cast<IY*>(this); 57 } 58 else { 59 // 查询不到IID,*ppv返回NULL 60 *ppv = NULL; 61 return E_NOINTERFACE; // 函数返回值返回E_NOINTERFACE,表示调用的组件不支持iid的接口 62 } 63 64 AddRef(); //之后会提到 65 66 return S_OK; // 代表函数成功执行 67 } 68 virtual ULONG STDMETHODCALLTYPE AddRef() { 69 70 // 简单实现方法 71 return ++m_lCount; 72 73 // 多线程编程下采用如下方法,这种方法确保同一时刻只会有一个线程来访问成员变量 74 // return InterlockedIncrement(&m_lCount); 75 }; 76 virtual ULONG STDMETHODCALLTYPE Release() { 77 // 简单实现方法 78 if (--m_lCount == 0) { 79 delete this; // 销毁自己 80 return 0; 81 } 82 83 return m_lCount; 84 85 // 多线程编程下采用如下方法,这种方法确保同一时刻只会有一个线程来访问成员变量 86 /*if (InterlockedDecrement(&m_lCount) == 0) { 87 delete this; 88 return 0; 89 } 90 return m_lCount;*/ 91 }; 92 93 public: 94 virtual void Fx1() { 95 cout << "Fx1" << endl; 96 } 97 98 virtual void Fx2() { 99 cout << "Fx2" << endl; 100 } 101 102 virtual void Fy1() { 103 cout << "Fy1" << endl; 104 } 105 106 virtual void Fy2() { 107 cout << "Fy2" << endl; 108 } 109 110 // 数据 111 public: 112 long m_IAA; 113 long m_IAB; 114 long m_IAC; 115 long m_lCount; 116 117 }; 118 119 int main(int argc, char* argv[]) 120 { 121 HRESULT hr; 122 123 // 创建组件 124 CA* pA = new CA(); // 引用计数1 125 126 // 从组件查询IUnknown接口 127 IUnknown* pIUnknown = NULL; 128 hr = pA->QueryInterface(IID_IUnknown, (void**)&pIUnknown); // 引用计数2 129 if (SUCCEEDED(hr)) { // 对于HRESULT返回值的判断,一般采用SUCCEEDED 130 131 pA->Release(); // pA不再使用,引用计数=1 132 pA = NULL; // 防止再不小心使用m_pA 133 134 // 从IUnknown查询IX接口 135 IX* pIX = NULL; 136 hr = pIUnknown->QueryInterface(IID_IX, (void**)&pIX); // 引用计数2 137 if (SUCCEEDED(hr)) { 138 // 调用IX接口方法 139 pIX->Fx1(); 140 pIX->Fx2(); 141 } 142 143 IY* pIY = NULL; 144 hr = pIUnknown->QueryInterface(IID_IY, (void**)&pIY); // 引用计数3 145 if (SUCCEEDED(hr)) { 146 pIY->Fy1(); 147 pIY->Fy2(); 148 } 149 150 if ((void*)pIX != (void*)pIY) { 151 cout << "pIX != pIY" << endl; 152 } 153 154 if ((void*)pIUnknown != (void*)pIY) { 155 cout << "pIUnknown != pIY" << endl; 156 } 157 158 if ((void*)pIUnknown != (void*)pIX) { 159 cout << "pIUnknown != pIX" << endl; 160 } 161 162 pIY->Release(); // pIY不再使用,引用计数2 163 pIY = NULL; 164 165 // 从IX查询IY 166 IY* pIY2 = NULL; 167 hr = pIX->QueryInterface(IID_IY, (void**)&pIY2); //引用计数3 168 169 pIX->Release(); // pIX不再使用,引用计数2 170 pIX = NULL; 171 172 if (SUCCEEDED(hr)) { 173 pIY2->Fy1(); 174 pIY2->Fy2(); 175 } 176 177 pIY2->Release(); // pIY2不再使用,引用计数1 178 pIY2 = NULL; 179 } 180 181 //===================================== 182 // 目前引用计数位1,因为pIUnknown还在使用 183 //===================================== 184 IX* pIX2 = NULL; 185 hr = pIUnknown->QueryInterface(IID_IX, (void**)&pIX2); 186 if (SUCCEEDED(hr)) { 187 IX* pIX3 = NULL; 188 pIX3 = pIX2; // 执行了赋值 189 pIX3->AddRef(); // 由于上一句执行了赋值,所以引用计数需要自增,引用计数为3 190 191 pIX3->Fx1(); 192 pIX3->Fx2(); 193 194 pIX3->Release(); // pIX3不再使用,引用计数为2 195 pIX3 = NULL; 196 } 197 pIX2->Release(); // pIX2不再使用,引用计数为1 198 pIX2 = NULL; 199 200 pIUnknown->Release(); // pIUnknown不再使用,引用计数为0,Release函数里执行了delete this 201 pIUnknown = NULL; 202 203 // delete pA; 不需要再调用 204 205 getchar(); 206 return 0; 207 }
这里需要注意AddRef函数和Release的意义和其使用的时机:
- AddRef,使组件的引用计数自增1。
- 在返回之前调用AddRef。比如组件的构造函数,QueryInterface函数。
- 在赋值之后调用AddRef比如pIX3 = pIX2,这时需要pIX3->AddRef();
- Release,使组件引用计数自减1,如果引用计数为零释放本身的内存资源。
- 在接口使用完之后,调用Release。
- 查看Section2Demo1,关于AddRef 与Release的使用,并适当注释一些AddRef或Release查看CA的析构函数是否运行或会不会出现对无效指针(野指针)的操作。
三、引用计数的优化
分析以下代码:
if(SUCCEEDED(hr)){ IX *pIX3 = NULL; pIX3 = PIX2; PIX3->AddRef(); PIX3->Fx1(); PIX3->Fx2(); PIX3->Release(); PIX3 = NULL; }
对于pIX3与pIX3来说,他们都是同一个接口,生命周期是一样的,这个接口在一个块中,执行了一次AddRef,一次的Release,其实就相当于这二者都没有执行。是否可以进行优化呢?考虑以下优化:
if(SUCCEEDED(hr)){ IX *pIX3 = NULL; pIX3 = PIX2; PIX3->Fx1(); PIX3->Fx2(); }
引用计数优化原则:
一、输入参数原则:
- 输入参数指的是给函数传递某个值的参数。在函数体中将会使用这个值但却不会修改它或将其返回给调用者。在C++中,输入参数实际上就是那些按值传递的参数。
- 对传入函数的接口指针,无需调用AddRef与Release
void Fun(IX *pIX){ IX *pIX2 = pIX; // pIX2->AddRef(); //可优化 pIX2->Fx1(); pIX2->Fx2(); // pIX2->Release(); //可优化 }
二、局部变量原则
对于局部复制的接口指针,由于它们只是在函数的生命期内才存在,因此无需调用AddRef与Release
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!