C++基础教程面向对象(学习笔记(40))

本文深入探讨了C++中的容器类概念,详细介绍了容器类的功能、类型及其在组织和存储数据方面的重要作用。通过具体示例,展示了如何从头开始构建一个整数数组容器类,实现动态调整大小、插入、删除等关键功能。

容器类

在现实生活中,我们一直使用容器。您的早餐麦片放在一个盒子里,书中的页面都有封面和装订,您可以将任意数量的物品存放在的容器中。没有容器,与许多这些物体一起工作将非常不方便。想象一下,试着读一本没有装订的书,或者吃饭不用碗。这将是非常糟糕的。容器提供的价值主要在于它能够帮助组织和存储放在其中的物品。

同样,一个容器类是一个用于保存和组织另一个类型(另一个类或基本类型)的多个实例的类。存在许多不同种类的容器类,每种容器类在其使用中具有各种优点,缺点和限制。到目前为止,编程中最常用的容器是数组,您已经看过很多例子。虽然C ++具有内置的数组功能,但程序员通常会使用数组容器类(std :: array或std :: vector),因为它们提供了额外的好处。与内置数组不同,数组容器类通常提供动态调整大小(添加或删除元素时),在传递给函数时记住它们的大小,并进行边界检查。这不仅使数组容器类比普通数组更方便,而且更安全。

容器类通常实现相当标准化的最小功能集。大多数明确定义的容器将包括以下功能:

#创建一个空容器(通过构造函数)
#将新对象插入容器中
#从容器中删除对象
#报告当前容器中的对象数
#清空所有对象的容器
#提供对存储对象的访问
#对元素进行排序(可选)
有时,某些容器类将省略某些功能。例如,数组容器类通常省略insert和remove函数,因为它们很慢并且类设计者不鼓励使用它们。

容器类实现成员关系。例如,数组的元素是(属于)数组的成员。请注意,我们在传统意义上使用“member-of”,而不是C ++类成员意义。

容器的类型

容器类通常有两种不同的类型。 值容器是存储它们所持有的对象的副本的组合(因此负责创建和销毁这些副本)。 引用容器是存储指针或对其他对象的引用的聚合(因此不负责创建或销毁这些对象)。

与现实生活中的容器不同,容器可以容纳您放入其中的任何类型的对象,在C ++中,容器通常只容纳一种类型的数据。例如,如果您有一个整数数组,它将只保存整数。与其他一些语言不同,C ++通常不允许您在容器中混合类型。如果您需要容器来保存整数和双精度,那么通常需要编写两个单独的容器来执行此操作(或使用模板,这是一种高级C ++功能)。尽管它们的使用受到限制,但容器非常有用,它们使编程更容易,更安全,更快捷。

数组容器类

在这个例子中,我们将从头开始编写一个整数数组类,它实现了容器应具有的大多数常用功能。这个数组类将是一个值容器,它将保存它正在组织的元素的副本。

首先,让我们创建IntArray.h文件:

#ifndef INTARRAY_H
#define INTARRAY_H
 
class IntArray
{
};
 
#endif

我们的IntArray需要跟踪两个值:数据本身和数组的大小。因为我们希望我们的数组能够改变大小,所以我们必须做一些动态分配,这意味着我们必须使用指针来存储数据。

#ifndef INTARRAY_H
#define INTARRAY_H
 
class IntArray
{
private:
    int m_length;
    int *m_data;
};
 
#endif

现在我们需要添加一些允许我们创建IntArrays的构造函数。我们将添加两个构造函数:一个构造一个空数组,另一个允许我们构造一个预定大小的数组。

#ifndef INTARRAY_H
#define INTARRAY_H
 
#include <cassert> // assert()
 
class IntArray
{
private:
    int m_length;
    int *m_data;
 
public:
    IntArray():
        m_length(0), m_data(nullptr)
    {
    }
 
    IntArray(int length):
        m_length(length)
    {
        assert(length >= 0);
 
        if (length > 0)
            m_data = new int[length];
        else
            m_data = nullptr;
    }
};
 
#endif

