使用C++创建一个动态数组库

1. 动态数组的优点

与普通的数组(静态数组)不同,动态数组根据需要随时改变大小,但是静态数组库一旦定义,大小就不可改变了

2.动态数组库的接口(代码放在DynamicArray.h中),主要包括结构体的声明和操作动态数组的函数的声明

#ifndef SEVENTH_CPP_PROJECT_CLASS_INHERITANCE_DYNAMICARRAY_H
#define SEVENTH_CPP_PROJECT_CLASS_INHERITANCE_DYNAMICARRAY_H
struct DynamicArray;
typedef struct DynamicArray DArray;
typedef struct DynamicArray *PDArray;

PDArray InitDArray(int count);
int  GetLength(PDArray &p);
int* GetAddress(PDArray &p);
void InitElement(PDArray &p);
void TraverseDArray(PDArray &p);
void SwapElement(PDArray &p, int i, int j);
void BubbleSort(PDArray &p);
int  BinarySearch(PDArray &p, int key);
void ResizeDArray(PDArray &p, int new_length);
void AppendElement(PDArray &p, int ele_count);
void AppendIdxElement(PDArray &p, int idx, int key);
void DeleteElement(PDArray &p, int ele_count);
void DeleteIdxElement(PDArray &p, int idx);

#endif //SEVENTH_CPP_PROJECT_CLASS_INHERITANCE_DYNAMICARRAY_H

3.动态数组库的实现(代码放在DynamicArray.cpp中),主要包括结构体的定义和函数的实现

#include <iostream>
#include "DynamicArray.h"

using namespace std;
struct DynamicArray {
    int count;  // 动态数组的长度
    int* data;  // 指向动态数组的指针
};

PDArray InitDArray(int count) {
    // TODO 函数功能:初始化动态数组
    auto p = new DArray;  // 分配一个结构体,p指向它
    p->count = count;
    p->data = new int[count];
    return p;
}

int  GetLength(PDArray &p) {
    // TODO 函数功能:获取动态数组的长度
    return p->count;
}

int* GetAddress(PDArray &p) {
    // TODO 获取数组的基地址
    return p->data;
}

void InitElement(PDArray &p) {
    // TODO 为数组的每一个元素赋值
    int length = GetLength(p);
    int* addr = p->data;
    cout << "Please input "<<length<<" integers: ";
    for (int i = 0; i < length; i++) {
        // 用户输入元素的值
        cin >> addr[i];
    }
}

void TraverseDArray(PDArray &p) {
    // TODO 函数功能:遍历数组
    int* addr = p->data;
    for (int i = 0; i < GetLength(p); i++) {
        cout << addr[i] << " ";
    }
    cout << endl;
}

void SwapElement(PDArray &p, int i, int j) {
    // TODO 函数功能:交换数组i号元素和j号元素
    int length = GetLength(p);
    if (i >= length || j >= length || i < 0 || j < 0)  {
        // 判断i,j是否合法
        exit(1);
    } else {
        int* addr = p->data;
        int temp = addr[i];
        addr[i] = addr[j];
        addr[j] = temp;
    }
}

void BubbleSort(PDArray &p) {
    // TODO 函数功能:冒泡排序(升序)。冒泡排序的最坏时间复杂度为O(n**2),最好时间复杂度是O(n),空间复杂度为O(1)
    // TODO last,end和flag都是优化了冒泡排序,只是小优化,最坏最好时间夫再度并没有改变
    // 冒泡排序的时间复杂度取决比较次数和交换次数
    int* addr = p->data;
    int n = GetLength(p);
    // last记录了数组中最后一次发生交换的位置,end保证每趟冒泡排序的比较次数
    // last和end的存在减少了下一趟冒泡排序的比较次数
    int last = n-1;
    int end;
    for (int i = 0; i < n-1; i++) {
        end = last;
        bool flag = false;  // flag表示每一趟冒泡排序是否发生了交换
        for (int j = 0; j < end; j++) {
            if (addr[j] > addr[j+1]) {
                SwapElement(p, j, j+1);
                flag = true;  // 只要发生了交换,flag赋值为true
                last = j;     // 只要发生了交换,就更新last的值
            }
        }
        // TODO 在一趟冒泡排序中没有发生交换,说明整个数组已有序,直接退出循环
        if(!flag)
            break;
    }
}

