C++动态数组类的封装实例
本文导语: C++中的动态数组(Dynamic Array)是指动态分配的、可以根据需求动态增长占用内存的数组。为了实现一个动态数组类的封装,我们需要考虑几个问题:new/delete的使用、内存分配策略、类的四大函数(构造函数、拷贝构造函数、...
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