C++从入门到起飞之——vector模拟实现 全方位剖析!
秋风起,再归来~ 2024-08-29 12:05:02 阅读 69
🌈个人主页:秋风起,再归来~
🔥系列专栏:C++从入门到起飞
🔖克心守己,律己则安
目录
1、vector的成员变量
2、迭代器
3、size与capacity
4、[]运算符重载
5、reserve
6、push_back
7、empty
8、pop_back
9、resize
10、swap
11、insert
12、erase
13、构造函数系列
14、clear与赋值运算符重载
15、析构函数
16、vector类模版的打印函数
17、完结散花
1、vector的成员变量
>vector其实就是我们所熟悉的顺序表,我们在之前模拟实现顺序表的时候用到了一个指针a指向了我们要动态管理的内容,用size记录有效数据的个数,用capacity记录容量的大小。而在C++stl中顺序表vector的实现所用的成员变量却有所不同!
我们在查看了stl中vector的部分源代码后发现类模版vector中的成员变量是三个迭代器。
再通过查看源代码中的typedef可以发现vector中的迭代器iterator就是类模版参数T的原始指针!
>那这三个成员变量到底是什么意思呢?其实我们通过变量的命名就可以大致猜出他们的意义:start指向顺序表的开始,finish指向有效数据的结尾,end_of_storage指向有效空间的结尾。
但这仅仅只是我们的猜测而已,我们还要通过看源代码来证实我们的猜测!
将源代码和文档中vector迭代器的功能介绍相结合我们不难证实我们的猜测是正确的!
2、迭代器
<code>// Vector的迭代器是一个原生指针
typedef T* iterator;
typedef const T* conat_iterator;
iterator begin()
{
return start;
}
iterator end()
{
return finish;
}
conat_iterator begin() const
{
return start;
}
conat_iterator end() const
{
return finish;
}
3、size与capacity
size_t size()const
{
return (finish - start);
}
size_t capacity()const
{
return (end_of_storage - start);
}
4、[]运算符重载
T& operator[](size_t pos)
{
assert(pos < size());
return start[pos];
}
const T& operator[](size_t pos) const
{
assert(pos < size());
return start[pos];
}
5、reserve
>我们常犯的错误写法:
void reserve(size_t n)
{
if (n > capacity())
{
iterator tmp = new T[n];
//memcpy(tmp, start, sizeof(T) * size()); 浅拷贝
for (size_t i = 0; i < size(); i++)
{
tmp[i] = start[i];
}
delete[] start;
start = tmp;
finish = start + size();
end_of_storage = start + capacity();
}
}
错误的原因就在于,我们更新finish和end_of_storage时size是用旧空间的finish减新空间的start(我们先更新了start),而我们期望的是 旧空间的finish减旧空间的start,我们可以采用提前保存一份相对位置的方法来解决这个问题!
>正确的写法:
//reserve
void reserve(size_t n)
{
if (n > capacity())
{
size_t old_len = finish - start;
iterator tmp = new T[n];
//memcpy(tmp, start, sizeof(T) * size()); 浅拷贝
for (size_t i = 0; i < size(); i++)
{
tmp[i] = start[i];
}
delete[] start;
start = tmp;
finish = start + old_len;
end_of_storage = start + n;
}
}
>特别要注意的一点是,我们之前在模拟实现string类时,使用memcpy来拷贝数据雀氏没有问题,不过,在vector类模版中我们还用memcpy来一个一个字节的来拷贝数据就会有大问题!当vector中的元素是内置类型时倒也没有问题,但是,当元素是自定义类型且里面的数据有申请资源时(如元素为string对象)就会有大坑。原因就在于, memcpy是浅拷贝,我们扩容时,新空间和旧空间的资源是同一块,我们释放旧空间后,新空间所对应的资源也就不存在了!
6、push_back
void push_back(const T& val)
{
if (size() == capacity())
{
reserve(capacity() == 0 ? 4 : capacity() * 2);
}
*finish = val;
++finish;
}
7、empty
//empty
bool empty()
{
return start == finish;
}
8、pop_back
//pop_back
void pop_back()
{
assert(!empty());
finish--;
}
9、resize
>注意当n小于size时,resize相当于删除数据,当n>size时,在size后面用val填充。这里val的缺省值是用T()匿名构造的对象!
//resize(大多数情况用来初始化)
void resize(size_t n, const T& val = T())
{
if (n <= size())
{
finish = start + n;
}
else
{
reserve(n);
while (finish < start + n)
{
*finish = val;
++finish;
}
}
}
10、swap
//swap
void swap(vector<T>& v)
{
std::swap(start, v.start);
std::swap(finish, v.finish);
std::swap(end_of_storage, v.end_of_storage);
}
11、insert
关于insert这里我想要说一点关于迭代器失效的问题,一定要记住,我们在pos位置插入数据后,pos就失效了,我们一定不要再用pos去访问数据了。虽然我们在insert内部已近对pos进行了更新,但形参的改变不会影响实参。这里如果用引用也是行不通的,当我们用一个容器的迭代器去传参时,函数返回一个临时变量(具有常性),不能传给普通的引用。更不能加const,不然内部的pos无法更新! 如果实在要用到pos,那我们就必须在外面更新pos,用pos接收函数的返回值即可!
//insert
iterator insert(iterator pos, const T& val)
{
assert(pos >= start);
assert(pos < finish);
//扩容
if (finish == end_of_storage)
{
size_t old_pos = pos - start;
reserve(capacity() == 0 ? 4 : capacity() * 2);
pos = start + old_pos;
}
iterator tmp = finish;
while (tmp!=pos)
{
*tmp = *(tmp - 1);
tmp--;
}
*pos = val;
finish++;
return pos;
}
12、erase
erase也同样面临迭代器失效的问题,我们在使用的时候一定要注意!
//erase
iterator erase(iterator pos)
{
assert(pos >= start);
assert(pos < finish);
iterator tmp =pos + 1;
while (tmp != finish)
{
*(tmp-1) = *tmp;
tmp++;
}
finish--;
return pos;
}
13、构造函数系列
>默认构造
//构造函数
//vector() {};
//C++11 前置生成默认构造
vector() = default;
>n个value构造
//n个value构造
vector(size_t n, const T & val = T())
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
>迭代器构造
//迭代器构造
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{
while (first != last)
{
push_back(*first);
++first;
}
}
关于迭代器构造这里头也有一些小坑:
void test6()
{
vector<int> v1(10, 1);
print_container(v1);
}
这里我们期望用10个1来构造v1
这里报了一个非常奇怪的报错,我明明期望调用的不是迭代器的构造,但这里却调到了!
究其原因在于,编译器通过我们传递的参数选择调用最匹配的函数, vector(size_t n, const T & val = T())的参数类型一个为size_t,另一个为int,而vector(InputIterator first, InputIterator last)通过推导俩个参数的类型都为int,整形在传参时默认是int类型,所以编译器认为最为匹配的函数是迭代器构造函数!
那我们要怎么解决这个问题呢?其实非常简单,我们只要再重载一个俩个参数类型都为int的构造函数就可以了!
<code>vector(int n, const T & val = T())
{
reserve(n);
for (size_t i = 0; i < n; i++)
{
push_back(val);
}
}
此时编译器看到有现成的最为匹配的函数当然不会再通过函数模版去生成一个函数,毕竟编译器和我们一样喜欢偷懒!
>vector(const vector<T>&v)
//vector(const vector& v)
vector(const vector<T>&v)
{
reserve(v.size());
/*for (size_t i = 0; i < v.size(); i++)
{
push_back(v[i]);
}*/
for (auto& e : v)
{
push_back(e);
}
}
14、clear与赋值运算符重载
//clear
void clear()
{
finish = start;
}
>赋值运算符重载
//赋值运算符重载
/*vector<T>& operator=(const vector<T>&v)
{
if (this != &v)
{
clear();
reserve(v.size());
for (auto& e:v)
{
push_back(e);
}
}
return *this;
}*/
vector<T>& operator=(vector<T> tmp)
{
swap(tmp);
return *this;
}
15、析构函数
//析构函数
~vector()
{
if (start)
{
delete[] start;
start = finish = end_of_storage = nullptr;
}
}
16、vector类模版的打印函数
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;
}
值得注意的是:以上代码中vector<T>::const_iterator it = v.begin();这种写法是有问题的!
C++规定,没有实例化的类模板里面取东西,编译器不能区分这里const_iterator是类型还是静态成员变量!
正确的写法:typename vector<T>::const_iterator it = v.begin()或auto it = v.begin()
17、完结散花
好了,这期的分享到这里就结束了~
如果这篇博客对你有帮助的话,可以用你们的小手指点一个免费的赞并收藏起来哟~
如果期待博主下期内容的话,可以点点关注,避免找不到我了呢~
我们下期不见不散~~
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。