【C++初阶】模拟实现vector

在这里插入图片描述

👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨


一、简单剖析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;
 }

如上所示,我们并不知道源码中的成员变量startfinishend_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底层实际上实现了一个空间配置器(内存池),而这里选择使用newdelete代替,因此要写析构函数。

【代码实现】

在这里插入图片描述

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 用迭代器区间初始化

【函数原型】

在这里插入图片描述

【代码实现】

一个类模板中还可以再套模板

在这里插入图片描述

【测试结果】

在这里插入图片描述

以上代码出现了非法的间接寻址,意思就是不是指针或者不是解引用的对象被解引用,当调试以上测试代码会发现:调用了迭代器区间初始化的函数

  • 可是为什么会调用迭代器区间初始化的函数呢?

首先以上两个参数是101,编译器会认为是int类型的,本应该调用用n个val构造,但是其第一个参数是无符号的整型,而迭代器区间初始化的参数是是模板,因此编译器会调用更匹配的函数(迭代器区间初始化),而迭代器本应类似一个指针,这里却是一个普通的变量,解引用就导致了非法的间接寻址。

  • 解决方案:nval构造接口进行函数重载(库里也是这样实现的)

在这里插入图片描述

四、相关代码

Gitee仓库链接:点击跳转

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用和提供了关于实现vector的两种方法。其中,引用展示了一个使用reserve和push_back方法的示例,而引用展示了一个使用new和memcpy函数的示例。这两种方法都是常见的实现vector的方式。 在第一种方法中,通过reserve函数可以预留足够的内存空间,然后使用push_back函数逐个将元素添加到vector中。这种方法的好处是可以避免不必要的内存重分配,提高了效率。 而第二种方法使用new操作符在堆上分配内存空间,并使用memcpy函数将已有的vector对象的数据复制到新的内存空间中。通过这种方式,可以实现深拷贝,即两个vector对象拥有独立的内存空间。这种方法的好处是可以在不修改原始vector对象的情况下创建一个新的vector对象。 除了以上两种方法,还可以使用其他方式实现vector类。例如,可以使用动态数组来实现vector的底层数据结构,然后通过成员函数实现vector的各种操作,如增加、删除、查找等。 总结来说,c语言模拟实现vector的关键是动态内存管理和对元素的增删改查操作。可以使用预留空间和逐个添加元素的方式,也可以使用动态数组和复制数据的方式来实现vector类。具体的实现方式可以根据需求和实际情况选择。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [C++——vector模拟实现](https://blog.csdn.net/weixin_49449676/article/details/126813526)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值