【C++】:深入理解vector(2):vector深度剖析及模拟实现 - 教程

目录

 一 怎么看源代码

二 vector的部分源代码

1 迭代器类型的成员变量

2 迭代器

3 指定位置插入

4 其他

三 自己实现vector

1 头文件

2 类

3 构造函数和析构函数

4 尾插

5 capacity()

6 reverse

7 insert

8 erase

9 iterator补充

10 resize

11 深拷贝和浅拷贝

1 传统写法

2 现代写法

四      memcpy拷贝时出现的问题 

​编辑

五 理解动态二维数组


深入理解vector(1)链接:【【C++】深入理解vector(1):vector的使用和OJ题

 一 怎么看源代码

在我们尝试去看源代码的时候,因为源代码的复用性较高,比较冗杂,对于我们这种没有足够实战经验的uu来说不是特别友好,所以我们可以借助工具去阅读源代码。

链接:Source insight

在看源代码的时候,要学会抓核心:了解内容,抓框架,注释,画图。

画图是一项很有用的功能,在面对很多比较复杂,容易搞混的代码和情况的时候,能很直观的反映问题。


二 vector的部分源代码

1 迭代器类型的成员变量

start:指向 vector 内部数组的起始位置,即第一个元素的存储地址

start:指向 vector 内部数组的起始位置,即第一个元素的存储地址

end_of_storage:指向 vector 当前已分配内存空间的末尾位置

2 迭代器

3 指定位置插入

4 其他

在学习STL源代码的时候,可以让AI帮忙写相关的注释帮助理解,也可以看书:《STL源码剖析》


三 自己实现vector

1 头文件

#pragma once
#include

2 类

namespace zz
{
  template
  class vector
  {
    public:
      //typedef T* iterator;
      using iterator = T*;
      using const_iterator = const T*;
    //.........
    private:
     iterator _start;
     iterator _finish;
     iterator _end_of_storage'
};

其中,使用using定义了T*重命名为iterator

在此处,using的作用和typedef是一样的,但是using比typedef的作用更广泛,这个我们后面再讲

注意:后面写的函数实现等内容都是再类中的public中实现的,后面不再赘述。

模板不能声明和定义分离

3 构造函数和析构函数

(1)构造函数

vector():
 : _start(nullptr);
 , _finish(nullptr);
 , _end_of_storage(nullage);
  {}

在 C++ 中,这个构造函数定义末尾的 {} 是函数体的标志,用于表示构造函数的实现部分。

具体来说:

  • 前面的 vector() 是构造函数的声明(因为是无参构造,所以括号内为空)。
  • 中间的 :_start(nullptr), _finish(nullptr), _end_of_storage(nullptr) 是成员初始化列表,用于在进入函数体之前初始化类的成员变量(这里将三个指针都初始化为 nullptr)。
  • 最后的 {} 是构造函数的函数体,由于成员变量已经通过初始化列表完成了初始化,且这个构造函数不需要额外执行其他逻辑,所以函数体为空。

简单理解:{} 在这里表示 “构造函数的具体执行代码”,只是当前这个构造函数没有需要执行的额外代码,所以用空的函数体即可。如果后续需要在构造时添加其他操作(比如打印日志),就可以在 {} 内部编写代码。

(2)析构函数

~vector():
{
 if(_start)
 {
   delete[] _start;
   _start = _finish = _endd_of_storage;
 }
}

4 尾插

void push_back(const T& x)
{
  if(_finsh == _end_ of_storage)
  {
    reverse(capacity()==0 ? 4 : capacity()*2);
  }
  *_finish = x;
  ++_finish;
}

因为finish指向最后一个有效数据的下一个位置,所以先赋值,再finish++

注意:因为我们的成员函数不含capacity,所以我们需要自己实现一个capacity ,还有扩容reverse函数

5 capacity()

size_t capacity() const
{
  return end_of_storage - _start;
}

6 reverse

void reserve(size_t n)
{
 if(n > capacity() )
 {
   size_t sz = size();
   T* tmp = new T[n];
   if(_start)
   {
     memcpy(tmp, _start, sizeof(T)*sz);
     delete[] _start;
   }
   _start = tmp;
   _finish = _start + sz;
   _end_of_storage = _start + n;
}

7 insert

但是这样写其实是有问题的,造成了迭代器失效

在开辟了新空间里之后,_start和_finish都指向了新的空间,但是此时pos还指向的是原来的旧         空间,但是此时旧空间已经释放,所以会造成pos为野指针

修改:

iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start);
			assert(pos <= _finish);
			//
			if (_finish == _end_of_storage)
			{
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : capacity() * 2);
				pos = _start + len;
			}
			// Ų
			iterator end = _finish - 1;
			while (end >= pos)
			{
				*(end + 1) = *end;
				--end;
			}
			*pos = x;
			++_finish;
			return pos;
		}

修改点:用len记录pos到_start的距离,扩容完后,重新确定pos的位置

那为啥要返回pos呢:

当it传递给pos后,形参的改变不会影响实参

8 erase

iterator erase(iterator pos)
{
 assert(pos >= _start);
 assert(pos <= _finih);
 iterator it = pos + 1;
 while(it != _finissh)
 {
   *(it-1) = *it;
  ++it;
 }
 --finish;
 return pos;
}

erase和insert都会造成迭代器失效

vector 的 erase 函数删除元素时,会执行 “元素挪动” 操作

