无意中发现一个有趣的问题,连接点中用于保存接收器对象的容器CComDynamicUnkArray有一些小问题。
calloc和_recalloc的参数传递问题
首先看一下连接点的声明:
template <class T, const IID* piid, class CDV = CComDynamicUnkArray >
class ATL_NO_VTABLE IConnectionPointImpl :
public _ICPLocator<piid>
{
typedef CComEnum<IEnumConnections, &__uuidof(IEnumConnections), CONNECTDATA,
_Copy<CONNECTDATA> > CComEnumConnections;
typedef CDV _CDV;
public:
~IConnectionPointImpl();
STDMETHOD(_LocCPQueryInterface)(
_In_ REFIID riid,
_COM_Outptr_ void** ppvObject)
{
#ifndef _ATL_OLEDB_CONFORMANCE_TESTS
ATLASSERT(ppvObject != NULL);
#endif
if (ppvObject == NULL)
return E_POINTER;
*ppvObject = NULL;
if (InlineIsEqualGUID(riid, __uuidof(IConnectionPoint)) || InlineIsEqualUnknown(riid))
{
*ppvObject = this;
AddRef();
#if defined(_ATL_DEBUG_INTERFACES) && !defined(_ATL_STATIC_LIB_IMPL)
_AtlDebugInterfacesModule.AddThunk((IUnknown**)ppvObject, _T("IConnectionPointImpl"), riid);
#endif // _ATL_DEBUG_INTERFACES
return S_OK;
}
else
return E_NOINTERFACE;
}
STDMETHOD(GetConnectionInterface)(_Out_ IID* piid2)
{
if (piid2 == NULL)
return E_POINTER;
*piid2 = *piid;
return S_OK;
}
STDMETHOD(GetConnectionPointContainer)(
_Outptr_ IConnectionPointContainer** ppCPC)
{
T* pT = static_cast<T*>(this);
// No need to check ppCPC for NULL since QI will do that for us
return pT->QueryInterface(__uuidof(IConnectionPointContainer), (void**)ppCPC);
}
STDMETHOD(Advise)(
_Inout_ IUnknown* pUnkSink,
_Out_ DWORD* pdwCookie);
STDMETHOD(Unadvise)(_In_ DWORD dwCookie);
STDMETHOD(EnumConnections)(_COM_Outptr_ IEnumConnections** ppEnum);
CDV m_vec;
};
我们可以看到IConnectionPointImpl是一个模板类,它有一个成员:CDV m_vec;
CDV是一个模板参数,它有一个默认值CComDynamicUnkArray,通常大家用的都是这个默认值CComDynamicUnkArray。
看一下CComDynamicUnkArray::Add函数:
inline DWORD CComDynamicUnkArray::Add(_In_ IUnknown* pUnk)
{
IUnknown** pp = NULL;
if (m_nSize == 0)
{
// Create array with _DEFAULT_VECTORLENGTH number of items.
ATLTRY(pp = (IUnknown**)calloc(sizeof(IUnknown*),_DEFAULT_VECTORLENGTH));
if (pp == NULL)
return 0;
memset(pp, 0, sizeof(IUnknown*)*_DEFAULT_VECTORLENGTH);
m_ppUnk = pp;
m_nSize = _DEFAULT_VECTORLENGTH;
}
// Walk array and use empty slots if any.
DWORD dwCookie = 1;
for (pp = begin(); pp < end(); pp++)
{
if (*pp == NULL)
{
*pp = pUnk;
return dwCookie; // cookie minus one is index into array
}
dwCookie++;
}
// No empty slots so resize array.
// # of new slots is double of current size.
int nAlloc = 0;
HRESULT hr = AtlMultiply(&nAlloc, m_nSize, 2);
if (FAILED(hr))
{
return 0;
}
pp = (IUnknown**)_recalloc(m_ppUnk, sizeof(IUnknown*),nAlloc);
if (pp == NULL)
return 0;
m_ppUnk = pp;
memset(&m_ppUnk[m_nSize], 0, sizeof(IUnknown*)*m_nSize);
m_ppUnk[m_nSize] = pUnk;
dwCookie = m_nSize+1;
m_nSize = nAlloc;
return dwCookie; // cookie minus one is index into array
}
注意:
ATLTRY(pp = (IUnknown**)calloc(sizeof(IUnknown*),_DEFAULT_VECTORLENGTH));
和
pp = (IUnknown**)_recalloc(m_ppUnk, sizeof(IUnknown*),nAlloc);
是不是参数传错了?
calloc的原型是:
void *calloc(
size_t num,
size_t size
);
_recalloc的原型是:
void *_recalloc(
void *memblock
size_t num,
size_t size
);
上面两行代码明明参数传反了啊。calloc的第一个参数是元素的个数,第二个参数是每个元素的大小。可是Add()函数里面,反过来了。_recalloc也是这个问题。
从函数调用来说肯定是错了。
但是会不会导致问题呢?如果是,岂不是很危险?
为了让自己放心,我跟进去calloc函数,发现calloc会调用这个函数:
extern "C" void * __cdecl _calloc_dbg_impl(
size_t nNum,
size_t nSize,
int nBlockUse,
const char * szFileName,
int nLine,
int * errno_tmp
)
{
void * pvBlk;
/* ensure that (nSize * nNum) does not overflow */
if (nNum > 0)
{
_VALIDATE_RETURN_NOEXC((_HEAP_MAXREQ / nNum) >= nSize, ENOMEM, NULL);
}
nSize *= nNum;
/*
* try to malloc the requested space
*/
pvBlk = _nh_malloc_dbg_impl(nSize, _newmode, nBlockUse, szFileName, nLine, errno_tmp);
/*
* If malloc() succeeded, initialize the allocated space to zeros.
* Note that unlike _calloc_base, exactly nNum bytes are set to zero.
*/
if ( pvBlk != NULL )
{
memset(pvBlk, 0, nSize);
}
RTCCALLBACK(_RTC_Allocate_hook, (pvBlk, nSize, 0));
return(pvBlk);
}
我们会看到这一行代码:nSize *= nNum;
然后nSize被传入了_nh_malloc_dbg_imple,那么就是说元素个数和每个元素大小会被乘起来,然后分配内存。
这样的话,calloc参数位置搞错好像也没有什么问题。
不管怎么样,ATLTRY(pp = (IUnknown**)calloc(sizeof(IUnknown*),_DEFAULT_VECTORLENGTH));肯定是参数传错了,但是它不会导致问题。
_recalloc的问题也差不多。
它跑到这里了:
extern "C" _CRTIMP void * __cdecl _recalloc_dbg
(
void * memblock,
size_t count,
size_t size,
int nBlockUse,
const char * szFileName,
int nLine
)
{
size_t size_orig = 0, old_size = 0;
void * retp = NULL;
/* ensure that (size * count) does not overflow */
if (count > 0)
{
_VALIDATE_RETURN_NOEXC((_HEAP_MAXREQ / count) >= size, ENOMEM, NULL);
}
size_orig = size * count;
if (memblock != NULL)
{
old_size = _msize((void*)memblock);
}
retp = _realloc_dbg(memblock, size_orig, nBlockUse, szFileName, nLine);
if (retp != NULL && old_size < size_orig)
{
memset ((char*)retp + old_size, 0, size_orig - old_size);
}
return retp;
}
看这个size_orig = size * count;,也是乘起来去分配内存,那么位置搞错也没有什么关系了。
OK,结论就是:
1. calloc参数传错了,但是不会导致问题
2. _recalloc参数也传错了,但是不会导致问题。
有关_recalloc的另外一个问题,如果_recalloc需要分配更多的内存,而原来的那个地址又不能扩展了,那又如何?
_recalloc位置变换
首先看容器的Add()函数里面有这么一行:
pp = (IUnknown**)_recalloc(m_ppUnk, sizeof(IUnknown*),nAlloc);
m_ppUnk指向原来的一个位置,比如地址是A,它现在已经有1024个字节了,然后又需要分配4096个字节。这个时候有两种情况:
1. A地址后面还有空闲的空间可以分配出4096个字节,那么返回值pp也指向A
2. A地址后面不足4096字节,比如2048以后的空间已经分配给其他用途了,那么pp就可能指向另外一个地址,并不是A了。也就是说系统重新在另外一个地方开辟了空间用来存放1024 + 4096个字节。
如果#2发生了,那么会不会有个问题就是:原来的1024个字节内存上存放的信息就没有了?
看了一下MSDN对于_recallo的介绍 http://msdn.microsoft.com/en-us/library/ms235322.aspx
A combination of realloc and calloc. Reallocates an array in memory and initializes its elements to 0.
如果所有的内存都被清零了,那原来的信息岂不是丢了?
做个简单测试,写下如下代码:
void* p = _recalloc(NULL, 1, 1024);
memset(p, 'A', 1024);
void* p2 = _recalloc(p, 4, 1024);
运行一下,发现p和p2的地址不一样。
这个说明_recalloc的时候确实是新开辟了一个空间(#2).
然后看看p2,0x00700aa8指向的内存:
最后一个A是0x700EA7 - 0x700AA8 + 1 = 0x400, 也就是十进制的1024个字节。这么说,好像是在开辟新空间的时候,原来的信息还是copy过来了,后面新增长的就全市0x00了。那是不是说_recalloc如果发现原来的地址不能分配出更多内存,而需要在另外一个地方分配的时候,会把原来的数据都copy到新的地方呢?
看了一下MSDN对于_recalloc的说明,好像也没有发现对于这一部分的描述。但是看到_recalloc是由realloc和calloc组成的。查看了一下realloc函数,发现有这么一句话:
The contents of the block are unchanged up to the shorter of the new and old sizes, although the new block can be in a different location.
其实前面半句,我也不是很明白,是不是说:块的内容不会发生变化除非新长度比旧的长度来的短,但是新的块可以是在另外一个地方。
基本应该没错。
那么这样的话,就算#2发生了,也不要紧,旧的数据还是会被copy过去的,然后新增长的内存就统统被清零了。
OK, CComDynamicUnkArray::Add的问题就讲完了。
另外,如果我们不喜欢用CComDynamicUnkArray,还可以用自定义的容器,
template <class T, const IID* piid, class CDV = CComDynamicUnkArray >
class ATL_NO_VTABLE IConnectionPointImpl :
public _ICPLocator<piid>
自定义一个容器,然后传给IConnectionPointImpl就是了,第三个模板。
CComDynamicUnkArray是用C写的,我们完全可以自己写个容器,可以用STL的vector。
比如写个类,叫做CMyArray,然后实现CComDynamicUnkArray的每一个函数,可以直接用std::vector来存放。
实际上CMyArray就是个adapter。将std::vector适配一下,这样就可以在IConnectionPointImpl里面使用。这也就是适配器模式的一个应用。