网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
list_node\* _next;
T _data;
// 结点初始化
list\_node(const T& x)
:\_next(nullptr)
,\_prev(nullptr)
,\_data(x)
{}
};
### 3.2 链表初始化
template
class list
{
typedef list_node node;
public:
list()
{
//node里的data是T类型 T()构造
_head = new node(T());
_head->_next = _head;
_head->_prev = _head;
}
private:
node* _head;
};
链表中只有一个哨兵位头结点,无参构造(初始化),要使得\_next和\_prev都指向自己
**(带头双向循环链表的自洽,这里不理解的可以去参考数据结构中最开始的链表初始化,思路是一致的)**
### 3.3 push\_back
void push_back(const T& x)
{
node* newnode = new node(x);
node* tail = _head->_prev;
// _head tail newnode
_head->_prev = newnode;
tail->_next = newnode;
newnode->_next = _head;
newnode->_prev = tail;
}
创建出来的newnode新结点调用node(x) 使用node的构造函数来创建结点
(不再像数据结构中还需要调用BuyListNode接口malloc空间来初始化,代码如下所示)
LTNode* BuyListNode(LTDataType x)
{
LTNode* node = malloc(sizeof(LTNode));
if (node == NULL)
{
perror(“malloc fail”);
exit(-1);
}
node->data = x;
node->next = NULL;
node->prev = NULL;
return node;
}
### 3.4 迭代器 (重点)
迭代器是属于内嵌类型的,那什么是内嵌类型呢? – 定义在类里面的就叫做内嵌类型
>
> C++的内嵌类型分为两种
>
>
> * 1. 定义在类里面的内部类,内部类是外部类的友元
> * 2. typedef定义的类型名
> 编译器在查找类型时,默认都是去全局找,而对于内嵌类型定义在类内部,那么在全局当中是找不到的,所以对于内部类和typedef的数据使用都需要声明类域
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/ac173b10c4114298b769fe39246472aa.png)
> ![在这里插入图片描述](https://img-blog.csdnimg.cn/a0b038773f9347b8aad291ccfa6a009e.png)
>
>
>
### 3.5 类的封装
typedef __list_iterator iterator;
#### 3.5.1 类成员及构造
template
struct __list_iterator
{
typedef list_node node;
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;
}
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
typedef list_node<T> node;
node\* _pnode;
// 构造函数
\_\_list\_const\_iterator(node\* p)
:\_pnode(p)
{}
// 解引用 operator\*
// 返回const T&
const T& operator\*()
{
return _pnode->_data;
}
[外链图片转存中…(img-vzV4UTjR-1715700365884)]
[外链图片转存中…(img-hcAC7BEu-1715700365884)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新