当前位置: 首页 > article >正文

【C++】vector的使用练习 + 模拟实现

  • 目录

    前言

    一、vector的介绍

    二、vector的使用

    三、vector的简单练习题

    四、模拟实现 vector

    1.基本框架

    2.功能实现

    3.完整代码

    总结



前言

        本文主要介绍C++的【STL】容器之一的 vector,从基本功能介绍,到常用接口使用演示,接着还有 5 道vector使用练习题加强接口使用熟练度,最后模拟实现 vector,加强对 vector 的理解乃至完全掌握。


一、vector的介绍

  • vector是C++【STL】中的一类容器,表示大小可以变化的数组。可以理解为顺序表一样的东西。
  • 与string类一样,vector也是一个类模版,第二个参数为空间配置器有默认参数我们先不管,第一个参数就是我们平时实例化时需要传递的元素类型。

比如

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	//基本类型
	vector<int> v1;
	vector<char> v2;
	vector<double> v3;

	//类类型
	vector<string> v4;
	vector<vector<int>> v5;//类似二维数组

	//...等

	return 0;
}


二、vector的使用

使用STL的三个境界:能用,明理,能扩展 ,那么下面学习vector,我们也是按照这个方法去学习

 1.vector 的构造

  • (1)是默认构造,不传参
  • (2)使用 n 个 val 进行初始化构造
  • (3)使用迭代器区间进行构造
  • (4)拷贝构造

除此之外,C++11中还有一种我们常用的构造:

  • 使用 initializer_list<value_type> 类的容器进行构造。
  • 是不是看着很陌生,简单演示一下你就知道这是什么了:

initializer_list<value_type> 构造演示:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v1 = { 1,2,3,4,5,6,7,8 };
	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

运行结果:


扩展: 

  • 使用大括号括起来的一串数字进行初始化 { 1,2,3,4,5,6,7,8 }
  • 这个大括号括起来的容器名就叫 initializer_list,它也是一个容器,不过该容器比较特殊,下面我们简单介绍一些该容器

initializer_list 介绍:

  • 首先它也是一个类模版,并且只需要传递一个类型的参数
  • 它不支持修改类的接口,它的接口非常简单,如下:
  • 除了基本的构造外,就只有迭代器的接口和 size 接口,因此该容器就我们目前看来,仅用作遍历和对其他容器进行赋值

我们也可以直接声明并使用该容器:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	initializer_list<int> l1 = { 1,2,3,4,5,6 };
	for (auto& e : l1)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

 运行结果:


2.vector的遍历

  • (1)首先,vector 重载了 [ ] 运算符

  • 普通版本和重载的const版本
  • 返回的是下标 n 对应元素的引用

因此可以使用 [ ] 进行遍历:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v1 = { 10,9,8,7,6,5 };
	for (size_t i = 0; i < v1.size(); i++)
	{
		cout << v1[i] << " ";
	}
	cout << endl;
}

运行结果:


  • (2)迭代器遍历

  • 关于迭代器的详细介绍在上一篇string中,因为STL很多设计都是互通的,因此不再赘述。
  • 以下演示正向的普通迭代器

演示:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v1 = { 1,2,3,4,5,6 };
	vector<int>::iterator it = v1.begin();//可以简写为: auto it = v1.begin()
	while (it != v1.end())
	{
		cout << *it << " ";
		++it;
	}
	cout << endl;

	return 0;
}

运行结果:


  • (3)范围for遍历

请记住,只要容器支持了迭代器,那么它就支持范围for,我们模拟实现的迭代器,它也会自动支持范围for,因为范围for的底层就是迭代器实现的。

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v1 = { 4,5,6,7,8,9,10 };
	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

运行结果:


3.常用接口介绍

有了 string 接口使用的基础,这一块学起来就很快

<1>.空间容量相关接口

容量空间接口说明
size获取数据个数
capacity获取容量大小
empty判断是否为空
resize改变vector的size
reserve改变vector的capacity
shrink_to_fit缩容使capcity尽量接近size大小
  • 扩容方面:capacity的代码在vs和g++下分别运行会发现,vs下capacity是按1.5倍增长的,g++是按 2 倍增长的。这个问题经常会考察,不要固化的认为,vector增容都是2倍,具体增长多少是根据具体的需求定义的。vs是PJ版本STL,g++是SGI版本STL。
  • reserve只负责开辟空间,如果确定知道需要用多少空间,reserve可以缓解vector增容的代 价缺陷问题。
  • resize在开空间的同时还会进行初始化,影响size。

测试扩容代码: (vs)

#include <iostream>
#include <vector>
using namespace std;

