1. list学习
1.1. list 的介绍
https://legacy.cplusplus.com/reference/list/list/?kw=list
这里我们还是和前面一样,跟着文档走,主要学习和 string 和 vector 中没有的函数
list 是一个带头双向循环链表
list允许在O(1) 的时间复杂度的情况下,进行插入删除
下面是 list 中实现的函数
1.2. 迭代器的使用
void test_list1()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
list<int>::iterator it = lt.begin();
while(it != lt.end())
{
cout << *it << " ";
it++;
}
cout << endl;
for(auto e : lt)
{
cout << e << " ";
}
cout << endl;
list<int>::reverse_iterator it2 = lt.rbegin();
while(it2 != lt.rend())
{
cout << *it2 << " ";
it2++;
}
cout << endl;
}
正向,反向,范围for,和之前一样的写法
由于之前对string 和 vector 中的函数学习,这里的迭代器应该没什么问题,至于迭代器详细的内容,后面会放在迭代器的模拟实现中介绍。
1.3. Operation
由于 list 的结构和原先不同(list是带头双向循环链表),所以这里的函数主要介绍前面没有的,比如这里的 Operation 都是以前 string 和 vector 中没见过的函数。
1.3.1. reverse
上面写反向迭代器的时候,我们使用过这个单词,是翻转的意思,所以这个函数在这起到的作用就是翻转链表。
void test_list2()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
for(auto e : lt)
{
cout << e << " ";
}
cout << endl;
lt.reverse();
for(auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
注:一定要注意,翻转的单词是 reverse,reserve是 保留。在 string 中是保留传入的空间大小
1.3.2. sort
看名字就知道,排序
注意,这里的排序默认顺序是 从小到大
void test_list2()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
for(auto e : lt)
{
cout << e << " ";
}
cout << endl;
lt.reverse();
for(auto e : lt)
{
cout << e << " ";
}
cout << endl;
lt.sort();
for (auto e : lt)
{
cout << e << " ";
}
cout <<endl;
}
这里我们先逆置,然后在排序,就可以将原本由大到小的顺序,排成由小到大的样子。
sort本身不是太难,sort的底层采用的是归并排序。
但是下面还有三个问题
1. 为什么不使用库中的排序函数
在算法库 中,我们可以找到,库中原本已经实现了一个 sort 排序函数,但是为什么在 list 库中还要自己实现一个呢
我们先看看 算法库 中的 sort 有什么特点
这里的 sort 使用的是模版,模版参数是 RandomAccessIterator
random(随机) iterator(迭代器)
这一长串的意思是 随机访问迭代器
那我们的 list 使用的是什么迭代器呢
我们打开 list 的 begin 函数
这里清楚的写了,BidirectionalIterator 双向迭代器
我们之前学的有 正向迭代器,反向迭代器,但是这是从功能上的分类。
在传入参数的时候,这里的迭代器存在性质上的区别,分别有 :
单向,双向,随机 三种迭代器
单向迭代器(只支持++,不支持–):单链表,哈希表
双向迭代器(即支持++,也支持–):双向链表,list
随机迭代器(除了支持++,–,还支持+,-操作):顺序表 vector / string。
string中的迭代器:
这里我们可以看见 string 中的迭代器是随机访问迭代器,所以我们就可以在算法库中的sort中传入 string 类型的对象。
如果我们强行使用的话
void test_list2()
{
list<int> lt;
lt.push_back(1);
lt.push_back(67);
lt.push_back(9);
lt.push_back(4);
lt.push_back(5);
for (auto e : lt)
{
cout << e << " ";
}
sort(lt.begin(),lt.end());
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
这里就会出现问题
算法库中的 sort 使用的是快排,存在迭代器之间±的操作
随机访问迭代器支持 ± 操作,所以没问题。
双向链表物理空间不连续,它的迭代器是双向迭代器,不能支持 ± 。
2. sort逆序输出
上面我们说了,list 的 sort 只能支持 从小到大 的顺序
那么如果我们想要 从大到小 的顺序排序呢?
(这里的内容稍微有点超纲,后面学习会详细补充,这里先简单介绍怎么用)
想要 降序,就需要使用 greater
greater() 创建匿名对象
void test_list2()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
for (auto e : lt)
{
cout << e << " ";
}
lt.sort()//从小到大排序
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
lt.sort(greater<int>());//从大到小排序
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
list 的 sort , 使用的是归并排序,使用归并代价能小点。
3. 效率问题
这里我们用 list 中的 sort 和 算法库中的 sort 进行一个时间对比,看两者效率上有何差距
void test_op()
{
srand(time(0));
const int N = 100000;
vector<int> v;
v.reserve(N);
list<int> lt1;
list<int> lt2;
for (int i = 0; i < N; i++)
{
ayto e = rand();
lt1.push_back(e);
v.push_back(e);
}
int begin1 = clock();
sort(v.begin(), v.end());
int end1 = clock();
int begin2 = clock();
lt1.sort();
int end2 = clock();
printf("vector sort:%d\n", end1 - begin1);
printf("list sort:%d\n", end2 - begin2);
}
(一般排序默认都是在 release 版本下,跑的更快)
因为我们可以使用其他容器的迭代器对 list 初始化
所以我们可以先在 vector 中把数据排好序,然后再用排好序的数据对 list 初始化(典型的空间换时间)
int begin1 = clock();
sort(v.begin(), v.end());
lt2.assign(v.begin(), v.end());
int end = clock();
这里 vector排序 + 数据复制 的速度能比 list 的 sort 快一点,但是实际上差距已经不是太大了。
list 的 sort 在性能上并没有太大的优势,但是相对来说方便点。
1.3.3. merge
merge 合并
单词的意思应该比较好理解,这个函数就是用来合并两个链表的。
void test_list3()
{
list<int> lt1{1, 3, 5, 7, 9};
list<int> lt2{2, 4, 6, 8, 10};
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl;
for (auto e : lt2)
{
cout << e << " ";
}
cout << endl;
lt1.merge(lt2);
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl;
for (auto e : lt2)
{
cout << e << " ";
}
cout << endl;
这里有几点需要注意:
- 这里的数组合并,前提是两个数组必须都是有序的
- 这里的合并,是直接把传入 list对象 的节点转移,并不会创建新的节点。
- merge 是 成员函数,所以调用方式是,list对象调用,传入一个 list 对象。
- 合并以后得新链表,顺序是从小到大的。
如果我们传入的并不是两个有序的 list 对象
list<int> lt1{1, 3, 5, 7, 9};
list<int> lt2{10, 8, 6, 4, 2};
这里就直接崩了,所以使用 merge 之前,最好也使用 sort 先进行排序。
1.3.4. unique
unique 独特的
这个函数是用来去重的,但是和上面一样,数据必须要有序。
void test_list3()
{
list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(3);
lt.push_back(3);
lt.push_back(4);
lt.push_back(4);
lt.push_back(5);
lt.push_back(5);
lt.push_back(6);
lt.sort();
lt.unique();
for (auto e : lt)
{
cout << e << " ";
}
cout << endl;
}
这里的去重的方法采用的是 双指针的方法,必须要有序,所以这里还是最好配合 sort 进行使用。
1.3.5. splice
splice 粘接
就是把一个链表转移到另一个链表上,但是这里和上面的 merge 一样,是转移节点,并不是复制节点。
void test_list4()
{
list<int> lt1, lt2;
list<int>::iterator it;
for (int i = 1; i <= 4; i++)
{
lt1.push_back(i);
}
for (int i = 1; i <= 3; i++)
{
lt2.push_back(i * 10);
}
it = lt1.begin();
++it;
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl;
for (auto e : lt2)
{
cout << e << " ";
}
cout << endl;
lt1.splice(it, lt2);
for (auto e : lt1)
{
cout << e << " ";
}
cout << endl;
for (auto e : lt2)
{
cout << e << " ";
}
cout << endl;
}
这里就不需要像上面那样必须有序。
注意传入的参数
上面我们使用的是第一种,第一个参数是被插入的对象,第二个参数是往第一个对象里插入的对象。
其他方式基本上差不太多,知道怎么使用就行了。
就是要注意这里插入是 节点直接插入,会改变传入参数的链表。
2. list 的模拟实现
2.1. 成员变量
和我们之前学过的双向链表一样,每一个节点都是由 data , next , prev 组成的。
所以我们先写这个节点的成员变量
namespace xsz
{
template<class T>
struct list_node
{
T _data;
list_node<T> _next;
list_node<T> _prev;
};
}
节点的结构体,这里使用模版,支持传入的各种类型。
有了节点的结构体,下面就开始实现带头双向循环链表
成员变量:
template<class T>
class list
{
typedef list_node<T> Node;
private:
Node* _head;
};
2.2. 构造函数
我们先实现 list 的构造函数
template<class T>
class list
{
typedef list_node<T> Node;
public:
void empty_init()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
list()
{
empty_init();
}
private:
Node* _head;
}
这里我们使用了 new 来动态开辟空间,所以我们可以给节点的结构体写一个构造函数 (new = operator new + 构造函数),这样我们就不需要自己手动创建节点了。
template<class T>
struct list_node
{
T _data;
list_node<T>* _next;
list_node<T>* _prev;
list_node(const T& x = T())
:_data(data)
,_next(nullptr)
,_prev(nullptr)
{}
};
注意这里的额 list_node 的构造函数,里面的缺省参数是匿名对象,这里不能直接给 0 或者其他的类型,T 的类型是不确定的,可能是 int, float , 也可能是 string 等类型,所以最好传入 匿名对象。
2.3. push_back
尾插,思路还是双向链表的尾插,唯一不同的就是注意模版即可
void push_back(const T& x)
{
Node* newnode = new Node(x);
Node* tail = _head->_prev;
tail->_next = newnode;
newnode->_prev = tail;
_head->_prev = newnode;
newnode->_next = _head;
}
2.4. 迭代器的实现
xsz::list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
it++;
}
这里实现迭代器比前面模拟实现 string 和 vector 是有难度的。
迭代器,在使用上我们可以把他当成指针,但是底层不一定是指针实现的。
前面模拟实现的 string 和 vector 是数组实现的顺序表,物理空间是连续的,所以我们直接对指针 ++,-- 的操作就可以完成迭代器的操作
但是 list 是链表,物理空间不连续,所以我们不能直接使用指针进行++,–的操作
templatee<class T>
struct __list_node Node;
{
typedef list_node<T> Node;
typedef __list_iterator<T> self;
Node* node;
__list_iterator(Node* node)
:_node(node)
{};
self& operator++()
{
_node =_node->_next;
reeturn *this;
}
T& operator*()
{
return _node->_data;
}
bool operator!=(const self& s)
{
return _ndoe != s._node;
}
};
这里我们实现了 " -> " 的运算符重载,如果想详细了解的话,可以先看下面 3.3 的细节问题中的第3点。
这里我们就简单模拟实现了 list 的普通的迭代器
这里我们已经实现了 3 个类了,一个是节点的结构体,一个是迭代器的结构体,一个是 list 对象的类,不要搞混。
这里理解一下这个迭代器的结构体实现了什么功能。
首先,我们在 list 类中创建 iterator,就是创建了一个 __list_node 的结构的对象
这个对象,我们是传入 node 节点的指针初始化 _node
,我们在这个结构体中实现了 ++,–,* 等操作,所以我们直接可以对 _node 进行上述操作,如 ++ 我们返回 _node->_next ,这样就能完成我们迭代器的需求。
按照上面的过程,我们最后就可以像使用数组的指针的方法一样使用迭代器。
上面实现迭代器的方式,我们称之为封装
typedef __list_node<T> iterator;
iterator begin()
{
return iterator(_head->_next);
}
iterator end()
{
return _head;
}
我不需要知道 这个迭代器底层是什么实现的,这和我这里使用迭代器没什么关系,我这里使用迭代器的方式和 string , vector 是一样的,所以这就是封装的好处,对使用者来说方便。
这里还需要注意:
begin 返回第一个节点的位置,end 返回最后一个节点后面的位置。这里 _head 作为头结点,不存储值,所以begin 需要返回 _head->_next,end 返回最后一个节点的下一个位置,就是 _head 的位置。
因为我们返回值都是 iterator,我们这里直接返回 _head ,编译器会自己用这个值 去构造一个 iteratoer 的对象。
实现了普通迭代器,我们也可以使用范围for 对 list 进行输出
void test_list1()
{
xsz::list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
xsz::list<int>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
it++;
}
cout << endl;
for (auto e : lt)
{
cout << e << " ";
}
}
至于 const 迭代器怎么实现,文章后面会说
2.5. insert
插入,老朋友了
以前链表就是想,但是这里的 insert 作为容器内的函数,我们就不能像以前那样用指针来修改,我们就需要使用 迭代器来修改。
pos 前插入 val
void insert(iterator pos, const T& val)
{
Node* newnode = new Node(val);
Node* cur = pos._node;
Node* prev = cur->_prev;
newnode->_next = pos;
pos->_prev = newnode;
newnode->_prev = prev;
prev->_next = newnode;
}
注意, vector 的 insert 存在迭代器失效的问题,但是 list 这里并不会存在 迭代器失效的问题。
前面 vector 出现迭代器失效的问题,是因为:在插入元素后,当前迭代器指向的位置会发生改变,有可能会出现野指针
list 是链表插入,不存在迭代器失效的问题。
不过我们也可以写上返回值,返回值就是插入元素位置的 迭代器。
iterator insert(iterator pos, const T& val)
{
//...
return newnode;
}
2.6. erase
删除,删除当前迭代器所指的节点。
void erase(iterator pos)
{
Node* cur = pos._node;
Node* prev = cur->-prev;
Node* next = cur->_next;
delete cur;
prev->_next = next;
next->_prev = prev;
}
这里就要注意,删除当前节点后,迭代器就会失效,原先迭代器指向的节点已经删除了。
所以为了使使用起来能方便点,我们可以让 erase 有个返回值,这个返回值就是删除节点的下一个节点。
iterator erase(iterator pos)
{
//...
return next;
}
2.7. pop,push
有了上面实现的 insert 和 erase,我们就可以实现 pop_back , pop_front , push_back , push_front.
上面我们虽然实现了 push_back , 但是push_back 和 insert 的函数实现基本相似,所以我们也可以使用 insert 实现 push_back。
void push_back(const T& x)
{
insert(end()--,x);
}
void push_front(const T& x)
{
insert(begin(),x);
}
void pop_back()
{
erase(end()--);
}
void pop_front()
{
erase(begin());
}
因为 begin(), end() 能返回迭代器,所以这里可以使用这两个函数来返回。也可以直接给 insert 和 erase 传入 _head->_next 或者 _head->_prev;
这样复用上面函数,实现 push, pop, 可以减少代码量,方便我们实现。
2.8. clear
删除除了头结点之外的所有节点。
我们之前使用的是 使用指针遍历链表,这里我们可以使用迭代器遍历链表
void clear()
{
iterator it = begin();
while (it != end())
{
it = erase(it);
}
}
我们上面实现的 erase ,会返回删除节点的下一个结点,所以这里我们 erase it 后并不需要对 it++,直接接受返回值即可。
2.9. 析构函数
直接复用上面的 clear 函数,然后删除头结点即可。
~list()
{
claer();
delete _head;
_head = nullptr;
}
我们创建节点的时候,每个节点都只是 new 了一块空间,并没有 new Node[] ,所以这里我们使用 delete删除节点就行了。
2.10. 拷贝构造
list(list<T>& lt)
{
empty_init();
for (auto e : lt)
{
push_back(e);
}
}
先对调用函数的对象,进行初始化,然后用 push_back 把 需要拷贝的对象中的顺序一个一个 push进调用函数的对象。
原本我们这里应该传入 const 对象,因为传入的对象我们不能对其修改,但是由于我们上面没有实现 const 迭代器,这里传入 const 的话吗,下面 范围for没有const迭代器使用,就会出现问题。
2.11. 赋值运算符重载
拷贝构造,在 string 的模拟实现中学过,有两种实现方式
第一种是先删除原有数据,然后拿传入数据覆盖进去。
第二种是直接用传入的对象构造一个对象,然后让构造的新对象和需要被赋值的对象成员变量进行交换。
第一种:
list<T>& operator=(const list<T>& lt)
{
if (this != <)
{
clear();
for (auto e : lt)
{
push_back(e);
}
}
return *this;
}
第二种的现代写法
void swap(list<T>& tmp)
{
std::swap(_head, tmp._head);
}
list<T>& operator=(list<T> tmp)
{
swap(tmp);
return *this;
}
2.12. size
返回链表中有数据的节点个数。
这里可以是遍历一遍链表,创建一个 count 变量,最后返回,也可以直接修改 双向链表的成员变量,新建一个 _size 变量,每次插入数据++,删除数据–。
因为我们上面 push,pop 都是直接复用的 insert 和 erase,所以我们只需要对 insert 和 erase 中 加上 _size++ 和 _size-- 即可。
size_t size()
{
return _size();
}
template <class T>
class list
{
//...
private:
Node* _head;
size_t _size;
};
3. const迭代器的实现
上面我们简单实现了 list 的普通迭代器,但是没有实现 const 迭代器,这里我们先简单说一下原因;
我们的const迭代器可以这样写吗
const iterator begin()const
{
return _head->_next;
}
我们要实现的迭代器是可以遍历整个链表的,const迭代器是可以遍历 const 修饰的 list 对象的。
遍历简单来说就是支持上面 ++,==,等操作。
iterator----对普通对象实现可读可写可遍历的操作
const_iterator----const对象实现可读,可遍历
const iterator----只读。
我们前面 对普通迭代器进行封装,所以普通迭代器支持了 ++,–的操作,但是上面那种写法,传入的是 const 对象,返回的是 const 修饰的 迭代器。
因为权限问题,我们这个 const 的 迭代器,是没办法调用普通迭代器的成员函数的。
所以想要让 const 支持的迭代器也能实现 ++,–的操作。
3.1. 方法一:新类
直接和上面实现 iterator 一样,我们直接实现一个 __list_const_iterator 类,这个类支持 const_iterator;
template<class T>
struct __list_const_iterator
{
typedef list_node<T> Node;
typedef __list_const_iterator<T> self;
Node* _node;
__list_const_iterator(Node* node)
: _node(node)
{}
self& operator++()
{
_node = _node->_next;
return *this;
}
self operator++(int)
{
self tmp(*this);
_node = _node->_next;
return tmp;
}
self& operator--()
{
_node = _node->_prev;
return *this;
}
self operator--(int)
{
Node* tmp(*this);
_node = _node->_prev;
return tmp;
}
T& operator*()
{
return _node->_data;
}
// it->_a1 = 10
T* operator->()
{
return &_node->_data;
}
bool operator!=(const self& s)
{
return _node != s._node;
}
bool operator==(const self& s)
{
return _node == s._node;
}
};
template<class T>
class list
{
public:
typedef __list_iterator<T> iterator;
typedef __list_const_iterator<T> const_iterator;
//...
这样,在 xsz 这个命名空间,我们就是实现了 4个类,节点一个结构体,iterator 一个类, const_iterator 的类,还有 list 的类。
在实现了这个之后,我们前面的拷贝构造就可以 传入const对象了。
3.2. 大佬的方式
既然这里的 const_iterator 和 iterator 的主要函数基本上没变,那么我们可不可以把他们写在一起?
我们可以参考一下 stl 中的原码是怎么实现的
原码中是把 T,T& ,T* 都作为模版参数 T,Ref,Ptr传入 __list_iterator 的类中,我们可以试试
template<class T, class Ref, class Ptr>
class __list_iterator
{
typedef list_node<T> Node;
typedef __list_iterator<T, Ref, Ptr> self;
Node* _node;
typedef Ptr pointer;
typedef Ref reference;
__list_iterator(Node* node)
:_node(node)
{}
self& operator++()
{
_node = _node->_next;
return *this;
}
self operator++(int)
{
self tmp(_node);
_node = _node->_next;
return tmp;
}
self& operator--()
{
_node = _node->_prev;
return *this;
}
self operator--(int)
{
self tmp(_node);
_node = _node->_next;
return tmp;
}
reference operator*()
{
return _node->_data;
}
pointer operator->()
{
return &_node->_data;
}
bool operator==(const self& s)
{
return _node == s._node;
}
bool operator!=(const self& s)
{
return _node != s._node;
}
};
template<class T>
class list
{
typedef list_ndoe Node;
typedef __list_iterator<T, T&, T*> iterator;
typedef __list_iterator<T, const T&, const T*> const_iterator;
//...
这里可能不是那么好理解,简单来说,我们可以从表面上把 模版传参理解成含函数传参,只不过模版传的是类型。
传入 T&,T*,那么迭代器的模版就要生成一个对应的迭代器类,传入 const T&, const T* ,迭代器就要生成这个类。
3. 3. 细节问题
1. typename
我们上面实现了 const 类,此时我们想要使用 const 来完成 自己的 print 函数
template<class T>
void print_list(const list<T>& lt)
{
list<T>::const_iterator it = lt.begin();
while (it != lt.end())
{
cout << *it <<" ";
it++;
}
}
我们会发现,这里运行不了
注意这句代码
list<T>::const_iterator it = lt.begin();
list::const_iterator
主要我们要搞清楚这是什么,我们上面认为这是一个类型名,但是这种写法和静态成员变量的使用会发生冲突。
编译器会认为 const_iterator 可能是静态成员变量,也可能是类型名,会起冲突。
所以我们要在前面强调,他就是个类型名
typename list<T>::const_iterator it = lt.begin();
这样这里就能正常运行了。
2. print_container
上面我们实现了 print_list 函数,可以对我们模拟实现的 list 中的各种类型进行输出。
既然所有的迭代器遍历写法都是相同的,我们有没有办法写一个函数,让这个函数对所有的容器都能输出?
templatee<typename container>
void print_container(const container& con)
{
typename container::const_iterator it = con.begin();
while (it != end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
3. “ -> ”的运算符重载
这里有个小优化。
如果我们模拟实现的 list 对象内部成员是 自定义类型
struct AA
{
AA(int a1 = 0, int a2 = 0)
:_a1(a1)
,_a2(a2)
{}
int _a1;
int _a2;
};
void test_list3()
{
xsz::list<AA> lt;
lt.push_back(AA(1,2));
lt.push_back(AA(3,4,));
lt.push_back(AA(5,6));
xsz::list<AA>::iterator it = lt.begin();
while (it != lt.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
首先,这里最先报的错就是 cout << *it 这里,因为我们的 AA 类并没有实现cout 的功能,所以这里会出问题,那么应不应该支持 cout 呢。
如果支持 cout 我们应该怎么输出,是 _a1 和 _a2 中间空一格吗,还是不空,他们中间要加上 ‘|’ 吗,不确定,因为不一定满足所有的需求,所以最好的方法就是不写 cout(vector 就没有实现 cout,cin的操作)。
这种情况下,要么 AA 实现一个 a1函数 a2函数的接口,返回 _a1,_a2 的值,要么函数成员变量设置为公有让直接访问,外面爱怎么输出怎么输出。
这里是采用了公开成员变量让外界访问
所以我们这里直接访问就行
cout << (*it)._a1 << " " << (*it)._a2 << endl;
OK,那么问题来了,我们实现的迭代器是想像指针那样使用的,但是这里用到了解引用,很麻烦,但是我们可以直接使用 -> 吗
cout << it._node->_data._a1 << endl;
我们就要这样写,因为 it 并不是 _data 的指针,it 是迭代器,成员变量是 _data,我们不能像使用指针那样使用它。
因此我们可以想办法让 -> 返回的就是 _data 的地址,这里就能通过 重载 -> 实现
T* operatoe->()
{
return &_node->_data;
}
还有一个细节:
(it-> ) 会返回 _data 的地址,明明应该后面再加上一个 ->才可以使用
(it->)_a1;
这里也是个编译器的优化,只需要一个就可以实现了。
我们这里的 -> 也是为了 自定义类型准备的。
4. 深拷贝问题
前面模拟实现 vector 存在一个问题
void test1()
{
xsz::vector<string> v;
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
v.push_back("11111111111111111");
for (auto e : v)
{
cout << e << " ";
}
cout << endl;
}
这里之前发生的问题是只有最后一个能输出,因为memcpy 只能是浅拷贝,也就是只会把 内部成员 stirng对象的地址拷过去,所以前面4个都是乱码。
但是 list 我们需要考虑这个问题吗
这里使用 我们模拟实现的 list 是没有问题的。
因为每个 string 对象对 list 来说只是相当于一个节点,在进行其他节点插入操作的时候,是不会印象其他节点的地址的。这也是链表优于数组顺序表的地方。
最后呢,因为前面考试多,加上英语4级考试,事情确实比较多,最近还要做数据结构和数字电路逻辑的课设,写的速度会慢很多。
总之这里先祝愿看到的大学生考试不挂科,考英语46级的,考研的必过😁