【C++】探秘vector的底层实现

欢迎拜访Madison-No7个人主页
文章主题: 探秘string的底层实现
隶属专栏我的 C++ 成长日志
写作日期:2025年10月15日

目录

一、vector的成员变量

二、vector的成员函数:

size:

capacity:

reserve:(深拷贝)

push_back:

pop_back:

operator[]:

迭代器:

insert(迭代器失效问题):

erase(迭代器失效问题):

resize:

vector():

~vector():

operator=:

printvector


一、vector的成员变量

vector容器底层基本结构:

成员变量:

cpp 复制代码
template<class T>
class vector
{
    public:
	    typedef T* iterator;
	    typedef const T* const_iterator;
    private:
        //C++11,允许在成员变量声明的地方给缺省值
	    iterator _start=nullptr;
	    iterator _finish=nullptr;
	    iterator _end_of_storage=nullptr;
}

STL容器的成员变量使用迭代器的原因是:迭代器提供了访问STL容器的通用方式,即我们不需要关心容器的底层实现是怎么样的,我们可以通过迭代器就能对容器进行操作,也就是迭代器是我们访问容器的接口、桥梁。

即:容器使用迭代器,迭代器提供通用访问方式

二、vector的成员函数:

size:

获取容器里的有效数据个数。

cpp 复制代码
size_t _size() const
{
	return _finish - _start;
}

capacity:

获取容器的容量。

cpp 复制代码
size_t _capacity()
{
	return _end_of_storage - _start;
}

reserve:(深拷贝)

扩容或预开辟一块空间。

典型错误写法:

cpp 复制代码
void reserve(size_t n)
{
	//开空间
	T* temp = new T[n];
	//将旧空间数据拷贝到新空间
	memcpy(temp, _start, sizeof(T) * _size());
	
	//释放旧空间
	delete[] _start;

	//此写法是错误的
	_start = temp;
	_finish = _start + _size();
    //        _start(更新后的)+_finish(更新前的)-_start(更新后的)==finish==nullptr
	_end_of_storage = _start + n;
}

我们期望的是开空间后,_start、_finish、_end_of_storage都应指向开辟的空间,而不是空指针。但是如果像上面那样写,_finish就会是nullptr。

正确写法:(对于浅拷贝)

cpp 复制代码
		void reserve(size_t n)
		{
			//开空间
			T* temp = new T[n];
			//将旧空间数据拷贝到新空间
			memcpy(temp, _start, sizeof(T) * _size());
		
			//释放旧空间
			delete[] _start;

			//        temp+_finish(更新前的) - _start(更新前的)==temp;
			_finish = temp + _size();
			_start = temp;
			_end_of_storage = _start + n;
		}

但是这样写有点别扭,vector成员变量的顺序正常来讲应该是:_start、_finish、_end_of_storage

所以我们修正一下成员变量的顺序:

cpp 复制代码
	void reserve(size_t n)
	{
        //记录旧的size
		size_t old_size = _size();
		//开空间
		T* temp = new T[n];
		//将旧空间数据拷贝到新空间
		memcpy(temp, _start, sizeof(T) * _size());
		
		//释放旧空间
		delete[] _start;

		_start = temp;
		_finish = temp + old_size;
		_end_of_storage = _start + n;
	}

但是这里还存在一些问题:

上面这种扩容逻辑,当 T 是内置类或者是无需进行深拷贝的自定义类型来说,是完全满足的。但是当 T 是需要进行深拷贝的内置类型时,上面这种扩容方式就会出现大问题。以 vector<string> 为例,即当 T 是 string 的时候。

cpp 复制代码
	void test_vector9()
	{
		vector<string> v;
		string s1("hello world");
		v.push_back(s1);
		v.push_back(s1);
		v.push_back(s1);
		v.push_back(s1);
		printvector(v);
		//底层需要扩容了
		v.push_back(s1);
		printvector(v);
	}

如果简单的用 memcpy 将旧空间的数据拷贝到新空间,那么新旧空间中存储的 string 对象指向同一个堆区上的字符串,接着在执行 delete[] _start; 销毁旧空间的时候,由于该 _start 是一个 string* 的指针,所以会先调用 string 的析构函数,将对象中申请的空间释放,即释放 _str 指向的空间,接着再去调用 operator delete 函数释放 string 对象的空间。这样一来,新空间中存储的 string 对象就有问题了,它们的成员变量 _str 指向的空间已经被释放了。这里的问题就出在 memcpy 执行的是浅拷贝。我们需要让temp中的string对象中的_str指向一块新的空间,可以通过string的赋值重载实现_str的深拷贝:

cpp 复制代码
	void reserve(size_t n)
	{
        //记录旧的size
		size_t old_size = _size();
		//开空间
		T* temp = new T[n];
		//将旧空间数据拷贝到新空间
        //浅拷贝
		//memcpy(temp, _start, sizeof(T) * _size());
		//深拷贝
        for (size_t i=0;i< old_size;i++)
	    {
		     temp[i] = _start[i];
	    }
		//释放旧空间
		delete[] _start;

		_start = temp;
		_finish = temp + old_size;
		_end_of_storage = _start + n;
	}

push_back:

cpp 复制代码
		void push_back(const T& x)
		{
			//判断空间满了吗?
			if (_size() == _capacity())
			{
				//扩容
				reserve(_capacity() == 0 ? 4 : _capacity() * 2);
			}
			//插入数据
			(*_finish) = x;
			_finish++;
		}

pop_back:

cpp 复制代码
	void pop_back()
	{
		//如果条件表达式的结果为真(非 0):assert 什么也不做,程序继续执行。
		assert(_size() != 0);
		--_finish;
	}

operator[]:

可以像数组一样访问vector容器。

cpp 复制代码
	//可读可写
	T& operator[](size_t pos)
	{
		assert(pos < _size());
		return _start[pos];
	}
	//只读
	const T& operator[](size_t pos) const
	{
		assert(pos < _size());
		return _start[pos];
	}

迭代器:

cpp 复制代码
//可写可读
iterator begin()
{
	return _start;
}
iterator end()
{
	return _finish;
}
//只读
const_iterator begin() const
{
	return _start;
}
const_iterator end() const
{
	return _finish;
}

迭代器的主要作用就是让算法能够不用关心底层数据结构,其底层实际就是一个指针,或者是对
指针进行了封装 ,比如: vector 的迭代器就是原生态指针 T*

insert(迭代器失效问题):

pos位置插入x对象

典型错误写法:

cpp 复制代码
iterator insert(iterator pos, const T& x)
{
    // 判断pos的有效性
	assert(pos >= _start);
	assert(pos <= _finish);
	//判断需要扩容吗?
	if (_size() == _capacity())
	{
		//扩容
		reserve(_capacity() == 0 ? 4 : _capacity() * 2);
	}
	//挪动数据
	iterator end = _finish - 1;
	while (end >= pos)
	{
		*(end + 1) = *end;
		end--;
	}
	//插入数据
	(*pos) = x;
	++_finish;
	return pos;
}

对挪动数据过程分析:

存在迭代器失效问题:

首先我们要理解 vector 迭代器的核心作用是 "标记元素的逻辑位置",它记录的是元素在内存中的具体地址,比如元素v[2]存储在内存地址0x1000,则指向它的迭代器本质上就是0x1000这个地址标记,也就是说迭代器和元素之间建立了一一对应的关系。

1.扩容导致失效:

这里的迭代器失效本质是野指针,由扩容导致的。一定得记住扩容后,要更新pos的指向。

迭代器失效的底层原因分析:

扩容后,_start、_finish、_end_of_storage都指向了新的空间,而end=_finish-1,即end的指向也跟着更新了,但是pos还是指向原来的位置(无效地址),当while(end>=pos)时,end和pos指向的都是不同的空间,循环判断就失去意义了。紧接着去 pos 指向的位置(原来的位置已释放)填入数据,就会造成非法访问,造成程序崩溃。为了避免这个问题,我们要把 pos 的相对位置保存下来,扩完容之后再去更新 pos。

正确写法:

cpp 复制代码
	iterator insert(iterator pos, const T& x)
	{
		assert(pos >= _start);
		assert(pos <= _finish);
		//判断需要扩容吗?
		if (_size() == _capacity())
		{
			//保存pos的相对位置
			size_t len = pos - _start;
			//就要扩容
			reserve(_capacity() == 0 ? 4 : _capacity() * 2);
			//扩容后,更新扩容后pos在新空间的相对位置
			pos = _start + len;
		}
		//挪动数据
		iterator end = _finish - 1;
		while (end >= pos)
		{
			*(end + 1) = *end;
			end--;
		}
		//插入数据
		(*pos) = x;
		++_finish;
		return pos;
	}

2.不需要扩容也会导致迭代器失效:

插入位置 pos 及之后的所有元素会向后移动一位,这就破环了pos及以后所有元素的与迭代器的对应关系( 位置意义变了),**即指向 pos 及之后元素的迭代器都会失效,**但pos 之前的迭代器仍然有效,因为元素位置未变。

解决迭代器失效的方法:

要访问就要更新迭代器,始终使用 insert 的返回值更新迭代器(该返回值指向新插入的元素)。

对于更新迭代器,就需要insert函数返回一个有效迭代器,将更新后的 pos 返回。可能会有小伙伴觉得,直接把形参的 pos 变成引用不香嘛?这样对形参的更新就相当于是对实参的更新。想法很好,但是不现实,因为实参很有可能具有常性,例如实参如果用 begin()、end(),他俩都是传值返回,会产生一个临时变量,该临时变量具有常性,如果形参 pos 用引用的话,就需要加 const 进行修饰,但是,如果用 const 进行修饰,那在函数内部就不能对 pos 进行更新,因此形参 pos 不能用引用。

**注意:**Vs下会强制检查迭代器(Vs下的迭代器不是由原生指针实现的)失效,如果迭代器失效,访问就会报错,Linux的g++编辑器下检查不严格。
会引起其底层空间改变的操作,都有可能造成迭代器失效

erase(迭代器失效问题):

返回值是指向被删除元素下一个位置的迭代器。

cpp 复制代码
	iterator erase(iterator pos)
	{
		assert(pos >= _start);
		assert(pos < _finish);
		//不需要扩容,涉及到挪动数据覆盖
		iterator begin = pos + 1;
		while (begin != _finish)
		{
			*(begin - 1) = *begin;
			begin++;
		}
		--_finish;
		return pos;
	}

erase删除pos位置元素后,pos位置之后的元素会往前搬移,没有导致底层空间的改变,理论上讲迭代器不应该会失效。

vector 的迭代器的核心作用是 "标记元素的逻辑位置",它记录的是元素在内存中的具体地址,比如元素v[2]存储在内存地址0x1000,则指向它的迭代器本质上就是0x1000这个地址标记,也就是说迭代器和元素之间建立了一一对应的关系,而删除操作破坏了原有的逻辑位置与内存地址的对应关系。

导致迭代器失效的两种情况:

(1)删除的不是最后一个元素

vector 会将pos 之后的所有元素**向前移动一位,**覆盖被删除元素的位置,导致: pos原本指向的内存地址,现在存储的是原 pos+1 位置的元素,迭代器标记的地址虽然存在,但已不属于原元素的有效位置。

(2)删除的是最后一个元素

此时虽然不需要移动其他元素,但被删除元素的内存地址会被排除在 vector 的有效范围之外,该地址已不属于 vector 的有效元素区间,访问会导致越界。

