65.9K
CodeProject 正在变化。 阅读更多。
Home

CArray 陷阱

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (17投票s)

2000年9月28日

CPOL
viewsIcon

401580

本文介绍了 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_pDataSetSize 中被删除,当返回到执行 SetAtGrow 中的 m_pData[nIndex] = newElement 行时,newElement 是对刚刚被删除的旧 m_pData 的引用!

所需条件

问题仅在以下所有三个条件都为真时才会发生

  1. CArray 模板的第二个参数是一个引用。
  2. 你调用以下 CArray 函数之一,并将现有的数组元素作为 newElement 参数传递
    1. Add
    2. SetAtGrow
    3. InsertAt
  3. 在 2) 中添加元素会在 SetSize 函数中导致内存分配。

考虑到所有这些条件,你可能认为这有点牵强。实际上并非如此。虽然我编造了上面显示的代码示例,以便演示这个问题,但真正的错误是在运行我们的应用程序时,使用客户发送的文件时发现的,因为应用程序给出了不正确的结果。我们使用 BoundsChecker 运行我们的应用程序,它发现 CArray 引用了一个悬空指针。一旦修改了这段代码,应用程序就能正常工作了。

解决方法

有许多方法可以避免/修复这个问题

  • 不要将引用作为 CArray 的第二个参数使用。这对于小型类型(如 int)来说是一个不错的解决方案,但对于大型结构来说效率不高。

    (例如,CArray 会导致问题,但 CArray 没问题。)

  • 对元素进行临时复制,然后将其添加到数组中。
  • 修复 Afxtempl.h,以便在删除之前进行赋值(如果你在 Microsoft 工作)。
© . All rights reserved.