C++ STL初阶(6)vector的简易实现(下)

书接上回,此篇我们就vector中不同于string的一些经典迭代器特点进行记录和讲解

目录

1. erase的迭代器失效问题

2.拷贝构造和赋值运算符

3.构造函数:迭代器区间初始化


 1. erase的迭代器失效问题

(先用库中vector的试试):

第一次对re解引用是没问题的,因为v1中有4,此时re指向这个4

( 如果没有找到也是不能解引用的,find函数在没有找到时默认返回的是end()

第二次re就失效了所以会出现:

can't dereference(间接引用) invalidated iterator

说明此时的迭代器re已经失效

正确操作:用re接受erase的返回值,返回的是被erase走的下一个元素的现在的迭代器

并且检查re在不等于end()时才能被访问。

如果不检查(也就是不使用40行的代码,如下图),又恰好find了345:

因此,erase最后一个元素时也需要注意,返回值是end(),end()也是不能被解引用访问的。

小结:

我们认为erase(it)之后,it失效。


 为什么erase之后编译器要认为被erase的迭代器都失效了呢? 

1.可能是因为删除的是最后一个位置,然而it依然指向这个位置,存在一个越界访问。(失效不一定是野指针)

2.删除操作的实现中,有可能会出现“删除元素超过一半就缩容”的逻辑,毕竟没有规定erase是否要缩容。缩容就会开新空间,那此时it又变成野指针了。(原因同iterator)

VS编译器的本质:

VS的iterator是一个封装的类型,里面有一个标志,VS的工程师在这件事上做的比较绝对,只要使用了erase等操作,直接算作失效(封装的iterator中有个标志,使用了erase之后将这个标志变成false,不能再被访问)。

但并不是每种编译器都会认为这是一种报错,比如g++就是例外

Linux下:执行刚刚相同的、但不用re接受返回值的代码

               

不会报错,能够访问。g++的检查没有这么严格。

但是为了代码的通用性:

所有平台下,我们都认为erase(it)之后it失效

解决方案同Insert,在返回值处进行补救,返回原顺序表中被erase的元素的下一个元素

 所以一定要这样使用:

it=v1.erase(it);

如果删除了末尾元素,则会返回v1.end(),这依然是一个不能被访问的迭代器。

所以即使在访问更新过后的it时,最好也先进行检查。

例如,希望删除一个数组中的偶数:

       

如果不写it=v1.erase(it);,就会报错:

如刚才所说,在linux环境下,g++中依然不会强制检查,这样写没报错,但是这样写是不正确的。

只有在所有的环境下都能跑的代码才叫正确的代码。

            

                                     所以我们认为insert和erase的迭代器都失效了。


对上一篇文章中的代码的进行一个小小的修补:

接口中该加上const的就加上const,方括号访问需要实现两个版本。

                   


2.拷贝构造和赋值运算符

2.1 拷贝构造

在我们主动写之前,编译器默认生成的是浅拷贝。直接按照字节拷贝

    拷贝构造的写法依然是分传统写法(自己开空间再赋值)和现代写法(交给构造函数),

不过我们此处写点更方便的: 

vector(const vector<int>& v) {
	for (auto e : v) {
		this->push_back(e);
	}
}
//不是现代写法也不是传统写法,灵活即可

注意:只要手动实现了任意构造,编译器就不会再自己生成构造函数。拷贝构造也算构造。所以如果只写了一个拷贝构造,而没有实现其他构造:

以下代码会因为没有对应的构造而报错:

                                       

             

使用指令:default,强制生成一个可以用的默认构造:

vector() = default;

加在类内部即可:

注意:拷贝构造必须传引用,否则会无限递归而报错 

还可以通过加reserve(v.capacity());来提升效率。

                         


2.2 赋值运算符

使用经典的现代写法,“将工作交给别人”

现代写法一般需要实现一个swap函数。

比如我们要执行 v1=v3;

先将v3作为参数传给v(此时调用的是拷贝构造而非赋值,也就是vector<T> v= v3)

这样的传值传参就可以省略一步构造函数,并且不用担心在swap时破坏v3

传引用返回便于连等。

void swap(vector<T>& v) {
	std::swap(this->_start, v._start);
	std::swap(this->_finish, v._finish);
	std::swap(this->_end_of_storage, v._end_of_storage);
}

 简单测试一下swap函数:

正如C++STL初阶(3):string模拟实现的完善-CSDN博客z

在拷贝构造的现代写法中一样,我们可以先传引用进来,然后再使用变量tmp拷贝构造一次:

反正都有一次深拷贝,不如直接在参数处使用传值传参,将赋值运算符进一步优化: 

返回值使用的是vector<T>& ,这样能使用连等来连续赋值。


3.构造函数:迭代器区间初始化range constructor与fill constructor

先在类模版中写一个新的函数模版:         

template<class Inputiterator>
vector(Inputiterator first, Inputiterator last) {
	while (first != last) {
		push_back(*first);
		++first;
	}
}

类模版的成员函数也可以写成函数模版

目的是支持任意种类容器的迭代器作为参数传入,并进行初始化

再看看官网对范围构造的解释: 

               

                 

使用string测试:

或者使用list:

       (此处用string的测试比较怪异,把字符按照数字(ASCII)处理了)

        类模版中的成员函数写成函数模版,就可以实现如上图中的,将string、list等的迭代器区间传入vector

                       


就像标准库里的方法一样,使用匿名对象作为参数,实现fill constructor             

vector(size_t n, const T& value = T()) {
	reverse(n);
	for (int i = 0; i < n; i++) {
		push_back(value);
	}
}

关于匿名对象做参数:

为了迎合类中的泛型概念,cpp让内置类型也升级而具有构造。

                                

int的默认值为0,所以k也是0,string和char的默认值都是/0


非法间接寻址(使用重载解决

再来看一个奇怪的例子:

第一排只传了size_t 的 n,因此有10个空的string在v1这个vector中,v2中则是10个xxx​​​​​​​

                    ​​​​​​​

如果将"xxx"换成1(没有引号):

会报出错误:非法间接寻址

                                     

因为编译器会选择更对口味的参数,会把10 和1推演成Inputiterator

不管下面是size_t 和 string还是size_t 和int

都不如上面这个推演出来Inputiterator的更合适

对比上下两种构造:

        

但是因为int不能解引用,所以报错了。

解决方案:

1.   写成10u(表示这是一个无符号数),无符号数会和size_t匹配

                                     

2.重载一个    

vector(int n, const T& value = T()) {
	reserve(n);
	for (int i = 0; i < n; i++) {
		push_back(value);
	}
}

标准库中的vector中也是这样解决的:

                                                                   还重载了long版本


4. initialize_list

先复习一下前置知识:

关于花括号赋值(列表初始化):

                      

你甚至可以省略花括号前面的等于号

花括号其实就是一种隐式类型转换:将花括号中的参数用于构造加拷贝构造----->被优化成构造

笔者在这里建议大家只记并使用上面两种即可,也就是

A aa1(1);//单参数构造
A aa2=1;//单参数隐式类型转换

A aa3(1,2);//多参数的构造
A aa4={1,2}//多参数的隐式类型转换

​

但是这些都是确定个数的构造,vector是不确定个数的,所以不能算是传统意义的隐式类型转换

             

比如A有两个参数,其中一个是缺省参数,那么可以传一个或者两个,但是不能传三个、四个。

参数个数是有限制的,不过给vector传参时是没有限制的。

参数个数不固定,和A类不是同种类型的隐式类型转换,C++提出来一个新的解决办法:initializer_list

initializer_list是一个新增的类型,方便用于初始化。

initializer_list  支持把花括号中任意个数的值传给它自己 ,想要使用它需要加上头文件

#include <initializer_list>

可以看下initializer_list的内部结构:由两个指针,sizeof计算出来大小是8(32位下)

两个指针管理的是一个常量数组

initializer_list的内部函数只有以下三种,建议使用范围for

所以,对于:

vector<int> v7={1,2,3,4,5};//隐式类型转换
vector<int> v8({10,20,30,40});//直接构造

           

其实相当于是把花括号中的内容当作一个initializer_list,然后用这个initializer_list去构造vector,本质也可以算是隐式类型转换。

所以我们如果要在自己的vector中实现花括号构造,就需要自己实现initializer_list的构造

而initializer_list中可以使用begin,end,sizeof,所以直接push_back il中的元素就可以了。

                            


小试牛刀

对于自定义类型:

                                         

           

请说明现在为什么会报错:

因为花括号里面的内容会传给initializer_list,而我们的initializer_list构造的逻辑是用for循环来一个一个push_back,push_back就涉及到扩容函数reverse,扩容又需要new T[] ,new T[]就需要默认构造,而此时的A的构造没有合适的默认构造,所以就报错了


甚至单参数的可以不加括号:

(直接A{2,2}相当于是忽略了赋值符号)

 非常混乱,我们稍加总结:

A aa1(1);//单参数构造
A aa2=1;//单参数隐式类型转换

A aa3(1,2);//多参数的构造
A aa4={1,2}//多参数的隐式类型转换

5. vector中放string的问题
​​​​​​​​​​​​​​

原来四个,假设我们此时继续insert或者push_back

扩容之后八个:

因为在reverse中我们使用的是memcpy来进行浅拷贝,拷贝的是指针,之后又释放了旧空间

(delete[]  _start    ,会先对每个对象调用析构函数,再释放空间),所以刚开始的四个string就失效了。

运行后会报错:


解决方法:将memcpy改成深拷贝。这也是为什么之前讲类的构造的时候说,只要涉及自己开空间的,都建议写成深拷贝。库中string的赋值运算符自然是深拷贝或者引用计数拷贝,可以直接写

tmp[i]=_start[i],就完成了深拷贝。

       ​​​​​​​修改:

这里的深拷贝不在vector本身,而在跨一层的vector的数据类型string的拷贝出了问题。

不止string,其实vector的vector,vector的list , vector中元素为需要开空间的自定义类型时,都存在这个问题。

不过,如果在此处使用initializer_list就能避免这个问题:

 因为其底层逻辑是一次性开出来这么多。


6. 关于default

C++STL 初阶(5)vector的简易实现(上)_c++ 自己实现一个vector 并且无拷贝放入-CSDN博客

 在上一文中,我们略微提及到default的使用来强制生成默认构造。

但是,如果我们要自己生层默认构造,就必须屏蔽这一语句

否则就会因为有两个参数都为void的默认构造而重载失败。

但是如果在我们自己写的vector中加入一个全缺省参数

根据预处理和编译的原则,就不算重载失败,能正常调用。

但是如果我们此时调用默认构造,还是会报错:歧义调用

因此,我们一般在不自己实现默认构造时才会使用到default 

  • 27
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值