C++第六讲:STL--vector的使用及模拟实现

爆炒脑仁 2024-10-27 08:35:13 阅读 75

C++第六讲:STL--vector的使用及模拟实现

1.vector简介2.vector的常见接口介绍2.1constructor -- 构造2.2destructor -- 析构2.3begin、end2.3.1vector和string的区别、vector<string>

2.4rbegin、rend2.5cbegin、cend2.6crbegin、crend2.7size、max_size、resize、capacity、empty、reserve2.8shrink_to_fit2.9operator[]、at2.10front、back2.11data2.12assign2.13push_back、pop_back2.14insert、erase2.15swap、clear2.16vector扩容规则

3.vector在OJ题中的使用3.1练习13.2练习2

4.vector的模拟实现4.1vector标准库中的源码观察4.2vector的实现4.3vector实现注意事项4.3.1注意事项14.3.2注意事项2 -- 迭代器失效问题4.3.2.1扩容引起的迭代器失效4.3.2.1.1情况14.3.2.1.2情况2

4.3.2.2删除数据,数据位置移动导致的迭代器失效

4.3.3注意事项3 -- 类模板中的函数模板问题4.3.4注意事项4 -- sting中深浅拷贝问题

1.vector简介

vertor是一个真正的容器,底层实现是一个动态开辟的数组:

在这里插入图片描述

所以说我们可以将vector简单地理解成:

在这里插入图片描述

vector的一些接口的使用和string相同,所以对于那些简单的使用,就一笔带过,本文详细要讲的还是vertor的模拟实现,以及一些其它的科普知识

2.vector的常见接口介绍

尽管不会详细讲述接口的作用,但是还是会讲接口一一列出来

2.1constructor – 构造

构造的使用:

<code>int main()

{ -- -->

vector<int> first;//使用vector模板实例化一个对象

vector<int> second(4, 100);//4个数据,每个数据为100

vector<int> third(second.begin(), second.end());//迭代器区间初始化

vector<int> forth(second);//拷贝初始化

return 0;

}

这些都是一些简单的构造,要着重处理的是:

在这里插入图片描述

使用initializer_list进行初始化:

<code>int main()

{ -- -->

vector<int> v = { 10, 20, 30, 40 };//使用数组进行初始化

vector<int> v1({ 20, 30, 40 });//和上面的功能是一样的

//使用数组初始化的底层其实是遍历数组,将数组中的元素一个一个地尾插到vector中

//auto il1 = { 1, 2, 3, 4, 5, 6 };

//initializer_list<int> il2 = { 1, 2, 3, 4, 5, 6 };//等价于上面的auto

//int a[] = { 1, 2, 3, 4, 5, 6 };

return 0;

}

2.2destructor – 析构

2.3begin、end

迭代器,使用起来也十分简单:

int main()

{

vector<int> v({ 1, 2, 3, 4, 5, 6, 7, 8, 9 });

//这表明迭代器的范围应该在vector<int>中,这样就可以更加清楚地理解迭代器了

vector<int>::iterator it = v.begin();

while (it != v.end())

{

cout << *it << " ";

it++;

}

cout << endl;//1 2 3 4 5 6 7 8 9

return 0;

}

既然有了迭代器,那么就可以使用范围for:

int main()

{

vector<int> v({ 1, 2, 3, 4, 5, 6, 7, 8, 9 });

for (auto e : v)

{

cout << e << " ";

}

cout << endl;//1 2 3 4 5 6 7 8 9

return 0;

}

2.3.1vector和string的区别、vector

下面还有一种使用范围for时需要注意的地方:

int main()

{

//看下面两个有什么区别:

vector<char> v;

string s1;

//1.vector中没有关于字符串的概念,只有尾插和尾删,所以并不能直接插入一个字符串,而string中能

//2.vector中并没有对于字符串特有的操作,比如连接、查找子串、替换子串等

//3.vector中不仅能存储字符数据,还能够针对于其它任何类型的数据,而string是专门处理字符的类

return 0;

}

int main()

