C++动态数组类的封装实例
C++中的动态数组(DynamicArray)是指动态分配的、可以根据需求动态增长占用内存的数组。为了实现一个动态数组类的封装,我们需要考虑几个问题:new/delete的使用、内存分配策略、类的四大函数(构造函数、拷贝构造函数、拷贝赋值运算符、析构函数)、运算符的重载。涉及到的知识点很多,对此本文只做简单的介绍。
一、内存分配策略
当用new为一个动态数组申请一块内存时,数组中的元素是连续存储的,例如vector和string。当向一个动态数组添加元素时,如果没有空间容纳新元素,不可能简单地将新元素添加到内存中的其他位置——因为元素必须连续存储。所以必须重新分配一块更大的内存空间,将原来的元素从旧位置移动到新空间中,然后添加新元素,释放旧的内存空间。如果我们每添加一个新元素,就执行一次这样的内存分配和释放操作,效率将会慢到不行。
为了避免上述的代价,必须减少内存重新分配的次数。所以我们采取的策略是:在不得不分配新的内存空间时,分配比新的空间需求更大的内存空间(通常为2倍)。这样,在相当一段时间内,添加元素时就不用重新申请内存空间。注意,只有当迫不得已时才可以分配新的内存空间。
二、类的四大函数
一个C++类一般至少有四大函数,即构造函数、拷贝构造函数、拷贝赋值运算符、析构函数。如果类未自己定义上述函数,C++编译器将为其合成4个默认的版本。但是往往编译器合成的并不是我们所期望的,为此我们有必要自己定义它们。
1.构造函数
类的构造函数(constructor)用来初始化类对象的非static数据成员,无论何时只要类的对象被创建,就会执行构造函数。
classFoo{
public:
Foo();//构造函数
Foo(string&s);
//...
};
构造函数的名字和类名相同,没有返回类型。类可以包含多个构造函数(重载),它们之间在参数数量或类型上需要有所区别。构造函数有一个初始化部分和一个函数体,成员的初始化是在函数体执行之前完成的。
2.拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数(copyconstructor)。
classFoo{
public:
Foo();
Foo(constFoo&);//拷贝构造函数
//...
};
拷贝构造函数定义了如何用一个对象初始化另一个同类型的对象。拷贝初始化通常使用拷贝构造函数来完成。拷贝初始化发生在下列情况中:
使用等号(=)初始化一个变量
将一个对象作为实参传递给一个非引用类型的形参
从一个返回类型为非引用类型的函数返回一个对象
用花括号列表初始化一个数组中的元素
3.拷贝赋值运算符
类的拷贝赋值运算符(copy-assignmentoperator)是一个名为operator=的函数。类似于其他任何函数,它也有一个返回类型和一个参数列表。
classFoo{
public:
Foo();
Foo&operator=(constFoo&);//赋值运算符
//...
};
拷贝赋值运算符定义了如何将一个对象赋值给另一个同类型的对象。赋值运算符是一个成员函数也是一个二元运算符,其左侧运算对象就绑定到隐式的this指针,右侧运算对象作为显式参数传递。注意:为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用。
4.析构函数
类的析构函数(destructor)用来释放类对象使用的资源并销毁类对象的非static数据成员,无论何时只要一个对象被销毁,就会自动执行析构函数。
classFoo{
public:
~Foo();//析构函数
//...
};
析构函数的名字由波浪号(~)加类名构成,也没有返回类型。由于析构函数不接受参数,因此它不能被重载。析构函数有一个函数体和一个析构部分,销毁一个对象时,首先执行析构函数体,然后按初始化顺序的逆序销毁成员。
三、运算符的重载
重载的运算符是具有特殊名字的函数:它们的名字由关键字operator和其后要定义的运算符号共同组成。和其他函数一样,重载的运算符也包含返回类型、参数列表、函数体,比如拷贝赋值运算符。
当我们定义重载的运算符时,必须首先决定是将其声明为类的成员函数还是声明为一个普通的非成员函数。有些运算符必须作为成员,而另一些运算符作为普通函数比作为成员更好:
赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员。
复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同。
改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减、解引用运算符,通常应该是成员。
具有对称性的运算符可能转换任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数。
当然,除了赋值运算符之外,我们还需要为动态数组定义下标运算符operator[]。下标运算符必须是成员函数。为了让下标可以出现在赋值运算符的任意一端,下标运算符函数通常返回所访问元素的引用。
四、动态数组类的封装
下面给出了动态数组DArray类的接口:
classDArray
{
private:
double*m_Data;//存放数组的动态内存指针
intm_Size;//数组的元素个数
intm_Max;//预留给动态数组的内存大小
private:
voidInit();//初始化
voidFree();//释放动态内存
inlineboolInvalidateIndex(intnIndex);//判断下标的合法性
public:
DArray();//默认构造函数
DArray(intnSize,doubledValue=0);//构造函数,设置数组大小,默认值为dValue
DArray(constDArray&arr);//拷贝构造函数
DArray&operator=(constDArray&arr);//拷贝赋值运算符
~DArray();//析构函数
voidPrint();//输出显式所有数组元素的值
intGetSize();//获取数组的大小(元素个数)
voidSetSize(intnSize);//重新设置数组的大小,若nSize小于原大小,截断;否则,新元素置0
doubleGetAt(intnIndex);//获取指定位置元素
voidSetAt(intnIndex,doubledValue);//重置指定元素的值
voidPushBack(doubledValue);//追加一个新元素到数组末尾
voidDeleteAt(intnIndex);//删除指定位置地元素
voidInsertAt(intnIndex,doubledValue);//插入一个新的元素到数组中
doubleoperator[](intnIndex)const;//重载下标运算符[]
};
下面是实现方法:
voidDArray::Init()
{
m_Size=0;//默认情况下数组不包含元素
m_Max=1;
m_Data=newdouble[m_Max];
}
voidDArray::Free()
{
delete[]m_Data;
}
boolDArray::InvalidateIndex(intnIndex)
{
if(nIndex>=0&&nIndex<m_Size)
returnfalse;
else
returntrue;
}
//默认构造函数
DArray::DArray()
{
Init();
}
//构造函数
DArray::DArray(intnSize,doubledValue)
{
if(nSize==0)
Init();
else
{
m_Size=nSize;
m_Max=nSize;
m_Data=newdouble[m_Max];
for(inti=0;i<nSize;++i)
m_Data[i]=dValue;
}
}
//拷贝构造函数
DArray::DArray(constDArray&arr)
{
m_Size=arr.m_Size;/*复制常规成员*/
m_Max=arr.m_Max;
m_Data=newdouble[m_Max];/*复制指针指向的内容*/
memcpy(m_Data,arr.m_Data,m_Size*sizeof(double));
}
//拷贝赋值运算符
DArray&DArray::operator=(constDArray&arr)
{
if(this==&arr)/*自赋值*/
return*this;
m_Size=arr.m_Size;
m_Max=arr.m_Max;
/*先将右侧对象拷贝到临时对象中,然后再销毁左侧对象*/
double*m_Temp=newdouble[m_Max];
memcpy(m_Temp,arr.m_Data,m_Size*sizeof(double));
delete[]m_Data;
m_Data=m_Temp;
return*this;
}
//析构函数
DArray::~DArray()
{
Free();
}
//打印数组
voidDArray::Print()
{
if(m_Size==0)
{
cout<<"Error:Theemptyarraycan'tbePrinted."<<endl;
exit(0);
}
else
{
for(inti=0;i<m_Size;++i)
cout<<m_Data[i]<<"";
cout<<endl;
}
}
//获取数组大小
intDArray::GetSize()
{
returnm_Size;
}
//重置数组大小
voidDArray::SetSize(intnSize)
{
if(nSize<m_Size)/*截断*/
{
for(inti=nSize;i<m_Size;++i)
m_Data[i]=0;
}
if(m_Size<=nSize&&nSize<=m_Max)/*新增元素置0*/
{
for(inti=m_Size;i<nSize;++i)
m_Data[i]=0;
}
if(nSize>m_Max)/*需要重新分配空间*/
{
m_Max=nSize;
double*temp=newdouble[m_Max];
memcpy(temp,m_Data,m_Size*sizeof(double));
for(inti=m_Size;i<nSize;++i)
temp[i]=0;
delete[]m_Data;
m_Data=temp;
}
m_Size=nSize;/*设置数组大小*/
}
//获取指定位置元素
doubleDArray::GetAt(intnIndex)
{
if(InvalidateIndex(nIndex))
{
cout<<"Error:theindexofGetAtisinvalid!"<<endl;
exit(0);
}
returnm_Data[nIndex];
}
//设置指定位置元素的值
voidDArray::SetAt(intnIndex,doubledValue)
{
if(InvalidateIndex(nIndex))
{
cout<<"Error:theindexofSetAtisinvalid!"<<endl;
exit(0);
}
else
{
m_Data[nIndex]=dValue;
}
}
//追加一个新元素到数组末尾
voidDArray::PushBack(doubledValue)
{
if(m_Size<m_Max)
{
m_Data[m_Size]=dValue;
}
else
{
m_Max=m_Max*2;
double*temp=newdouble[m_Max];
memcpy(temp,m_Data,m_Size*sizeof(double));
delete[]m_Data;
m_Data=temp;
m_Data[m_Size]=dValue;
}
++m_Size;/*数组大小加1*/
}
//从数组中删除一个元素
voidDArray::DeleteAt(intnIndex)
{
if(InvalidateIndex(nIndex))
{
cout<<"Error:theindexofDeleteAtisinvalid."<<endl;
exit(0);
}
else
{
for(inti=nIndex;i<m_Size;++i)
m_Data[i]=m_Data[i+1];
m_Data[m_Size-1]=0;
--m_Size;
}
}
//插入一个新元素到指定位置
voidDArray::InsertAt(intnIndex,doubledValue)
{
if(nIndex<0||nIndex>m_Size)
{
cout<<"Error:theindexofInsertAtisinvalid!"<<endl;
exit(0);
}
if(m_Size<m_Max)/*未满,插入*/
{
for(inti=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=newdouble[m_Max];
memcpy(temp,m_Data,m_Size*sizeof(double));
delete[]m_Data;
m_Data=temp;
for(inti=m_Size-1;i>=nIndex;--i)
m_Data[i+1]=m_Data[i];
m_Data[nIndex]=dValue;
}
++m_Size;/*数组大小加1*/
}
//重载下标运算符[]
doubleDArray::operator[](intnIndex)const
{
if(nIndex<0||nIndex>=m_Size)
{
cout<<"Error:theindexin[]isinvalid!"<<endl;
exit(0);
}
returnm_Data[nIndex];
}
经过简单的测试,暂时还没有发现Bug。可能测试并不全面,感兴趣的读者可以进一步测试并完善该程序。
附:String类的实现
C++的一个常见面试题是让你实现一个String类,限于时间,不可能要求具备std::string的功能,但至少要求能正确管理资源。
如果你弄懂了上面DArray类的写法,那么实现String类应该就不难了。因为面试官一般只是想考查你能不能正确地写出构造函数、析构函数、拷贝构造函数、拷贝赋值运算符以及+、[]、<<、>>运算符重载等等。下面给出一个String类的接口,你可以自己试试手实现一下:
classString{
friendostream&operator<<(ostream&,String&);//重载<<运算符
friendistream&operator>>(istream&,String&);//重载>>运算符
public:
String();//默认构造函数
String(constchar*str);//带参构造函数
String(constString&rhs);//拷贝构造函数
String&operator=(constString&rhs);//拷贝赋值运算符
Stringoperator+(constString&rhs)const;//operator+
booloperator==(constString&);//operator==
booloperator!=(constString&);//operator!=
char&operator[](unsignedint);//operator[]
size_tsize()const;
constchar*c_str()const;
~String();//析构函数
private:
char*m_data;//用于保存字符串
};
本文所述DArray类和String类的源码及测试代码可点击此处本站下载。