**总结:**删除vector任意位置上元素时,该位置的迭代器都会失效。这是 vector 作为连续容器的特性所决定的(与 list 等链表容器的迭代器行为不同)。

对于迭代器失效,我们可以使用erase的返回值更新迭代器,也就是重新建立迭代器与元素之间的有效位置关系。

cpp 复制代码
	void test_vector8()
	{
		int a[] = { 1, 2, 3, 4 };
		std::vector<int> v(a, a + sizeof(a) / sizeof(int));
		// 使用find查找3所在位置的iterator
		auto pos = std::find(v.begin(), v.end(), 4);
		pos=v.erase(pos);
		cout << *(pos-1) << endl; 
	}

经过前面的分析,我们知道删除vector任意位置上元素,该位置的迭代器都会失效,使用erase的返回值更新迭代器,就可以解决迭代器失效问题。但是这里删除的是最后一个元素,erase 返回新的finish 迭代器,pos-1刚好指向删除后 vector 的最后一个有效元素(3)。所以不会发生越界访问。

**注意:**在VS下,对于迭代器失效检查很极端,只要迭代器失效了,程序就会崩掉,而对于Linux的g++编辑器来讲,检测迭代器失效并不严格。

我们来看这样的情况,如果我们要求删除vector里的所有偶数,我们先向vector里插入1-5的数字,然后再删除偶数。

cpp 复制代码
	void test_vector2()
	{
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(5);
        printvector(v);
		//删除偶数
		auto it = v.begin();
		while (it != v.end())
		{
			if ((*it) % 2 == 0)
			{
				it = v.erase(it);
			}
				++it;
		}
		printvector(v);
	}

这样来看似乎没有问题。但是如果插入的是1-4呢?

cpp 复制代码
	void test_vector2()
	{
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		printvector(v);
        //删除偶数
		auto it = v.begin();
		while (it != v.end())
		{
			if ((*it) % 2 == 0)
			{
				it = v.erase(it);
			}
			++it;
			
		}
		printvector(v);
	}

大家可以看到,崩溃了。为什么?

无论*it是否是偶数,it都会++,当it指向的位置是偶数且刚好是最后有效元素时,毫无疑问,会删除这个位置的元素,finish--,但是it++,刚好错过了相遇,以后再也不能相遇了,此时it依然不等于v.end(),所以会进入while循环,it指向的是偶数,调用erase函数,此时pos>finish,assert断言失败,所以系统崩溃了;

如果插入的是1、2、3、4、4、5?

cpp 复制代码
	void test_vector2()
	{
		vector<int> v;
		v.push_back(1);
		v.push_back(2);
		v.push_back(3);
		v.push_back(4);
		v.push_back(4);
		v.push_back(5);
		printvector(v);
		//删除偶数
		auto it = v.begin();
		while (it != v.end())
		{
			if ((*it) % 2 == 0)
			{
				it = v.erase(it);
			}
			++it;
		}
		printvector(v);
	}

程序并没有崩溃,出现了偶数删不干净的问题。

出现这个问题本质是因为无论it指向的是否是偶数,it都会++,当几个偶数连在一起时,就会出现删不干净的问题。修正方法就是:当it指向的是偶数,调用erase删除偶数,it不动,当it指向的不是偶数时,再++it;

修正:

cpp 复制代码
void test_vector2()
{
	vector<int> v;
	v.push_back(1);
	v.push_back(2);
	v.push_back(3);
	v.push_back(4);
	v.push_back(4);
	v.push_back(5);
	printvector(v);
	//删除偶数
	auto it = v.begin();
	while (it != v.end())
	{
		if ((*it) % 2 == 0)
		{
			it = v.erase(it);
		}
		else
		{
			++it;
		}
	}
	printvector(v);
}

resize:

cpp 复制代码
    //T()为val的缺省值,如果T为内置类型,初始化为0 ,如果T为自定义类型,调用默认构造	
    void resize(size_t n, T val = T())
 	{
		if (n<=_size())
		{
			_finish = _start + n;
		}
		else if (n>_size())
		{
			reserve(n);
			while (_finish<(_start+n))
			{
				*_finish = val;
				++_finish;
			}
		}
	}

