程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> VC >> 關於VC++ >> 深入分析MFC中的CArray類

深入分析MFC中的CArray類

編輯:關於VC++

我們在使用vc進行比較復雜的編程時,經常需要用到復雜的數組結構,並希望能實現動態 管理。由於C++並不支持動態數組,MFC提供了一個CArray類來實現動態數組的功能。有效的 使用CArray類,可以提高程序的效率。
MFC提供了一套模板庫,來實現一些比較常見的 數據結構如Array,List,Map。CArray即為其中的一個,用來實現動態數組的功能。

CArray是從CObject派生,有兩個模板參數,第一個參數就是CArray類數組元素的變量類型 ,後一個是函數調用時的參數類型。

我們有一個類 class Object,我們要定義一個 Object的動態數組,那麼我們可以用以下兩種方法:

CArray<Object,Object> Var1;
CArray<Object,Object&> Var2;

Var1與Var2哪一個的效率要高呢? Var2的效率要高。為什麼呢?接下來我們對CArray的源代碼做一個剖析就清楚了。

先 了解一下CArray中的成員變量及作用。

TYPE* m_pData;  // 數據保存地址的指針
int m_nSize;    // 用戶當前定義的數組的大小
int m_nMaxSize;  // 當前實 際分配的數組的大小
int m_nGrowBy;  // 分配內存時增長的元素個數

首先來 看它的構造函數,對成員變量進行了初始化。

CArray<TYPE, ARG_TYPE>::CArray()
{
  m_pData = NULL;
  m_nSize = m_nMaxSize = m_nGrowBy = 0;
}

SetSize成員函數是用來為數組分配空間的, 從這裡著手,看CArray是如何對數據進行管理的。SetSize的函數定義如下:

void SetSize( int nNewSize, int nGrowBy = -1 );

nNewSize 指定數組的大小

nGrowBy 如果需要增加數組大小時增加的元素的個數。

對SetSize的代碼,進行分析 。(由於代碼太長,只列出部分重要部分)

void CArray<TYPE, ARG_TYPE>::SetSize(int nNewSize, int nGrowBy)
{
  if (nNewSize == 0)
  {
    // 第一種情況
    // 當nNewSize為0時,需要將數 組置為空,
    // 如果數組本身即為空,則不需做任何處理
    // 如 果數組本身已含有數據,則需要清除數組元素
    if (m_pData != NULL)
     {
      //DestructElements 函數實現了對數組元素析構函數的調用
      //不能使用delete m_pData 因為我們必須要調用數組元素的析構函數
      DestructElements<TYPE>(m_pData, m_nSize);
      // 現在才能釋放內存
      delete[] (BYTE*)m_pData;
       m_pData = NULL;
    }
    m_nSize = m_nMaxSize = 0;
  }
  else if (m_pData == NULL)
  {
    // 第二種情況
     // 當m_pData==NULL時還沒有為數組分配內存
    //首先我們要為數組分 配內存,sizeof(TYPE)可以得到數組元素所需的字節數
    //使用new 數組分配 了內存。注意,沒有調用構造函數
    m_pData = (TYPE*) new BYTE[nNewSize * sizeof(TYPE)];
    //下面的函數調用數組元素的構造函數
     ConstructElements<TYPE>(m_pData, nNewSize);
    //記錄下當前數組元 素的個數
    m_nSize = m_nMaxSize = nNewSize;
  }
  else if (nNewSize <= m_nMaxSize)
  {
    // 第三種情況
     // 這種情況需要分配的元素個數比已經實際已經分配的元素個數要少
    if (nNewSize > m_nSize)
    {
      // 需要增加元素的情況
      // 與第二種情況的處理過程,既然元素空間已經分配,
       // 只要調用新增元素的構造函數就Ok
      ConstructElements<TYPE> (&m_pData[m_nSize], nNewSize-m_nSize);
    }
    else if (m_nSize > nNewSize)
    {
      // 現在是元素減少的情況, 我們是否要重新分配內存呢?
      // No,這種做法不好,後面來討論。
      // 下面代碼釋放多余的元素,不是釋放內存,只是調用析構函數
       DestructElements<TYPE>(&m_pData[nNewSize], m_nSize- nNewSize);
    }
    m_nSize = nNewSize;
  }
   else
  {
    //這是最糟糕的情況,因為需要的元素大於m_nMaxSize,
    // 意味著需要重新分配內存才能解決問題
    // 計算需要分配 的數組元素的個數
    int nNewMax;
    if (nNewSize < m_nMaxSize + nGrowBy)
      nNewMax = m_nMaxSize + nGrowBy;
     else
      nNewMax = nNewSize; 
    // 重新分配一塊內存
    TYPE* pNewData = (TYPE*) new BYTE[nNewMax * sizeof(TYPE)];
     //實現將已有的數據復制到新的的內存空間
    memcpy(pNewData, m_pData, m_nSize * sizeof(TYPE));
    // 對新增的元素調用構造函數
    ConstructElements<TYPE>(&pNewData[m_nSize], nNewSize- m_nSize);
    //釋放內存
    delete[] (BYTE*)m_pData;
     //將數據保存
    m_pData = pNewData;
    m_nSize = nNewSize;
    m_nMaxSize = nNewMax;
  }
}

注意上面代碼 中標注為粗體的代碼,它們實現了對象的構造與析構。如果我們只為對象分配內存,卻沒有 調用構造與析構函數,會不會有問題呢?

如果只是使用c++的基本數據類型,如果 int,long,那的確不會有什麼問題。如果使用的是一個類,比如下面的類:

class Object
{
public:
 Object(){ ID = 0; }
 ~Object();
protected:
 int ID;
};

我們只為Object類分配了空間,也能正常使 用。但是,類的成員變量ID的值卻是不定的,因為沒有初始化。如果是一個更復雜的組合類 ,在構造函數中做了許多工作,那可能就不能正常運行了。同樣,刪除的數組元素時 ,也一定要調用它的析構函數。我們來看下面的Preson類

class Preson
{
public:
  Preson()
  {
    name = new char[10];
  }
  ~Preson()
  {
    delete []name;
  }
  char* name;
  int  age;
}

如果我沒調用構造函數,那麼對 name操作肯定會出錯。我們調用了構造函數後,刪除元素時,如果不調用析構函數,那麼, name所指向的內存不能正確釋放,就會造成內存洩漏。我們來看一下 ConstructElements與DestructElements如何實現構造與析構函數的調用。
下面是 ConstructElements函數的實現代碼

template<class TYPE>
AFX_INLINE void AFXAPI ConstructElements(TYPE* pElements, int nCount)
{
  // first do bit-wise zero initialization
  memset((void*)pElements, 0, nCount * sizeof(TYPE));
  for (; nCount--; pElements++)
    ::new ((void*)pElements) TYPE;
}

ConstructElements是一個模板函數。對構造函數 的調用是通過標為黑體的代碼實現的。可能很多人不熟悉new 的這種用法,它可以實現指定 的內存空間中構造類的實例,不會再分配新的內存空間。類的實例產生在已經分配的內存中 ,並且new操作會調用對象的構造函數。因為vc中沒有辦法直接調用構造函數,而通過這種方 法,巧妙的實現對構造函數的調用。
再來看DestructElements 函數的代碼

template<class TYPE>
AFX_INLINE void AFXAPI DestructElements (TYPE* pElements, int nCount)
{
  for (; nCount--; pElements++)
    pElements->~TYPE();
}

DestructElements函數同樣是一個模板函 數,實現很簡單,直接調用類的析構函數即可。

如果定義一個CArray對象 CArray<Object,Object&> myObject ,對myObject就可象數組一樣,通過下標來 訪問指定的數組元素。通過[]來訪問數組元素是如何實現的呢?其實只要重載運算符[]即可 。CArray[]有兩種實現,區別在於返回值不同。我們來看看代碼:

template<class TYPE, class ARG_TYPE>
AFX_INLINE TYPE CArray<TYPE, ARG_TYPE>::operator[](int nIndex) const
  { return GetAt(nIndex); }
template<class TYPE, class ARG_TYPE>
AFX_INLINE TYPE& CArray<TYPE, ARG_TYPE>::operator[](int nIndex)
  { return ElementAt(nIndex); }

前一種情況是返回的對象的實例,後一種情況是返回對象的引 用。分別調用不同的成員函數來實現。我們來比較一下這兩個函數的實現(省略部 分):

TYPE  GetAt(int nIndex) const
  { ASSERT(nIndex >= 0 && nIndex < m_nSize);
    return m_pData[nIndex]; }
TYPE& ElementAt(int nIndex)
  { ASSERT(nIndex >= 0 && nIndex < m_nSize);
    return m_pData[nIndex]; }

除了返回值不同 ,其它都一樣,有什麼區別嗎?我們來看一個實例說明。

CArray<int,int&> arrInt;
arrInt.SetSize(10);
int n = arrInt.GetAt(0);
int& l = arrInt.ElementAt(0);
cout << arrInt[0] <<endl;
n = 10;
cout << arrInt[0] <<endl;
l = 20;
count << arrInt[0] << endl;

結 果會發現,n的變化不會影響到數組,而l的變化會改變數組元素的值。實際即是對C++中引用 運算符的運用。CArray下標訪問是非安全的,它並沒有超標預警功能。雖然使用 ASSERT提示,但下標超范圍時沒有進行處理,會引起非法內存訪問的錯誤。前面談到 模板實例化時有兩個參數,後一個參數一般用引用,為什麼呢?看看Add成員函數就可以明。 Add函數的作用是向數組添加一個元素。下面是它的定義:

int CArray<TYPE, ARG_TYPE>::Add(ARG_TYPE newElement)

Add函數使用的參數是模板參數的二個參 數,也就是說,這個參數的類型是我們來決定的,可以使用Object或Object&的方式。熟 悉C++的朋友都知道,傳引用的效率要高一些。如果是傳值的話,會在堆棧中再產生一個新的 對象,需要花費更多的時間。
下面來分析一下Add函數的代碼:
 

template<class TYPE, class ARG_TYPE>
AFX_INLINE int CArray<TYPE, ARG_TYPE>::Add(ARG_TYPE newElement)
{
  int nIndex = m_nSize;
  SetAtGrow(nIndex, newElement);
  return nIndex;
}

它實際是通過SetAtGrow函數來完成這個功能的,它的作用是設置指定元素的值。 下面是SetAtGrow的代碼:

template<class TYPE, class ARG_TYPE>
void CArray<TYPE, ARG_TYPE>::SetAtGrow(int nIndex, ARG_TYPE newElement)
{
  if (nIndex >= m_nSize)
    SetSize(nIndex+1, -1);
   m_pData[nIndex] = newElement;
}

SetAtGrow的實現也很簡單,如果指定的 元素已經存在,就把改變指定元素的值。如果指定的元素不存在,也就是 nIndex>=m_nSize的情況,就調用SetSize來調整數組的大小。
其實,到這裡,大家 對CArray類的內部實現有了一定的了解,只要看看MSDN的文檔,就可以靈活運用了。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved