「数组」实现动态数组的功能 / 数据结构模版(C++)

目录

概述

命名空间

成员变量

创建销毁

整体赋值

内存管理 

数据控制

数据访问

Code


概述

动态数组,顾名思议即可变长度的数组。数组这种数据结构的实现是在栈空间或堆空间申请一段连续的可操作区域。

实现可变长度的动态数组结构,应该有以下操作:申请一段足够长的空间,如果数据的存入导致空间已满,则申请一段更长的空间,将原有数据复制过去后加入新数据,同时释放原空间。

//栈空间(在栈上原地生成长度为len的数组空间)
int arr[len];

//堆空间(在堆上申请长度为len的空间,获得首地址交给指针arr)
int *arr=new int[len];

//在堆上实现动态数组
int *temp=new int[len*2];//申请更长的空间
strcpy(temp,arr,sizeof(int)*len)//strcpy以字节为单位将arr指向的数据复制到temp中
delete[]arr;//释放原空间
arr=temp;//temp赋值给arr

接下来我们通过封装array类,实现动态数组的一些基本功能 。(Code和测试案例附后)


命名空间

C++有自己的std命名空间下的array,为了进行区分,封装一个自己的动态数组命名空间custom_dynamic_array

使用namespace关键字封装,使用时可以声明using namespace custom_dynamic_array;全局声明,或custom_dynamic_array::局部声明。

namespace custom_dynamic_array{
    ...
}

//全局声明
using namespace custom_dynamic_array;

//局部声明
custom_dynamic_array::...
 

成员变量

定义class类array,封装三个成员变量:T*val; size_t val_size; size_t val_capacity;

