【原创】【长期更新】【未完待续】自制vector类型

继《自制string类型》以来的第二篇自制类型的文章。马上要开学了,时间也不多了,争取在今年写完吧。

一,vector类型简单介绍

1.简介

1.1.STL

STL是一个C++自带的一个数据结构的类,包括栈,队列,字符串等功能。这些数据结构统称为容器(containers)。

1.2.vector

vector(向量),是一种经常被使用的容器。它可以看做是一种动态大小的数组,并且可以存放任何类型的数据(即泛型)。
这里给大家一个使用的例子:

#include<iostream>
#include<vector>
using namespace std;
int main(){
	vector<int> v;
	for(int i=0;i<10;i++){
		v.push_back(i);
		cout<<v[i]<<endl;
	}
	return 0;
} 

关于更加具体的内容,笔者参考了一个网页:网页链接

2.vector有哪些函数?

(来源:网页链接

1.构造函数
vector()//创建一个空vector
vector(int nSize)//创建一个vector,元素个数为nSize
vector(int nSize,const t& t)//创建一个vector,元素个数为nSize,且值均为t
vector(const vector&)//复制构造函数
vector(begin,end)//复制[begin,end)区间内另一个数组的元素到vector中
2.增加函数
void push_back(const T& x)//向量尾部增加一个元素X
iterator insert(iterator it,const T& x)//向量中迭代器指向元素前增加一个元素x
iterator insert(iterator it,int n,const T& x)//向量中迭代器指向元素前增加n个相同的元素x
iterator insert(iterator it,const_iterator first,const_iterator last)//向量中迭代器指向元素前插入另一个相同类型向量的[first,last)间的数据
3.删除函数
iterator erase(iterator it)//删除向量中迭代器指向元素
iterator erase(iterator first,iterator last)//删除向量中[first,last)中元素
void pop_back()//删除向量中最后一个元素
void clear()//清空向量中所有元素
4.遍历函数
reference at(int pos)//返回pos位置元素的引用
reference front()//返回首元素的引用
reference back()//返回尾元素的引用
iterator begin()//返回向量头指针,指向第一个元素
iterator end()//返回向量尾指针,指向向量最后一个元素的下一个位置
reverse_iterator rbegin()//反向迭代器,指向最后一个元素
reverse_iterator rend()//反向迭代器,指向第一个元素之前的位置
5.判断函数
bool empty() const//判断向量是否为空,若为空,则向量中无元素
6.大小函数
int size() const//返回向量中元素的个数
int capacity() const//返回当前向量所能容纳的最大元素值
int max_size() const//返回最大可允许的vector元素数量值
7.其他函数
void swap(vector&)//交换两个同类型向量的数据
void assign(int n,const T& x)//设置向量中前n个元素的值为x
void assign(const_iterator first,const_iterator last)//向量中[first,last)中元素设置成当前向量元素

二,泛型

1.什么是泛型?

最简单的例子:

#include<iostream>
#include<vector>
using namespace std;
int main(){
	int a=5,b=10;cout<<max(a,b);
	float c=3.14,d=9.99;cout<<max(c,d);
	char e='x',f='*';cout<<max(e,f);
} 

这里,max函数即可以处理int,又可以处理float,char类型的最大值。这就是泛型。max函数无论针对哪一个类型,操作都是相同的。因此,我们使用通用类型,让函数不关注类型只关注具体的操作。
有人会问,其实使用函数重载不就能完成了吗?(不了解函数重载是什么的,请看我前面写过的《自制string类型》,里面有说明)但是,函数重载要重复写好几次,不方便。

2.泛型的实现

2.1.函数模板

在写真正的vector泛型之前,我们首先使用MAX函数练习一下,借此看看如何写泛型。

#include<iostream>
#include<vector>
using namespace std;
template<typename T>
T MAX(T a,T b){
	if(a>b)return a;
	else return b;
}
int main(){
	int a=5,b=10;cout<<MAX(a,b);
	float c=3.14,d=9.99;cout<<MAX(c,d);
	char e='x',f='*';cout<<MAX(e,f);
}

template表示定义一个叫做T的类型,这个类型是一个通用类型。这个语句告诉编译器,要开始泛型编程,其中T是要使用的泛型类型。
执行的时候,编译器会自动根据参数的类型,把T转换为int,float等类型,进行计算。
注意,泛型的函数不会进行自动类型转换,例如cout<<MAX('a',100);这个语句,如果使用的是泛型类型,会编译错误,但是使用普通类型不会报错,因为普通类型的函数会进行自动类型转换。

2.2.类模板

写法和函数模板非常类似。

template<typename T>
class Vector{
	T *numbers;
	
};

在声明Vector类型的时候,需要注意:声明不能写成:

Vector v;

而必须写成:

Vector<int>v;

这样才可以告诉编译器需要定义的类型。

三,相关函数写法

1.构造函数

终于来到写函数的时候了。第一个写的函数一定是构造函数。我们知道,vector其实是一个“动态数组”,而动态数组的实现需要依靠指针。我们使用一个变量n来记录目前vector内部的元素个数。
最开始的时候,整个向量内部为空,那么n为0,并且无法存放数据。在放入数据的时候,向量会申请空间,来放入数据。
四个构造函数:

class Vector{
	T *numbers;
	int n;
	Vector(){
		n=0;
	}
	Vector(int nsize){//初始化大小
		n=nsize;
		numbers=(T*)malloc(nsize*sizeof(T));
	}
	Vector(int nsize,T t){//初始化大小并放入nsize个t
		n=nsize;
		numbers=(T*)malloc(nsize*sizeof(T));
		for(int i=0;i<nsize;i++)numbers[i]=t;
	}
	Vector(Vector <int> &v){//复制另一个vector的东西
		n=v.n;
		numbers=(T*)malloc(n*sizeof(T));
		for(int i=0;i<nsize;i++)numbers[i]=v.numbers[i];
	}
};

为什么使用malloc不用new?因为malloc出来的内存可以realloc,而new的不行。代码很简单,就不多说了。

2.增加函数

2.1.push_back

功能:往最后一个元素的后面再加一个元素

void push_back(T x){
		n++;
		if(n==1)numbers=(T*)malloc(n*sizeof(T));
		else numbers=(T*)realloc(numbers,n*sizeof(T));
		T[n-1]=x;
}

如果只有一个元素,那么执行Malloc。如果已经有元素了,那么执行realloc。
但是,不断的realloc是非常慢的。下面内容引用自《征服C指针》:

我们经常需要对数组顺次追加元素,这种情况下,如果每追加一个元素都利用 realloc()进
行内存区域扩展,将会发生什么呢?
如果手气不错,后面正好有足够大的空地儿,这样还好。如果不是这样,就需要频繁地复制
区域中的内容,这样自然会影响运行效率。另外,不断地对内存进行分配、释放的操作,也
会引起内存碎片化。

最好的做法是一次性多分配一些空间(例如100个sizeof(T))。但是这样就要多在类里面加一个变量,函数也会变得很复杂。我们还是考虑把realloc封装起来吧。

class Vector{
	T *numbers;
	int n;//大小 
	int memory;//已经分配的内存 
	
	T* Allocate(T *mem,int newsize){
		n=newsize;
		if(memory>=n)return NULL;
		else memory=newsize+100,return realloc(mem,newsize+100);
	}

如果返回NULL,说明内存已经足够,否则就返回realloc的新的地址。

void push_back(T x){
		n++;
		if(n==1)numbers=(T*)malloc(n*sizeof(T));
		else if (Allocate(numbers,n*sizeof(T))!=NULL)numbers=Allocate(numbers,n*sizeof(T));
		numbers[n-1]=x;
	}

上面是修改的push_back示例。这样就方便多了。
当然,为了实现起来方便,本文之后的所有例程都写作realloc的形式,便于理解和阅读。

2.2.insert函数

insert的作用是插入,我们目前是使用数组进行存储的,因此,插入和删除需要移动元素(顺序表,如果是链表则没有这个问题)
为了制造起来方便,我们这样规定:vector插入的东西必须是一个vector中的东西或者数组。至于什么迭代器iterartor,就不做了。
同时,为了初始化方便,我们制作一个可变长函数用于初始化元素,如下:
Vector(数组长度n,元素1,元素2,...,元素n);

Vector(int nsize,...){
		n=nsize;
		numbers=(T*)malloc(nsize*sizeof(T));
		va_list ap;
		va_start(ap,nsize);
		for(int i=0;i<n;i++)numbers[i]=va_arg(ap,T);
		va_end(ap);
	}

可变长函数的基本格式:

函数名 (参数,...){
  va_list ap;
  va_start(ap,[省略号前最后一个元素]);
  for(int i=0;i<[个数];i++){
    ...va_arg(ap,[参数类型]);//用于取得省略的参数
  }
  va_end(ap);
}

(以下为9月1日 22:00的更新内容)

插入元素的操作是非常简单的,只需要后移元素,腾出位置给要插入的元素,再把要插入的元素放进去。
插入元素(数组版本)

void insert(int pos,int *start,int *end){
  int len=end-start+1;
  numbers=realloc(numbers,len+n);
  for(int i=n-1;i>=pos+1;i++){
    numbers[i+len]=numbers[i];
  }
  memcpy(numbers+pos,start,len);
  n+=len;
}

元素后移,然后直接使用memcpy进行元素的拷贝。注意移动元素的时候,要从后往前复制元素。当然,这个操作可以使用memmove实现。多说几句,memmove在需要复制的内存有重叠的时候,会自动考虑从后往前和从前往后,而memcpy不会。因此,这类情况,由于从前往后会导致后面的元素被覆盖,因此我们需要从后往前,因为后面是空的内存,这样就不会出问题。

注:一开始的时候,这个函数忘记写n+=len一句,已修正。

(以下为9月2日 18:00的更新内容)
插入元素的向量版本,也就是插入另外一个向量中的内容:

void insert(int pos,Vector v,int begin,int end){//插入另一个vector中的内容
  int len=end-begin+1;
  int a[len];//C99支持以变量作为数组元素个数
  for(int i=0;i<len;i++){
    a[i]=v.numbers[i+begin];
  }
  insert(pos,a,a+len);//调用数组版本的插入操作
}

需要注意的是int a[len]这种写法,在老版本的C语言是不支持的,因为必须使用数字常量来指定元素的个数。
并且,这里说的常量不包括const定义的常量,这种常量其实只是“只读变量”,只是在编译的时候进行检查而已。(局部变量是这个结论,但是全局变量不是这个结论。全局变量的内容在内存中放置在只读的区域,强制修改会segmentation fault)而C99以及之后的版本是支持使用变量作为数组的元素个数的。C++的,const的含义也是改变的,也可以使用const定义的常量来作为数组的元素个数。

(以下为9月3日的更新内容)

3.删除函数

3.1.pop_back函数

作用是删除向量中最后一个元素。实现起来也是非常简单的。

void pop_back(){
    --n;
}

其实根本没有必要删除,只需要将长度减去1,这样再次放入元素的时候,会直接覆盖掉上次未删除的的元素。在遍历的时候,由于是根据长度n遍历,所以也不会出现问题。

3.2.clear函数

作用是清空向量中所有的元素。

void clear(){
    n=0;
}

非常简单,原理上面已经说过了,只需要将大小设定为0即可。

(以下为9月4日的更新内容)

3.3.erase函数

其实和string中的erase很相似。

2.3.3.erase操作
erase(int p,int n)
删除从p开始的n个字符。
void erase(int p,int n){
int front=p+1,rear=p+n;
while(str[rear]!=’\0’){
str[front]=str[rear];
++front;++rear;
}
str[front]=’\0’;
}
我们使用覆盖的方法,设置一头一尾两个指针,每次把尾指针的内容复制到头指针,直到尾指针指向的字符为0。如果不为0,那么就继续下一个字符。例如,把abcdefg的第三个字符到第五个字符删除。我们用列表的方式来看一下。
1 2 3 4 5 6 7
a b c d e f g
a b F d e F g
a b f G e f G
a b f g 0 f g
其中,大写字母表示头指针和尾指针所在的位置。可以看到,把后面的字符逐个放到前面,最后添上0即可。因为添上了0,最后不用删除,字符串自动结束。

实际上,vector的erase,算法和string的erase函数都是类似的。其实我们完全可以把string看作是一个元素为char的vector。只不过,vector判断结束的方法是根据rear是否等于n,而string的则是判断str[rear]是否等于结束符\0。

void erase(int p,int n){
  int front=p+1,rear=p+n;
  while(rear!=n){
    numbers[front]=numbers[rear];
    ++front;++rear;
  }
  n=n-p+1;
}

不过参数中的n和vector类的n重名了,我们最好把参数的n修改一下名字,代码略。

(以下为9月5日的更新内容)

4.遍历函数

4.1.at函数

最近进度感觉还是很快的。at函数返回的是某一个元素的引用,其实引用就是一个受限的指针,只不过在写法上不太相同。
为什么这样说呢?我们看两个函数,已经是很老的例子了,swap函数。

void swap_p(T *a,T *b){
  T temp=*a;*a=*b;*b=temp;
}
void swap_r(T &a,T &b){
  T temp=a;a=b;b=temp;
}

p是pointer的略语,r是reference的略语。
我们观察函数,发现两个不同点:
1,定义不同:指针是T*,引用是T&。
2,解引用不同:指针需要在引用数值时添加*,引用则不需要。

因此,我们发现:引用其实就是在做“引用这个地址对应的元素”的时候,不需要写*号而已。
那么,我们再验证一下,at函数。

T& at_r(int i){
  return numbers[i];
}
T *at_p(int i){
  return numbers+i;
}

在调用的时候,指针版本的at_p,需要写作*at_p(i)的形式,来取数值,而引用版本的不需要。这也再次证明,其实引用并不是什么所谓的“别名”,就是一个受限的指针。写法上,不需要写*号,这是唯一区别。内部上,其实引用还是按照指针来传递。只不过在使用的时候,看上去像值传递一样,因为不用写*号。
真正的at函数,就按照at_r写了。
上面关于什么“引用和指针”的内容,看不懂也没关系,如果你还是按照“引用就是别名”这一思想去理解,在写代码的时候也没有太大问题。但是,这是事实。再例如《征服C指针》说的,C语言不存在多维数组,本质其实是“数组的数组”。既然我们在研究C++的语法,这点东西我希望大家能够理解。

如果感觉这段说的不清楚,可以看这个网页。这里也非常感谢原文作者的科普。

4.2.[]运算符

运算符重载好久不写了,忘记的人自己看看《自制string》吧。

T &operator[](int i){
  return numbers[i];
}

很简单吧。不再多说了。

(以下为9月13日 20:00更新的内容)
看在阅读量到了10的份上,我就再更新一下吧。其实对于遍历函数这一部分,是非常简单的。

4.3.front和back

T &front(){
  return *numbers;
}
T &rear(){
  return numbers[n-1];
}

感觉太简单了?讲点干货吧。front里面返回的是numbers[0],也可以写作*numbers。这两种写法是等价的。最早C语言中,下标运算符其实只是指针的简写形式,*(a+i)写作a[i]。因此,说什么“a[i]是a的数组的第i个元素”,这种说法是错误的。(更准确的说,这个说明在部分情况下是错误的)*a也不一定是什么“指向a的指针”。因为:例如数组元素a[2]
a[2]=*(a+2)=*(2+a)=2[a]
把a[2]写成2[a]完全不会报错。这种情况,还能说是“名叫2的数组的第a个元素”吗?而且,文中*a的写法,其实是:
a[0]=*(a+0)=*a
这样可以少写几个字符。
当然,把a[i]写成i[a],对于我们写代码没有任何帮助,因为它太另类了。

posted @ 2021-08-31 13:21  计算机知识杂谈  阅读(98)  评论(0编辑  收藏  举报