// 测试vector的默认扩容机制
void TestVectorExpand()
{
    size_t sz;
    vector<int> v;
    sz = v.capacity();
    cout << "making v grow:\n";
    for (int i = 0; i < 100; ++i)
    {
        v.push_back(i);
        if (sz != v.capacity())
        {
            sz = v.capacity();
            cout << "capacity changed: " << sz << '\n';
        }
    }
}

int main()
{
    TestVectorExpand();

    return 0;
}

运行结果:


(1)size ,capacity 和 empty

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v1;
	for (int i = 0; i < 50; i++)
	{
		v1.push_back(i);
	}
	cout << v1.empty() << endl;
	cout << "szie: " << v1.size() << endl;
	cout << "capacity: " << v1.capacity() << endl;

	return 0;
}

运行结果:


(2)resize

声明:

  • 两个版本,第二个版本的 val 是当 n > size 时,给新空间初始化的值

演示:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v1 = { 1,2,3,4 };
	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	//n < size,删除数据
	v1.resize(2);
	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	//n > size(大于capacity就会扩容)
	v1.resize(10, 8);
	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

运行结果:


(3)reserve、shrink_to_fit

  • 只能扩容,不能缩容,用于提前开空间避免多次扩容消耗效率
  • 用于缩容

演示:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v1 = { 1,2,3,4 };
	cout << "capacity: " << v1.capacity() << endl;

	v1.reserve(10);
	cout << "capacity: " << v1.capacity() << endl;

	v1.shrink_to_fit();
	cout << "capacity: " << v1.capacity() << endl;

	return 0;
}

运行结果:


<2>.vector的增删查改

vector增删查改接口说明
push_back尾插
pop_back尾删
find查找。(注意这个是算法模块(std)实现,不是vector的成员接口)
insert在position之前插入val
erase删除position位置的数据
swap交换两个vector的数据空间

(1)push_back、pop_back

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v1;
	v1.push_back(1);//尾插
	v1.push_back(2);//尾插
	v1.push_back(3);//尾插
	v1.push_back(4);//尾插
	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	v1.pop_back();//尾删
	v1.pop_back();//尾删
	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

运行结果:


(2)find

  • 注意该 find 是算法库中的 find,vector并没有像 string 一样实现 find 接口,string 是因为字符串查找很多时候并不是对单一字符的查找,所以 string 单独实现了 find 的接口用于满足多样化的需求。
  • 而对于 vector ,乃至后面的STL容器如 list、栈和队列等,都是使用统一的算法库中的find。
  • 算法库中的find,是在一段迭代器区间中寻找 val。成功返回指定的迭代器位置,失败则返回last,也就是end()返回的的位置

演示:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v1 = { 84,26,96,51,7 };
	auto it = find(v1.begin(), v1.end(), 96);
	if(it != v1.end())
    {
        cout << *it << endl;
    }

	return 0;
}

运行结果:


(3)insert、erase

  • 插入的位置都是以迭代器进行控制
  • (1)是在指定位置插入一个val
  • (2)是在指定位置插入 n 个 val
  • (3)是在指定位置插入一个迭代器区间

  • erase删除元素
  • 第一个是删除一个指定位置的元素,第二个是删除一个迭代器区间的数据

演示:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	//插入
	vector<int> v1 = { 1,2,3,4 };
	v1.insert(v1.end(), 5);
	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	v1.insert(v1.begin(), 3, 8);
	v1.insert(v1.end(), 5);
	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	//删除
	v1.erase(v1.begin());
	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	v1.erase(v1.begin() + 3, v1.end());
	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

运行结果:


(4)swap

  • 底层就是互换指针,效率高

  • 和 string 一样重载了全局的swap,这样可以直接 swap(v1,v2) 进行调用了,底层一样是指针交换,效率高。

演示:

#include <iostream>
#include <vector>
using namespace std;