{

//但是我们可以这样写:

vector<string> v;

//这样就可以使用针对字符串的操作了:

string s1 = "张三";

v.push_back(s1);

v.push_back("李四");

for (auto e : v)

{

cout << e << " ";

}

cout << endl;

v[0] += 'x';//这里使用的是string中的+=,因为v[0]就是一个string类的对象

v[0] += "apple";

for (auto e : v)

{

cout << e << " ";

}

cout << endl;

//v[0][0]++;//张 -》 峙

//C++中,对于汉字的编码,是通过汉字的后一个字节来将相同读音的汉字排在一起的

v[0][1]++;//张 -》 掌

v[0][1]++;//掌 -》 涨

for (auto e : v)

{

cout << e << " ";

}

cout << endl;

return 0;

}

但是这里会出现问题:

在这里插入图片描述

2.4rbegin、rend

反向迭代器

2.5cbegin、cend

const_iterator

2.6crbegin、crend

const_reverse_iterator

2.7size、max_size、resize、capacity、empty、reserve

2.8shrink_to_fit

这个接口是将capacity改变成适合它的size的函数,但是不建议使用,首先,它只是一个请求,并不是强制的,其次,如果实现决定减少容量,可能会造成内存的重新分配,导致所有的迭代器会失效,迭代器的失效在后边会详细阐述

2.9operator[]、at

访问,[]访问越界时会断言报错,at是抛异常

2.10front、back

放回的是头、尾元素的引用

2.11data

vector的实现底层是一个数组,而data函数的作用就是将底层的数组返回

在这里插入图片描述

2.12assign

将新内容赋给vector,覆盖原来的旧内容

2.13push_back、pop_back

尾插、尾删元素

2.14insert、erase

不同的是,vector中的insert和erase函数传入的位置只能是迭代器:

<code>int main()

{ -- -->

vector<int> v = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };

v.insert(v.begin(), 20);

v.insert(v.begin()+2, 10);//20 1 10 2 3 4 5 6 7 8 9

for (auto e : v)

{

cout << e << " ";

}

cout << endl;

v.erase(v.begin() + 2);//20 1 2 3 4 5 6 7 8 9

for (auto e : v)

{

cout << e << " ";

}

cout << endl;

return 0;

}

2.15swap、clear

clear函数不改变capacity

2.16vector扩容规则

结论:vector中的扩容是按照1.5倍来进行扩容的

//测试代码:

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';

}

}

}

在这里插入图片描述

3.vector在OJ题中的使用

3.1练习1

链接: 只出现一次的数字

这道题实现十分简单,只需要异或即可,重点是观察vector的使用:

在这里插入图片描述

<code>class Solution { -- -->

public:

int singleNumber(vector<int>& nums) {

int sum = 0;

for(auto e : nums)

{

sum ^= e;

}

return sum;

}

};

3.2练习2

链接: 杨辉三角OJ

这一题就相当不一样了,我们通过画图来解释一下:

在这里插入图片描述

所以这里我们就看出了C++和C语言的不同,下面我们用代码来实现一下杨辉三角,实现起来也是比较轻松的:

<code>class Solution { -- -->

public:

vector<vector<int>> generate(int numRows) {

vector<vector<int>> vv(numRows);

for(int i = 0; i<numRows; i++)

{

vv[i].resize(i+1, 1);//使用resize能够在开辟空间的同时进行初始化,所以说更好用

}

for(int i = 2; i<numRows; i++)

{

for(int j = 1; j<vv[i].size()-1; j++)

{

vv[i][j] = vv[i-1][j] + vv[i-1][j-1];

}

}

return vv;

}

};

4.vector的模拟实现

4.1vector标准库中的源码观察

以后我们对于文档的查看会相当频繁,也会十分重要,所以我们现在看一下原码:

源码可能有几百行,也有可能会有几千行,所以当我们看源码时,不能够逐字逐句地去看,这样效率很低,我们首先要做的就是观察这些源码的大致框架,这里我们只看一个stl函数:push_back,所以我们只拿这一个函数举例:

在这里插入图片描述

通过上述分析,我们可以了解该函数的使用了,但是也体会到了源文件的恐怖!了解了上述的源文件之后,我们也可以看着尝试写出自己的vector:

4.2vector的实现

因为对于vector实现时的注意事项在下边会一一点出来,所以这里直接放代码:

<code>//因为模板函数的声明和定义分离可能会造成一些问题,所以我们只使用两个文件:.h和test.cpp

namespace Mine

