资讯专栏INFORMATION COLUMN

[C/C++ -STL]vector底层实现机制刨析

lowett / 3006人阅读

摘要:并且由于的连续性,且循环中有迭代器的自加,所以在删除一个元素后,迭代器需要减。隶书方案二与方案一在迭代器的处理上是类似的,不过对元素的访问采用了迭代器的方法。

一、vector底层实现机制刨析

通过分析 vector 容器的源代码不难发现,它就是使用 3 个迭代器(可以理解成指针)来表示的:
其中statrt指向vector 容器对象的起始字节位置;
finish指向当前最后一个元素的末尾字节
end_of指向整个 vector 容器所占用内存空间的末尾字节。
如图 演示了以上这 3 个迭代器分别指向的位置

如图 演示了以上这 2个迭代器分别指向的位置

在此基础上,将 3 个迭代器两两结合,还可以表达不同的含义,例如:
start 和 finish 可以用来表示 vector 容器中目前已被使用的内存空间;
finish 和 end_of可以用来表示 vector 容器目前空闲的内存空间;
start和 end_of可以用表示 vector 容器的容量。

二、vector的核心框架接口的模拟实现

1.vector的迭代器实现

typedef T* Iteratot;
typedef T* const_Iteratot;

Iteratot cend()const {			return final_end;		}		Iteratot cbegin()const {			return start;		}			Iteratot end() {			return final_end;		}		Iteratot begin() {			return start;		}

vector的迭代器是一个原生指针,他的迭代器和String相同都是操作指针来遍历数据:

  • begin()返回的是vector 容器对象的起始字节位置;
  • end()返回的是当前最后一个元素的末尾字节;

2.reserve()扩容

	void reserve(size_t n) {			if (n > capacity()) {				T* temp = new T  [n];				//把statrt中的数据拷贝到temp中				size_t size1 = size();				memcpy(temp, start, sizeof(T*) * size());							start = temp;			  final_end = start + size1;				finally = start + n;			}		}

当 vector 的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么 vector 就需要扩容。vector 容器扩容的过程需要经历以下 3 步:

  • 完全弃用现有的内存空间,重新申请更大的内存空间;
  • 将旧内存空间中的数据,按原有顺序移动到新的内存空间中;
  • 最后将旧的内存空间释放。

这也就解释了,为什么 vector 容器在进行扩容后,与其相关的指针、引用以及迭代器可能会失效的原因。

由此可见,vector 扩容是非常耗时的。为了降低再次分配内存空间时的成本,每次扩容时 vector 都会申请比用户需求量更多的内存空间(这也就是 vector 容量的由来,即 capacity>=size),以便后期使用。

vector 容器扩容时,不同的编译器申请更多内存空间的量是不同的。以 VS 为例,它会扩容现有容器容量的 50%。

使用memcpy拷贝问题
reserve扩容就是开辟新空间用memcpy将老空间的数据拷贝到新开空间中。
假设模拟实现的vector中的reserve接口中,使用memcpy进行的拷贝,以下代码会发生什么问题?

int main(){bite::vector<bite::string> v;v.push_back("1111");v.push_back("2222");v.push_back("3333");return 0;}

问题分析:

  1. memcpy是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中
  2. 如果拷贝的是自定义类型的元素,memcpy即高效又不会出错,但如果拷贝的是自定义类型元素,并且
    自定义类型元素中涉及到资源管理时,就会出错,因为memcpy的拷贝实际是浅拷贝。




    结论:如果对象中涉及到资源管理时,千万不能使用memcpy进行对象之间的拷贝,因为memcpy是浅拷贝,否则可能会引起内存泄漏甚至程序崩溃。
    3.尾插尾删(push_back(),pop_back())
	void push_back(const T&var) {			if (final_end ==finally) {				size_t newcode = capacity() == 0 ? 4 : capacity() * 2;				reserve(newcode);			}			*final_end = var;			++final_end;		void pop_back() {					final_end--;		}

插入问题一般先要判断空间是否含有闲置空间,如果没有,就要开辟空间。我们final_end==finally来判断是否含有闲置空间。如果容器含没有空间先开辟4字节空间,当满了后开2capacoity()空间。在final_end部插入数据就行了。对final_end加以操作。
4.对insert()插入时迭代器失效刨析

		Iteratot insert(Iteratot iterator,const T&var) {			assert(iterator <= final_end && iterator >= start);			size_t pos = iterator - start;			if (final_end == finally) {								size_t newcode = capacity() == 0 ? 4 : capacity() * 2;				reserve(newcode);				}			//插入操作			auto it = final_end;			while (it >= start+pos) {				*(it+1)=*it;				it--;			}			*iterator = var;			final_end++;						return iterator;		}

假设这是一段vector空间要在pos插入数据,但是刚刚好final_end和final在同一位置,这个容器满了,要对这这个容器做扩容操作。首先对开辟和这个空间的2呗大小的空间

接着把老空间数据拷贝到新空间中释放老空间。


由于老空间释放了pos指向的内存不见了。pos指针就成了野指针。
这如何解决呢就是在老空间解决之间保存这个指针,接着让他重新指向新空间的原来位置。

而insert()函数返回了这个位置迭代器这为迭代器失效提供了方法,这个方法就是重新赋值。让这个指针重新指向该指向的位置。

5.对erase()数据删除时迭代器失效刨析

	Iteratot erase(Iteratot iterator) {				assert(iterator <= final_end && iterator >= start);				auto it = iterator;				while (it <final_end) {					*it = *(it+1);					it++;				}				final_end--;				return iterator;			}


vector使用erase删除元素,其返回值指向下一个元素,但是由于vector本身的性质(存在一块连续的内存上),删掉一个元素后,其后的元素都会向前移动,所以此时指向下一个元素的迭代器其实跟刚刚被删除元素的迭代器是一样的。
以下为解决迭代器失效方案:

#include #include using namespace std; int main(){    int a[] = {1, 4, 3, 7, 9, 3, 6, 8, 3, 3, 5, 2, 3, 7};    vector<int> vector_int(a, a + sizeof(a)/sizeof(int));    /*方案一*/    // for(int i = 0; i < vector_int.size(); i++)    // {    //     if(vector_int[i] == 3)    //     {    //         vector_int.erase(vector_int.begin() + i);    //         i--;    //     }    // }  /*方案二*/    // for(vector::iterator itor = vector_int.begin(); itor != vector_int.end(); ++itor)    // {    //     if (*itor == 3)    //     {    //         vector_int.erase(itor);    //         --itor;    //     }     // } /*方案三*/vector<int>::iterator v = vector_int.begin();while(v != vector_int.end()){    if(*v == 3)    {        v = vector_int.erase(v);        cout << *v << endl;    }    else{        v++;    }} /*方案四*/// vector::iterator v = vector_int.begin();// while(v != vector_int.end())// {//     if(*v == 3)//     {//         vector_int.erase(v); //     }//     else{//         v++;//     }// }     for(vector<int>::iterator itor = vector_int.begin(); itor != vector_int.end(); itor++)    {        cout << * itor << "  ";    }    cout << endl;    return 0;}

一共有四种方案。

方案一表明vector可以用下标访问元素,显示出其随机访问的强大。并且由于vector的连续性,且for循环中有迭代器的自加,所以在删除一个元素后,迭代器需要减1。

方案二与方案一在迭代器的处理上是类似的,不过对元素的访问采用了迭代器的方法。

方案三与方案四基本一致,只是方案三利用了erase()函数的返回值是指向下一个元素的性质,又由于vector的性质(连续的内存块),所以方案四在erase后并不需要对迭代器做加法。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/124077.html

相关文章

  • jQuery源码解析之你并不真的懂事件委托及target和currenttarget的区别

    摘要:源码源码行被点击了点击了,即委托的事件被点击了优先添加委托,再添加其他即委托在上的事件数量在下标为的位置插入委托事件解析可以看到,是优先添加委托事件,再添加自身事件,触发事件的时候也是按这个顺序。 showImg(https://segmentfault.com/img/remote/1460000019419722); 前言:请先回顾下我之前写的一篇文章:JavaScript之事件委...

    khs1994 评论0 收藏0
  • php 验证格式的函数总结

    摘要:检查数据是否是格式判断是否为有效邮件地址判断是否为有效网址判断字符串是否为空判断是否为指定长度内字符串判断是否为合法用户名判断是否为合法用户密码判断是否为合法电话号码判断是否是某一范围内的合法值判断是否为合法邮编固定长度判断上传文件的扩展名 // ※CheckMoney($C_Money) 检查数据是否是99999.99格式// ※CheckEmailAddr($C_mailaddr)...

    lushan 评论0 收藏0
  • 从零开始写个编译器吧 - 开始写词法分析器(2)

    摘要:读到一个非数字非英文字母非下划线字符。此时立即跳转回状态。以一个双引号开始,并以一个双引号结束。另外,在读和时源代码不许结束,即读到符号,若结束,则判定为词法错误。对于而言,也有一些其他的词法错误判定,如,不能换行。 对于非 Normal 状态,我只需要关心两个过程: 何时从 Normal 跳转到该状态; 何时从该状态跳回 Normal 状态。 在上一章中,我已经写好了从 Nor...

    MarvinZhang 评论0 收藏0
  • 如何用 CSS 网格快速做出网站原型

    摘要:简评网格模块是创建网站模型的绝佳工具。如果你对网格完全陌生,你可能要浏览一下我的分钟介绍网格的文章。每一行代表一行,每一个字符,,,代表一个网格元素。无论标签在标记中是如何放置的,我们都能随意转换。这被称为源代码的独立性,这是的一大进步。 简评:CSS 网格模块是创建网站模型的绝佳工具。它是我尝试过的任何其他系统中最快让你体验布局的工具。 我们的网格 我们将从模仿一个经典网站的非常基本...

    thursday 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<