👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨
目录
- 一、简单剖析vector的源码
- 二、准备工作
- 三、模拟实现vector常见操作
- 3.1 无参的默认构造
- 3.2 获取容量 - capacity()
- 3.3 获取元素个数 - size()
- 3.4 尾插 - push_back()
- 3.5 扩容reserve + memcpy的浅拷贝问题
- 3.6 迭代器 - begin() + end()
- 3.7 析构函数
- 3.8 operator[]
- 3.9 插入 - insert()
- 3.10 删除元素 - erase()
- 3.11 判空 - empty()
- 3.11 尾删 - pop_back()
- 3.12 增加/缩减有效数据 - resize()
- 3.13 拷贝构造 + memcpy浅拷贝问题
- 3.14 交换 - swap()
- 3.15 赋值运算符重载 - operator=
- 3.16 用n个val构造
- 3.17 用迭代器区间初始化
- 四、相关代码
一、简单剖析vector的源码
vector
是一个动态数组,它使用连续的内存存储元素。当向vector
中插入元素时,如果导致当前内存不足以容纳所有元素,底层会重新分配更大的内存空间,并将所有元素复制到新的内存中。因此 ,vector
的结构和string
是一样的,不同的是 vector
是类模板:
template<class T>
class vector
{
T* _str; // 指向动态开辟的空间
size_t _size; // 有效个数
size_t _capacity; // 空间
};
然而vector
的 【源码】 还是和以上有所差别的:
template <class T, class Alloc = alloc>
class vector
{
public:
typedef T* iterator;
private:
iterator start;
iterator finish;
iterator end_of_storage;
}
如上所示,我们并不知道源码中的成员变量start
、finish
、end_of_storage
是什么,只能知道它们是指针(vector
底层物理空间是连续)。因此接下来得去看它的成员函数,但成员函数非常多,想要快速了解应该重点看构造函数 + 插入操作。
vector()
:start(0),
finish(0),
end_of_storage(0)
{}
vector(size_type n, const T& value)
{
fill_initialize(n, value);
}
vector(int n, const T& value)
{
fill_initialize(n, value);
}
vector(long n, const T& value)
{
fill_initialize(n, value);
}
explicit vector(size_type n)
{
fill_initialize(n, T());
}
尴尬的是构造函数看不出什么所以然来,因此接下来可以看插入接口:
// 尾插
void push_back(const T* x)
{
if (finish != end_of_storage)
{
construct(finish, x);
++finish;
}
else
{
insert_aux(end(), x);
}
}
有插入操作必定涉及到扩容:
void reserve(size_type n)
{
if (capacity() < n)
{
const size_type old_size = size();
iterator tmp = allocate_and_copy(n, start, finish);
destroy(start, finish);
deallocate();
start = tmp;
finish = tmp + old_size;
end_of_storage = start + n;
}
}
从上我们就猜出:
start
:从名字上来看可能是指向起始位置的指针finish
:从尾插接口可以看出,相当于指向数据的有效个数的指针end_of_storage
:从扩容接口可以看出,相当于指向当前容量的指针
二、准备工作
为了方便管理代码,分两个文件来写:
Test.cpp
- 测试代码逻辑vector.h
- 模拟实现vector
接口
三、模拟实现vector常见操作
3.1 无参的默认构造
- 为了防止与库的
vector
冲突,要重新写一个命名空间域wj
- 要注意初始化列表的顺序,是按照成员变量的顺序来赋值的!
3.2 获取容量 - capacity()
- 知识点:
指针 - 指针
返回的是元素个数。
3.3 获取元素个数 - size()
3.4 尾插 - push_back()
【函数原型】
【代码实现】
3.5 扩容reserve + memcpy的浅拷贝问题
【函数原型】
vector
的扩容并不是原地扩容,而是会重新分配更大的内存空间,并将所有元素复制到新的内存中。
【代码实现】
以上代码看似没有问题,其实漏洞百出:
虽然打印出来了,但是程序还是存在bug
。
这是一个隐藏的深拷贝问题:
memcpy
是内存的二进制格式拷贝,将一段内存空间中内容原封不动的拷贝到另外一段内存空间中- 如果拷贝的是内置类型的元素,
memcpy
既高效又不会出错,但如果拷贝的是自定义类型元素,并且自定义类型元素中涉及到动态内存开辟时,就会出错,因为memcpy
的拷贝实际是浅拷贝。
尾插"55555"
就要进行扩容,memcpy
拷贝数据到新空间(浅拷贝);接下来delete
旧空间,而delete
对于自定义类型先是执行析构函数,完成_str
中资源的清理,然后再释放对象的空间。
接着_start
重新指向新拷贝的空间,然后出了作用域会再次调用析构函数,导致_str
所指向的内容被析构了两次,因此导致程序报错。
解决方法是:遍历赋值调用T
类型赋值重载,实现深拷贝。
3.6 迭代器 - begin() + end()
【函数原型】
【代码实现】
3.7 析构函数
vector
底层实际上实现了一个空间配置器(内存池),而这里选择使用new
和delete
代替,因此要写析构函数。
【代码实现】
3.8 operator[]
【函数原型】
【代码实现】
3.9 插入 - insert()
vector
存在迭代器失效问题,因此要特别注意扩容部分:vector的扩容后,原空间被销毁,插入位置可能失效
【代码实现】
3.10 删除元素 - erase()
【代码实现】
3.11 判空 - empty()
3.11 尾删 - pop_back()
3.12 增加/缩减有效数据 - resize()
【函数原型】
【代码实现】
- 这里有一个新玩法:为什么第二个参数给了一个缺省值是
T()
val
可以不给值,那么就是一个默认值,但是这个默认值不能直接写0
,这样就写死了。T
是模板参数,可以是任意类型,对于int
,其默认值是0
,对于char
,其默认值'\0'
等等
正确做法:缺省值给一个匿名对象,让其自动调用它的默认构造函数,那么内置类型也会有构造函数吗?理论上没有,但是有了模板以后,C++对内置类型进行升级,内置类型也有默认构造函数
【证明】
3.13 拷贝构造 + memcpy浅拷贝问题
由于成员变量中有动态开辟的空间,因此要手动写拷贝构造,默认不写的话是浅拷贝。成员变量会指向同一块空间,会导致析构两次的问题
【代码实现】
在reserve
接口讲解到:使用memcpy
导致string
对象的浅拷贝,因此要改掉memcpy
还有一种写法可以避免以上的问题:
3.14 交换 - swap()
【函数原型】
【代码实现】
3.15 赋值运算符重载 - operator=
和拷贝构造函数同样的原理,也要手动写深拷贝
这里直接使用现代写法
3.16 用n个val构造
【函数原型】
可以复用resize
,但之前要对成员变量初始化,不然内置类型的指针默认是随机值,导致resize
内部一堆野指针问题。或者可以直接在成员变量给缺省值。
或者也可以不复用
3.17 用迭代器区间初始化
【函数原型】
【代码实现】
一个类模板中还可以再套模板
【测试结果】
以上代码出现了非法的间接寻址,意思就是不是指针或者不是解引用的对象被解引用,当调试以上测试代码会发现:调用了迭代器区间初始化的函数。
- 可是为什么会调用迭代器区间初始化的函数呢?
首先以上两个参数是10
和1
,编译器会认为是int
类型的,本应该调用用n个val构造
,但是其第一个参数是无符号的整型,而迭代器区间初始化的参数是是模板,因此编译器会调用更匹配的函数(迭代器区间初始化),而迭代器本应类似一个指针,这里却是一个普通的变量,解引用就导致了非法的间接寻址。
- 解决方案:对
n
个val
构造接口进行函数重载(库里也是这样实现的)
四、相关代码
Gitee
仓库链接:点击跳转