自写动态数组——参考《C++语言程序设计》(清华大学第4版)

简介: 自写动态数组——参考《C++语言程序设计》(清华大学第4版)

@[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新特性constexprconst,前者标记常量,后者标记只读。下面提供详细链接。
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;
}
相关文章
|
存储 C++
C++面向对象语言程序设计期末实验
C++面向对象语言程序设计期末实验
56 0
C++-基于参考灰度图上色GrayToColorFromOther
C++-基于参考灰度图上色GrayToColorFromOther
|
编译器 程序员 C++
c++语言程序设计——头文件和引用系统头文件、用户头文件的定义及使用方法
c++语言程序设计——头文件和引用系统头文件、用户头文件的定义及使用方法
c++语言程序设计——头文件和引用系统头文件、用户头文件的定义及使用方法
|
C++ iOS开发
高级语言程序设计II 实验报告四学生学籍系统,使用c++
          高级语言程序设计II 实验报告四               姓名:许恺 学号:2014011329 日期:6月26日               1. 实验目的 制作学生学籍系统,使用c++知识,又补充和查找的功能 2. 设计思路 建立三个类,student储存读出来的文件内容,change对文件进行补充和读取,seek进行查找和打印。
952 0
|
iOS开发 C++
高级语言程序设计II 实验报告一c++的基本语法和编码模式
    高级语言程序设计II 实验报告一       姓名:许恺 学号:2014011329 日期:2015年5月25日                                         1. 实验目的 掌握c++的基本语法和编码模式,学会用c++编写小型的项目 2. 设计思路 没什么思路,就是按照 c的代码去改编,毕竟对于c++我还是知之甚少。
865 0
|
C++ iOS开发
高级语言程序设计II 实验报告二文本文件形式读写c++
    高级语言程序设计II 实验报告二       姓名:许恺 学号:2014011329 日期:6月4日                                           1. 实验目的 学会用文本文件形式读写,通过c++实现。
1055 0
|
C++ iOS开发
高级语言程序设计II 实验报告三c++使用文本文件和二进制文件的读写比较两者的区别和优劣
                高级语言程序设计II 实验报告三       姓名:许恺 学号:2014011329 日期:2015年5月25日                     1. 实验目的 通过在c++中使用文本文件和二进制文件的读写比较两者的区别和优劣,同时锻炼c++的编程能力。
1214 0
|
C++ 容器
C++参考的翻译或校对
做新年规划的时候,我说过要翻译C++常用类的参考。C++的参考,其实别人已经翻译完了,只是部分内容需要校对。由于网站结构中大量使用了模板,同一个函数只需要翻译一个地方,所以四天就弄完了。
743 0
|
物联网 C++ 人工智能
2014秋C++第13周项目4参考-数组的排序
课程主页在http://blog.csdn.net/sxhelijian/article/details/39152703,课程资源在云学堂“贺老师课堂”同步展示,使用的帐号请到课程主页中查看。  【项目4 - 数组的排序】(1)编写函数,完成冒泡排序,要求不能改变下面的main函数。 //两个函数bubble_sort和output_array的声明 int main( ) {
1073 0