{ -- -->

template<class T>

class vector

{

public:

typedefT* iterator;

typedefconst T* const_iterator;

//无参构造函数

vector()

:_start(nullptr)

, _finish(nullptr)

, _endofstorage(nullptr)

{ }

//析构函数

~vector()

{

if (_start)

{

delete[] _start;

}

_start = _finish = _endofstorage = nullptr;

}

//size、capacity的返回

size_t size() const

{

return _finish - _start;

}

size_t capacity() const

{

return _endofstorage - _start;

}

尾插

//void push_back(const T& x)

//{

////既然要插入,那么就要检查空间的大小是否充足

//if (_finish == _endofstorage)

//{

//reserve(capacity() == 0 ? 4 : capacity() * 2);

//}

//*_finish = x;

//_finish++;

//}

//reserve -- 空间开辟

void reserve(size_t n)

{

//reserve不一定只在push_back函数中使用,还可能会直接使用

//所以这里要比较n和空间的大小

if (n > capacity())

{

//为了改变tmp的指向,我们需要知道原来start和tmp差的距离为多少

size_t oldSize = size();

//开辟空间的逻辑为:先创建一块空间,再将原来空间的内容复制过来

T* tmp = new T[n];

if (_start)

{

//memcpy(tmp, _start, sizeof(T) * size());

for (int i = 0; i < oldSize; i++)

{

tmp[i] = _start[i];

}

delete[] _start;

}

_start = tmp;

_finish = _start + oldSize;

_endofstorage = _start + n;

}

}

//访问操作符

T& operator[](size_t pos)

{

assert(pos < size());

return _start[pos];

}

//迭代器实现

iterator begin()

{

return _start;

}

iterator end()

{

return _finish;

}

const_iterator begin() const

{

return _start;

}

const_iterator end() const

{

return _finish;

}

//判空

bool empty()

{

return _endofstorage == _finish;

}

//删除最后一个元素

void pop_back()

{

assert(!empty);

_finish--;

}

//尾插

void push_back(const T& x)

{

既然要插入,那么就要检查空间的大小是否充足

//if (_finish == _endofstorage)

//{

//reserve(capacity() == 0 ? 4 : capacity() * 2);

//}

//*_finish = x;

//_finish++;

insert(_finish, x);

}

//插入函数

iterator insert(iterator pos, const T& x)

{

assert(pos >= _start && pos <= _finish);

//对于插入数据,仍然需要判断空间的大小

if (_finish == _endofstorage)

{

//和上面的处理相似,需要先知道pos位置到开始位置的距离,然后再修正pos的位置即可

int len = pos - _start;

reserve(capacity() == 0 ? 4 : capacity() * 2);

pos = _start + len;

}

//将pos之后的数据向后移动一位

iterator i = _finish -1;

while (i >= pos)

{

*(i + 1) = *i;

i--;

}

*pos = x;

_finish++;

return pos;

}

//erase -- 删除函数的实现

iterator erase(iterator pos)

{

assert(pos >= _start);

assert(pos < _finish);

//要删除pos位置的数据,我们将pos位置之后的数据向前移动就行了

iterator it = pos + 1;

while (it < _finish)

{

*(it - 1) = *it;

it++;

}

_finish--;

return pos;

}

//resize -- 重新分配空间

void resize(T& n, T& val = T())//因为传入的不一定是什么类型,所以T()就表示调用T类型对象的构造函数

//默认构造其实是没有默认构造的,而有了模板之后,C++对默认构造进行了升级,那么就可以使用默认构造了

{

if (n <= size())

{

//当要缩容时,直接改变_finish的指向即可

_finish = _start + n;

}

else

{

//需要扩容时,扩容,而且将扩容的空间进行初始化

reserve(n);

while (_finish < _start + n)

{

*_finish = val;

_finish++;

}

}

}

//拷贝构造函数

vector(const vector<T>& v)

{

reserve(v.capacity());

//这里我们可以按照传统的写法:开空间,拷贝,也可以这样:

for (auto& e : v)

{

push_back(e);

}

}

//交换函数

void swap(vector<T>& v)

{

swap(_start, v._start);

swap(_finish, v._finish);

swap(_endofstorage, v._endofstorage);

}

//赋值运算符重载

vector<T>& operator= (vector<T> v)

{

swap(*this, v);

return *this;

}

//迭代区间构造

template <class InputIterator>

//写成函数模板的优点为:扩大了传入参数的范围,可以是int形,甚至可以是string形

vector(InputIterator first, InputIterator last)

{

while (first != last)

{

push_back(*first);

first++;

}

}

//n个val进行的构造

vector(size_t n, const T& val = T())

{

//先写reserve是为了避免一直开辟空间的问题

reserve(n);

for (size_t i = 0; i < n; i++)

{

push_back(val);

}

}

vector(int n, const T& val = T())

{

reserve(n);

for (int i = 0; i < n; i++)

{

push_back(val);

}

}

//使用数组进行初始化

vector(initializer_list<T> il)

{

reserve(il.size());

for (auto& e : il)

{

push_back(e);

}

}

private:

iterator _start;

iterator _finish;

iterator _endofstorage;

};

}

