C++中的動態數組(Dynamic Array)是指動態分配的、可以根據需求動態增長佔用記憶體的數組。為了實現一個動態數組類的封裝,我們需要考慮幾個問題:new/delete的使用、記憶體配置策略、類的四大函數(建構函式、拷貝建構函式、拷貝賦值運算子、解構函式)、運算子的重載。涉及到的知識點很多,對此本文只做簡單的介紹。
一、記憶體配置策略
當用new為一個動態數組申請一塊記憶體時,數組中的元素是連續儲存的,例如 vector和string。當向一個動態數組添加元素時,如果沒有空間容納新元素,不可能簡單地將新元素添加到記憶體中的其他位置——因為元素必須連續儲存。所以必須重新分配一塊更大的記憶體空間,將原來的元素從舊位置移動到新空間中,然後添加新元素,釋放舊的記憶體空間。如果我們每添加一個新元素,就執行一次這樣的記憶體配置和釋放操作,效率將會慢到不行。
為了避免上述的代價,必須減少記憶體重新分配的次數。所以我們採取的策略是:在不得不分配新的記憶體空間時,分配比新的空間需求更大的記憶體空間(通常為2倍)。這樣,在相當一段時間內,添加元素時就不用重新申請記憶體空間。注意,只有當迫不得已時才可以分配新的記憶體空間。
二、類的四大函數
一個C++類一般至少有四大函數,即建構函式、拷貝建構函式、拷貝賦值運算子、解構函式。如果類未自己定義上述函數,C++編譯器將為其合成4個預設的版本。但是往往編譯器合成的並不是我們所期望的,為此我們有必要自己定義它們。
1.建構函式
類的建構函式(constructor)用來初始化類對象的非static資料成員,無論何時只要類的對象被建立,就會執行建構函式。
class Foo { public: Foo(); // 建構函式 Foo(string &s); // ... };
建構函式的名字和類名相同,沒有傳回型別。類可以包含多個建構函式(重載),它們之間在參數數量或類型上需要有所區別。建構函式有一個初始化部分和一個函數體,成員的初始化是在函數體執行之前完成的。
2.拷貝建構函式
如果一個建構函式的第一個參數是自身類類型的引用,且任何額外參數都有預設值,則此建構函式是拷貝建構函式(copy constructor)。
class Foo { public: Foo(); Foo(const Foo&); // 拷貝建構函式 // ... };
拷貝建構函式定義了如何用一個對象初始化另一個同類型的對象。拷貝初始化通常使用拷貝建構函式來完成。拷貝初始化發生在下列情況中:
使用等號(=)初始化一個變數
將一個對象作為實參傳遞給一個非參考型別的形參
從一個傳回型別為非參考型別的函數返回一個對象
用花括弧列表初始化一個數組中的元素
3.拷貝賦值運算子
類的拷貝賦值運算子(copy-assignment operator)是一個名為operator=的函數。類似於其他任何函數,它也有一個傳回型別和一個參數列表。
class Foo { public: Foo(); Foo& operator=(const Foo&); // 賦值運算子 // ... };
拷貝賦值運算子定義了如何將一個對象賦值給另一個同類型的對象。賦值運算子是一個成員函數也是一個二元運算子,其左側運算對象就綁定到隱式的this指標,右側運算對象作為顯式參數傳遞。注意:為了與內建類型的賦值保持一致,賦值運算子通常返回一個指向其左側運算對象的引用。
4.解構函式
類的解構函式(destructor)用來釋放類對象使用的資源並銷毀類對象的非static資料成員,無論何時只要一個對象被銷毀,就會自動執行解構函式。
class Foo { public: ~Foo(); // 解構函式 // ... };
解構函式的名字由波浪號(~)加類名構成,也沒有傳回型別。由於解構函式不接受參數,因此它不能被重載。解構函式有一個函數體和一個析構部分,銷毀一個對象時,首先執行解構函式體,然後按初始化順序的逆序銷毀成員。
三、運算子的重載
重載的運算子是具有特殊名字的函數:它們的名字由關鍵字operator和其後要定義的運算子號共同組成。和其他函數一樣,重載的運算子也包含傳回型別、參數列表、函數體,比如拷貝賦值運算子。
當我們定義重載的運算子時,必須首先決定是將其聲明為類的成員函數還是聲明為一個普通的非成員函數。有些運算子必須作為成員,而另一些運算子作為普通函數比作為成員更好:
賦值(=)、下標([ ])、調用(( ))和成員訪問箭頭(->)運算子必須是成員。
複合賦值運算子一般來說應該是成員,但並非必須,這一點與賦值運算子略有不同。
改變對象狀態的運算子或者與給定類型密切相關的運算子,如遞增、遞減、解引用運算子,通常應該是成員。
具有對稱性的運算子可能轉換任意一端的運算對象,例如算術、相等性、關係和位元運算符等,因此它們通常應該是普通的非成員函數。
當然,除了賦值運算子之外,我們還需要為動態數組定義下標運算子operator []。下標運算子必須是成員函數。為了讓下標可以出現在賦值運算子的任意一端,下標運算子函數通常返回所訪問元素的引用。
四、動態數組類的封裝
下面給出了動態數組DArray類的介面:
class DArray { private: double *m_Data; // 存放數組的動態記憶體指標 int m_Size; // 數組的元素個數 int m_Max; // 預留給動態數組的記憶體大小 private: void Init(); // 初始化 void Free(); // 釋放動態記憶體 inline bool InvalidateIndex(int nIndex); // 判斷下標的合法性 public: DArray(); // 預設建構函式 DArray(int nSize, double dValue = 0); // 建構函式,設定數組大小,預設值為dValue DArray(const DArray& arr); // 拷貝建構函式 DArray& operator=(const DArray& arr); // 拷貝賦值運算子 ~DArray(); // 解構函式 void Print(); // 輸出顯式所有數組元素的值 int GetSize(); // 擷取數組的大小(元素個數) void SetSize(int nSize); // 重新設定數組的大小,若nSize小於原大小,截斷;否則,新元素置0 double GetAt(int nIndex); // 擷取指定位置元素 void SetAt(int nIndex,double dValue); // 重設指定元素的值 void PushBack(double dValue); // 追加一個新元素到數組末尾 void DeleteAt(int nIndex); // 刪除指定位置地元素 void InsertAt(int nIndex, double dValue); // 插入一個新的元素到數組中 double operator[](int nIndex) const; // 重載下標運算子[] };
下面是實現方法:
void DArray::Init() { m_Size = 0; // 預設情況下數組不包含元素 m_Max = 1; m_Data = new double[m_Max]; } void DArray::Free() { delete [] m_Data; } bool DArray::InvalidateIndex(int nIndex) { if(nIndex>=0 && nIndex<m_Size) return false; else return true; } // 預設建構函式 DArray::DArray() { Init(); } // 建構函式 DArray::DArray(int nSize, double dValue) { if(nSize == 0) Init(); else { m_Size = nSize; m_Max = nSize; m_Data = new double[m_Max]; for(int i=0; i<nSize; ++i) m_Data[i]=dValue; } } // 拷貝建構函式 DArray::DArray(const DArray& arr) { m_Size = arr.m_Size; /*複製常規成員*/ m_Max = arr.m_Max; m_Data = new double[m_Max]; /*複製指標指向的內容*/ memcpy(m_Data, arr.m_Data, m_Size*sizeof(double)); } // 拷貝賦值運算子 DArray& DArray::operator=(const DArray& arr) { if(this == &arr) /*自賦值*/ return *this; m_Size = arr.m_Size; m_Max = arr.m_Max; /* 先將右側對象拷貝到臨時對象中,然後再銷毀左側對象*/ double *m_Temp = new double[m_Max]; memcpy(m_Temp, arr.m_Data, m_Size*sizeof(double)); delete [] m_Data; m_Data = m_Temp; return *this; } // 解構函式 DArray::~DArray() { Free(); } // 列印數組 void DArray::Print() { if(m_Size == 0) { cout << "Error: The empty array can't be Printed." << endl; exit(0); } else { for(int i=0; i<m_Size; ++i) cout << m_Data[i] << " "; cout << endl; } } // 擷取數組大小 int DArray::GetSize() { return m_Size; } // 重設數組大小 void DArray::SetSize(int nSize) { if(nSize < m_Size) /*截斷*/ { for(int i=nSize; i<m_Size; ++i) m_Data[i] = 0; } if(m_Size<=nSize && nSize<=m_Max) /*新增元素置0*/ { for(int i=m_Size; i<nSize; ++i) m_Data[i] = 0; } if(nSize > m_Max) /*需要重新分配空間*/ { m_Max = nSize; double *temp = new double[m_Max]; memcpy(temp, m_Data, m_Size*sizeof(double)); for(int i=m_Size; i<nSize; ++i) temp[i] = 0; delete [] m_Data; m_Data = temp; } m_Size = nSize; /*設定數組大小*/ } // 擷取指定位置元素 double DArray::GetAt(int nIndex) { if(InvalidateIndex(nIndex)) { cout << "Error: the index of GetAt is invalid!" << endl; exit(0); } return m_Data[nIndex]; } // 設定指定位置元素的值 void DArray::SetAt(int nIndex, double dValue) { if(InvalidateIndex(nIndex)) { cout << "Error: the index of SetAt is invalid!" << endl; exit(0); } else { m_Data[nIndex] = dValue; } } // 追加一個新元素到數組末尾 void DArray::PushBack(double dValue) { if(m_Size < m_Max) { m_Data[m_Size] = dValue; } else { m_Max = m_Max*2; double* temp = new double[m_Max]; memcpy(temp, m_Data, m_Size*sizeof(double)); delete [] m_Data; m_Data = temp; m_Data[m_Size] = dValue; } ++m_Size; /*數組大小加1*/ } // 從數組中刪除一個元素 void DArray::DeleteAt(int nIndex) { if(InvalidateIndex(nIndex)) { cout << "Error: the index of DeleteAt is invalid." << endl; exit(0); } else { for(int i=nIndex; i<m_Size; ++i) m_Data[i] = m_Data[i+1]; m_Data[m_Size-1] = 0; --m_Size; } } // 插入一個新元素到指定位置 void DArray::InsertAt(int nIndex, double dValue) { if(nIndex<0 || nIndex>m_Size) { cout << "Error: the index of InsertAt is invalid!" << endl; exit(0); } if(m_Size < m_Max) /* 未滿,插入 */ { for(int i=m_Size-1; i>=nIndex; --i) m_Data[i+1] = m_Data[i]; m_Data[nIndex] = dValue; } else /* 重新分配空間 */ { m_Max = m_Max*2; double* temp = new double[m_Max]; memcpy(temp, m_Data, m_Size*sizeof(double)); delete [] m_Data; m_Data = temp; for(int i=m_Size-1; i>=nIndex; --i) m_Data[i+1] = m_Data[i]; m_Data[nIndex] = dValue; } ++m_Size; /* 數組大小加1 */ } // 重載下標運算子[] double DArray::operator[](int nIndex) const { if(nIndex<0 || nIndex>=m_Size) { cout << "Error: the index in [] is invalid!" << endl; exit(0); } return m_Data[nIndex]; }
經過簡單的測試,暫時還沒有發現Bug。可能測試並不全面,感興趣的讀者可以進一步測試並完善該程式。
附:String類的實現
C++ 的一個常見面試題是讓你實現一個 String 類,限於時間,不可能要求具備 std::string 的功能,但至少要求能正確管理資源。
如果你弄懂了上面DArray類的寫法,那麼實現String類應該就不難了。因為面試官一般只是想考查你能不能正確地寫出建構函式、解構函式、拷貝建構函式、拷貝賦值運算子以及+、[ ]、<<、>>運算子多載等等。下面給出一個String類的介面,你可以自己試試手實現一下:
class String{ friend ostream& operator<< (ostream&,String&); //重載<<運算子 friend istream& operator>> (istream&,String&); //重載>>運算子 public: String(); // 預設建構函式 String(const char* str); // 帶參建構函式 String(const String& rhs); // 拷貝建構函式 String& operator=(const String& rhs); // 拷貝賦值運算子 String operator+(const String& rhs) const; //operator+ bool operator==(const String&); //operator== bool operator!=(const String&); //operator!= char& operator[](unsigned int); //operator[] size_t size() const; const char* c_str() const; ~String(); // 解構函式 private: char *m_data; // 用於儲存字串 };
本文所述DArray類和String類的源碼及測試代碼可點擊此處本站下載。