int  BinarySearch(PDArray &p, int key) {
    // TODO 对已排好序的数组,二分查找
    // 若查找到key在数组中的索引idx,则返回,否则返回-1
    int low = 0, high = GetLength(p), idx = -1, mid;
    int* addr = p->data;
    while(low <= high) {
        mid = (low + high)/2;
        if (addr[mid] == key) {
            idx = mid;
            break;
        } else if (addr[mid] > key) {
            high = mid - 1;
        } else if (addr[mid] < key) {
            low = mid + 1;
        }
    }
    return idx;
}

void ResizeDArray(PDArray &p, int new_length) {
    // TODO 函数功能:将数组的长度改为new_size
    // 当前数组长度length小于>new_length,那么会有(length-new_length)个元素被舍弃
    int length = p->count;
    int* temp = new int[new_length];
    int min = new_length < length ? new_length : length;
    for (int i = 0; i < min; i++) {
        // 将原数组拷贝到新数组中去
        temp[i] = p->data[i];
    }
    if(new_length > length) {
        // 为多余的数组元素赋值
        cout << "Please input " << new_length - length << " integers: ";
        for (int i = length; i < new_length; i++) {
            cin >> temp[i];
        }
    }
    // 释放原来占用的堆内存并且更新p指向的动态数组的结构体成员
    delete[] p->data;
    p->count = new_length;
    p->data = temp;
}

void AppendElement(PDArray &p, int ele_count) {
    // TODO 函数功能:在原来的数组附加ele_count个新元素
    // 可以直接调用ResizeDArray函数
    ResizeDArray(p, GetLength(p) + ele_count);
}

void AppendIdxElement(PDArray &p, int idx, int key) {
    // TODO 函数功能:在数组的索引idx处插入一个元素key
    int length = p->count;
    if (idx < 0 || idx > length) exit(1);  // idx不合法,直接退出
    int* temp = new int[length+1];
    for (int i = 0; i < key; i++) {
        temp[i] = p->data[i];
    }
    temp[idx] = key;
    for (int j = idx; j < length; j ++) {
        temp[j+1] = p->data[j];
    }
    // 释放原数组占用的堆内存
    delete[] p->data;
    p->count = length + 1;
    p->data = temp;
}

void DeleteElement(PDArray &p, int ele_count) {
    // TODO 函数功能:删除数组中ele_count个元素,实际上是删除数组最后的ele_count个元素
    // 可以直接调用ResizeDArray函数
    int length = GetLength(p);
    if (ele_count > length) exit(1);  // 如果删除的元素个数不合法,直接退出
    ResizeDArray(p, length - ele_count);
}

void DeleteIdxElement(PDArray &p, int idx) {
    // TODO 删除数组中索引为idx的元素
    int length = GetLength(p);
    int* temp = new int[length-1];
    if (idx < 0 || idx >= length) exit(1);  // idx不合法,直接退出
    for (int i = 0; i < idx; i++) {
        temp[i] = p->data[i];
    }
    for (int i = idx; i < length - 1; i++) {
        temp[i] = p->data[i+1];
    }
    // 释放原来数组占用的堆内存
    delete[] p->data;
    p->data = temp;
    p->count = length - 1;
}

4.动态数组库的使用与测试(代码放在main.cpp中)

#include <iostream>
#include <string>
#include "DynamicArray.h"

using namespace std;