int main()
{
	vector<int> v1 = { 1,2,3,4 };
	vector<int> v2 = { 99,88,77,44 };

	cout << "交换前v1: ";
	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
	cout << "交换前v2: ";
	for (auto& e : v2)
	{
		cout << e << " ";
	}
	cout << endl;

	swap(v1, v2);//交换

	cout << "交换后v1: ";
	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
	cout << "交换后v2: ";
	for (auto& e : v2)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

运行结果:


4.小结

以上接口能满足我们大部分的需求了,当然 vector 还有一部分接口,比如:

  • 关系运算符重载的接口:
  • 赋值运算符重载:
  • 等等,还有像 at、assign等,不常用,但需要时我们可直接查阅资料即可。


三、vector的简单练习题

1.杨辉三角:

链接:118. 杨辉三角 - 力扣(LeetCode)

思路:

  • 使用二维数组进行实现,也就是 vector<vector<int>> v,这样 v 的每个元素都是一个 vector<int> 对象,可以对每个元素进行长度控制。
  • 初始化时将所有元素初始化为1,接着从第三行开始计算,使用嵌套循环,无需计算开头和结尾处,这一点在循环上进行限制,计算公式 v[i][j] = v[i-1][j] + v[i-1][j-1];

题解:

class Solution {
public:
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> vv(numRows, vector<int>());

        //数据全初始化为1
        for(size_t i=0; i<vv.size(); ++i)
        {
            vv[i].resize(i+1, 1);
        }

        //遍历二维数组,计算数值
        for(size_t i=0; i<vv.size(); ++i)
        {
            for(size_t j=1; j<vv[i].size()-1; ++j)
            {
                vv[i][j] = vv[i-1][j]+vv[i-1][j-1];
            }
        }

        return vv;
    }
};


2.只出现一次的数字I

链接:136. 只出现一次的数字 - 力扣(LeetCode)

思路:

  • 这题比较简单,只是一道开胃菜,只要我们知道两个相同的数异或就会等于0,并且0异或任意数还是等于任意数即可。

题解:

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int ret = 0;
        for(auto e : nums)
        {
            ret = ret^e;
        }
        
        return ret;
    }
};


3.只出现一次的数字II

链接:137. 只出现一次的数字 II - 力扣(LeetCode)

思路1:

  • 先说一下我的思路,我是先将数据进行排序,然后每三个一组进行检查是否三个完全相等,只要遇到不相等的一组就进一步判断,确定返回值。
  • 说到排序,这里需要介绍算法库中的排序算法:sort
  • sort 是一个函数模版,模版参数就是迭代器,只不过这里是随机迭代器,关于这点后面文章会详细讲到。我们使用 sort 时只需要传递需要排序的迭代器区间就行。sort 的底层大概是快排可能还融合了其他的排序算法,总之效率肯定高。
  • 题目要求时间复杂度为O(n),空间复杂度为O(1),虽然排序消耗了一定性能,但是进过测试可以跑过

题解1:

class Solution {
public:
    int singleNumber(vector<int>& nums)
    {
        sort(nums.begin(), nums.end());//排序

        int index = 0;
        while (index < nums.size() - 1)
        {
            //每三个一判断
            if (nums[index] != nums[index + 1] || nums[index + 1] != nums[index + 2])
            {
                return nums[index] == nums[index + 1] ? nums[index + 2] : nums[index];
            }
            index += 3;//每次跳3个
        }

        //这里说明最后不足以凑成3个,那么就只有1个数据
        return nums[index];
    }
};

思路2:

  • 这个不太容易想到,它既满足了时间复杂度O(n)的要求,又满足了空间复杂度O(1)的要求。
  • 首先通过上一道题,我们联想到2进制层面,观察 3 5 3 3 这四个数的二进制位:
  • 我们观察得到,3 3 3的二进制中,只要出现1的一列,就有3个1,我们仔细想想也能发现,每3个相同的数的二进制,当二进制的某一位为1时,那么这一列就会出现3个1,也就是3的倍数,我们将所有的数以二进制进行对齐排列,我们会发现,除去只出现一次的那个数,剩下二进制出现1的一列,出现1的总和就是3的倍数。那么加上哪个只出现一次的数,当二进制的某一列的1的总数不能被3整除时,那么就说明该列多了一个1,那么接下来,我们就可以一步一步复刻出这个只出现一次的数。
  • 我们用total记录所有数二进制每一列1的总和,如果 total 能够被3整除,那么说明只出现一次的数在该位置是一定是0,否则,该位置是一定是1,我们将32个二进制位一位一位复原,就能复刻出只出现一次的那个数。最后返回该数即可。

题解:

class Solution {
public:
    int singleNumber(vector<int>& nums) {
        int ans = 0;
        for (int i = 0; i < 32; ++i) 
        {
            // 统计该每个数字第i个比特位为1的总数
            int total = 0;
            for (int num: nums) 
            {
                total += ((num >> i) & 1);
            }

            // 如果total能够被3整除,说明只出现一次的数字在该位置上一定是0
            // 否则在该位置上一定是1
            if (total % 3) 
            {
                ans |= (1 << i);
            }
        }
        return ans;
    }
};


4.只出现一次的数字III

链接:260. 只出现一次的数字 III - 力扣(LeetCode)

思路:

  • 首先,根据题目,元素的个数肯定是2的倍数,还是先排序,排序后,开始开始遍历,每次遍历两个,将两个数进行异或运算,通过结果是否为0判断两个数是否相同。也就是下图:
  • 当然,这只是第一种情况,还有第二种情况,数据已经排好序,如:1,2,2,3,这时候如果按照上面思路就会返回1,2,不符合题意,而这时候,我们就需要控制下标,将每次跳过两个数的起点下标+1,当然遇到两个数不相等时需要进一步判断具体哪个数是需要返回的,这时候我们就通过将第二个数与第三个数进行比较,如果第二个数与第三个数相等,就说明第一个数是需要返回的数,这时候将第一个数尾插到待返回的vector中,并且重新设置下标继续遍历,反之如果第二个数与第三个数不相等,也就是上图中的情况,直接返回这两个不相等的数。

题解:

class Solution {
public:
    vector<int> singleNumber(vector<int>& nums) {
        sort(nums.begin(), nums.end());//先排序
        vector<int> ret;//设置返回值
        size_t i = 0;//外置,后面有用
        for(; i<nums.size()-1; i+=2)//下标i要小于倒数第二个数,避免越界
        {
            int num = nums[i]^nums[i+1];//异或
            if(num!=0)
            {
                if(i+2<nums.size()-1 && nums[i+2]==nums[i+1])
                {
                    ret.push_back(nums[i]);
                    i=i+1;
                    continue;
                }
                ret = {nums[i],nums[i+1]};
                break;
            }
        }
        //i是最后一位下标说明最后一位数就是待返回数
        if(i == nums.size()-1)
        {
            ret.push_back(nums[i]);
        }
        
        return ret;
    }
};


5.删除有序数组中的重复项

链接:26. 删除有序数组中的重复项 - 力扣(LeetCode)

思路:

  • 最后以一道较简单的题结束练习吧。
  • 根据题意,数据已有序,需删除重复项,为了保证时间效率,我们先不在原数组中操作删除数据,我们先创建一个数组 v,将不重复项全部尾插到 v,最后计算 v 的大小 k,利用循环将原数组的前 k 个数改为数组 v 中的值。

 题解:

class Solution {
public:
    int removeDuplicates(vector<int>& nums) {
        vector<int> v;
        for(size_t i = 0; i < nums.size()-1; i++)
        {
            if(nums[i]!=nums[i+1])
            {
                v.push_back(nums[i]);
            }
        }

        //无论最后两个数是否相等,最后一个数必然需要插入
        v.push_back(nums[nums.size()-1]);

        int k = v.size();
        for(size_t i=0; i<k; i++)
        {
            nums[i]=v[i];
        }

        return k;
    }
};


四、模拟实现 vector

注意:因为实现顺序的不同,同一个接口在后面会有优化和改动,模拟的最终结果在最后,中间是实现的思路过程

1.基本框架

  • 我们前面实现 string 时,类的基本成员变量为:char*str、size_t size、size_t capacity。但是 vector 就不一样了。

我们可以查阅官方实现 vector 的源码:

源码中,vector 的成员变量为:也就是三个迭代器变量

iterator start;
iterator finish;
iterator end_of_storage;

继续查找迭代器定义:

本质其实就是对指针的封装重命名(指针在源码中是被封装成类的) :

typedef T value_type;
typedef value_type* pointer;
typedef value_type* iterator;

我们看一下源码中的构造(部分):

 vector() : start(0), finish(0), end_of_storage(0) {}
 vector(size_type n, const T& value) { fill_initialize(n, value); }

我们再继续查看 size() 和 capacity,看看它们是怎么返回元素个数和容量的:

size_type size() const { return size_type(end() - begin()); }
size_type capacity() const { return size_type(end_of_storage - begin()); }

我们发现:它们是通过指针相减来计算元素个数和容量大小的


更多的源码内容我们不再继续看了,接下来正式开始模拟实现基本框架:

基本框架模拟:

#pragma once
#include <iostream>
#include <assert.h>
using namespace std;

namespace txp
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;//迭代器
		typedef const T* const_iterator;//const迭代器
        
        //默认构造
        vector()
        {}
        
        //默认析构
        ~vector()
        {
	        if (_start)
	        {
	        	delete[] _start;
	           	_start = _finish = _endofstorage = nullptr;
        	}
        }

	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _endofstorage = nullptr;
	};
}
  • 对于 vector 的模拟,我们同原版一样采用模版类进行实现,为了与原版 vector 区分,因此我们需要使用命名空间 namespce 进行隔离。
  • 迭代器,我们就直接使用指针进行重命名。
  • 定义三个成员变量,_start 指向第一个元素地址,_finish 指向最后一个元素的下一个空间地址,_endofstorage 指向空间容量的末尾地址。
  • 默认构造就什么都不传,使用默认值初始化,析构就销毁堆空间。


2.功能实现

(1)迭代器、size() 、capacity() 和 [ ]运算符重载

//迭代器
iterator begin()
{
	return _start;
}

iterator end()
{
	return _finish;
}

const_iterator begin() const
{
	return _start;
}

const_iterator end() const
{
	return _finish;
}

//返回size
size_t size() const
{
	return _finish - _start;
}

//返回capacity
size_t capacity() const
{
	return _endofstorage - _start;
}

//重载[]
T& operator[](size_t i)
{
	assert(i < size());

	return _start[i];
}

//const版本
const T& operator[](size_t i) const
{
	assert(i < size());

	return _start[i];
}
  • 这几个实现起来简单明了,不过多解释

(2)reserve 和 push_back

//reserve
void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t oldSize = size();
		T* tmp = new T[n];
		if (_start)
		{
			memcpy(tmp, _start, sizeof(T) * oldSize);对于string一类来说,存在浅拷贝问题
		}
	
		delete[] _start;
		_start = tmp;
		_finish = _start + oldSize;
		_endofstorage = _start + n;
	}
}

//push_back
void push_back(const T& x)
{
	//原始写法
	if (_finish == _endofstorage)
	{
		reserve(capacity() == 0 ? 4 : capacity() * 2);
	}
	*_finish = x;
	++_finish;
}
  • reserve 只有一个参数 n ,并且只在 n>capacity 时才起作用进行扩容,扩容这里有第一个坑,就是 size() 大小,这里设置变量 oldSize 就是记录原空间大小,因为如果不记录原空间大小,当我们 delete[] _start 后,原空间销毁,此时 _finish 的更新就会出现问题,因为原空间消失,而 size() 又需要根据 _finish 地址来确定,因此无法更新 _finish,所以设置参数 oldSize 用来帮助 _finish 进行更新。而空间内容拷贝使用函数 memcpy 就是第二个大坑,这里是关于深拷贝的问题,我们待会儿再解释和解决这个问题
  • push_back,在我们没有实现 insert 之前,先采用原始写法,判断是否需要扩容和尾插元素。
  • push_back 扩容策略是第一次开4个空间,后面2倍扩容

解释第二个大坑,深拷贝问题:

如果我们使用 memcpy 进行空间拷贝,在以下情况就会报错:

#include "vector.h"

int main()
{
	txp::vector<string> v1;
	v1.push_back("11111111");
	v1.push_back("11111111");
	v1.push_back("11111111");
	v1.push_back("11111111");
	v1.push_back("11111111");

	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

运行结果:

原因解释:

  • 首先,当我们尾插到第五个数据时,就需要扩容,问题就是 memcpy 是按照字节进行复制的,而 vector 的底层是三个指针,这时 memcpy 就只会将地址复制过来,而指针指向的空间的内容就没有复制,这就是浅拷贝的问题。
  • 示意图:

解决方法:

//reserve
void reserve(size_t n)
{
	if (n > capacity())
	{
		size_t oldSize = size();
		T* tmp = new T[n];
		if (_start)
		{
			for (size_t i = 0; i < oldSize; i++)
			{
				tmp[i] = _start[i];
			}
		}
	
		delete[] _start;
		_start = tmp;
		_finish = _start + oldSize;
		_endofstorage = _start + n;
	}
}
  • 使用 for 循环进行拷贝,这样的好处就是对于类类型,它门会调用对应的赋值运算符重载函数,不会导致浅拷贝。


(3)insert

iterator insert(iterator pos, const T& x)
{
	assert(pos >= _start && pos <= _finish);

    //扩容
	if (_finish == _endofstorage)
	{
		size_t len = pos - _start;
		reserve(capacity() == 0 ? 4 : capacity() * 2);
		pos = _start + len;
	}

    //移动数据,从后往前挪
	iterator i = _finish - 1;
	while (i >= pos)
	{
		*(i + 1) = *i;
		--i;
	}
	*pos = x;
	++_finish;

	return pos;
}

迭代器失效问题: 

  • 首先说明这是修正后的版本,这里需要解释的就是迭代器失效问题。这里有两个迭代器失效的问题
  • insert 插入数据,很多时候时候就需要扩容,扩容我们调用 reserve,reserve 会更新 _start,_finish,_endofstorage,但是参数中的 pos 就没有更新,pos 指向空间被销毁了,这就是迭代器失效问题。解决方法就是上面的,先计算原 pos 与 _start 直接的长度扩容后更新 pos。其实前面 reserve 中更新 _finish 也是迭代器失效的问题。
  • 第二个迭代器失效的问题就是返回值了,你可能会好奇为什么返回值是一个迭代器,而且返回的是 pos 的位置。观察仔细的同学前面就可能已经发现官方的 insert 也是有返回值的。

第二个迭代器失效的场景:

void test2()
{
	std::vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);

	int x;
	cin >> x;
	auto it = find(v1.begin(), v1.end(), x);
	if (it != v1.end())
	{
        //更新it
		it = v1.insert(it, x * 10);
		cout << *it << endl;
	}
}
  • 如果我们不更新 it 你猜会发生什么,it 指向的原空间会因为 insert 中的扩容而销毁。
  • 当我们解引用 it 的时候程序就会崩溃。
  • 所以解决方法就是设置返回值。

push_back 优化:

//push_back
void push_back(const T& x)
{
	//复用写法
	insert(_finish, x);
}
  • 复用写法,减少代码量。


(4)补充构造函数

//initializer_list<value_type> il构造
vector(initializer_list<T> il)
{
	reserve(il.size());//提前开空间提升效率
	for (auto& e : il)
	{
		push_back(e);
	}
}

//拷贝构造
vector(const vector<T>& v)
{
	reserve(v.capacity());//提前扩容,提升效率
	for (auto& e : v)
	{
		push_back(e);//push_back就包括了开空间和扩容
	}
}

//迭代器构造
//类模版的成员函数,也可以写成模版函数,支持范围更广
template <class InputIterator>
vector(InputIterator first, InputIterator last)
{
	reserve(last - first);
	while (first != last)
	{
		push_back(*first);
		++first;
	}
}

//n个val初始化构造
vector(size_t n, const T& val = T())
{
	reserve(n);
	for (size_t i = 0; i < n; i++)
	{
		push_back(val);
	}
}
//避免int调用到迭代器的模版函数,重载一个构造
vector(int n, const T& val = T())
{
	reserve(n);
	for (size_t i = 0; i < n; i++)
	{
		push_back(val);
	}
}
  • 支持initializer_list<value_type> 构造后,就可以在初始化时使用大括号连续初始化了,提前开空间提升效率,push_back进行拷贝赋值。
  • 拷贝构造同理,使用 push_back 即可,push_back 就包含了开空间扩容等操作。
  • 迭代器构造,迭代器构造为什么要写成模版函数,因为可以支持其他容器的迭代器区间进行初始化,下面可以演示一下:
#include "vector.h"

int main()
{
	string s1 = "abcdef";
	txp::vector<int> v1(s1.begin(), s1.end());
	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	return 0;
}

运行结果:

  • 接着我们看第四个构造,使用 n 个 val 进行初始化构造,第二个参数可以调用 T() 当缺省参数,T() 可以是所有类的匿名对象,包括内置类型也可以这样使用,因为 c++ 中哪怕内置类型也有构造函数。因为 c++ 有模版,所以相当于将c语言中的内置类型全升级成了类。
  • 这里有个疑惑,为什么要写两个差不多的这样的构造?
  • 因为如果我们没有写重载的 n 个 val 的构造,那么当我们声明:vector<int> v(6,1); 时,因为 size_t 和 int 是有区别的,我们知道有函数模版存在时,会调用类型最接近的函数,那么因为 size_t 与 int 的区别,所以会调用到上面的迭代器模版函数,这时候迭代器就会初始化为 int,当 *int 时就会报错。这种情况只有 int 有,其他如 double 等就不会调用到迭代器模版,因为 vector<int> v(6,1) 中,6和1都是 int 类型,1换成double类时,迭代器就不能匹配两个参数了,因此会调用到 n 个 val 的构造,此时 int 会强转为 size_t。
  • 解决方法就是重载一个 int 类型的 n 个 val 构造。


(5)swap、= 重载、empty和pop_back

//swap交换
void swap(vector<T>& v)
{
	std::swap(_start, v._start);
	std::swap(_finish, v._finish);
	std::swap(_endofstorage, v._endofstorage);
}

//赋值运算符重载
vector<T>& operator=(vector<T> v)
{
	swap(v);
	return *this;
}

//判空
bool empty() const
{
	return _start == _finish;
}

//尾删
void pop_back()
{
	assert(!empty());
	--_finish;
}
  • swap 交换其实就是交换底层迭代器地址就行。
  • 赋值运算符重载这里选择的是代码量最小的现代写法,首先参数 v 没有写成引用,这样传参时会调用拷贝构造,这样 v 就复制了一份vector对象,而 v 正好是当前对象需要的,因此调用 swap 交换两者,当前对象就完成了赋值,并且 v 因为是形参,出了函数就会自动调用析构销毁,所以正好将当前对象的原空间销毁了。一举两得,最后返回当前对象即可。
  • empty 判空只需要检查地址,pop_back 尾删只需要移动_finish指针即可。


