@[toc]
一、静态数组&&动态数组?
众所周知:所谓静态数组其实是我们大一最初接触的一个知识点,这里不多说,需要回顾的读者可以看博主以前这期数组——参考《C和指针》的博客。
静态数组:①直接访问(随机访问)内存连续;②大小在编译时就确定了,运行时候无法修改,使用静态数组不能有效地避免下标越界地问题。
动态数组: 顾名思义:可以自己动的数组,就变成了动态数组,这里所谓的自己动,就是它的大小可以随着我们用户的需求改变而改变。如果了解一些STL
那么我们就知道vector
其实就是一个动态数组。它是由任意多个位置连续的、类型相同的元素组成,其元素个数在程序运行时改变。
目标: 实现一个简单的动态数组。
二、数组类模板定义
直接拉出此类的定义,为啥这样定义?刚入门C++的同学肯定会特别疑惑,不慌,后续咱们一个函数一个函数地介绍:包括函数地参数,返回值,为啥要重载等。
#include<iostream>
#include<cassert>
using namespace std;
template<class T>
class Array {
public:
Array(int sz=50); //构造函数
Array(const Array<T>& a); //复制构造函数
~Array();//析构函数
Array<T>& operator=(const Array<T>&rhs); //重载“=”使数组对象可以整体赋值
T& operator[](int i); //重载"[]"运算符,使Array对象可以起到C++普通数组的作用
const T& operator[](int i)const; //"[]"运算符对const的重载
operator T* (); //重载到T*类型的转化,使Array对象可以起到C++普通数组的作用
operator const T* ()const; //到T*类型的转化操作符对const的重载
int getSize() const; //取到数组的大小
void resize(int sz); //修改数组的大小
private:
T* list; //T类型的指针,用于存放动态数组的内存首地址
int size; //数组大小(元素个数)
};
三、自写:构造函数&&析构函数
3.1 构造函数:Array(int sz=50)
这里用到了模板template
,不熟悉的小伙伴没关系,其实就是给函数多传入一个T
类型,这样的话,函数中的T
可以被用户指定类型。也就是类名带一个参数T
就等价于原来的类型。
问题:不是说数组是连续的内容吗?你这里new
出来的空间是连续的吗?符合数组的特征吗?
回答: new
是在堆区申请空间,但是它申请的是连续的数组空间,醒醒!想啥呢?不是堆区或栈区决定空间是否连续,是申请的数据类型决定空间是否连续。
//构造函数
template<typename T>
Array<T>::Array(int sz)
{
assert(sz>=0); //sz为数组大小(元素个数),应当非负
this->size = sz; //设置数组元素个数
this->list = new T[size]; //动态分配size个大小为T类型的元素空间
}
3.2 析构函数~Array()
知识点: new
出来的数组,释放内存时候,要加[]
如下:
//析构函数
template<typename T>
Array<T>::~Array()
{
delete []list; //释放new出来的空间,有new就必须有delete
}
四、复制构造函数:Array<T>::Array(const Array<T> &a)
4.1代码
话不多说,先上代码:
//复制构造函数
template<typename T>
Array<T>::Array(const Array<T> &a)
{
//从a对象取得数组大小,并赋值给当前对象的成员
this->size = a.size;
//为对象申请内存并进行出错检查
this->list = new T[list];
//从对象a复制数组元素到本对象
for (int i = 0; i < this->size; ++i)
{
this->list[i] = a.list[i];
}
}
4.2 疑问?
问题①:为啥没有返回值?
回答: 醒醒构造函数,析构函数有个der的返回值。
问题②: 为啥传入的是一个引用?
回答:传引用比传值效率高,少了一步拷贝复制的操作,自然效率高一些。讨论拷贝构造函数和赋值函数
问题③: 为啥是一个const
引用?
回答:很明显,这里是不想改变原来对象的值,所以使用const
吧,a
对象作为一个只读对象。
回顾: C++11新特性constexpr
和const
,前者标记常量,后者标记只读。下面提供详细链接。
constexpr关键字详细解读链接`
4.3 深复制和浅复制
初学者在拿到需求的时候,往往会想着直接这样写不就行了:注意以下为错误写法
,
//浅复制
template<typename T>
Array<T>::Array(const Array<T> &a)
{
//从a对象取得数组大小,并赋值给当前对象的成员
this->size = a.size;
//为对象申请内存并进行出错检查
this->list=a.list;
}
如下图所示,当a,b
都指向同一块内存,就会存在很多隐患。例如:当a
被析构的时候,a
所指的内存中的元素已经消失了,但是b
还指向那块内存。还有就是当程序结束时候,会自动调用2次析构,并且析构同一块内存,这当然是不允许的。所以我们要为b
重新new
一块内存。
`
五、重载“=”运算符(赋值函数):Array<T>& Array<T>::operator=(const Array<T>& rhs)
5.1 代码
//重载“=”运算符,将rhs对象的值赋值给本对象
template<typename T>
Array<T>& Array<T>::operator=(const Array<T>& rhs)
{
//赋值对象和被赋值对象不等再进行操作
if (rhs != this)
{
//如果两数组大小不同,则重新申请空间,并赋值
if (rhs.size!=this->size)
{
delete[] this->list;
this->size = rhs->size;
this->list = new T[this->size];
}
//一次给新对象赋值
for (int i = 0; i < this->size; ++i)
{
this->list[i] = rhs.list[i];
}
}
return *this;
}
5.2 问题
问题①: 为啥形参类型是const Array<T>&
呢?
回答:同上,引用为了提高效率,防止拷贝,const
关键字为了防止修改原来的值。
问题②:返回的是*this
指针是个啥啥东西?
回答:明显this
是个指针,指针解引用就是个对象,所以返回的是本对象。
问题③:你都说返回的是个对象了?那你的函数返回值为啥是个Array<T>&
引用?引用底层不是指针吗?返回是对象?为啥接住的是指针?
这里要清楚一点函数返回值和函数返回类型的关系:返回类型只是决定分一块什么类型的内存来存储该返回值。 这里用引用接住返回的对象,可以防止多一次多余的拷贝构造。返回值是引用类型,return *this
;就是将返回的引用绑定到*this
上。我返回了一个对象了嘛,没有;我创建了对象了嘛,也没有;我只是给引用绑定了值而已。如果深研究这一点你可以:看这篇博客:博客链接:讨论拷贝构造函数和赋值函数
问题④: 啥时候调用拷贝构造函数?
回答:还有形参实参是传值的情况,函数返回值是形参实参的情况。总之一句话:①旧对象 初始化 新对象 才会调用拷贝构造函数。 对此将特意出一期博客去探讨博客链接:讨论拷贝构造函数和赋值函数
问题⑤:为啥重载[]
和T*
有const
和非const
之分?但是重载=
缺没有
醒醒,你用等号就是为了改变某值,加const
就是防止改变,这不就是本末倒置了吗?
六、重载[]
运算符
6.1 代码
使用第一种声明方式,[ ]
不仅可以访问元素,还可以修改元素。使用第二种声明方式,[ ]
只能访问而不能修改元素。在实际开发中,我们应该同时提供以上两种形式,这样做是为了适应const
对象,因为通过 const
对象只能调用 const
成员函数,如果不提供第二种形式,那么将无法访问const
对象的任何元素。
//重载“[]”运算符,使得Array对象可以起到C++普通数组的作用
template<typename T>
T& Array<T>::operator[](int i)
{
assert(i >= 0 && i < this->size); //检查下标是否越界
return this->list[i]; //返回下标为n的数组元素
}
template<typename T>
const T& Array<T>::operator[](int i)const
{
assert(i >= 0 && i < this->size); //检查下标是否越界
return this->list[i]; //返回下标为n的数组元素
}
6.2 问题
问题①: 为啥重载[]
运算符返回值是引用类型?
回答:同上,为了防止拷贝构造,而导致降低效率。
问题②:为啥重载该运算符有const
和非const
之分?
回答:const
函数是常成员函数,他只能由常对象调用,他是常对象唯一的对外接口,所以是常对象的就调用const
函数,其他的调用非const
函数。
回顾:const
修饰的对象只能const
修饰的成员函数,不能调非const
修饰的对象函数。原因参考Innovator_cjx的博客
问题③: 如果只有一个重载的函数?又会怎么样?
回答:非const
对象可以调用const
函数,但是const
对象不能调用非const
函数,否则会出现类型不匹配。如果不提供第二种形式,那么将无法访问const
对象的任何元素。
七、重载T*
类型(指针转化运算符)的转化
对于自定义的类型对象,编译器无法提供自动转化功能,所以我们要自己编写重载的指针类型转化函数。C++中如果想将自定义类型T的对象隐式或者显式转化为S类型
有两种办法:
①隐式:operator S
定义为T
的成员函数。
②显式: 用static_cast
显示转化为S类型时,该成员函数会被调用。
7.1 代码
//重载T*类型的转化
template<typename T>
Array<T>::operator T* ()
{
return this->list;
}
//重载T*类型转化操作符对const的重载
template<typename T>
Array<T>::operator const T* ()const
{
return this->list;
}
7.2 问题?
问题①: 为啥有const
和非const
之分?
同上,const
类型对象不可调用非const
的函数,所以必须自己写一个const
类型的函数。
问题②: 为啥重载指针转化运算符?
重载指针转化运算符,将Array
类的对象名转化为T
类型的指针,指向当前对象中的私有数组,因而可以像使用普通数组首地址一样使用Array
类的对象名。
问题③: 重载类型转化为啥没有返回类型?参考刘哩子不会写代码的博客
与以前的重载运算符不同的是,类型转换运算符重载函数没有返回类型,因为“类型名”就代表了它的返回类型,而且也没有任何参数。在调用过程中要带一个对象实参。
转化操作符的重载函数不用指定返回值类型,这是由于这种情况下重载函数的返回类型与操作符名称一致,因此C++标准规定不能为这类函数指定返回类型void
也不要写。
当对象本身是常数时,为了避免通过指针对数组内容进行修改,只能将对象转化为常指针。
八、取到数组的大小getSize()
template<typename T>
int Array<T>::getSize()const
{
return this->size;
}
九、修改数组的大小resize(int sz)
//修改数组的大小
template<typename T>
void Array<T>::resize(int sz)
{
assert(sz>=0); //判断修改数组大小是否合法
if (this->size == sz) //如果修改大小和原大小一样,就没必要修改
{
return;
}
//把现在数组的值,保存在宁外一个数组里头
T* newlist = new T[sz];
//将sz和this->size中较小的一个选出来
int n = (sz<this->size)?sz:this->size;
//以此把原数组的元素拷贝到新数组中
for (int i = 0; i < n; ++i)
{
newlist[i] = this->list[i];
}
delete[]list; //已经拷贝完了,就可以释放原数组的内存了
this->list = newlist; //本对象的数组已经宁有其人了
this->size = sz; //本对象的大小也变了
}
十、怎样重载 运算符号函数 呢?
看到这里可能已经有点晕乎乎了,究竟怎样重载运算符?为啥有些时候又带const
有些时候又不带?有些时候写两个有些时候写一个?仔细总结一下这些个规律:
10.1 如何确定重载函数运算的返回类型?
首先我们要知道,这个返回类型将来要用来做什么?
一个含算术运算符(+ -
)的表达式,可以用来继续参加其他运算或者放在等号右边用于给其他对象赋值,还可以有很多其他用途,但是绝不会放在等号左边,再被赋值。你见过a+b=c
这样的表达式吗?当然没有,这是不被允许的。
但是[]
运算符就不同了,我们经常写这样得表达式a[1]=3
,这时候[]
运算符得结果被放在等号左边,称为一个左值。
如果一个函数得返回值是一个对象的值,它是不应该成为左值的,对于+ -
这样的运算符,以对象类型作为返回值是应该的,因为对其结果赋值没有任何意义。然后[]
运算符就不同了,恰恰经常需要将它作为左值。实现这个愿望的办法就是,将`"[]"
重载函数的返回值指定为引用。由于引用表示的是对象的别名,并且是指向一块内存空间的,所以可以通过引用改变对象的值。
结论:看该运算符的结果,将来的用途,如果用作左值,可以用引用,否则就用对象。 强调一点:返回类型只是决定分一块什么类型的内存来存储该返回值。
10.2 什么时候加const
参考rockkyy的博客
对于Array
的常对象而言,由于不希望通过[]
来修改其值,所以返回类型为常引用。
回顾: 之所以用引用是为了在T
较复杂的类型时,避免创建新对象时,执行复制构造的开销。
函数加上const
后缀表示此函数不修改类成员变量,如果在函数里修改了则编译报错,起到一个保护成员变量的作用(对象不能访问非公有成员变量,但是可以通过成员函数去访问、修改,返回类型前加上const
就防止通过成员函数去修改成员变量)。
十二、完整代码
#include<iostream>
#include<cassert>
using namespace std;
#define DIS true
template<class T>
class Array {
public:
Array(int sz=50); //构造函数
Array(const Array<T>& a); //复制构造函数
~Array();//析构函数
Array<T>& operator=(const Array<T>&rhs); //重载“=”使数组对象可以整体赋值
T& operator[](int i); //重载"[]"下标运算符运算符,使Array对象可以起到C++普通数组的作用
const T& operator[](int i)const; //"[]"下标运算符对const的重载
operator T* (); //重载到T*类型的转化,使Array对象可以起到C++普通数组的作用
operator const T* ()const; //到T*类型的转化操作符对const的重载
int getSize() const; //取到数组的大小
void resize(int sz); //修改数组的大小
Array<T>& Test(const Array<T>& a); //测试函数
private:
T* list; //T类型的指针,用于存放动态数组的内存首地址
int size; //数组大小(元素个数)
};
//重载T*类型的转化
template<typename T>
Array<T>::operator T* ()
{
return this->list;
}
template<typename T>
Array<T>::operator const T* ()const
{
return this->list;
}
//重载“[]”运算符,使得Array对象可以起到C++普通数组的作用
template<typename T>
T& Array<T>::operator[](int i)
{
assert(i >= 0 && i < this->size); //检查下标是否越界
return this->list[i]; //返回下标为n的数组元素
}
template<typename T>
const T& Array<T>::operator[](int i)const
{
assert(i >= 0 && i < this->size); //检查下标是否越界
return this->list[i]; //返回下标为n的数组元素
}
//重载“=”运算符,将rhs对象的值赋值给本对象
template<typename T>
Array<T>& Array<T>::operator=(const Array<T>& rhs)
{
//赋值对象和被赋值对象不等再进行操作
if (&rhs != this)
{
//如果两数组大小不同,则重新申请空间,并赋值
if (rhs.size!=this->size)
{
delete[] this->list;
this->size = rhs->size;
this->list = new T[this->size];
}
//一次给新对象赋值
for (int i = 0; i < this->size; ++i)
{
this->list[i] = rhs.list[i];
}
}
cout << "调用重载=" << endl;
return *this;
}
//构造函数
template<typename T>
Array<T>::Array(int sz)
{
assert(sz>=0); //sz为数组大小(元素个数),应当非负
this->size = sz; //设置数组元素个数
this->list = new T[size]; //动态分配size个大小为T类型的元素空间
}
//析构函数
template<typename T>
Array<T>::~Array()
{
delete[]list; //释放空间
}
//复制构造函数
template<typename T>
Array<T>::Array(const Array<T> &a)
{
//从a对象取得数组大小,并赋值给当前对象的成员
this->size = a.size;
//为对象申请内存并进行出错检查
this->list = new T[this->size];
//从对象a复制数组元素到本对象
for (int i = 0; i < this->size; ++i)
{
this->list[i] = a.list[i];
}
cout << "调用复制构造函数" << endl;
}
//取得数组大小
template<typename T>
int Array<T>::getSize()const
{
return this->size;
}
//修改数组的大小
template<typename T>
void Array<T>::resize(int sz)
{
assert(sz>=0); //判断修改数组大小是否合法
if (this->size == sz) //如果修改大小和原大小一样,就没必要修改
{
return;
}
//把现在数组的值,保存在宁外一个数组里头
T* newlist = new T[sz];
//将sz和this->size中较小的一个选出来
int n = (sz<this->size)?sz:this->size;
//以此把原数组的元素拷贝到新数组中
for (int i = 0; i < n; ++i)
{
newlist[i] = this->list[i];
}
delete[]list; //已经拷贝完了,就可以释放原数组的内存了
this->list = newlist; //本对象的数组已经宁有其人了
this->size = sz; //本对象的大小也变了
}
template<typename T>
Array<T>& Array<T>::Test(const Array<T>& a)
{
*this = a;
return *this;
}
template<typename T>
void fun(const Array<T> &a)//里头的const代表只读
{
cout<<a[1];
}
int main()
{
Array<int> my(3);//调用构造
my[1] = 2;
fun(my); //const T& Array<T>::operator[](int i)const
return 0;
}