我们还需要一些函数来帮助我们清理IntArrays。首先,我们将编写一个析构函数,它只是解除分配任何动态分配的数据。其次,我们将编写一个名为erase()的函数,它将擦除数组并将长度设置为0。

~IntArray()
{
    delete[] m_data;
    // 我们不需要在这里将m_data设置为null或m_length为0,因为无论如何都会在此函数之后立即销毁该对象
}
 
void erase()
{
    delete[] m_data;
 
    // 我们需要确保在这里将m_data设置为nullptr
    // 留下指向释放的内存!
    m_data = nullptr;
    m_length = 0;
}

现在让我们重载[]运算符,以便我们可以访问数组的元素。我们应该检查索引以确保它是有效的,这最好使用assert()函数完成。我们还将添加一个访问函数来返回数组的长度。这是迄今为止的一切:

#ifndef INTARRAY_H
#define INTARRAY_H
 
#include <cassert> // for assert()
 
class IntArray
{
private:
    int m_length;
    int *m_data;
 
public:
    IntArray():
        m_length(0), m_data(nullptr)
    {
    }
 
    IntArray(int length):
        m_length(length)
    {
        assert(length >= 0);
 
        if (length > 0)
            m_data = new int[length];
        else
            m_data = nullptr;
    }
 
    ~IntArray()
    {
        delete[] m_data;
        //我们不需要在这里将m_data设置为null或m_length为0,因为无论如何都会在此函数之后立即销毁该对象
    }
 
    void erase()
    {
        delete[] m_data;
        //  我们需要确保在这里将m_data设置为nullptr
        // 留下指向释放的内存!
        m_data = nullptr;
        m_length = 0;
    }
 
    int& operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }
 
    int getLength() { return m_length; }
};
 
#endif

此时,我们已经有了一个可以使用的IntArray类。我们可以分配给定大小的IntArrays,我们可以使用[]运算符来检索或更改元素的值。

但是,我们的IntArray还有一些我们无法做到的事情。我们仍然无法改变它的大小,仍然无法插入或删除元素,我们仍然无法对其进行排序。

首先,让我们编写一些允许我们调整数组大小的代码。我们将编写两个不同的函数来执行此操作。第一个函数Reallocate()将在调整大小时销毁数组中的所有现有元素,但速度很快。第二个函数Resize()将在调整大小时保留数组中的所有现有元素,但速度很慢。

 // reallocate调整数组的大小。任何现有元素都将被销毁。此功能运行迅速。
    void reallocate(int newLength)
    {
        // 首先,我们删除任何现有元素
        erase();
 
        // 如果我们的数组现在是空的,请返回此处
        if (newLength <= 0)
            return;
 
        // 然后我们必须分配新元素
        m_data = new int[newLength];
        m_length = newLength;
    }
 
    // resize调整数组的大小。将保留任何现有元素。此函数运行缓慢。
    void resize(int newLength)
    {
        // 如果数组已经是原来的长度,我们不用做什么
        if (newLength == m_length)
            return;
 
        //如果我们要调整为空数组,请执行此操作并返回
        if (newLength <= 0)
        {
            erase();
            return;
        }
 
        //现在我们可以假设newLength至少有1个元素。
        //该算法的工作原理如下:首先,我们将分配一个新的数组。
       // 然后我们将从现有数组复制元素到新数组。
        //一旦做到这一点,我们可以销毁旧数组,并使m_data指向新数组。
        //首先,我们必须分配一个新的数组
        int *data = new int[newLength];
         //然后,我们必须找出有多少元素要复制到现有的数组到新数组。
          //在两个数组中较小的一个,我们想要复制尽可能多的元素。
        if (m_length > 0)
        {
            int elementsToCopy = (newLength > m_length) ? m_length : newLength;
 
            // 现在逐个复制元素
            for (int index=0; index < elementsToCopy ; ++index)
                data[index] = m_data[index];
        }
 
        // 现在我们可以删除旧数组,因为我们不再需要它了
        delete[] m_data;
        //并使用新的数组代替!请注意,这只是生成了m_data指向与我们动态分配的新数组相同的地址。
        //因为数据是动态分配的,所以当数据超出范围时不会被破坏。
        m_data = data;
        m_length = newLength;
    }

呼!这有点棘手!

许多数组容器类都会停在这里。但是,如果您想了解如何实现插入和删除功能,我们将继续编写这些功能。这两种算法都与resize()非常相似。

 void insertBefore(int value, int index)
    {
        // 完整性检查我们的指数值
        assert(index >= 0 && index <= m_length);
 
        // 首先创建一个比旧数组大一个元素的新数组
        int *data = new int[m_length+1];
 
        // 复制index之前的所有元素
        for (int before=0; before < index; ++before)
            data[before] = m_data[before];
 
        // 将我们的新元素插入新数组中
        data [index] = value;
 
        // 复制index元素后的所有值
        for (int after=index; after < m_length; ++after)
            data[after+1] = m_data[after];
 
        // 最后,删除旧数组,然后指向新数组
        delete[] m_data;
        m_data = data;
        ++m_length;
    }
 
    void remove(int index)
    {
        // 完整性检查我们的指数值
        assert(index >= 0 && index < m_length);
 
        // 如果这是数组中的最后一个元素,请将数组设置为空并挽救
        if (m_length == 1)
        {
            erase();
            return;
        }
 
        // 首先创建一个比旧数组小的新数组
        int *data = new int[m_length-1];
 
        // 复制index之前的所有元素
        for (int before=0; before  < index; ++before)
            data[before] = m_data[before];
 
        // 复制已删除元素后的所有值
        for (int after=index+1; after < m_length; ++after )
            data[after-1] = m_data[after];
 
        // 最后,删除旧数组,然后指向新数组
        delete[] m_data;
        m_data = data;
        --m_length;
    }
 
    // 为了方便,添加一些额外的函数
    void insertAtBeginning(int value) { insertBefore(value, 0); }
    void insertAtEnd(int value) { insertBefore(value, m_length); }

这是我们的IntArray容器类的全部内容。
IntArray.h:

#ifndef INTARRAY_H
#define INTARRAY_H
 
#include <cassert> //assert()
 
class IntArray
{
private:
    int m_length;
    int *m_data;
 
public:
    IntArray():
        m_length(0), m_data(nullptr)
    {
    }
 
    IntArray(int length):
        m_length(length)
    {
        assert(length >= 0);
        if (length > 0)
            m_data = new int[length];
        else
            m_data = nullptr;
    }
 
    ~IntArray()
    {
        delete[] m_data;
        // 我们不需要在这里将m_data设置为null或m_length为0,因为无论如何都会在此函数之后立即销毁该对象
    }
 
    void erase()
    {
        delete[] m_data;
        //  我们需要确保在这里将m_data设置为nullptr
        // 留下指向释放的内存!
        m_data = nullptr;
        m_length = 0;
    }
 
    int& operator[](int index)
    {
        assert(index >= 0 && index < m_length);
        return m_data[index];
    }
 
    // reallocate调整数组的大小。任何现有元素都将被销毁。此功能运行迅速。
    void reallocate(int newLength)
    {
        // 首先,我们删除任何现有元素
        erase();
 
        // 如果我们的数组现在是空的,请返回此处
        if (newLength <= 0)
            return;
 
        // 然后我们必须分配新元素
        m_data = new int[newLength];
        m_length = newLength;
    }
 
    //resize调整数组的大小。将保留任何现有元素。此功能运行缓慢。
    void resize(int newLength)
    {
        //如果数组已经是原来的长度,我们就完成了
        if (newLength == m_length)
            return;
 
        // 如果我们要调整为空数组,请执行此操作并返回
        if (newLength <= 0)
        {
            erase();
            return;
        }
 
        //现在我们可以假设newLength至少有1个元素。
        //该算法的工作原理如下:首先,我们将分配一个新的数组。
       // 然后我们将从现有数组复制元素到新数组。
        //一旦做到这一点,我们可以销毁旧数组,并使m_data指向新数组。
        //首先,我们必须分配一个新的数组
        int *data = new int[newLength];
         //然后,我们必须找出有多少元素要复制到现有的数组到新数组。
          //在两个数组中较小的一个,我们想要复制尽可能多的元素。
        if (m_length > 0)
        {
            int elementsToCopy = (newLength > m_length) ? m_length : newLength;
 
            // 现在逐个复制元素
            for (int index=0; index < elementsToCopy ; ++index)
                data[index] = m_data[index];
        }
 
        // 现在我们可以删除旧数组,因为我们不再需要它了
        delete[] m_data;
        //并使用新的数组代替!请注意,这只是生成了m_data指向与我们动态分配的新数组相同的地址。
        //因为数据是动态分配的,所以当数据超出范围时不会被破坏。
        m_data = data;
        m_length = newLength;
    }
 
    void insertBefore(int value, int index)
    {
        // 完整性检查我们的指数值
        assert(index >= 0 && index <= m_length);
 
        // 首先创建一个比旧数组大一个元素的新数组
        int *data = new int[m_length+1];
 
        // 复制index之前的所有元素
        for (int before=0; before < index; ++before)
            data[before] = m_data[before];
 
        // 将我们的新元素插入新数组中
        data [index] = value;
 
        // 复制index元素后的所有值
        for (int after=index; after < m_length; ++after)
            data[after+1] = m_data[after];
 
        // 最后,删除旧数组,然后指向新数组
        delete[] m_data;
        m_data = data;
        ++m_length;
    }
 
    void remove(int index)
    {
        // 完整性检查我们的指数值
        assert(index >= 0 && index < m_length);
 
        // 如果这是数组中的最后一个元素,请将数组设置为空并挽救
        if (m_length == 1)
        {
            erase();
            return;
        }
 
        // 首先创建一个比旧数组小的新数组
        int *data = new int[m_length-1];
 
        // 复制index之前的所有元素
        for (int before=0; before  < index; ++before)
            data[before] = m_data[before];
 
        // 复制已删除元素后的所有值
        for (int after=index+1; after < m_length; ++after )
            data[after-1] = m_data[after];
 
        // 最后,删除旧数组,然后指向新数组
        delete[] m_data;
        m_data = data;
        --m_length;
    }
 
    // 为了方便,添加一些额外的函数
    void insertAtBeginning(int value) { insertBefore(value, 0); }
    void insertAtEnd(int value) { insertBefore(value, m_length); }
 
    int getLength() { return m_length; }
};
 
#endif

现在,让我们测试它只是为了证明它的工作原理:

#include <iostream>
#include "IntArray.h"
 
int main()
{
    // 声明一个包含10个元素的数组
    IntArray array(10);
 
    // 使用数字1到10填充数组
    for (int i=0; i<10; i++)
        array[i] = i+1;
 
    // 将数组大小调整为8个元素
    array.resize(8);
 
    // 在index为5的元素前面插入数字20
    array.insertBefore(20, 5);
 
    //删除索引为3的元素
    array.remove(3);
 
    //添加30和40到结尾和开始
    array.insertAtEnd(30);
    array.insertAtBeginning(40);
 
    // 打印出所有数字
    for (int j=0; j<array.getLength(); j++)
        std::cout << array[j] << " ";
 
    return 0;
}

这会产生结果:

40 1 2 3 5 20 6 7 8 30
虽然编写容器类可能相当复杂,但好消息是你只需要编写一次。容器类工作后,您可以根据需要随时使用和重用它,而无需任何额外的编程工作。

值得明确提及的是,即使我们的示例IntArray容器类包含内置数据类型(int),我们也可以轻松使用用户定义的类型(例如Point类)。

还有一件事:如果标准库中的类满足您的需求,请使用它而不是创建自己的类。例如,不使用IntArray,最好使用std :: vector 。它经过测试,高效,与标准库中的其他类很好地配合使用。但这并不总是可行的,所以知道如何在需要时创建自己的东西是很好的。一旦我们讨论了一些更基本的主题,我们将在标准库中更多地讨论容器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值