前言
通过上一节的学习,我们知道了vector中可以存放各种类型的数据,这就意味着vector之中不仅仅可以存放int、char等内置类型,还可以存放vector和string等类型,我们结合底层的具体情况来具体分析
vector的复用(套娃)
我们首先需要知道,顺序表中含有三个核心成员:
1.指针:该指针指向具体的数组
2.size:表示该数组之中含有有效数据的个数
3.capacity:表示该数组的容量
我们先来假设顺序表的类型是int,那么此时指针的类型就是int*,那么此时数组中的每个数据的类型就是int:
同理,我们把int类型换成vector<int>类型,情况就会如下图所示:
此时指针的类型就变成了vector<int>*,那么此时数组中的每个数据的类型就是vector<int>了。而每个vector<int>类型的数据中都含有三个核心成员:指针、size、capacity,其中每个指针的数据类型都是int*
这样的情况就有点像二维数组了,假设我们需要开辟一个10*5的int类型的二维数组:
vector<int> v(5, 1);
vector<vector<int>> vv(10, v);
假设此时我们需要访问二维数组中的某个数据,就可以用如下的方式访问:
vv[2][1] = 2;
之所以可以用这样的形式去访问,是因为它的底层实现是这样的:
template<class t>
class vector
{
t& operator[](int i)
{
assert(i < _size);
return _a[i];
}
private:
t* _a;
size_t _size;
size_t _capacity;
};
vector的底层运用了模板,当我们写出上述代码的时候,编译器就会自动生成这些代码:
// vector<int>
class vector
{
int& operator[](int i)
{
assert(i < _size);
return _a[i];
}
private:
int* _a;
size_t _size;
size_t _capacity;
};
vector<vector<int>>
class vector
{
vector<int>& operator[](int i)
{
assert(i < _size);
return _a[i];
}
private:
vector<int>* _a;
size_t _size;
size_t _capacity;
};
所以我们就可以知道,下面的两句代码是等效的:
vv[2][1] = 2;
vv.operator[](2).operator[](1) = 2;
假设我们需要遍历这个二维数组就会很方便:
for (size_t i = 0; i < vv.size(); i++)
{
for (size_t j = 0; j < vv[i].size(); j++)
{
cout << vv[i][j] << " ";
}
cout << endl;
}
cout << endl;
介绍到这里我们就来看几个题目加深印象(leetcode第118题):
在做这个题目之前我们需要知道这个题目使用静态数组是无法解决问题的,原因有以下两点:
1.该数组是需要动态开辟的
2.该数组不是规整的,即每一行的数据个数都是不相同的
下面我们来讲解这个题目的解题步骤:
首先我们需要开辟numRows行的数据:
vector<vector<int>> vv(numRows);
开完行以后我们就需要去开列,这里需要注意:每一列的数据个数都是不相同的,我们先默认给每一列的数据初始化为0。再来观察一下杨辉三角的特征,我们可以知道每一行的第一个数据和最后一个数据都是1(或者resize的时候全部改成1):
for (size_t i = 0; i < numRows; i++)
{
vv[i].resize(i + 1, 0);
vv[i].front() = vv[i].back() = 1;
// == vv[i][0] = vv[i][vv[i].size() - 1] = 1;
}
随后我们要对其他位置上的数据进行处理,我们通过观察可以知道,某位的数据等于上一行同一列和上一列的数据之和:
for (size_t i = 2; 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];
}
}
到这里,代码就全部完成了,我们将代码整合一下并且提交:
class Solution
{
public:
vector<vector<int>> generate(int numRows)
{
vector<vector<int>> vv(numRows);
for (size_t i = 0; i < numRows; i++)
{
vv[i].resize(i + 1, 0);
vv[i].front() = vv[i].back() = 1;
// == vv[i][0] = vv[i][vv[i].size() - 1] = 1;
}
for (size_t i = 2; 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;
}
};
vector的模拟实现
下面我们来模拟实现一下vector
首先我们先创造一个命名空间来封装一下
在开始之前我们需要知道:
1.模板是不能分离到两个文件中的,因为如果分离到两个文件中去的话,就会出现链接错误(之前string分离了是因为没有写模板)
2.vector的源代码在底层实现的时候有三个核心成员:_start、_finish、_end_of_storage
那么了解到这里我们就可以先写出vector的基本框架:
namespace xiaobao
{
template<class T>
class vector
{
public:
typedef T* iterator;
private:
iterator _start = nullptr;
iterator _finish = nullptr;
iterator _end_of_storage = nullptr;
};
}
接下来我们就来逐步完成成员函数代码的编写:
size、capacity、begin、end等函数的实现比较简单,也不做过多的解释了:
size_t size()
{
return _finish - _start;
}
size_t capacity()
{
return _end_of_storage - _start;
}
我们再来实现reserve:
我们首先来判断要开辟的空间是否大于当前的空间,如果要开辟的空间小于当前空间,则不做任何处理,保持原空间不变;大于则继续扩容
我们首先要先创建一个变量tmp,用于保存新开辟的空间(不能直接赋值),接着用memcpy拷贝原来的数据,最后释放旧的空间并且指向新的空间:
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
memcpy(tmp, _start, size() * sizeof(T));
delete[] _start;
_start = tmp;
_finish = _start + size();
_end_of_storage = tmp + n;
}
}
我们继续实现push_back函数,首先我们需要判断剩余的空间是否足够,如果没有满就直接实现尾插,如果空间满了就开辟新空间:
void push_back(const T& x)
{
if (_finish != _end_of_storage)
{
reserve(capacity() == 0 ? 4 : capacity * 2);
}
*_finish = x;
++_finish;
}
接下来我们测试一下push_back能否正常使用:
为了方便完成调试,我们先来重载一下[]:
T& operator[](size_t i)
{
assert(i < size());
return _start[i];
}
void test_vector01()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
v.push_back(5);
for (size_t i = 0; i < v.size(); i++)
{
cout << v[i] << " ";
}
cout << endl;
}
通过测试我们发现使用push_back函数程序会崩溃,经过检查发现_finish是一个空指针,所以此时我们就需要先检查reserve的代码:
通过调试可以知道是这个代码出现了问题,在这里调用size函数会出现很大的问题,因为在最开始的时候三个变量都是旧空间的数据,经过 _start = tmp; 这一串代码,start变量已经指向了新空间,然而finish此时仍旧是旧空间
此时调用size就会有问题,因为finish指向的是扩容前的,start指向的是扩容后的,修改以后的reserve代码应该为:
void reserve(size_t n)
{
if (n > capacity())
{
T* tmp = new T[n];
memcpy(tmp, _start, size() * sizeof(T));
delete[] _start;
_finish = _start + size();
_start = tmp;
_end_of_storage = tmp + n;
}
}
或者可以这样写(标准库的写法):
void reserve(size_t n)
{
if (n > capacity())
{
size_t old_size = size();
T* tmp = new T[n];
memcpy(tmp, _start, size() * sizeof(T));
delete[] _start;
_start = tmp;
_finish = _start + old_size;
_end_of_storage = tmp + n;
}
}
**************************************************************************************************************
假设我们要测试下面函数的运行:
template<class T>
void print_vector(const vector<T>& v)
{
vector<T>::const_iterator it = v.begin();
auto it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
我们会很奇怪的发现:该函数报了一个很离谱的错误:
我们明明写了;,但是为什么提示没有写呢?
因为此时类模板是没有实例化的,::在类中取东西的时候可以识别是取类型,也可以识别是取静态成员变量,只有实例化以后才可以直接用::取东西
规定:没有实例化的类模板里面取东西,编译器不能够区分,此时要取的是类型就要加上typename,没有加typename就默认是静态成员变量
template<class T>
void print_vector(const vector<T>& v)
{
// 规定,没有实例化的类模板里面取东西,编译器不能区分这里const_iterator
// 是类型还是静态成员变量
typename vector<T>::const_iterator it = v.begin();
auto it = v.begin();
while (it != v.end())
{
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
**************************************************************************************************************
接着实现一下pop_back:
void pop_back()
{
assert(!empty());
--_finish;
}
因为vector的接口实现起来都不复杂,所以这后面就仅仅讲解一下稍微复杂一点的接口:
insert接口的实现:
假设我们要在某一个位置pos处插入一个数据,那么我们先要将pos之后的数据都整体向后挪动一位再插入数据我们根据逻辑可以写出代码如下:
iterator insert(iterator pos, const T& x)
{
// 扩容
if (_finish == _end_of_storage)
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
iterator end = _finish - 1;
while (end >= pos)
{
*(end + 1) = *end;
--end;
}
*pos = x;
++_finish;
return pos;
}
我们来测试一下:
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);
print_vector(v);
v.insert(v.begin() + 2, 30);
print_vector(v);
}
此时代码不需要扩容,没有任何问题,但是我们把测试代码改一下:
void test_vector2()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
print_vector(v);
v.insert(v.begin() + 2, 30);
print_vector(v);
}
此时代码需要扩容而且出现了问题
我们通过调试发现:扩容的代码并没有问题,而是挪动数据的过程出现了问题,数据挪动到pos没有停止,而是一直持续的向后挪动,这是为什么呢?
这个问题就叫做迭代器失效,我们画一个图来看一下:
因为在扩容后pos指针没有指向新空间,还是指向原来的空间,此时pos指针就类似于一个野指针,这样就会越界访问,一直持续的往后挪动数据。
要解决这个问题我们就要记录相对位置,确定pos改变以后的位置
iterator insert(iterator pos, const T& x)
{
// 扩容
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;
}
**************************************************************************************************************
假设我们要在vector中查找,但vector没有实现find接口,此时库中有一个find函数可以供我们使用:
find需要传两个迭代器表示开始和结束的区间、还有要查找的值,注意这里的迭代器区间是左闭右开,如果没有找到就返回last(右边是开区间,不包含last)
我们来测试一下:
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);
print_vector(v);
v.insert(v.begin() + 2, 30);
print_vector(v);
int x;
cin >> x;
auto p = find(v.begin(), v.end(), x);
if (p != v.end())
{
v.insert(p, 40);
(*p) *= 10;
}
print_vector(v);
}
我们发现这里出了错误,因为我们本来打算让2乘以10,但这里是40乘以10,这是为什么呢?
这里其实也是一种迭代器失效,迭代器insert以后pos就失效了,不要去访问,因为p指向的是旧空间。但是我们刚刚明明修正了,为什么还是不行呢?
因为形式参数的改变不会影响实际参数,所以还会失效。
并且这里还不能使用引用,因为这样很多语法就不会支持,例如:
v.insert(v.begin() + 2, 30);
这里修改以后返回的就是一个临时变量了,就不支持这样的语法了
所以这里我们需要注意以后insert了就不要直接访问,要访问就要更新位置:
p = v.insert(p, 40);
(*(p+1)) *= 10;
**************************************************************************************************************
结尾
因为这里牵扯到了迭代器失效,要详细讲解又得需要大块内容,所以本章就先到这里结束了,下一节我们接着学vector,希望可以给您带来帮助,谢谢您的浏览!!!