继《自制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],对于我们写代码没有任何帮助,因为它太另类了。

【原创】【长期更新】【未完待续】自制vector类型的更多相关文章

  1. 我的SQL总结---未完待续

    我的SQL总结---未完待续 版权声明:本文为博主原创文章,未经博主允许不得转载. 总结: 主要的SQL 语句: 数据操作(select, insert, delete, update) 访问控制(g ...

  2. 关于DOM的一些总结(未完待续......)

    DOM 实例1:购物车实例(数量,小计和总计的变化) 这里主要是如何获取页面元素的节点: document.getElementById("...") cocument.query ...

  3. virtualbox搭建ubuntu server nginx+mysql+tomcat web服务器1 (未完待续)

    virtualbox搭建ubuntu server nginx+mysql+tomcat web服务器1 (未完待续) 第一次接触到 linux,不知道linux的确很强大,然后用virtualbox ...

  4. MVC丶 (未完待续······)

         希望你看了此小随 可以实现自己的MVC框架     也祝所有的程序员身体健康一切安好                                                     ...

  5. odoo11 model+Recordset 基础未完待续

    Model 一个模型代表了一个业务对象 本质上是一个类,包含了同django flask一样的数据字段 所有定义在模型中的方法都可以被模型本身的直接调用 现在编程范式有所改变,不应该直接访问模型,而是 ...

  6. Go web编程学习笔记——未完待续

    1. 1).GOPATH设置 先设置自己的GOPATH,可以在本机中运行$PATH进行查看: userdeMacBook-Pro:~ user$ $GOPATH -bash: /Users/user/ ...

  7. PAT A1098 Insertion or Heap Sort (25 分)——堆排序和插入排序,未完待续。。

    According to Wikipedia: Insertion sort iterates, consuming one input element each repetition, and gr ...

  8. Hibernate二级缓存(未完待续)

    1.Hibernate的cache介绍: Hibernate实现了良好的Cache机制,可以借助Hibernate内部的Cache迅速提高系统的数据读取性能.Hibernate中的Cache可分为两层 ...

  9. asp.net面试题总结1(未完待续。。。。)

    1.MVC中的TempData\ViewBag\ViewData区别? 答:页面对象传值,有这三种对象可以传. Temp:临时的 Bag:袋子 (1)  TempData  保存在Session中,C ...

  10. git安装与使用,未完待续... ...

    ​ 目录 一.git概念 二.git简史 三.git的安装 四.git结构 五.代码托管中心-本地库和远程库的交互方式 六.初始化本地仓库 七.git常用命令 1.add和commit命令 2.sta ...

随机推荐

  1. asp .net core中swagger的简单使用

    相信swagger大家不太陌生,简单来说就是把web api接口以ui的形式呈现到页面上,供方便调用和展示.这边文章就带大家初步简单使用swagger. (1)首先需要安装包:Swashbuckle. ...

  2. Docker部署ELK之部署kibana7.6.0(2)

    1. 拉取kibana镜像 sudo docker pull kibana:7.6.0 2. 输入命令构建kibana容器,关于挂载kibana配置文件的问题,也可以先构建一个容器,然后把配置文件co ...

  3. golang web框架 kratos中的日志框架

    kratos是bilibili开源的一个web框架. 日志用法: logger.go package kratoslog import ( "flag" "github. ...

  4. SQL注入:基本查询原理

    SQL注入概述 sql注入不需要精通sql的各种命令,只需要了解几个命令并会使用即可. SQL注入:一种针对于数据库的攻击 现在的web网站都需要数据库的支持. SQL部分重要内容: 库:databa ...

  5. SpringCloud升级之路2020.0.x版-21.Spring Cloud LoadBalancer简介

    本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们使用 Spri ...

  6. JSON.stringify()还可以这么用

    最近做项目的时候遇到一个对象深拷贝的问题,网上看了下发现最为简便的方法是JSON.stringify(),比如你要深拷贝一个对象,可以这么做: var test={ a:"hello&quo ...

  7. 数据结构--Dijkstra算法最清楚的讲解

    迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个节点到其他节点的最短路径.它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止 ###基本思想 通过Dij ...

  8. [转]C# 互操作性入门系列(二):使用平台调用调用Win32 函数

    传送门 C#互操作系列文章: C# 互操作性入门系列(一):C#中互操作性介绍 C# 互操作性入门系列(二):使用平台调用调用Win32 函数 C# 互操作性入门系列(三):平台调用中的数据封送处理 ...

  9. 在npm Vue单页面的环境下,兄弟组件之间通信Bus

    参考1:https://www.cnblogs.com/wangruifang/p/7772631.html 参考2:https://www.jianshu.com/p/b3d09c6c87bf 在m ...

  10. Linux中的静态库与动态库

    什么是库文件? 库文件是事先编译好的方法的合集.比如:我们提前写好一些数据公式的实现,将其打包成库文件,以后使用只需要库文件就可以,不需要重新编写. Linux系统中: 1.静态库的扩展名为.a:2. ...