⸝⋆ ━━━┓
- 个性标签 - :来于“云”的“羽球人”。 Talk is cheap. Show me the code
┗━━━━━━━ ➴ ⷯ本人座右铭 : 欲达高峰,必忍其痛;欲戴王冠,必承其重。
👑💎💎👑💎💎👑
💎💎💎自💎💎💎
💎💎💎信💎💎💎
👑💎💎 💎💎👑 希望在看完我的此篇博客后可以对你有帮助哟👑👑💎💎💎👑👑 此外,希望各位大佬们在看完后,可以互相支持,蟹蟹!
👑👑👑💎👑👑👑
一:简单使用
1.push_front()
push_front():此函数功能就是头插一个数据(在第一个有效数据的前面进行插入)
此函数的参数:任意类型的数据(注意这里是使用引用,避免对象过大)
使用示范:
void test()
{
list<int>l1;// 创建一个list 类型对象
// 对当前对象l1 进行数据的头插
l1.push_front(4);
l1.push_front(3);
l1.push_front(2);
l1.push_front(1);
list<int>::iterator it = l1.begin();
// 遍历当前l1这个对象的所有数据
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
2.push_back()
此函数功能:尾插一个数据(就是在尾结点进行插入数据)
使用示范:
void test()
{
list<int>l1;// 创建一个list 类型对象
// 对当前对象l1 进行数据的尾插
l1.push_back(1);
l1.push_back(2);
l1.push_back(3);
l1.push_back(4);
list<int>::iterator it = l1.begin();
// 遍历当前l1这个对象的所有数据
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
3.pop_front()
pop_front() 函数功能:把第一个有效数据对应的节点进行释放
使用示范:
void test()
{
list<int>l1;// 创建一个list 类型对象
// 对当前对象l1 进行数据的头插
l1.push_front(4);
l1.push_front(3);
l1.push_front(2);
l1.push_front(1);
list<int>::iterator it = l1.begin();
// 遍历当前l1这个对象的所有数据
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
l1.pop_front();//删除第一个数据
cout << "pop_front:";
it = l1.begin();
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
运行结果:
4. pop_back()
pop_back() 函数功能:把最后一个有效数据对应的节点进行释放
使用示范:
void test()
{
list<int>l1;// 创建一个list 类型对象
// 对当前对象l1 进行数据的头插
l1.push_front(4);
l1.push_front(3);
l1.push_front(2);
l1.push_front(1);
list<int>::iterator it = l1.begin();
// 遍历当前l1这个对象的所有数据
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
l1.pop_back();//删除最后一个数据
cout << "pop_back:";
it = l1.begin();
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
运行结果:
5. insert()
insert()函数功能: 在指定位置之前进行数据插入
此函数支持:某个位置插入一个数据,也支持从当前位置插入n 个数据;还支持一段区间数据的插
入
注意insert()函数不存在迭代器失效的问题
使用示范:
void test()
{
list<int>l1;// 创建一个list 类型对象
// 对当前对象l1 进行数据的头插
l1.push_front(4);
l1.push_front(3);
l1.push_front(2);
l1.push_front(1);
list<int>::iterator it = l1.begin();
// 遍历当前l1这个对象的所有数据
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
it = l1.begin();
l1.insert(++it, 22);
it = l1.begin();
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
运行结果:
6. erase()
erase() 函数功能:对指定位置删除
此函数支持:对某个位置的删除;也支持一段区间的删除
list 里面的erase 函数存在迭代器失效的问题:当删除偶数的时候就会失效
使用示范:
void test()
{
list<int>l1;// 创建一个list 类型对象
// 对当前对象l1 进行数据的头插
l1.push_front(4);
l1.push_front(3);
l1.push_front(2);
l1.push_front(1);
list<int>::iterator it = l1.begin();
// 遍历当前l1这个对象的所有数据
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
it = l1.begin();
l1.erase(++it);
it = l1.begin();
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
运行结果:
7. swap()
swap() 函数功能: 对多个list 类型对象进行数据交换
使用示范:
void test()
{
list<int>l1;// 创建一个list 类型对象
// 对当前对象l1 进行数据的头插
l1.push_front(4);
l1.push_front(3);
l1.push_front(2);
l1.push_front(1);
list<int>l2;
l2.push_back(11);
l2.push_back(22);
l2.push_back(33);
l2.push_back(44);
l2.push_back(55);
cout << " 交换之前对应的数据:" << endl;
list<int>::iterator it = l1.begin();
cout << "l1:";
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
list<int>::iterator it1 = l2.begin();
cout << "l2:";
while (it1 != l2.end())
{
cout << *it1 << " ";
++it1;
}
cout << endl;
l1.swap(l2);
cout << "交换之后对应的数据:" << endl;
it = l1.begin();
cout << "l1:";
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
it1 = l2.begin();
cout << "l2:";
while (it1 != l2.end())
{
cout << *it1 << " ";
++it1;
}
cout << endl;
}
运行结果:
不知道一些老铁是否和我一样有个问题:为什么算法库里面有一个swap() 而list 容器还要
自己实现swap(),这是为啥???
这就涉及到迭代器的分类了
迭代器分类
迭代器按功能分:正向迭代器和反向迭代器
迭代器按性质分为:
单向迭代器:只支持 ++ 比如单链表,哈希表
双向迭代器:支持++ 和-- 比如list ,红黑树(map,set)
随机迭代器:++ , -- ,+ ,- 比如vector ,string……
而我们的list 迭代器是双向的,但是算法库里面的swap () 要求迭代器的类型是随机的,所以list
只能自己实现一个swap () 函数
list 迭代器:
8.clear()
clear() 函数功能:清除有效数据
使用示范:
void test()
{
list<int>l1;// 创建一个list 类型对象
// 对当前对象l1 进行数据的头插
l1.push_front(4);
l1.push_front(3);
l1.push_front(2);
l1.push_front(1);
list<int>::iterator it = l1.begin();
// 遍历当前l1这个对象的所有数据
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
l1.clear();//清除有效数据
it = l1.begin();
while (it != l1.end())
{
cout << *it << " ";
++it;
}
cout << endl;
}
运行结果:
二:模拟实现相关函数
在我们模拟list 相关函数之前需要对list 这个容器有一定的了解。通过前面数据结构对链表的
学习,再结合官方文档模拟实现应该是不成问题。
首先list 这一个容器是由3个类进行封装实现的:一个类是节点,一个类是迭代器,一个类是链表
。其中节点和迭代器都是为链表类进行服务的
对list这一容器实现的关键在于迭代器的实现:通过封装和运算符重载从而实现对迭代器的封
装。
准备工作:
注意我这里为了避免与库里面的list 发生冲突,就把自己写的相关代码放在自己定义的y这个命名空
间了(各位在模拟实现的时候,随意,看自己)
1)定义一个节点类
对于双向循环链表的每一个节点而言无非就是由3部分构成的:数据域;前一个节点的地址;后一
个节点的地址
template<class T>
struct list_node // 之所以使用struct 而不用class ,没有特殊情况下,前者默认属性是pubblic
{
T _val;
list_node<T>* _next;
list_node<T>* _pre;
list_node(const T& x = T()) //对于T 的类型并不明确,不能指定_val 就是int类型,就初始化为0,所以采用匿名对象的形式
:_val(x)
, _next(nullptr)
, _pre(nullptr)
{}
};
2)定义一个迭代器类
此时的迭代器也是模拟指针的行为:所以迭代器还是指向一个一个的节点
template <class T>
struct list_iterator// 迭代器类
{
typedef list_iterator<T> self;//对当前迭代器进行重命名
typedef list_node Node;
Node* node;//迭代器其实就是指向每一个的节点的
//迭代器涉及到的相关函数
list_iterator(Node* node) // 不需要实现析构函数
:_node(node)
{}
T& operator*()
{
return _node->_val;
}
T* operator->()
{
return &_node->_val;//返回当前数据对应的地址
}
self& operator++()
{
_node = _node->_next;
return *this;//前置++ 不涉及到临时对象,可以使引用返回
}
self operator++(int)//后置++
{
Node* tmp(_node);//创建一个临时对象,因为后置++ 返回的是++之前的对象
_node = _node->_next;
return tmp;
}
self& operator--()
{
_node = _node->_pre;
return *this;
}
self operator--(int) // 后置--
{
Node* tmp(_node);
_node = _node->_pre;
return tmp;
}
};
3) const 迭代器
接下来思考一个问题是不是const 迭代器就是对普通迭代器加一个const 进行修饰即可???
看看以下写法是否正确
普通迭代器: iterator
const 迭代器: const iterator
答案自然是错误的:
相信有不少人在初学迭代器相关内容的时候,cosnt 迭代器无非就是对普通迭代器加一个cosnt 进
行修饰嘛。OK 那思考以下问题
cosnt T*
T* const
是否有区别??? 若有区别,具体又是啥区别???
有区别的
cosnt T* : 是对 T* 进行修饰,换言之就是对当前指针指向的内容修饰(当前的数据不可以
修改,只能读)
T* cosnt : 修饰的是当前指针,不能对指针进行修改
cosnt 迭代器和普通迭代器是两个不同的类型
const 迭代器模拟
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)
{}
//拷贝构造
_list_const_iterator(const 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->_pre;
return *this;
}
//后置--
self operator--(int)
{
self tmp(*this);
_node = _node->_pre;
return tmp;
}
//相比较于普通迭代器内部实现:只是对数据const T* ,const T& 进行了更改
const T* operator->()
{
return &_node->_val;
}
const T& operator*()
{
return _node->_val;
}
//对一些比较运算符进行重载的时候,因为并不该变对象,所以最好加上const 修饰
bool operator!=(const self& it)
{
return _node != it._node;
}
bool operator==(const self& it)
{
return !(_node == it._node);
}
};
通过对比普通迭代器和cosnt 迭代器我们发现:有很多的函数都是重复的,,所以我们可以使用模
板来进行简化
编译器会根据当前数据类型的不同,自动生成对应的函数
假设存储数据类型是T (表示所有的数据类型)
普通迭代器对应的参数类型:T,T&,T*
const 迭代器对应参数类型:T ,const T&,const T*
使用模板:Ref 表示数据的引用,Ptr 表示当前数据的指针类型
template<class T,class Ref,class Ptr> // 此时的Ref可以表示T& 或者 coonst T& ;Ptr可以表示T* 或者const T*
struct _list_iterator //迭代器其实就是对当前节点进行一系列的封装
{
typedef _list_iterator<T,Ref,Ptr> self;// 进行重命名
typedef list_node<T> Node;
Node* _node;
_list_iterator(Node* node)// 迭代器的构造函数 不需要写析构函数(析构函数是把当前这个节点释放,但是在使用迭代器的时候并不需要进行节点释放在某些场景下)
:_node(node)
{}
//T& operator*()
//{
// return _node->_val;
//}
//T* operator->()// 意义:对自定义类型数据访问更加方便,原始访问:(*it).某个具体成员
//{
// return &(_node->_val);//返回当前数据的地址
//}
//self& operator++()
//{
// _node = _node->_next;//注意返回的是一个迭代器
// return *this;
//}
//self& operator++(int)
//{
// Node* tmp(_node);
// _node = _node->_next;
// return tmp;
//}
//self*& operator--()
//{
// _node = _node->_pre;
// return _node;
//}
//self*& operator--(int)
//{
// Node* tmp(_node);
// _node = _node->_pre;
// return tmp;
//}
Ref operator*()
{
return _node->_val;
}
Ptr operator->()// 意义:对自定义类型数据访问更加方便,原始访问:(*it).某个具体成员
{
return &(_node->_val);//返回当前数据的地址
}
self& operator++()
{
_node = _node->_next;//注意返回的是一个迭代器
return *this;
}
self& operator++(int)
{
Node* tmp(_node);
_node = _node->_next;
return tmp;
}
self& operator--()
{
_node = _node->_pre;
return _node;
}
self& operator--(int)
{
Node* tmp(_node);
_node = _node->_pre;
return tmp;
}
bool operator==(const self& s)
{
return _node == s;
}
bool operator!=(const self& s)
{
//return _node != s;// 应该比较的是 _node
return _node != s._node;
}
};
4) 反向迭代器的定义
4.1) 库里面反向迭代器使用
反向迭代器其实就是对正向迭代器的一个反向移动:正向迭代器向右移动,反向迭代器向左移动
而且反向迭代器的所有可能和正向迭代器的实现是一样的。所有可以使用正向迭代器复用并适配出
一个反向迭代器
4.2) 类模板的定义
4.3)类的成员函数实现
构造函数:使用一个正向迭代器构造出一个反向迭代器
一些相关运算符重载
Self& operator++()
{
//对正向迭代器--
--_it;
return *this;
}
Self& operator--()
{
//对正向迭代器++
++_it;
return *this;
}
// 注意这里的形参类型必须是const 引用,当比较2个迭代器的时候是比较的当前迭代器的临时拷贝的对象,就有常性
bool operator!=( const Self& s)
{
return _it != s._it;
}
bool operator==(const Self& s)
{
return _it == s._it;
}
Refence operator*()
{
// 先对正向迭代器--,在进行解引用
iterator cur(_it);// 没有进行重命名
return *(--cur);
}
Ptr operator->()
{
// 返回反向迭代器的指针
return &(operator*());
}
stl 定义的反向迭代器
画个草图更易于理解:
小结一下:
反向迭代器是对正向迭代器的一个封装,进而适配出想要的对应类型的反向迭代器,而对于list 的
正向迭代器而言,又是对指针进行 的封装,可以说是这里进行了双层的封装
5)定义一个链表类
struct list//链表类
{
typedef list_node<T> Node;
private:
size_t _size;//统计节点的个数
Node* _head;//哨兵位
pubblic:
void empty_init()
{
_head = new Node;
//构成环
_head->_next = _head->_pre = _head;
_size = 0;
}
typedef list_iterator<T> self;//对当前迭代器进行重命名
list()
{
empty_init();
}
};
1.push_back() 模拟实现
尾插也就是在尾结点(_head-> _pre)后面进行数据插入,之后在进行指针连接
void push_back(const T& x) // 使用T:来表示任意类型数据
{
Node* tail = _head->_pre;
Node* newnode = new Node(x);
tail->_next = newnode;
newnode->_next = _head;
newnode->_pre = tail;
//注意不要忘记对_head->pre进行链接
_head->_pre = newnode;
}
2.push_front() 模拟实现
头插:在第一个数据前面插入(_head-> _next)
void pop_back()
{
Node* del = _head->_pre;
del->_pre->_next = _head;
_head->_pre = del->_pre;
delete del;
}
3. pop_back() 模拟实现
尾删:把最后一个数据对应的节点释放,同时让倒数第二个节点成为新的尾结点
void pop_back()
{
Node* del = _head->_pre;
del->_pre->_next = _head;
_head->_pre = del->_pre;
delete del;
}
4. pop_front() 模拟实现
void pop_front()
{
Node* del = _head->_next;
_head->_next = del->_next;
del->_next->_pre = _head;
delete del;
}
5. insert( ) 模拟实现
insert( )一般默认是在指定位置之前进行数据的插入
注意节点连接的先后顺序问题
iterator insert(iterator pos,const T& x)
{
//默认在pos 位置之前插入
Node* newnode = new Node(x);
Node* pre = pos._node->_pre;
newnode->_next = pos._node;
pos._node->_pre = newnode;
newnode->_pre = pre;
pre->_next = newnode;
++_size;
return newnode;//返回插数据对应的迭代器
}
6. erase ()模拟实现
erase( ) 默认是把指定位置的数据删除
相信初次模拟此函数的时候,有不少老铁是这样写的吧:
注意这样写是有问题的:迭代器失效
当我们再访问pos 下一个位置的时候就会出现迭代器失效的问题
解决:返回下一个节点对应的迭代器
正确代码:
iterator erase(iterator pos)
{
//进行判空
//assert(!empty());
//删除pos位置数据
Node* pre = pos._node->_pre;
Node* next = pos._node->_next;
pre->_next = next;
next->_pre = pre;
delete pos._node;//此时迭代器失效
--_size;
return next;//返回下一个节点,解决迭代器失效问题
}
7.clear ( ) 模拟实现
一般写法:就是一个节点一个节点进行释放
其实完全可以借助erase( ) 函数进行编写:因为erase () 函数返回的就是下一个节点的迭
代器
void clear()//清除有效数据
{
list<T>::iterator it = begin();// 其实是this 对象调用当前函数
//传统写法:就是保留下一个节点,删除当前节点
//现代写法:
while (it != end())
{
it = erase(it);//erase() 返回的就是下一个节点对应的迭代器
}
_size = 0;
}
8. 析构函数
此时还是借用clear ( ) 函数进行
只不过只需要把头结点进行释放即可
~list()
{
clear();
delete _head;
_head = nullptr;
}
9. swap()函数模拟实现
直接调用库里面的函数 swap(),交换链表对应的头结点以及_size 的大小即可
void swap(list<int>& l)
{
std::swap(_head, l._head);
std::swap(_size, l._size);
}
10.operator=() 函数重载
传统写法:
// l2 = l1 :l2是对l1 的拷贝构造
//传统写法
iterator operator=(const list<int>& l)//l 指向对象 l1 this 指向当前对象l2
{
//第一次写拷贝构造函数的时候,编译错误:const 对象调用非const对象,权限扩大
//解决:需要const 迭代器
//const 与非const迭代器区别:无非就是对应函数返回值类型不一样==》 借用函数模板
clear();//对l2进行数据清理
list<int>::iterator it = l.begin();
while (it != l.end())
{
push_back(*it);
++it;
}
}
注意一些细节的实现:当我们没有实现对 const 迭代器的时候,此时编译是有问题的,因为
此时对象的属性是cosnt ,编译器只能调用非const 对应的函数,自然造成权限放大的问题,
导致编译不过
解决:写一个cosnt 迭代器
现代写法:
list<int>& operator=( list<int>& l)
{
swap(l);
return *this;
}
11 list 迭代器实现
1)list 普通迭代器
typedef _list_iterator<T,T&,T*> iterator;
iterator begin()
{
//以下代码的调试过程是一样 的:不管你是隐式类型转换还是显示类型转换,都会去调用单参数的构造
//return (iterator)_head->_next;
return _head->_next;
}
iterator end()
{
return _head;
}
2) list 的const 迭代器
typedef _list_iterator<T,const T&,const T*> const_iterator;
const_iterator begin() const
{
//以下代码的调试过程是一样 的:不管你是隐式类型转换还是显示类型转换,都会去调用单参数的构造
//return (const_iterator)_head->_next;
return _head->_next;
}
const_iterator end() const
{
return _head;
}
12 typename 使用
在list 类里面实现一个模板函数:打印list 容器里面的数据
此时发现:编译报错
分析:
在编译阶段,list<T >是一个还没有实例化的模板,编译器就不能去这个类里面找。
就无法知道 list<T>::iterator it 到底是一个静态成员变量还是一个内嵌类型
解决:
把模板里面的class 换成 typename ,同时在 list<T>::iterator it 前面加一个typename ,告知编译器
list<T>::iterator it 是一个内嵌类型
结语:
以上就是我今日share 的内容。对于list 这个容器模拟实现的关键在于对迭代器的掌握(进行封
装和运算符重载,屏蔽底层实现的细节,让使用者以统一的方式进行使用,但是迭代器类型不同在
底层实现也是不一样的,可以这么说,迭代器其实就是模拟指针的行为),但是迭代器不一定就是
指针,通过对list 模拟实现,发现有了模板,我们可以省去许多不必要的操作。