(6)erase 和 resize

//erase,第二种迭代器失效的地方
iterator erase(iterator pos)
{
	assert(pos >= _start && pos < _finish);

	iterator i = pos + 1;
	while (i < _finish)
	{
		*(i - 1) = *i;
		++i;
	}
	--_finish;

	return pos;
}

首先说明 erase:

  • 挪动数据示意图:
  • 从后向前挪覆盖 pos 位置数据,最后 -- _finish 删除最后一个元素

这里要说的是第二种迭代器失效:

这里也是为什么 erase 有返回值的原因:

  • 返回值为 pos ,也就是被删除数据的原位置。
  • 在下面场景中,如果没有返回值更新,程序就会崩溃:
void test3()
{
	txp::vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);

	int x;
	cin >> x;
	auto it = find(v1.begin(), v1.end(), x);
	if (it != v1.end())
	{
		it = v1.erase(it);
		cout << *it << endl;//第二种迭代器失效
	}
}
  • 虽然,我们这里实现的 vector 即使不更新 it 也不会有问题,因为不涉及扩容等造成原指针指向的空间销毁问题。但是 vs 官方的 vector 这里就会报错,它不允许你使用失效的迭代器,迭代器失效不一定是指原空间被销毁,也可能是指对象发生变化。
  • 如果不使用返回值更新 it 的话,即使不报错,在一些场景下也会有逻辑错误问题,比如以下场景:

场景:删除数组中所有的偶数 

#include "vector.h"

void test1()
{
	txp::vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	v1.push_back(2);

	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	auto it = v1.begin();
	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			v1.erase(it);//假如不更新it
			++it;
		}
		++it;
	}

	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
}


int main()
{
	test1();

	return 0;
}

运行结果:

  • 很明显,存在逻辑错误没有删除所有的偶数

正确的写法:

#include "vector.h"

void test1()
{
	txp::vector<int> v1;
	v1.push_back(1);
	v1.push_back(2);
	v1.push_back(2);
	v1.push_back(3);
	v1.push_back(4);
	v1.push_back(5);
	v1.push_back(2);

	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;

	auto it = v1.begin();
	while (it != v1.end())
	{
		if (*it % 2 == 0)
		{
			it = v1.erase(it);
		}
		else
		{
			++it;
		}
	}

	for (auto& e : v1)
	{
		cout << e << " ";
	}
	cout << endl;
}

int main()
{
	test1();

	return 0;
}

运行结果:


resize:

//resize
void resize(size_t n, T val = T())
{
	if (n < size())
	{
		//删除数据
		_finish = _start + n;
	}
	else
	{
		//无论是否需要扩容,直接调用reserve检查
		reserve(n);

		//新空间初始化
		while (_finish < _start + n)
		{
			*_finish = val;
			++_finish;
		}
	}
}
  •  resize 有删除元素又有扩容的功能,因此通过 if 判断 n 是否大于size来分为两种情况,而参数 val 则是当增大size或者扩容时,给新空间初始化的值,当然可以不传,因此有缺省参数,缺省参数依旧使用 T() 匿名对象来赋值。
  • 当 n < size 时,需要删除元素,此时只需要修改 _finish 指向的位置即可,当 n >= size 时就需要判断是否扩容了,直接调用 reserve 即可,然后就是根据 val 为新空间赋值。


3.完整代码

vector 更多的接口没有一一实现,主要实现常用接口帮助我们更加了解这些接口,使用起来更加顺手。

#pragma once
#include <iostream>
#include <assert.h>
using namespace std;

namespace txp
{
	template<class T>
	class vector
	{
	public:
		typedef T* iterator;//迭代器
		typedef const T* const_iterator;//const迭代器

		//默认构造
		vector()
		{}

		//initializer_list<value_type> il构造
		vector(initializer_list<T> il)
		{
			reserve(il.size());//提前开空间提升效率
			for (auto& e : il)
			{
				push_back(e);
			}
		}

		//拷贝构造
		vector(const vector<T>& v)
		{
			reserve(v.capacity());//提前扩容,提升效率
			for (auto& e : v)
			{
				push_back(e);//push_back就包括了开空间和扩容
			}
		}

		//迭代器构造
		//类模版的成员函数,也可以写成模版函数,支持范围更广
		template <class InputIterator>
		vector(InputIterator first, InputIterator last)
		{
			reserve(last - first);
			while (first != last)
			{
				push_back(*first);
				++first;
			}
		}