vector():

cpp 复制代码
        //默认构造
        //使用缺省值初始化
        vector()
        {

        }
	    //类模板的成员函数,还可以继续是函数模板
		//迭代器区间构造
		template<class inputiterater>
		vector(inputiterater first, inputiterater last)
		{
			while (first!=last)
			{
				push_back(*first);
				first++;
			}
		}

		vector(size_t n,const T& val=T())
		{
			for (int i=0;i<n;i++)
			{
				push_back(val);
			}
		}
		
		vector(int n, const T& val = T())
		{
			for (int i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

      、
        //拷贝构造
        //特殊说明:在类里面可以用类名替代类型
        //vector(const vector<T>& v)
        vector(const vector& v)
        {
	        for (auto& ch: v )
	        {
		        push_back(ch);
	        }
        }

~vector():

cpp 复制代码
~vector()
{
	delete[] _start;
	_start = _finish = _end_of_storage = nullptr;
}

operator=:

cpp 复制代码
vector<T>&  operator=(const vector<T>& v)
{
	if (this!=&v)
	{
		clear();
		//进行深拷贝
		reserve(v._size());

		for (auto& ch : v)
		{
			push_back(ch);
		}
	}
	return *this;
}

//赋值重载法二
void swap(vector<T>& v)
{
	std::swap(_start,v._start);
	std::swap(_finish,v._finish);
	std::swap(_end_of_storage,v._end_of_storage);
}
vector<T>& operator=(vector<T> v)
{
	swap(v);
	return *this;
}

printvector:

由于vector容器没有重载流提取和流插入,因为也不好重置,因为vector容器不像string那样打印的格式单一,所以当我们需要打印vector时,自己实现即可。

cpp 复制代码
template<class T>
void printvector(const vector<T>& v)
{

    typename vector<T>::const_iterator it = v.begin();
	while (it!=v.end())
	{
		cout << *it << " ";
		it++;
	}
}

这是一个函数模板:

编译器在解析模板时,会分两个阶段:

**第一阶段:**模板 "自审",确保自身语法正确,不依赖具体类型。

也就是说模板参数(T)在解析阶段(第一阶段)是不确定的(仅为占位符),编译器不知道const_iterator是类型 还是静态成员变量 ,编译器遵循 "默认非类型" 原则,即把vector<T>::const_iterator 当作非类型(如静态变量),但实际上它是类型,必须用 typename 纠正这一默认行为,告诉编译器是类型,避免歧义性导致的错误。

**第二阶段:**模板 "适配",根据具体类型生成代码并验证语义是否正确,确保模板能正确工作在该类型上。

这种 "两阶段" 机制既保证了模板的泛型灵活性(一次定义适配多类型),又通过编译期检查确保了类型安全,是 C++ 模板的核心设计思想。


完。

今天的分享就到这里,感谢各位大佬的关注,大家互相学习,共同进步呀!

相关推荐
晚风残3 小时前
【C++ Primer】第十二章:动态内存管理
开发语言·c++·c++ primer
我登哥MVP3 小时前
Ajax 详解
java·前端·ajax·javaweb
Swift社区3 小时前
LeetCode 401 - 二进制手表
算法·leetcode·ssh
派大星爱吃猫3 小时前
顺序表算法题(LeetCode)
算法·leetcode·职场和发展
vue学习3 小时前
docker 学习dockerfile 构建 Nginx 镜像-部署 nginx 静态网
java·学习·docker
_extraordinary_3 小时前
Java Spring日志
java·开发语言·spring
PHP源码3 小时前
SpringBoot校园二手商城系统
java·spring boot·springboot二手商城·java校园二手商城系统
liu****3 小时前
8.list的模拟实现
linux·数据结构·c++·算法·list