CArray 陷阱






4.93/5 (17投票s)
本文介绍了 CArray 类在某些情况下如何访问已删除的内存
问题
CArray
是我最喜欢的类之一。它可能比我使用的任何其他代码都节省了我更多的时间。因为它是一个如此流行的代码片段,我曾以为它已经被用于每种可能的情况,并且所有的缺陷都已经被修复了——所以几天前我们遇到这个问题时我很惊讶。
请看下面的代码,看看你是否能发现其中的错误
CArray< int,int&> my_carray; int some_number = 1; my_carray.Add(some_number); for(int i=0; i<=10; i++) { my_carray.Add(my_carray[0]); } TRACE("\nIndex\tValue"); for(int j=0; j<=10; j++) { TRACE("\n%d\t%d", j, my_carray[j]); }
TRACE
的输出是
Index Value
0 1
1 -572662307
2 1
3 1
4 1
5 -572662307
6 1
7 1
8 1
9 -572662307
10 1
可能不是你所期望的。
进入 Afxtempl.h
一些来自 Afxtempl.h 的代码片段将有助于展示幕后发生的事情。我们将从查看 Add
函数开始
AFX_INLINE int CArray< TYPE, ARG_TYPE>::Add(ARG_TYPE newElement) { int nIndex = m_nSize; SetAtGrow(nIndex, newElement); return nIndex; }
Add
中没有什么奇怪的地方,它只是调用了 SetAtGrow
template< class TYPE, class ARG_TYPE> void CArray< TYPE, ARG_TYPE>::SetAtGrow(int nIndex, ARG_TYPE newElement) { ASSERT_VALID(this); ASSERT(nIndex >= 0); if (nIndex >= m_nSize) SetSize(nIndex+1, -1); m_pData[nIndex] = newElement; }
注意,当 if
语句为真时,在分配 newElement
之前会调用 SetSize
。现在看看 SetSize
的代码:(这是一个很大的函数——有趣的部分在底部用粗体标出)
template< class TYPE, class ARG_TYPE> void CArray< TYPE, ARG_TYPE>::SetSize(int nNewSize, int nGrowBy) { ASSERT_VALID(this); ASSERT(nNewSize >= 0); if (nGrowBy != -1) m_nGrowBy = nGrowBy; // set new size if (nNewSize == 0) { // shrink to nothing if (m_pData != NULL) { DestructElements< TYPE>(m_pData, m_nSize); delete[] (BYTE*)m_pData; m_pData = NULL; } m_nSize = m_nMaxSize = 0; } else if (m_pData == NULL) { // create one with exact size #ifdef SIZE_T_MAX ASSERT(nNewSize <= SIZE_T_MAX/sizeof(TYPE)); // no overflow #endif m_pData = (TYPE*) new BYTE[nNewSize * sizeof(TYPE)]; ConstructElements< TYPE>(m_pData, nNewSize); m_nSize = m_nMaxSize = nNewSize; } else if (nNewSize <= m_nMaxSize) { // it fits if (nNewSize > m_nSize) { // initialize the new elements ConstructElements< TYPE>(&m_pData[m_nSize], nNewSize-m_nSize); } else if (m_nSize > nNewSize) { // destroy the old elements DestructElements< TYPE>(&m_pData[nNewSize], m_nSize-nNewSize); } m_nSize = nNewSize; } else { // otherwise, grow array int nGrowBy = m_nGrowBy; if (nGrowBy == 0) { // heuristically determine growth when nGrowBy == 0 // (this avoids heap fragmentation in many situations) nGrowBy = m_nSize / 8; nGrowBy = (nGrowBy < 4) ? 4 : ((nGrowBy > 1024) ? 1024 : nGrowBy); } int nNewMax; if (nNewSize < m_nMaxSize + nGrowBy) nNewMax = m_nMaxSize + nGrowBy; // granularity else nNewMax = nNewSize; // no slush ASSERT(nNewMax >= m_nMaxSize); // no wrap around #ifdef SIZE_T_MAX ASSERT(nNewMax <= SIZE_T_MAX/sizeof(TYPE)); // no overflow #endif TYPE* pNewData = (TYPE*) new BYTE[nNewMax * sizeof(TYPE)]; // copy new data from old memcpy(pNewData, m_pData, m_nSize * sizeof(TYPE)); // construct remaining elements ASSERT(nNewSize > m_nSize); ConstructElements< TYPE>(&pNewData[m_nSize], nNewSize-m_nSize); // get rid of old stuff (note: no destructors called) delete[] (BYTE*)m_pData; m_pData = pNewData; m_nSize = nNewSize; m_nMaxSize = nNewMax; } }
发生的情况是,m_pData
在 SetSize
中被删除,当返回到执行 SetAtGrow
中的 m_pData[nIndex] = newElement
行时,newElement
是对刚刚被删除的旧 m_pData
的引用!
所需条件
问题仅在以下所有三个条件都为真时才会发生
CArray
模板的第二个参数是一个引用。- 你调用以下
CArray
函数之一,并将现有的数组元素作为newElement
参数传递Add
SetAtGrow
InsertAt
- 在 2) 中添加元素会在
SetSize
函数中导致内存分配。
考虑到所有这些条件,你可能认为这有点牵强。实际上并非如此。虽然我编造了上面显示的代码示例,以便演示这个问题,但真正的错误是在运行我们的应用程序时,使用客户发送的文件时发现的,因为应用程序给出了不正确的结果。我们使用 BoundsChecker 运行我们的应用程序,它发现 CArray
引用了一个悬空指针。一旦修改了这段代码,应用程序就能正常工作了。
解决方法
有许多方法可以避免/修复这个问题
- 不要将引用作为
CArray
的第二个参数使用。这对于小型类型(如int
)来说是一个不错的解决方案,但对于大型结构来说效率不高。(例如,
CArray
会导致问题,但CArray
没问题。) - 对元素进行临时复制,然后将其添加到数组中。
- 修复 Afxtempl.h,以便在删除之前进行赋值(如果你在 Microsoft 工作)。