size_t 是C/C++标准在stddef.h中定义的(这个头文件通常不需要#include),size_t 类型专门用于表示长度,它是无符号整数。)

template 泛型,作为数组这种数据结构,他的数据单位应该是任意一种类型(int,double,class等),template 的作用区域是它随后的一个{}内,意为T将代表某种类型,至于某种类型到底是那种类型,将在class实例化时告知(如arrar arr,这句话的意思是创建一个储存double类型的动态数组类)

T* val表示指向array维护的存放数据的空间的指针val。

size_t size表示数组的当前存入内容的长度(数据长度)。

size_t val_capacity是数组的空间大小(真实长度)。

为什么维护了size和capacity两种长度?试想:每次申请长度都只是当前长度+1,则会在内存空间中反复向后利用函数申请新空间来维护数据,这同时耗费了时间和空间。

那么如果每次都申请真实长度size的两倍空间capacity,则每次申请后都有一段空闲区域容纳新元素,此时不需要申请新空间,只有当再次填满时才申请新空间,则减少了时间成本,提高了空间利用率。

namespace custom_dynamic_array{
    template 
    class array {
    private:
	    T* val;
	    int val_size;
	    int val_capacity;
    public:
        ...
    }
}

创建销毁

我们提供七种构造函数(创建动态数组)和一个析构函数(销毁动态数组)。

无参构造arary(),初始化val_size=0,val_capaciy=1(如果等于0的话,后续申请两倍capacity会失效),val=nullptr(空指针)。

(初始化列表array():成员变量(x),...{}在构造函数后加上:即为初始化列表,表示为成员变量赋为括号内的值x,而无须写入{}中)

提供初始空间的构造array(const int num),表示申请初始空间为num的动态数组。

(断言assert(num>0),定义在assert.h头文件中,在运行时会判断括号内的条件为真,否则抛错,用于保护程序运行的安全)

提供初始长度和初值的构造array(const int size,T target_val),为val指针申请两倍size的空间,利用for循环填充长度为size的初始值。

提供源数据地址的构造array(const T* begin,const T* end),从定长度数组中获取数据进行初始化,定义int size=end-begin(指针减法获得两指针的间隔长度(以指向的数据类型为单位))

(内存拷贝函数memcpy(指针1,指针2,以字节为单位的内存长度len),将指针2指向的长度为len的内容以字节为单位复制到指针1指向的空间)

拷贝构造array(const array& another),将another整体赋值给新array。

(const array&表示这个函数保证接受一个array类型的常量引用(即该array自身而非他的拷贝),常量const保证本函数不修改another)

*注意*:以下两个构造时C++11提供的构造,这属于我们的扩展内容。

移动构造:array(array&& another) noexcept,新array直接窃取another的所有数据。

这是由于array接受了一个&&右值引用,而右值往往即将死亡,他不如直接把他的数据偷过来,直接获得他的维护的底层数组的指针,然后把他的指针置为nullptr,防止他死亡时释放掉我偷来的内存。

noexcept表明这个函数是合理的,不会抛出异常(如果内部因为别的原因抛出异常那么程序将直接被杀掉)。

此为移动构造,避免了复制操作的时间和空间浪费。

初始化列表构造array(std::initializer_list ini_list)

我们的自定义数组类型应该尽量在行为上靠近基本类型的数组。

作为基本属组,它的行为是int a[]={1,2,3,4};为了让我们的数组拥有这种行为,应该使用初始化列表。initializer_list,即初始化列表,是C++11提供的新类型,使用{}进行初始化。

(注意这个初始化列表是一种类型,与上文无参构造处解释的初始化列表语法有所不同。)

我们在开头#include ,在对array初始化时,隐式构造一个无名initializer_list,然后将它作为初始化参数赋给array,就实现了字面意义上的arraya={1,2,3};

其中{1,2,3}隐式构造了一张无名initializer_list,然后它被作为参数传入a的初始化构造中。

函数体中使用const T*指针遍历初始化列表并依次赋给array

(std::initializer_list表示initializer_list内部的数据类型是T,未使用using namespace std时std::用于声明initializer_list的命名空间)

析构函数~array(),销毁数组释放空间,在主函数结束时自动调用,但在堆上申请的空间需要写入函数体否则无法释放。

		array() : val_size(0), val_capacity(1), val(nullptr) {};//无参构造
		array(const int num) : val_size(0), val_capacity(num) {//提供初始空间的构造
			assert(num > 0);
			val = new T[num];
		}
		array(const int size, T target_val) : val_size(size), val_capacity(size * 2) {//提供初始长度和初值的构造
			assert(size > 0);
			val = new T[size * 2];
			for (int i = 0; i < size; i++)val[i] = target_val;
		}
		array(const T* begin, const T* end) {//从固定长度数组中获取数据进行构造
			assert(begin != nullptr && end != nullptr);
			int size = end - begin;
			val = new T[size * 2];
			val_size = size;
			val_capacity = size * 2;
			memcpy(val, begin, sizeof(T) * size);
		}
		array(const array& another) : val_size(another.val_size), val_capacity(another.val_capacity) {//拷贝构造
			val = new T[another.val_capacity];
			memcpy(val, another.val, sizeof(T) * another.val_size);
		}
		array(array&& another) noexcept : val_size(another.val_size), val_capacity(another.val_capacity) {//移动构造
			val = another.val;
			another.val = nullptr;
		}
		array(const std::initializer_list ini_list) {//初始化列表构造
			int size = ini_list.size();
			val = new T[size * 2];
			int i = 0;
			for (const T* it = ini_list.begin(); it != ini_list.end(); it++)val[i++] = *it;
			val_size = size;
			val_capacity = size * 2;
		}
		~array() {
			delete[]val;
		}
		array& operator=(const array& another) {//重载等于号(它也可以作为构造函数在构造时使用)
			delete[]val;
			val = new T[another.val_capacity];
			memcpy(val, another.val, sizeof(T) * another.val_size);
			val_size = another.val_size;
			val_capacity = another.val_capacity;
			return *this;
		}

整体赋值

作为class类,array应该同时拥有类的性质的数组的性质,那么他就应该可以被视为整体来进行操控。
我们提供三种整体赋值的函数。

重载等于号(复制赋值)array& operator=(const array& another),等于号作为一个的成员函数被我们重新定义,接收另一个array another的常量引用并整体赋值并返回一个自身的常量引用(这是为连等=...=...=...服务的)。

(this指针:返回这个array类的对象本身)

另有函数重载实现移动赋值,与移动构造类似。详见Code。

提供新长度和填充值的内存分配void assign(const int size, T target_val),效果类似于提供长度和初始值的构造。

数据交换void swap(array& another),交换两个array的值。

		array& operator=(const array& another) {//重载等于号(它也可以作为构造函数在构造时使用)
			delete[]val;
			val = new T[another.val_capacity];
			memcpy(val, another.val, sizeof(T) * another.val_size);
			val_size = another.val_size;
			val_capacity = another.val_capacity;
			return *this;
		}
		array& operator=(array&& another)noexcept {//移动赋值运算符
			if (this == &another)return *this;
			delete[]val;
			val = another.val;
			val_size = another.val_size;
			val_capacity = another.val_capacity;
			another.val=nullptr;
			return *this;
		}
		void assign(const int size, T target_val) {
	        assert(size > 0);
	        delete[]val;
	        val = new T[size * 2];
        	for (int i = 0; i < size; i++)val[i] = target_val;
        	val_size = size;
        	val_capacity = size * 2;
        }
		void swap(array& another) {
			T* tempval=another.val;
			size_t tempsize = another.val_size,tempcapacity = another.val_capacity;
			another.val = val, another.val_size = val_size, another.val_capacity = val_capacity;
			val = tempval, val_size = tempsize, val_capacity = tempcapacity;
		}

内存管理 

我们提供四个size相关的函数和两个capacity相关的函数维护内存。

获取有效数据长度size_t size(),返回val_size。

(函数()与{ }之间的const表示函数体内部不进行数据的更改操作)

判断有效空间是否为空bool empty(),返回val_size是否不等于0,是则false,否则true。

提供新长度的长度重置void resize(const int size),在size变小时截断后续数据。

提供新长度和填充值的长度重置void resize(const int size, T target_val),在size变小时截断后续数据,在size变大时在新位置填充target_val。

获取实际空间长度size_t capacity()const,返回val_capacity。

延长空间void reserve(const int num),如果申请的新空间小于val_capacity,无事发生,否则申请更大的空间。

		size_t size()const {
			return val_size;
		}
		bool empty()const {
			return val_size ? false : true;
		}
		void resize(const int size) {
			assert(size > 0);
			T* temp = new T[size * 2];
			if (size < val_size)memcpy(temp, val, sizeof(T) * size);
			else memcpy(temp, val, sizeof(T) * val_size);
			delete[]val;
			val = temp;
			val_size = size;
			val_capacity = size * 2;
		}
		void resize(const int size, T target_val) {
			assert(size > 0);
			T* temp = new T[size * 2];
			for (int i = 0; i < size; i++)temp[i] = target_val;
			if (size < val_size)memcpy(temp, val, sizeof(T) * size);
			else memcpy(temp, val, sizeof(T) * val_size);
			delete[]val;
			val = temp;
			val_size = size;
			val_capacity = size * 2;
		}
		size_t capacity()const {
			return val_capacity;
		}
		void reserve(const int num) {
			if (num > val_capacity) {
				T* temp = new T[num];
				memcpy(temp, val, sizeof(T) * val_size);
				delete[]val;
				val = temp;
				val_capacity = num;
			}
		}

数据控制

与固定长度数组不同,我们需要调用成员函数来实现数据增删。

压入新元素void push_back(T elem),在末尾追加新元素elem,如果有剩余空间大于1则直接写入,否则申请两倍的新空间。

插入新元素void insert(const int pos, T element),在给定位置插入元素elem。

另有函数重载实现区间插入,详见Code。

删除末元素void pop_back(),删除最后一个元素。

删除元素void erase(const int pos),删除指定位置的元素。

另有函数重载实现区间删除,详见Code。

清空元素void clear(),将val_size置为0。

(memet(指针p,char val,以字节为单位的内存长度len),以字节为单位将val赋给p指向的长度为len的空间)

		void push_back(T elem) {
			if (val_capacity - val_size <= 1)reserve(val_capacity * 2);
			val[val_size++] = elem;
		}
		void insert(const int pos, T elem) {
			assert(pos >= 0 && pos <= val_size);
			if (val_capacity - val_size <= 1)reserve(val_capacity * 2);
			val_size++;
			for (int i = val_size - 1; i > pos; i--) val[i] = val[i - 1];
			val[pos] = elem;
		}
		void insert(const int pos,const T* begin,const T* end) {
			assert(pos >= 0 && pos <= val_size);
			const int len = end - begin;
			if (val_capacity - val_size <= len)reserve(val_capacity * 2);
			val_size += len;
			for (int i = val_size - 1; i > pos+len-1; i--) val[i] = val[i - len];
			for (int j = 0; j < len; j++)val[pos+j] = *(begin + j);
		}
		void pop_back() {
			assert(val_size > 0);
			val_size--;
		}
		void erase(const int pos) {
			assert(pos >= 0 && pos < val_size);
			if(pos != val_size-1)
			for (int i = pos + 1; i < val_size; i++) 
				val[i - 1] = val[i];
			val_size--;
		}
		void erase(const int begin,int end) {
			assert(begin >= 0 && begin < val_size);
			end = end >= val_size ? val_size : end;
			const int len = end - begin;
			if(end < val_size)
				for (int i = begin; i < end; i++) 
					val[i] = val[i + len];
			val_size -= len;
		}
		void clear() {
			memset(val, 0, sizeof(T) * val_size);
			val_size = 0;
		}

数据访问

我们通过编写返回值为引用类型的函数来实现数据的读写功能。

引用返回这个数据本身而非他的拷贝,它是一种比指针更安全的数据传递手段。

以下的所有接口函数都要被声明为const函数,这意味这函数体本身不执行任何的数据变更操作,但这不表示不可以通过接口函数实现数据变更操作。

重载[]号T& operator[](int pos),为了使我们的数组类有定长数组的特征,重载[]号使arr[]具有作用。[]接收一个int pos,返回pos对应位置的引用。不允许访问负坐标。为了和下一个函数区分,我们允许访问溢出(pos>=size)

成员函数式访问下标T& at(int pos),上一个函数是模仿固定长度数组的行为,这个函数则提供类的函数式的下标访问,他不允许访问负坐标和访问溢出。

获取头元素T& front(),获取第一个元素的引用。

获取尾元素T& back(),获取最后一个元素的引用。

直接获取底部*T data(),我们运用array类来维护底部的T[]数组,那么用户就应该可以直接获取底部地址。不使用常量修饰而直接暴露接口的行为是极其不安全的,但考虑到这是一篇面向入门读者的文章,我们考虑通过暴露接口来对接一些外部功能(如排序、翻转),而将安全性暂放次要。

		T& operator[](const int pos)const {
			assert(pos >= 0);
			return val[pos];
		}
		T& at(const int pos)const {
			assert(pos >= 0 && pos < val_size);
			return val[pos];
		}
		T& front()const {
			assert(val_size > 0);
			return val[0];
		}
		T& back()const {
			assert(val_size > 0);
			return val[val_size - 1];
		}
		T* data()const {
			return val;
		}

Code

*注意*:代码中对于接收T类型参数的函数,我使用了template 并使得函数接受了万能引用V&&,如果你对此感兴趣,可以自行学习,否则你可以全部改回T。

#include 
#include 
#ifndef CUSTOM_DAYNAMIN_ARRAY
#define CUSTOM_DAYNAMIN_ARRAY
namespace custom_dynamic_array {//封装一个命名空间
	template 
	class array {
	private:
		T* val;
		size_t val_size;
		size_t val_capacity;
	public:
		array() : val_size(0), val_capacity(1), val(nullptr) {};//无参构造
		array(const int num) : val_size(0), val_capacity(num) {//提供初始空间的构造
			assert(num > 0);
			val = new T[num];
		}
		template
		array(const int size, V&& target_val) : val_size(size), val_capacity(size * 2) {//提供初始长度和初值的构造
			assert(size > 0);
			val = new T[size * 2];
			for (int i = 0; i < size; i++)val[i] = target_val;
		}
		array(const T* begin, const T* end) {//从固定长度数组中获取数据进行构造
			assert(begin != nullptr && end != nullptr);
			int size = end - begin;
			val = new T[size * 2];
			val_size = size;
			val_capacity = size * 2;
			memcpy(val, begin, sizeof(T) * size);
		}
		array(const array& another) : val_size(another.val_size), val_capacity(another.val_capacity) {//拷贝构造
			val = new T[another.val_capacity];
			memcpy(val, another.val, sizeof(T) * another.val_size);
		}
		array(array&& another) noexcept : val_size(another.val_size), val_capacity(another.val_capacity) {//移动构造
			val = another.val;
			another.val = nullptr;
		}
		array(const std::initializer_list ini_list) {//初始化列表构造
			int size = ini_list.size();
			val = new T[size * 2];
			int i = 0;
			for (const T* it = ini_list.begin(); it != ini_list.end(); it++)val[i++] = *it;
			val_size = size;
			val_capacity = size * 2;
		}
		~array() {
			delete[]val;
		}
		array& operator=(const array& another) {//重载等于号(它也可以作为构造函数在构造时使用)
			delete[]val;
			val = new T[another.val_capacity];
			memcpy(val, another.val, sizeof(T) * another.val_size);
			val_size = another.val_size;
			val_capacity = another.val_capacity;
			return *this;
		}
		array& operator=(array&& another)noexcept {//移动赋值运算符
			if (this == &another)return *this;
			delete[]val;
			val = another.val;
			val_size = another.val_size;
			val_capacity = another.val_capacity;
			another.val = nullptr;
			return *this;
		}
		template
		void assign(const int size, V&& target_val) {
			assert(size > 0);
			delete[]val;
			val = new T[size * 2];
			for (int i = 0; i < size; i++)val[i] = target_val;
			val_size = size;
			val_capacity = size * 2;
		}
		void swap(array& another) {
			T* tempval = another.val;
			size_t tempsize = another.val_size, tempcapacity = another.val_capacity;
			another.val = val, another.val_size = val_size, another.val_capacity = val_capacity;
			val = tempval, val_size = tempsize, val_capacity = tempcapacity;
		}
		size_t size()const {
			return val_size;
		}
		bool empty()const {
			return val_size ? false : true;
		}
		void resize(const int size) {
			assert(size > 0);
			T* temp = new T[size * 2];
			if (size < val_size)memcpy(temp, val, sizeof(T) * size);
			else memcpy(temp, val, sizeof(T) * val_size);
			delete[]val;
			val = temp;
			val_size = size;
			val_capacity = size * 2;
		}
		template
		void resize(const int size, V&& target_val) {
			assert(size > 0);
			T* temp = new T[size * 2];
			for (int i = 0; i < size; i++)temp[i] = target_val;
			if (size < val_size)memcpy(temp, val, sizeof(T) * size);
			else memcpy(temp, val, sizeof(T) * val_size);
			delete[]val;
			val = temp;
			val_size = size;
			val_capacity = size * 2;
		}
		size_t capacity()const {
			return val_capacity;
		}
		void reserve(const int num) {
			if (num > val_capacity) {
				T* temp = new T[num];
				memcpy(temp, val, sizeof(T) * val_size);
				delete[]val;
				val = temp;
				val_capacity = num;
			}
		}
		template
		void push_back(V&& elem) {
			if (val_capacity - val_size <= 1)reserve(val_capacity * 2);
			val[val_size++] = elem;
		}
		template
		void insert(const int pos, V&& elem) {
			assert(pos >= 0 && pos <= val_size);
			if (val_capacity - val_size <= 1)reserve(val_capacity * 2);
			val_size++;
			for (int i = val_size - 1; i > pos; i--) val[i] = val[i - 1];
			val[pos] = elem;
		}
		void insert(const int pos, const T* begin, const T* end) {
			assert(pos >= 0 && pos <= val_size);
			const int len = end - begin;
			if (val_capacity - val_size <= len)reserve(val_capacity * 2);
			val_size += len;
			for (int i = val_size - 1; i > pos + len - 1; i--) val[i] = val[i - len];
			for (int j = 0; j < len; j++)val[pos + j] = *(begin + j);
		}
		void pop_back() {
			assert(val_size > 0);
			val_size--;
		}
		void erase(const int pos) {
			assert(pos >= 0 && pos < val_size);
			if(pos != val_size-1)
			for (int i = pos + 1; i < val_size; i++) 
				val[i - 1] = val[i];
			val_size--;
		}
		void erase(const int begin, int end) {
			assert(begin >= 0 && begin < val_size);
			end = end >= val_size ? val_size : end;
			const int len = end - begin;
			if (end < val_size)
				for (int i = begin; i < end; i++)
					val[i] = val[i + len];
			val_size -= len;
		}
		void clear() {
			memset(val, 0, sizeof(T) * val_size);
			val_size = 0;
		}
		T& operator[](const int pos)const {
			assert(pos >= 0);
			return val[pos];
		}
		T& at(const int pos)const {
			assert(pos >= 0 && pos < val_size);
			return val[pos];
		}
		T& front()const {
			assert(val_size > 0);
			return val[0];
		}
		T& back()const {
			assert(val_size > 0);
			return val[val_size - 1];
		}
		T* data()const {
			return val;
		}
	};
}
#endif

测试

#include 
#include "array.h"
using namespace custom_dynamic_array;//全局使用自己的命名空间;
template                 //在不全局使用std命名空间时,使用custom_dynamic_array::array....来实现同样效果
void show(::array& arr) {
    if (arr.empty())std::cout << "EMPTY";
    else for (int i = 0; i < arr.size(); i++)std::cout << arr[i] << ' ';
    std::cout << std::endl;
}
int main()
{
    std::cout << "---------------test1---------------" << std::endl;
    arraya; show(a);
    arrayb(5); show(b);
    arrayc(3, 3); show(c);
    double x[] = { 1.1,2.2,3.3 };
    arrayd(x, x + 3); show(d);
    arraye(d); show(e);
    arrayf = { 5,6,7,8,9 }; show(f);
    arrayg = a; show(g);
    std::cout <<"------------------------------------" << std::endl;
    std::cout << "---------------test2---------------" << std::endl;
    a = c; show(a);
    b.assign(6, 2); std::cout << b.size() << std::endl;
    c.assign(8, 2); show(c);
    b.swap(c); show(b);
    std::cout << std::endl;
    std::cout << "------------------------------------" << std::endl;
    std::cout << "---------------test3---------------" << std::endl;
    std::cout << b.size() << std::endl;
    std::cout << (b.empty() ? "YES" : "NO") << std::endl;
    b.resize(10, 5); show(b);
    std::cout << b.capacity() << std::endl;
    b.reserve(50);
    std::cout << b.capacity() << std::endl;
    std::cout << std::endl;
    std::cout << "------------------------------------" << std::endl;
    std::cout << "---------------test4---------------" << std::endl;
    a.push_back(10); a.push_back(10); a.push_back(10); a.push_back(10); show(a);
    a.insert(1, 7); show(a);
    int xx[3] = { 10,11,12 };
    a.insert(1, xx, xx + 2); show(a);
    a.pop_back(); show(a);
    a.erase(2); show(a);
    a.erase(4, 6); show(a);
    a.erase(2, 20); show(a);
    a.clear(); std::cout << (a.empty() ? "YES" : "NO") << std::endl;
    std::cout << std::endl;
    std::cout << "------------------------------------" << std::endl;
    std::cout << "---------------test5---------------" << std::endl;
    show(d);
    d[1] = 8.8; show(d);
    std::cout << d.front() << ' ' << d.back() << std::endl;
    std::cout << "------------------------------------" << std::endl;
    return 0;
}

「数组」实现动态数组的功能 / 数据结构模版(C++)_第1张图片

你可能感兴趣的:(「数组」,「数据结构」,算法,数据结构,c++)