int main() {
    // TODO 实例化一个string对象s,并使用getline函数为s赋值
    string s;
    cout << "Please input a string: ";
    getline(cin, s, '\n');
    cout << "Your input string is : " << s << endl;

    // TODO 动态数组库,在头文件中声明结构体,在源文件中定义结构体,这种方式使得我们在main.cpp中无法访问结构体,
    // TODO 只能通过一个指针p来访问这个结构体,而且不能显示的访问结构体成员,只能通过动态数组库的源文件访问它们
    // TODO 初始化一个动态数组,指针p指向这个动态数组,传入的参数为数组初始的长度
    PDArray p = InitDArray(5);
    cout << "p = "<<p<<"" << endl;
//    cout << p->count << endl;
//    cout << p->data << endl;  不能显式的访问结构体成员,只能通过动态数组库的源文件访问它们

    // TODO 获取数组的长度
    int length = GetLength(p);
    cout << length << endl;

    // TODO 获取数组的基地址
    int* addr = GetAddress(p);
    cout << "The base address of array is: " << addr << endl;

    // TODO 为数组赋值。数组元素需要用户输入
    InitElement(p);

    // TODO 遍历数组
    TraverseDArray(p);

    // TODO 交换两个元素的值
    SwapElement(p, 0, 2);
    TraverseDArray(p);

    // TODO 为数组进行升序排序(冒泡排序):最坏时间复杂度为O(n^2),最好时间复杂度为O(n),空间复杂度为O(1)
    BubbleSort(p);
    TraverseDArray(p);

    // TODO 基于排好序的数组,给定一个key,进行二分查找。成功则返回对应的索引,否则返回-1表示未能查找到指定的key
    int idx = BinarySearch(p, 3);
    cout << "idx = "<<idx<<"" << endl;

    // TODO 改变数组的大小
    cout << "Before resize array, p = "<<p<<"" << endl;
    cout << "Before resize array, p->data = "<<GetAddress(p)<<"" << endl;
    ResizeDArray(p, 3);
    cout << "After  resize array, p = "<<p<<"" << endl;
    cout << "After  resize array, p->data = "<<GetAddress(p)<<"" << endl;
    TraverseDArray(p);

    // TODO 数组附加指定数量的元素(实际上还是改变数组的大小)
    AppendElement(p, 3);  // 附加3个元素
    TraverseDArray(p);
    cout << "p->data = "<<GetAddress(p)<<"" << endl;

    // TODO 函数功能:在数组的索引idx处插入一个元素key
    AppendIdxElement(p, 2, 9);
    TraverseDArray(p);
    cout << "p->data = "<<GetAddress(p)<<"" << endl;

    // TODO 删除数组中指定数量的元素(实际上还是改变数组的大小), 从后面开始删
    DeleteElement(p, 2);  // 删除数组后面的2个元素
    TraverseDArray(p);
    cout << "p->data = "<<GetAddress(p)<<"" << endl;

    // TODO 删除数组中索引为idx的元素
    DeleteIdxElement(p, 3);
    TraverseDArray(p);
    cout << "p->data = "<<GetAddress(p)<<"" << endl;
    return 0;
}

5.结构体在DynamicArray.h头文件中声明,在DynamicArray.cpp中定义的原因

1.主要是为了数据的封装和信息隐藏,这样做之后,在main.cpp中,只能通过一个指向结构体的指针和动态数组库提供的接口来访问这个函数。

2.这是因为我们在main.cpp 中include "DynamicArray.h"了,但是头文件中并没有结构体的定义,只有结构体的声明

3.这种思想和类中的private成员一样,类的外部不可访问这些private成员,但是类的内部可以

4.在main函数中,不能显示的访问结构体的成员,比如:

// 在main.cpp中试图访问动态数组的元素个数
// 虽然10就是初始化的数组长度,但是就是不能访问了
PDArray p = InitDArry(10);
int count = p->count;     // 错误
int count = GetLength(p); // 正确

5.这种做法的最大好处就是信息隐藏。

作为一个库的使用者,库的提供者会为你提供头文件(接口),但是一般不会提供库的实现代码,而是直接提供库编译号的二进制代码。
如果提供了源代码,可能一个小小的修改,都会造成整个库的崩溃!甚至带来不可预期的危害。
只提供头文件(接口),保护了整个库的正常运作,试想一下,如果结构体定义在头文件中,你随意改一下,整个库就不能用了。
所以这种思想就是在头文件中提供的是纯粹的接口,告诉库的使用者能干啥而不是告诉你你可以修改啥。

6.如果某个库不让修改,还实现不了库使用者的需要,那么赶紧这个库就是个垃圾库!

posted @ 2023-07-24 16:54  Guanjie255  阅读(239)  评论(0编辑  收藏  举报