iterator it = pos + 1;
while (it != _finish) {
    *(it - 1) = *it;  // 后续元素向前挪动,覆盖被删除元素的位置
    ++it;
}
--_finish;  // 有效元素数量减少

这个过程会导致两类迭代器失效:

  • 被删除的 pos 迭代器本身失效pos 原本指向待删除的元素,但删除后,该位置被后续元素覆盖(或变成无效元素)。此时 pos 指向的内存虽然可能存在(未释放),但已不再是原来的元素,继续使用 pos 会访问错误的数据。

  • pos 之后的所有迭代器失效由于 pos 之后的元素都向前挪动了一个位置(例如,原 pos+1 位置的元素移动到 pos 位置,原 pos+2 移动到 pos+1 位置……),原本指向这些元素的迭代器(如 pos+1pos+2 等)现在指向的是 “原位置的下一个元素”,与预期不符,因此失效。

9 iterator补充

iterator begin()
		{
			return _start;
		}
iterator end()
		{
			return _finish;
		}
const_iterator begin() const
		{
			return _start;
		}
const_iterator end() const
		{
			return _finish;
		}

10 resize

调整容器有效数据个数

viod resize(size_t n, T val = T() )
{
  if(n < size() )//n小于当前元素个数
  {
    _finish = _start + n;//finish截断多于数据
  }
  else
  {
    reserve(n);
    while (_finish < _strt + n)
    {
       *_finish = val;
       ++_finish;
    }
}

这个时候就有uu有疑问了,这个参数中的 T val = T()是什么

这也是C++中对内置类型初始化的一种方式

例如:

//C98
	int j = int();
	int k = int(1);
// C++11
	int y = {};
	int t = {1};
	int z{ 2 };
	int m{};

不带括号的类型是将变量初始化为接近0的值(可以看为是0),具体内容我们到后面再讲解,现在先让大家认识一下

11 深拷贝和浅拷贝

拷贝构造只能用引用传参,不能用传值传参

深拷贝不止要拷贝对象,也要拷贝对象所指向的内容

1 传统写法

初始化列表,显示写了,用显示的值,没有显示写,看缺省值(在private中的定义直接给值);如果都没有,也要走初始化列表(自定义类型调默认构造,内置类型可能是随机值),初始化列表是一定要写的

没有显示的写,给缺省值的情况是这样写的;

public:
  vector()
   {}
//.......
private:
 _start = nullptr;
 _finish = nullptr;
 _end_of_storage = nullptr;

我们来看拷贝构造函数

	// 传统写法
		 v2(v1)
		vector(const vector& v)
		{
			reserve(v.capacity());
			for (const auto& e : v)
			{
				push_back(e);
			}
		}

这里的v是v1,this是v2

2 现代写法
	vector(const vector& v)
		{
			vector tmp(v.begin(), v.end());
			swap(tmp);
		}

不自己去开辟空间,通过他人开辟空间


四      memcpy拷贝时出现的问题 

在我们前面写的reserve接口中,如果函数模板T表示的时内置类型:Int,double之类的,不会出现问题了,但如果是自定义类型呢?例如string

这个时候就会出错,因为memcpy是浅拷贝,如果有像string中含有指向其他地方的成员变量时,就会出错。                                                                                                                                                                                                                                                                 

我们来看一下这个部分的详细解释:

那么我们如何修改呢?

(1)使用for循环

	void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size();
				T* tmp = new T[n];
				if (_start)
				{
					for (size_t i = 0; i < sz; i++)
					{
						tmp[i] = _start[i]; // 如果是string,调用string的赋值深拷贝
					}
					delete[] _start;
				}
				_start = tmp;
				_finish = _start + sz;
				_end_of_storage = _start + n;
			}
		}

如果此时时自定义对象,那么在for循环中,就会自动调用对应的赋值运算符重载

(2)swap

void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t sz = size();
				T* tmp = new T[n];
				if (_start)
				{
					for (size_t i = 0; i < sz; i++)
					{
						std::swap(tmp[i], _start[i]);  // 如果是string,调用string的交换,交换资源指向
                    }
					delete[] _start;
				}
				_start = tmp;
				_finish = _start + sz;
				_end_of_storage = _start + n;
			}
		}

// 如果是string,调用string的交换,交换资源指向


五 理解动态二维数组

以杨辉三角为例:

// 以杨慧三角的前n行为例:假设n为5
void test2vector(size_t n)
{
 // 使用vector定义二维数组vv,vv中的每个元素都是vector
 bit::vector> vv(n);
    // 将二维数组每一行中的vecotr中的元素全部设置为1
 for (size_t i = 0; i < n; ++i)
 vv[i].resize(i + 1, 1);
    // 给杨慧三角出第一列和对角线的所有元素赋值
 for (int i = 2; i < n; ++i)
 {
   for (int j = 1; j < i; ++j)
   {
     vv[i][j] = vv[i - 1][j] + vv[i - 1][j - 1];
   }
  }
}

bit::vector> vv(n); 构造一个vv动态二维数组,vv中总共有n个元素,每个元素 都是vector类型的,每行没有包含任何元素,如果n为5时如下所示:

vv中元素填充完成之后,如下图所示:

使用标准库中vector构建动态二维数组时与上图实际是一致的。

posted @ 2025-11-14 17:43  gccbuaa  阅读(18)  评论(0)    收藏  举报