网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
node\* _pnode;
// 构造函数
\_\_list\_iterator(node\* p)
:\_pnode(p)
{}
};
既然原生指针不支持,那么就亲自上手写一个满足自身需求的封装类,首先我们的需求是要遍历链表,所以需要一个结点的指针负责指向结点 ( `node* _pnode` ), 怎么初始化指针呢? – 根据所传参数初始化即可
#### 3.5.2 operator\* 重载解引用
解引用想要获取当前指针`_pnode`所指向的数据
//解引用 operator\*
T& operator\*()
{
return _pnode->_data;
}
#### 3.5.3 operator++ 重载++
++想指向当前指针的next(下一结点)
__list_iterator& operator++()
{
_pnode = _pnode->_next;
return *this;
}
返回值为啥要写成`__list_iterator<T>&`类型呢?
迭代器++后还是迭代器
#### 3.5.4 operator!= 重载不等于
bool operator!=(const __list_iterator<T>& it)
{
return _pnode != it._pnode;
}
### 3.6 迭代器区间
iterator begin()
{
//匿名对象
return iterator(_head->_next);
}
iterator end()
{
//匿名对象
//iterator it(\_head);
//return it;
return iterator(_head);
}
begin就是哨兵位的下一位(\_head->\_next) ,end就是哨兵位(\_head) 因为是带头双向循环,而且[ begin(),end() ) 是左闭右开区间,访问不到end() 正好契合迭代器区间的要求\_head->prev就是尾结点
到这里简易版本的list就实现了,测试接口
![在这里插入图片描述](https://img-blog.csdnimg.cn/f293bc82479246deb5b69801abb87082.png)
## 4. 深刻理解迭代器
### 4.1 从逻辑结构分析:
![在这里插入图片描述](https://img-blog.csdnimg.cn/673ce8a21fb940b9b71dcb90abd54e2e.png)
![在这里插入图片描述](https://img-blog.csdnimg.cn/c01980ca223144fdb51aa2d50d36c443.png)
迭代器的价值是什么?
如果说我们不采用迭代器的方式(STL库)来实现这些数据结构(vector/list),我们在提供这些访问方式是需要暴露底层的实现细节,在使用时必须告诉使用者我们底层实现的方式。同样的使用者在使用的过程中很可能不小心对底层的逻辑结构进行更改导致出现问题(非常不安全)
STL的大佬就提出了迭代器的概念(STL库中六大组件之一)
1. 将底层实现封装起来,不暴露底层的实现细节
2. 提供统一的访问方式,降低使用成本
C++是怎么做到可以提供统一的方式来访问呢?
类的独特价值,对于内置类型解引用直接访问到数据,而对于自定义类型无法做到,那么我们就进行运算符重载(按照自己的想法实现),
这其中C++的引用的作用也不可替代,传引用返回更改对应的数据
对于C语言来说,没有类,没有模板,没有运算符重载,没有引用,是无法实现的。
其实就相当于迭代器帮我们承受了底层的实现细节,我们在上层调用时才能轻松/一致
### 4.2 从内存的角度分析:
list的迭代器其中只包含node\* \_pnode的指针,所占字节为4字节,尽管list迭代器当中包含大量的自定义实现的函数接口,但这些接口都不占用内存空间,所以**归根结底 list迭代器占用内存空间大小和vector的原生指针一致**
vector的迭代器就是T\*类型的原生指针,所占字节为4字节
其中it = lt.begin() 时,发生了拷贝构造,又因为我们自身并未实现对迭代器的拷贝构造,所以编译器自动生成浅拷贝it和begin()所指向一个结点(符合要求)
### 4.3 通过调试来深刻理解:
![在这里插入图片描述](https://img-blog.csdnimg.cn/c9872471e58343f6bf421917eaa01b6e.png)
![在这里插入图片描述](https://img-blog.csdnimg.cn/ca2cbedde8e9487ba33085e7795fdde5.png)
## 5. 完善list功能
### 5.1 删除pos位置
void erase(iterator pos)
{
// 不能将哨兵位的头结点删除
assert(pos != end());
node\* prev = pos._pnode->_prev;
node\* next = pos._pnode->_next;
prev->_next = next;
next->_prev = prev;
delete pos._pnode;
}
但是这种删除无法有效解决迭代器失效的问题,所以需要记录迭代器的返回值
iterator erase(iterator pos)
{
// 不能将哨兵位的头结点删除
assert(pos != end());
node\* prev = pos._pnode->_prev;
node\* next = pos._pnode->_next;
prev->_next = next;
next->_prev = prev;
delete pos._pnode;
// 拿next来构造迭代器对象
return iterator(next);
}
### 5.2 在pos位置前插入
// 在pos位置之前插入 newnode
iterator insert(iterator pos, const T& x)
{
node* newnode = new node(x);
node* cur = pos._pnode;
node* prev = pos._pnode->_prev;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
// 更新迭代器
return iterator(newnode);
}
### 5.3 复用接口(头/尾-插入/删除)
// 尾插->也就是在end之前插入
void push\_back(const T& x)
{
insert(end(), x);
}
// 头插->在begin之前插入
void push\_front(const T& x)
{
erase(begin(),x);
}
// 头删 删除begin()位置
void pop\_front()
{
erase(begin());
}
// 尾删 删除end前位置
void pop\_back()
{
erase(--end());
}
### 5.4 析构函数
~list()
{
clear();
delete _phead;
_phead = nullptr;
}
void clear()
{
iterator it = lt.begin();
if (it != end())
{
// 这边erase过后迭代器不能++,因为迭代器已经失效了
// 但是我们记录下的erase过后迭代器的位置,所以用it =
it = erase(it);
}
}
### 5.5 拷贝构造(传统写法)
// 模拟stl库实现一下封装
void empty\_initialize()
{
_head = new node(T());
_head->_next = _head;
_head->_prev = _head;
}
// 拷贝构造
list(const list<T>& lt)
{
empty\_initialize();
for (const auto& e : lt)
{
push\_back(e);
}
}
传统写法的深拷贝 – 复用接口
先构造出带头双向循环的初结构,不断的进行push\_back()
### 5.6 赋值重载(传统写法)
// lt1 = lt3
list<T>& operator=(const list<T>& lt)
{
if (this != <)
{
clear(); // (this->)clear();
for (const auto& e : lt)
{
push\_back(e); // (this->)push\_back(e);
}
}
return \*this;
}
lt1(this) 当中是存在数据的,那么首先需要将lt1当中的数据清空 再不断的push\_back尾插数据
其中**const auto& e是重点** 不采用引用取别名的方式,e是需要拷贝构造出来的临时对象
### 5.7 拷贝构造(现代写法)
![在这里插入图片描述](https://img-blog.csdnimg.cn/a71083532f7446bd9a9609a73e16b627.png)
### 5.8 赋值重载(现代写法) – 推荐
// 拷贝构造 -- 现代写法
// 参数一定不能传引用
list<T>& operator=(const list<T> lt)
{
// 不传引用 lt就是拷贝构造的临时对象
// 二者交换以后,都正常析构
swap(lt);
return \*this;
}
调用拷贝构造创建出临时对象并不会影响lt本身,所以二者的交换也就是跟临时对象的交换,这种写法明显优于传统写法
传统写法是先清空数据再不断尾插,而现代写法是让编译器拷贝构造个临时对象再交换数据
## 6. const 迭代器(重点)
void print\_list(const list<int>& lt)
{
const list<int>:: iterator cit = lt.begin();
}
提问:在普通迭代器前加const进行修饰是否就是const迭代器?
![在这里插入图片描述](https://img-blog.csdnimg.cn/8fb08eb2ef9e4d55afa4599b1066e21b.png)
要实现const迭代器那么就是解引用返回const类型即可,解引用就无法更改数据(达到const迭代器的需求)
![在这里插入图片描述](https://img-blog.csdnimg.cn/3c4bad09a94b47cabd7b50c3954d3f0e.png)
但是我们无法这样实现,因为迭代器对象就是被const所修饰无法进行++操作,只能解引用
除非我们对operator++也进行重载专门重载成const T& operator++() const
但是operator++是需要修改迭代器的,所以无法实现对operator++的重载
那么真正的const迭代器该如何实现呢?
### 6.1 简易版本
直接创建一个const版本的迭代器(跟正常的迭代器区分开)
// 跟普通迭代器的区别
// 可以遍历 但是不能解引用修改 \*it
template <class T>
struct \_\_list\_const\_iterator
{
typedef list_node<T> node;
node\* _pnode;
// 构造函数
\_\_list\_const\_iterator(node\* p)
:\_pnode(p)
{}
// 解引用 operator\*
// 返回const T&
const T& operator\*()
{
return _pnode->_data;
}
__list_const_iterator<T>& operator++()
{
_pnode = _pnode->_next;
return \*this;
}
__list_const_iterator<T>& operator--()
{
_pnode = _pnode->_prev;
return \*this;
}
bool operator!=(const __list_const_iterator<T>& it)
{
return _pnode != it._pnode;
}
};
唯一的区别就是operator\* 返回的是 const T&
operator++ 还是用的普通迭代器的方式
### 6.2 大佬版本(模板的力量)
STL库是由大佬实现的,大佬是不会允许代码出现如此冗余的现象,迭代器和const迭代器的区别就在类名和operator\*的返回值上,其它完全一致
对于`vector<int> / vector<char> / vector<string>` 会根据类模板实例化出三个不同的类型
// 模板的力量:
// 增加1个模板参数
// typedef __list_iterator<T, T&> iterator;
// typedef __list_iterator<T, const T&> const_iterator;
template <class T, class Ret>
struct __list_iterator
{
typedef list_node node;
//typedef的力量
typedef __list_iterator<T, Ret> Self;
node\* _pnode;
// 构造函数
\_\_list\_iterator(node\* p)
:\_pnode(p)
{}
// 解引用 operator\*
Ret operator\*()
{
return _pnode->_data;
}
// iterator it
// \*it
// ++it;
//const T& operator\*() const
//{
// return \_pnode->\_data;
//}
Self& operator++()
{
_pnode = _pnode->_next;
return \*this;
}
Self& operator--()
{
_pnode = _pnode->_prev;
return \*this;
}
bool operator!=(const Self& it)
{
return _pnode != it._pnode;
}
};
这种大佬写的版本本质就是利用类模板会根据参数的不同实例化出不同类型的对象
增加一个Ret参数,根据传入的参数是T&还是const T&生成不同的迭代器
本质上跟我们自己实现两个迭代器没有区别,只是这个操作交给编译器(实例化两个对象) – 代码复用
这也是typedef的力量 `typedef __list_iterator<T, Ret> Self;` 一个简简单单的typedef就可以实现两个类型
对于类名和类型的区别:
普通类 类名等价于类型
类模板 类名不等价于类型
比如:list模板当中 类名叫list 而类型list
类模板中可以用类名来代替类型,但是不建议这样用
// lt2(lt1)
// list(const list<T>& lt) --推荐
list(const list& lt) // 不建议
{
empty\_initialize();
list<T> tmp(lt.begin(), lt.end());
swap(tmp);
}
// lt3 = lt1
// list<T>& operator=(list lt) --推荐
list& operator=(list lt) // 不建议
{
swap(lt);
return \*this;
}
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
list<T> tmp(lt.begin(), lt.end());
swap(tmp);
}
// lt3 = lt1
// list<T>& operator=(list lt) --推荐
list& operator=(list lt) // 不建议
{
swap(lt);
return \*this;
}
[外链图片转存中…(img-0sa5TMb3-1715723732829)]
[外链图片转存中…(img-eisdMmCJ-1715723732829)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!