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

一,vector类型简单介绍

1.简介

1.1.STL

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

1.2.vector

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

这里给大家一个使用的例子:

  1. #include<iostream>
  2. #include<vector>
  3. using namespace std;
  4. int main(){
  5. vector<int> v;
  6. for(int i=0;i<10;i++){
  7. v.push_back(i);
  8. cout<<v[i]<<endl;
  9. }
  10. return 0;
  11. }

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

2.vector有哪些函数?

(来源:网页链接

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

二,泛型

1.什么是泛型?

最简单的例子:

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

这里,max函数即可以处理int,又可以处理float,char类型的最大值。这就是泛型。max函数无论针对哪一个类型,操作都是相同的。因此,我们使用通用类型,让函数不关注类型只关注具体的操作。

有人会问,其实使用函数重载不就能完成了吗?(不了解函数重载是什么的,请看我前面写过的《自制string类型》,里面有说明)但是,函数重载要重复写好几次,不方便。

2.泛型的实现

2.1.函数模板

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

  1. #include<iostream>
  2. #include<vector>
  3. using namespace std;
  4. template<typename T>
  5. T MAX(T a,T b){
  6. if(a>b)return a;
  7. else return b;
  8. }
  9. int main(){
  10. int a=5,b=10;cout<<MAX(a,b);
  11. float c=3.14,d=9.99;cout<<MAX(c,d);
  12. char e='x',f='*';cout<<MAX(e,f);
  13. }

template表示定义一个叫做T的类型,这个类型是一个通用类型。这个语句告诉编译器,要开始泛型编程,其中T是要使用的泛型类型。

执行的时候,编译器会自动根据参数的类型,把T转换为int,float等类型,进行计算。

注意,泛型的函数不会进行自动类型转换,例如cout<<MAX('a',100);这个语句,如果使用的是泛型类型,会编译错误,但是使用普通类型不会报错,因为普通类型的函数会进行自动类型转换。

2.2.类模板

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

  1. template<typename T>
  2. class Vector{
  3. T *numbers;
  4. };

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

  1. Vector v;

而必须写成:

  1. Vector<int>v;

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

三,相关函数写法

1.构造函数

终于来到写函数的时候了。第一个写的函数一定是构造函数。我们知道,vector其实是一个“动态数组”,而动态数组的实现需要依靠指针。我们使用一个变量n来记录目前vector内部的元素个数。

最开始的时候,整个向量内部为空,那么n为0,并且无法存放数据。在放入数据的时候,向量会申请空间,来放入数据。

四个构造函数:

  1. class Vector{
  2. T *numbers;
  3. int n;
  4. Vector(){
  5. n=0;
  6. }
  7. Vector(int nsize){//初始化大小
  8. n=nsize;
  9. numbers=(T*)malloc(nsize*sizeof(T));
  10. }
  11. Vector(int nsize,T t){//初始化大小并放入nsize个t
  12. n=nsize;
  13. numbers=(T*)malloc(nsize*sizeof(T));
  14. for(int i=0;i<nsize;i++)numbers[i]=t;
  15. }
  16. Vector(Vector <int> &v){//复制另一个vector的东西
  17. n=v.n;
  18. numbers=(T*)malloc(n*sizeof(T));
  19. for(int i=0;i<nsize;i++)numbers[i]=v.numbers[i];
  20. }
  21. };

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

2.增加函数

2.1.push_back

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

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

如果只有一个元素,那么执行Malloc。如果已经有元素了,那么执行realloc。

但是,不断的realloc是非常慢的。下面内容引用自《征服C指针》:

我们经常需要对数组顺次追加元素,这种情况下,如果每追加一个元素都利用 realloc()进

行内存区域扩展,将会发生什么呢?

如果手气不错,后面正好有足够大的空地儿,这样还好。如果不是这样,就需要频繁地复制

区域中的内容,这样自然会影响运行效率。另外,不断地对内存进行分配、释放的操作,也

会引起内存碎片化。

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

  1. class Vector{
  2. T *numbers;
  3. int n;//大小
  4. int memory;//已经分配的内存
  5. T* Allocate(T *mem,int newsize){
  6. n=newsize;
  7. if(memory>=n)return NULL;
  8. else memory=newsize+100,return realloc(mem,newsize+100);
  9. }

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

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

上面是修改的push_back示例。这样就方便多了。

当然,为了实现起来方便,本文之后的所有例程都写作realloc的形式,便于理解和阅读。

2.2.insert函数

insert的作用是插入,我们目前是使用数组进行存储的,因此,插入和删除需要移动元素(顺序表,如果是链表则没有这个问题)

为了制造起来方便,我们这样规定:vector插入的东西必须是一个vector中的东西或者数组。至于什么迭代器iterartor,就不做了。

同时,为了初始化方便,我们制作一个可变长函数用于初始化元素,如下:

Vector(数组长度n,元素1,元素2,...,元素n);

  1. Vector(int nsize,...){
  2. n=nsize;
  3. numbers=(T*)malloc(nsize*sizeof(T));
  4. va_list ap;
  5. va_start(ap,nsize);
  6. for(int i=0;i<n;i++)numbers[i]=va_arg(ap,T);
  7. va_end(ap);
  8. }

可变长函数的基本格式:

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

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

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

插入元素(数组版本)

  1. void insert(int pos,int *start,int *end){
  2. int len=end-start+1;
  3. numbers=realloc(numbers,len+n);
  4. for(int i=n-1;i>=pos+1;i++){
  5. numbers[i+len]=numbers[i];
  6. }
  7. memcpy(numbers+pos,start,len);
  8. n+=len;
  9. }

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

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

(以下为9月2日 18:00的更新内容)

插入元素的向量版本,也就是插入另外一个向量中的内容:

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

需要注意的是int a[len]这种写法,在老版本的C语言是不支持的,因为必须使用数字常量来指定元素的个数。

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

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

3.删除函数

3.1.pop_back函数

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

  1. void pop_back(){
  2. --n;
  3. }

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

3.2.clear函数

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

  1. void clear(){
  2. n=0;
  3. }

非常简单,原理上面已经说过了,只需要将大小设定为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。

  1. void erase(int p,int n){
  2. int front=p+1,rear=p+n;
  3. while(rear!=n){
  4. numbers[front]=numbers[rear];
  5. ++front;++rear;
  6. }
  7. n=n-p+1;
  8. }

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

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

4.遍历函数

4.1.at函数

最近进度感觉还是很快的。at函数返回的是某一个元素的引用,其实引用就是一个受限的指针,只不过在写法上不太相同。

为什么这样说呢?我们看两个函数,已经是很老的例子了,swap函数。

  1. void swap_p(T *a,T *b){
  2. T temp=*a;*a=*b;*b=temp;
  3. }
  4. void swap_r(T &a,T &b){
  5. T temp=a;a=b;b=temp;
  6. }

p是pointer的略语,r是reference的略语。

我们观察函数,发现两个不同点:

1,定义不同:指针是T*,引用是T&。

2,解引用不同:指针需要在引用数值时添加*,引用则不需要。

因此,我们发现:引用其实就是在做“引用这个地址对应的元素”的时候,不需要写*号而已。

那么,我们再验证一下,at函数。

  1. T& at_r(int i){
  2. return numbers[i];
  3. }
  4. T *at_p(int i){
  5. return numbers+i;
  6. }

在调用的时候,指针版本的at_p,需要写作*at_p(i)的形式,来取数值,而引用版本的不需要。这也再次证明,其实引用并不是什么所谓的“别名”,就是一个受限的指针。写法上,不需要写*号,这是唯一区别。内部上,其实引用还是按照指针来传递。只不过在使用的时候,看上去像值传递一样,因为不用写*号。

真正的at函数,就按照at_r写了。

上面关于什么“引用和指针”的内容,看不懂也没关系,如果你还是按照“引用就是别名”这一思想去理解,在写代码的时候也没有太大问题。但是,这是事实。再例如《征服C指针》说的,C语言不存在多维数组,本质其实是“数组的数组”。既然我们在研究C++的语法,这点东西我希望大家能够理解。

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

4.2.[]运算符

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

  1. T &operator[](int i){
  2. return numbers[i];
  3. }

很简单吧。不再多说了。

(以下为9月13日 20:00更新的内容)

看在阅读量到了10的份上,我就再更新一下吧。其实对于遍历函数这一部分,是非常简单的。

4.3.front和back

  1. T &front(){
  2. return *numbers;
  3. }
  4. T &rear(){
  5. return numbers[n-1];
  6. }

感觉太简单了?讲点干货吧。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. SaToken学习笔记-01

    SaToken学习笔记-01 SaToken版本为1.18 如果有排版方面的错误,请查看:传送门 springboot集成 根据官网步骤maven导入依赖 <dependency> < ...

  2. 剖析虚幻渲染体系(10)- RHI

    目录 10.1 本篇概述 10.2 RHI基础 10.2.1 FRenderResource 10.2.2 FRHIResource 10.2.3 FRHICommand 10.2.4 FRHICom ...

  3. iNeuOS工业互网平台,在纸业领域的成功应用案例

    目       录 1.      项目背景... 2 2.      项目基本情况... 3 3.      概念解释... 5 1.   项目背景 最终用户是全国第5大纸业集团之一,年浆纸产能40 ...

  4. 【Java笔记】以并发修改异常为例总结的出错解决办法

    先来看出错代码: /*需求: 遍历已有集合 如果在集合中发现存在字符串元素"world" 则在"world"后添加元素"javaee" */ ...

  5. S3C2440—1.熟悉裸机开发板

    文章目录 一.板载资源介绍 二.安装驱动及上位机 1.USB的驱动及上位机 2.eop驱动安装 3.安装烧录软件oflash 三.烧写开发板 1.预备知识 2.烧写裸板 3.使用u-boot烧写程序 ...

  6. 剑指 Offer 61. 扑克牌中的顺子

    剑指 Offer 61. 扑克牌中的顺子 从扑克牌中随机抽5张牌,判断是不是一个顺子,即这5张牌是不是连续的.2-10为数字本身,A为1,J为11,Q为12,K为13,而大.小王为 0 ,可以看成任意 ...

  7. IDE快捷键的使用

    ctrl+ait+l,整理代码 ctrl+atl+v,生成等号左边的类型和变量 shift+方向键,选择内容 ctrl+方向键,自己领悟.常常与shift同时使用 ctrl+alt+方向键,光标前进或 ...

  8. RabbitMQ从零到集群高可用(.NetCore5.0) - RabbitMQ简介和六种工作模式详解

    一.RabbitMQ简介 是一个开源的消息代理和队列服务器,用来通过普通协议在完全不同的应用之间共享数据,RabbitMQ是使用Erlang(高并发语言)语言来编写的,并且RabbitMQ是基于AMQ ...

  9. node 报错 throw er; // Unhandled 'error' event 解决办法

    node 报错 Starting child process with 'node web.js' events.js:183 throw er; // Unhandled 'error' event ...

  10. 信号量-Semaphore、SemaphoreSlim

    核心类:Semaphore,通过int数值来控制线程个数. * 通过观察构造函数 public Semaphore(int initialCount, int maximumCount);: * in ...