		//n个val初始化构造
		vector(size_t n, const T& val = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; i++)
			{
				push_back(val);
			}
		}
		//避免int调用到迭代器的模版函数,重载一个构造
		vector(int n, const T& val = T())
		{
			reserve(n);
			for (size_t i = 0; i < n; i++)
			{
				push_back(val);
			}
		}

		//默认析构
		~vector()
		{
			if (_start)
			{
				delete[] _start;
				_start = _finish = _endofstorage = nullptr;
			}
		}

		//swap交换
		void swap(vector<T>& v)
		{
			std::swap(_start, v._start);
			std::swap(_finish, v._finish);
			std::swap(_endofstorage, v._endofstorage);
		}

		//赋值运算符重载
		vector<T>& operator=(vector<T> v)
		{
			swap(v);
			return *this;
		}

		iterator begin()
		{
			return _start;
		}

		iterator end()
		{
			return _finish;
		}

		const_iterator begin() const
		{
			return _start;
		}

		const_iterator end() const
		{
			return _finish;
		}

		//返回size
		size_t size() const
		{
			return _finish - _start;
		}

		//返回capacity
		size_t capacity() const
		{
			return _endofstorage - _start;
		}

		//重载[]
		T& operator[](size_t i)
		{
			assert(i < size());

			return _start[i];
		}
		//const版本
		const T& operator[](size_t i) const
		{
			assert(i < size());

			return _start[i];
		}

		//reserve
		void reserve(size_t n)
		{
			if (n > capacity())
			{
				size_t oldSize = size();
				T* tmp = new T[n];
				if (_start)
				{
					for (size_t i = 0; i < oldSize; i++)
					{
						tmp[i] = _start[i];
					}
				}
	
				delete[] _start;
				_start = tmp;
				_finish = _start + oldSize;
				_endofstorage = _start + n;
			}
		}

		//insert,解决迭代器失效问题:1.重新赋值pos,2.返回值
		iterator insert(iterator pos, const T& x)
		{
			assert(pos >= _start && pos <= _finish);

			if (_finish == _endofstorage)
			{
				size_t len = pos - _start;
				reserve(capacity() == 0 ? 4 : capacity() * 2);
				pos = _start + len;
			}

			iterator i = _finish - 1;
			while (i >= pos)
			{
				*(i + 1) = *i;
				--i;
			}
			*pos = x;
			++_finish;

			return pos;
		}

		//push_back
		void push_back(const T& x)
		{
			//复用写法
			insert(_finish, x);
		}

		//判空
		bool empty() const
		{
			return _start == _finish;
		}

		//尾删
		void pop_back()
		{
			assert(!empty());
			--_finish;
		}

		//erase,第二种迭代器失效的地方
		iterator erase(iterator pos)
		{
			assert(pos >= _start && pos < _finish);

			iterator i = pos + 1;
			while (i < _finish)
			{
				*(i - 1) = *i;
				++i;
			}
			--_finish;

			return pos;
		}

		//resize
		void resize(size_t n, T val = T())
		{
			if (n < size())
			{
				//删除数据
				_finish = _start + n;
			}
			else
			{
				//无论是否需要扩容,直接调用reserve检查
				reserve(n);

				//新空间初始化
				while (_finish < _start + n)
				{
					*_finish = val;
					++_finish;
				}
			}
		}

	private:
		iterator _start = nullptr;
		iterator _finish = nullptr;
		iterator _endofstorage = nullptr;
	};
}


总结

        以上就是本文的全部内容了,感谢你的支持。


http://www.kler.cn/a/549104.html

相关文章:

  • 解决前后端日期传输因时区差异导致日期少一天的问题
  • 当时只道是寻常
  • vue3.x 的provide 与 inject详细解读
  • golang基础库
  • Python 中的一种调试工具 assert
  • 聚簇索引和非聚簇索引
  • 排序算法衍生问题
  • 基于大数据的动漫网站数据分析系统的设计与实现
  • 建筑兔零基础自学python记录22|实战人脸识别项目——视频人脸识别(下)11
  • du-磁盘占用管理
  • day52 第十一章:图论part03
  • 深入理解 Vue3 中 ref 与 reactive 的区别及应用
  • 基于css实现正六边形的三种方案
  • macOS部署DeepSeek-r1
  • 2025年-数据库排名
  • 基于腾讯云TI-ONE 训练平台快速部署和体验 DeepSeek 系列模型
  • Jasper AI技术浅析(二):语言模型
  • 第32周:文献阅读
  • 2025最新出炉--前端面试题十一
  • 字符串-KMP算法详解-图文教程