4.3vector实现注意事项

4.3.1注意事项1

在这里插入图片描述

4.3.2注意事项2 – 迭代器失效问题

4.3.2.1扩容引起的迭代器失效

这次引起的迭代器失效本质是野指针问题,但是迭代器并不就是指针,VS所实现的迭代器就不仅仅是一个指针,而是一个复杂的类,其中除了对于指针的封装,而且还增加了对于迭代器的一些检查,我们可以通过指令来看到:

在这里插入图片描述

4.3.2.1.1情况1

在这里插入图片描述

4.3.2.1.2情况2

在vector类和string类的标准库实现中,并没有find接口的实现,但是,在标准库中,有find函数,所以我们可以直接使用标准库中的find函数

在这里插入图片描述

4.3.2.2删除数据,数据位置移动导致的迭代器失效

在这里插入图片描述

4.3.3注意事项3 – 类模板中的函数模板问题

模板类的成员函数,也可以时函数模板:

<code>//迭代区间构造

template <class InputIterator>

//写成函数模板的优点为:扩大了传入参数的范围,可以是int形,甚至可以是string形

vector(InputIterator first, InputIterator last)

{ -- -->

while (first != last)

{

push_back(*first);

first++;

}

}

//我们可以这样进行使用:

int main()

{

Mine::vector<int> v1;

v1.push_back(1);

v1.push_back(2);

v1.push_back(3);

v1.push_back(4);

v1.push_back(5);

for (auto e : v1)

{

cout << e << " ";

}

cout << endl;

//1.可以使用vector的迭代区间进行构造

vector<int> v2(v1.begin(), v1.end());

for (auto e : v2)

{

cout << e << " ";

}

cout << endl;

//2.可以使用string的迭代区间按进行构造

string s1("hello");

vector<int> v3(s1.begin(), s1.end());

for (auto e : v3)

{

cout << e << " ";

}

cout << endl;

return 0;

}

vector中还有一个构造:n个val进行的构造:

//n个val进行的构造

vector(size_t n, const T& val = T())

{

//先写reserve是为了避免一直开辟空间的问题

reserve(n);

fot(size_t i = 0; i < n; i++)

{

push_back(val);

}

}

但是当我们这样写时,会发生错误:

int main()

{

Mine::vector<int> v1;

v1.push_back(1);

v1.push_back(2);

v1.push_back(3);

v1.push_back(4);

v1.push_back(5);

for (auto e : v1)

{

cout << e << " ";

}

cout << endl;

Mine::vector<int> v2(10, 1);//err

//vector<double> v2(10, 1.1);

for (auto e : v2)

{

cout << e << " ";

}

cout << endl;

return 0;

}

因为Mine::vector v2(10, 1);调用的是vector(InputIterator first, InputIterator last)函数,该函数的参数更加匹配,所以我们要再创建一个int类型的构造来解决这个问题:

//n个val进行的构造

vector(size_t n, const T& val = T())

{

//先写reserve是为了避免一直开辟空间的问题

reserve(n);

for (size_t i = 0; i < n; i++)

{

push_back(val);

}

}

vector(int n, const T& val = T())

{

reserve(n);

for (int i = 0; i < n; i++)

{

push_back(val);

}

}

4.3.4注意事项4 – sting中深浅拷贝问题

在这里插入图片描述



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。