当前位置: 首页 > news >正文

【C++】list容器的模拟实现

目录

1. 节点(list_node) 的结构

2. 哨兵位头节点

3. list容器的成员变量

4. 插入/删除操作

4.1 插入操作(insert)

4.2 删除操作(erase)

5. 迭代器的实现

 6. 不同迭代器和const容器的限制

7. 重载operator->

8. 迭代器失效问题

insert操作

erase操作

9. 析构函数

10. 拷贝构造函数

11. 赋值运算符重载

传统写法

现代写法

12. C++11引入的列表初始化

13. 总结


C++标准库中的list底层是双向循环链表,这是一种与vector(动态数组)完全不同的数据结构,核心特点是节点独立存储,通过指针连接,因此在插入/删除操作上有独特优势。

1. 节点(list_node) 的结构

template<class T>
struct list_node 
{T _data;                  // 存储节点数据list_node<T>* _prev;      // 正确:指向前一个节点list_node<T>* _next;      // 正确:指向后一个节点// 节点构造函数(初始化数据和指针)list_node(const T& val = T()) : _data(val), _prev(nullptr), _next(nullptr) {}
};

2. 哨兵位头节点

曾经实现单链表的时候,进行尾插操作,那么我们要判断当前链表是否为空,如果链表为空,直接插入;如果链表不为空,找到尾节点再插入。为了简化边界判断,list中会额外创建一个哨兵位头节点(不存储实际数据),整个链表形成双向循环结构,链表为空时,哨兵位的_prev和_next都指向自己。

3. list容器的成员变量

list类内部只存储两个核心信息:

template<class T>
class list
{
private:list_node<T>* _head; // 指向哨兵位头节点的指针size_t _size;        // 记录有效元素个数(非必需,但方便快速获取大小)
};

4. 插入/删除操作

list的插入/删除操作远高于vector,核心原因是:只需修改指针,无需移动元素。

4.1 插入操作(insert)

//在 pos 迭代器指向的节点前插入val
iterator insert(iterator pos, const T& val)
{Node* cur = pos._node;   //pos 指向的节点Node* prev = cur->_prev; //pos 前一个节点Node* newnode = new Node(val);//创建新节点//调整指针:  prev  newnode  cur     newnode->_next = cur;newnode->_prev = prev;prev->_next = newnode;cur->_prev = newnode;++_size;  //有效元素+1    return newnode; //返回指向新节点的迭代器
}

4.2 删除操作(erase)

iterator erase(iterator pos)
{assert(pos != end());Node* cur = pos._node;   //要删除的节点Node* prev = cur->_prev; //前一个节点Node* next = cur->_next; //后一个节点//调整指针: prev  cur  nextprev->_next = next;next->_prev = prev;delete cur; //释放节点内存--_size;    //有效元素-1return next; //返回被删除元素的下一个有效迭代器
}

5. 迭代器的实现

list迭代器本质是节点指针的封装,通过重载++/--运算符实现遍历。

//普通迭代器(可修改元素)
template<class T>
struct list_iterator 
{typedef list_node<T> Node;typedef list_iterator<T> Self;Node* _node;list_iterator(Node* node) : _node(node) {}T& operator*() { return _node->_data; }    // 返回非const引用,允许修改T* operator->() { return &_node->_data; }  // 返回非const指针Self& operator++() { _node = _node->_next; return *this; }Self operator++(int) { Self temp(*this); _node = _node->_next; return temp; }Self& operator--() { _node = _node->_prev; return *this; }Self operator--(int) { Self temp(*this); _node = _node->_prev; return temp; }bool operator!=(const Self& s) const { return _node != s._node; }bool operator==(const Self& s) const { return _node == s._node; }
};

实现 list 的const迭代器const_iterator)的核心目标是:允许遍历元素但禁止通过迭代器修改元素的值,它的实现逻辑与普通迭代器(iterator)类似,需要通过修改解引用和箭头运算符的返回类型来限制写操作。

  • 普通迭代器(iterator):解引用返回T&,箭头运算符返回T*,允许通过迭代器修改元素(*it = value 或 it->member = value)。
  • const迭代器(const_iterator):解引用返回const T&,箭头运算符返回const T*,仅允许读取元素,禁止修改(*it 和 it->member 都是只读的)。

我们有两种方式实现它:

方式1:

直接复制普通迭代器的代码,仅修改operator*和operator->的返回类型,其余操作(++、--、比较等)完全复用,但是这种方式代码冗余,重复代码太多。

//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) {}// 核心区别:返回const引用/指针,禁止修改元素const T& operator*() { return _node->_data; }    // 只读const T* operator->() { return &_node->_data; }  // 只读Self& operator++() { _node = _node->_next; return *this; }Self operator++(int) { Self temp(*this); _node = _node->_next; return temp; }Self& operator--() { _node = _node->_prev; return *this; }Self operator--(int) { Self temp(*this); _node = _node->_prev; return temp; }bool operator!=(const Self& s) const { return _node != s._node; }bool operator==(const Self& s) const { return _node == s._node; }
};

方式2:

用模版参数复用代码,将普通迭代器和const迭代器的共性代码合并到一个模版中,仅通过参数控制是否为const。

template<class T, class Ref, class Ptr>  //Ref: T& 或 const T&;  Ptr: T* 或 const T*
struct list_iterator
{typedef list_node<T> Node;typedef list_iterator<T, Ref, Ptr> Self;Node* _node;list_iterator(Node* node) : _node(node) {}// 解引用和箭头运算符的返回类型由模板参数控制Ref operator*() { return _node->_data; }    // Ref为const T&时,返回只读引用Ptr operator->() { return &_node->_data; }  // Ptr为const T*时,返回只读指针// 移动操作完全复用(与是否const无关)Self& operator++() { _node = _node->_next; return *this; }Self operator++(int) { Self temp(*this); _node = _node->_next; return temp; }Self& operator--() { _node = _node->_prev; return *this; }Self operator--(int) { Self temp(*this); _node = _node->_prev; return temp; }bool operator!=(const Self& s) const { return _node != s._node; }bool operator==(const Self& s) const { return _node == s._node; }
};

list容器在定义普通迭代器和const迭代器这两种具体类型时,主动明确地把它们的具体值(比如T&或const T&)传给list_iterator模版,从而生成了“能改”和“不能改”两种不同功能的迭代器。在list类提供const版本的begin()和end(),用于const对象的遍历:

template<class T>
class list 
{
private:typedef list_node<T> Node;Node* _head;  // 哨兵节点size_t _size;public://1.定义普通迭代器:Ref=T&  Ptr=T* typedef list_iterator<T, T&, T*> iterator;//2.定义const迭代器:Ref=const T&  Ptr=const T*typedef list_iterator<T, const T&, const T*>  const_iterator;// 普通迭代器接口  iterator begin() { return _head->_next; }  //第一个有效节点iterator end() { return _head; }           //哨兵节点(尾后位置)// const迭代器接口(供const对象使用)   const_iterator begin() const { return _head->_next; }  //第一个有效节点const_iterator end() const { return _head; }           //哨兵节点(尾后位置)// …… 其他成员函数(构造、push_back等,省略)
};

遍历打印元素

template<class Container>
void print_container(const Container& con)
{typename Container:: const_iterator it = con.begin(); //auto it = con.begin();while (it != con.end()){//*it += 10;  编译错误:const_iterator禁止修改元素cout << *it << " "; //可以读取++it;}cout << endl;for (auto e : con){cout << e << " ";}cout << endl;
}

 6. 不同迭代器和const容器的限制

	void test_list2(){list<int> lst = { 1,2,3 };//1. 普通迭代器		list<int>::iterator it = lst.begin();*it = 10;//合法:可修改元素++it;//合法:可移动迭代器//2. const迭代器(元素不可修改的迭代器)list<int> ::const_iterator cit = lst.begin();//*cit = 20; 报错:不能修改元素 (const_iterator特性)++cit;//合法:可移动迭代器//3. const修饰迭代器(迭代器变量本身不可修改),【实际这种迭代器几乎不用】//情况A:const修饰普通迭代器const list<int>::iterator const_it1 = lst.begin();*const_it1 = 30;//合法:普通迭代器仍可修改元素  但只能改第一个元素,使用场景极窄!//++const_it1;  //报错:迭代器变量本身不可移动  无法修改第二个、第三个元素//情况B:const修饰const_iterator(迭代器不可移动,元素也不可修改)const list<int>::const_iterator const_it2 = lst.begin();//*const_it2 = 40;  //报错:不能修改元素     //++const_it2;      //报错:迭代器本身不可移动cout << *const_it2;       //只能读第一个元素,使用场景极窄!//4. const容器  ->"容器的状态不可变"->而容器的状态不仅包括内部指针,还包括其管理的元素const list<int> const_lst = { 4,5,6 };list<int>::const_iterator clst = const_lst.begin(); //const容器只能返回const迭代器//*clst = 50;  //报错:const容器元素不可修改++clst;        //合法:迭代器本身可移动//const_lst.push_back(7); //报错:容器对象状态不可改变(包括容器长度、节点数量、节点存储的数据等), //push_back是非const的成员函数,const容器只能调用const成员函数,添加、删除、清空元素同样都不可以。}

7. 重载operator->

为什么要重载operator->呢?

假设链表存储自定义对象,相当于链表的每一个节点存储的都是对象

struct Person
{string name;int age;
};list<Person> people;  // 存储Person对象的链表

当我们用迭代器it指向链表中的某个Person对象时,需要访问其成员(如name或age),如果没有重载operator->,访问方式会是:

list<Person> lst;
lst.push_back({ "张三", 20 });
auto it = lst.begin();  //迭代器,指向第一个Person对象(*it).name;   //先解引用迭代器得到Person对象,再用.访问成员           

当写*it时,本质是调用 it.operator*(),这个函数会通过迭代器的_node找到对应的链表节点,返回节点中_data的引用即(Person&类型),*it等价于 “迭代器所指向节点中的Person对象。

 operator->的重载逻辑

T* operator->()
{return &_node->_data;  // 返回指向节点中数据的指针
}

而有了operator->重载后,访问方式可以简化为:

list<Person> lst;
lst.push_back({ "张三", 20 });
auto it = lst.begin();  //迭代器,指向第一个Person对象it->name;   //迭代器用->访问成员(看似一步,实则两步)编译器会自动拆解为(it.operator->())->name

编译器会把 it->name; 这个表达式自动拆解为两步:

  • 第一步:显示触发重载操作,执行 it.operator->(),得到 Person* 指针(指向节点中存储的Person对象的指针),取名叫 p。
  • 第二步:再对 p 执行 “原生 -> 操作”:p->name(这一步是隐藏的,编译器自动补全)。

总共 2 次-> 相关操作,其中第 2 次是编译器按标准自动隐藏执行的,目的是让迭代器用起来和原生指针一样简单。

不管是标准库还是自定义的迭代器,只要正确重载了operator->,编译器就会自动补充第二次->,这是C++标准规定的行为,目的是让类类型的对象可以模拟指针的->操作。

这种设计的目的是让迭代器的用法和原生指针完全一致,降低使用成本,如果编译器不自动补充第二次->,用户就必须写成( it.operator->( ) ) -> name,不仅麻烦,还会让迭代器和原生指针的用法脱节,违背了迭代器“模拟指针”的设计初衷。

8. 迭代器失效问题

在C++中,list的迭代器失效问题和vector 等连续内存容器有显著区别,这源于list当节点式存储结构(非连续内存)。

insert操作

插入元素时,只会在目标位置创建新节点,并调整相邻节点的指针,不会改变原有任何节点的内存地址,因此,所有已存在的迭代器(包括插入位置的迭代器)都不会失效。

标准库实现insert,返回值为指向新插入元素的迭代器,插入后可直接通过返回值操作新元素。【4.1插入操作】

list<int> lst = {1, 3, 4};
auto it = lst.begin();  // 指向1的迭代器
lst.insert(++it, 2);    // 在3前插入2,lst变为{1,2,3,4}
// 原it仍指向3(有效),新节点2的迭代器需通过insert返回值获取

erase操作

删除元素时,被删除的节点会被销毁,指向该节点的迭代器会失效;但其它节点的内存地址没变,因此除了被删除节点的迭代器外,其他所有迭代器仍然有效。

erase返回指向被删除元素的下一个元素的迭代器,避免使用已失效的迭代器。【4.2删除操作】

//删除偶数
std::list<int> lst = {1, 2, 3, 4};
auto it = lst.begin();
while (it != lst.end()) 
{if (*it % 2 == 0) {//lst.erase(it);it已失效,不能再使用,下一次判断会导致未定义行为it = lst.erase(it);  //用返回值更新it(指向被删元素的下一个)} else {++it;  // 奇数不删除则正常移动迭代器}
}
// 最终lst为{1,3}

9. 析构函数

第一种实现:

~list()
{Node* current = _head->_next; //从第一个有效节点开始遍历while (current != _head){Node* nextNode = current->_next; //先保存下一个节点delete current; //销毁当前节点current = nextNode; //移动到下一个节点}//销毁哨兵节点delete _head;_head = nullptr;			//重置大小_size = 0;cout << "链表已销毁" << endl;
}

第二种实现:复用clear() 和 erase()

~list()
{clear();delete _head; //释放哨兵节点_head = nullptr;_size = 0;cout << "链表已销毁" << endl;
}void clear()
{auto it = begin();while (it != end()){it = erase(it); //复用erase逻辑删除单个节点}	
}

10. 拷贝构造函数

在链表中,必须手动实现拷贝构造函数,不能依赖编译器默认生成的默认拷贝构造函数,核心原因是:编译器默认的拷贝构造函数是浅拷贝,仅复制指针值,导致多个链表共享节点内存,引发双重释放、野指针等问题(原链表和拷贝出的新链表会共享同一份节点内存,当其中一个链表析构时,导致另一个链表的指针变成野指针,指向已释放的内存,若对其中一个链表修改,会直接影响另一个链表,两个链表析构时,会双重释放导致程序崩溃)。

手动实现拷贝构造函数需要完成深拷贝:为新链表创建独立的节点,确保每个链表拥有自己的资源。

//空初始化 (创建独立的哨兵节点_head,形成自循环,_size为0)
void empty_init()
{_head = new Node();_head->_next = _head;_head->_prev = _head;_size = 0;
}//拷贝构造函数  lt2(lt1)
list(const list<T>& lt)
{empty_init(); //初始化新链表的基础结构for (auto& e : lt){push_back(e);}
}

11. 赋值运算符重载

传统写法

//赋值运算符重载(传统写法)
list<T>& operator=(const list<T>& lt)
{//处理自赋值(避免释放自身资源后无法拷贝)if (this != &lt){//释放当前链表所有节点clear();//从lt复制元素到当前链表for (auto& e : lt){push_back(e);}}return *this;//返回自身引用(支持连续赋值如a=b=c)
}

现代写法

//交换两个链表的成员
void swap(list<T>& lt)
{std::swap(_head, lt._head);std::swap(_size, lt._size);
}//赋值运算符重载(现代写法) 
//利用拷贝构造函数创建临时副本,再交换成员变量  lt1 = lt3
list<T>& operator=(list<T> lt) //形参lt是按值传递,调用拷贝构造函数创建lt3的临时副本lt
{swap(lt); //交换当前对象与临时副本的资源return *this; //临时副本离开作用域自动析构
}//等价写法
//list<T>& operator=(const list<T>& lt)
//{
//	list<T> tmp(lt); //显式调用拷贝构造函数创建lt的临时副本
//	swap(tmp); 
//	return *this; 
//}

12. C++11引入的列表初始化

C++及以后标准中引入了列表初始化,使用方式:

std::list<int> lt{ 1,2,3,4 }; //等价于std::list<int> lt = { 1,2,3,4 };
std::list<int> lt2({1,2,3,4});//显示传参,语法较传统

上面代码执行过程:

  1. 当编译器看到{1,2,3,4}这个花括号初始化列表时,会自动生成一个std::initializer_list<int>类型的临时对象,并让它“包裹”花括号里面所有的元素。(具体操作:编译器会在栈上创建一个临时的int数组,存储1,2,3,4。)
  2. 调用std::list里面接收initializer_list<int>参数的构造函数,将步骤1创建的临时对象作为实参传递给这个构造函数。
  3. std::list构造函数内部会遍历这个临时对象,创建链表节点。
  4. 当lt构造完成后,临时对象和它指向的临时数组自动销毁。

像std::list、std::vector等标准容器都专门提供了接收initializer_list<T>参数的构造函数,对于自定义实现list,我们也想用这种方式初始化,就需要添加这个构造函数,例如:

template<typename T>
class list
{
public://接收initializer_list的构造函数
list(initializer_list<T> il)
{empty_init();for (auto& e : il){push_back(e);}
}……};

13. 总结

namespace cat
{//定义节点的结构template<class T>struct list_node{T _data;list_node<T>* _next;list_node<T>* _prev;list_node(const T& data = T()):_data(data),_next(nullptr),_prev(nullptr){}};//实现迭代器来模拟指针template<class T, class Ref, class Ptr>struct list_iterator{typedef list_node<T> Node;typedef list_iterator<T, Ref, Ptr> Self;Node* _node; //成员变量 _node指针变量专门用于指向链表中的某个节点list_iterator(Node* node):_node(node)			{}Ref operator*() //返回引用 *it = 100;{return _node->_data;}Ptr operator->() {return &_node->_data;}Self& operator++()//前置++    指针++返回指针本身,迭代器++返回迭代器本身{_node = _node->_next;return *this;}Self operator++(int) //后置++(有int占位参数):先保存当前状态,再移动,再返回原状态{                    //后置++不能返回引用,tmp是局部临时对象,出了作用域会销毁,如果返回引用,会导致悬垂引用问题(引用的对象已不存在)Self temp(*this); //保存当前迭代器 调用拷贝构造函数构造temp_node = _node->next;return temp; //返回原状态}Self& operator--()  {_node = _node->prev;return *this;}Self operator--(int){Self temp(*this);_node = _node->_prev;return temp;}bool operator!=(const Self& s) const{return _node != s._node;}bool operator==(const Self& s) const{return _node == s._node;}};//定义链表template<class T>class list{private:typedef list_node<T> Node;Node* _head;   //指向哨兵节点size_t _size;public:typedef list_iterator<T, T&, T*> iterator;typedef list_iterator<T, const T&, const T*>  const_iterator;iterator begin(){/*iterator it(_head->_next);return it;*/			return _head->_next;//返回指向第一个元素节点的迭代器,函数返回的类型(Node*)和函数声明的返回类型(iterator对象)不匹配//iterator类有单参数构造函数,支持隐式类型转换,自动调用构造函数将_head->next作为参数,创建一个临时的iterator对象//等价于return iteartor(_head->next); (显示调用构造函数)}iterator end(){return _head;}const_iterator begin()const{return _head->_next;}const_iterator end()const{return _head;}//空初始化void empty_init(){_head = new Node();_head->_next = _head;_head->_prev = _head;_size = 0;}//无参构造函数list(){empty_init();}list(initializer_list<T> il) //接收initializer_list<T>参数的构造函数{empty_init();for (auto& e : il){push_back(e);}}//拷贝构造函数  lt2(lt1)list(const list<T>& lt){empty_init();for (auto& e : lt){push_back(e);}}//赋值运算符重载(现代写法)  lt1 = lt3list<T>& operator=(list<T> lt){swap(lt);return *this;}//析构函数~list(){clear();delete _head;_head = nullptr;cout << "链表已销毁" << endl;}//清除元素void clear(){auto it = begin();while (it != end()){it = erase(it);}_size = 0;			}//交换两个链表的成员void swap(list<T>& lt){std::swap(_head, lt._head);std::swap(_size, lt._size);}		//尾插void push_back(const T& x){insert(end(), x);}//头插void push_front(const T& x){insert(begin(), x);}//插入数据iterator insert(iterator pos, const T& val){Node* cur = pos._node;Node* prev = cur->_prev;Node* newnode = new Node(val);//prev  newnode  curnewnode->_next = cur;newnode->_prev = prev;prev->_next = newnode;cur->_prev = newnode;++_size;return newnode;//返回指向新节点的迭代器}//删除数据iterator erase(iterator pos){assert(pos != end());Node* cur = pos._node;Node* prev = cur->_prev;Node* next = cur->_next;//prev  cur  nextprev->_next = next;next->_prev = prev;delete cur;--_size;return next; //返回被删除元素的下一个有效迭代器}//尾删void pop_back(){erase(--end());}//头删void pop_front(){erase(begin());}size_t size() const{return _size;}//判空bool empty() const{return _size == 0;}        };template<class Container>void print_container(const Container& con){typename Container:: const_iterator it = con.begin(); //auto it = con.begin();while (it != con.end()){//*it += 10;// error!cout << *it << " ";++it;}cout << endl;for (auto e : con){cout << e << " ";}cout << endl;}void test_list(){list<int> lt;lt.push_back(1);lt.push_back(2);lt.push_back(3);lt.push_back(4);print_container(lt);list<int> lt2 = { 1,2,3,4 };//调用接收initializer_list<int>参数的构造函数list<int> lt3({ 1,2,3,4 });	//同上const list<int>& lt4{ 1,2,3 }; //lt4是临时对象的引用print_container(lt4);}


文章转载自:

http://IA6QTqUW.sfhjx.cn
http://HasD1pBq.sfhjx.cn
http://w0xnlIM8.sfhjx.cn
http://VOu8O9jc.sfhjx.cn
http://mATdMhj8.sfhjx.cn
http://kaQwPW1w.sfhjx.cn
http://2GsJ0FAx.sfhjx.cn
http://t4kkbjPQ.sfhjx.cn
http://xKv87Wue.sfhjx.cn
http://7VlsbWnT.sfhjx.cn
http://ajqiJYUX.sfhjx.cn
http://e04hmmta.sfhjx.cn
http://iqDBqeMi.sfhjx.cn
http://wPzCOXyx.sfhjx.cn
http://e4ZuroE9.sfhjx.cn
http://lBsFliCL.sfhjx.cn
http://Mz0QOHZI.sfhjx.cn
http://9HFQG6H6.sfhjx.cn
http://FUMPh9od.sfhjx.cn
http://hubVaAFP.sfhjx.cn
http://SQwfOd4A.sfhjx.cn
http://3b2rLLsE.sfhjx.cn
http://3Vd2YyME.sfhjx.cn
http://HrGwG8WG.sfhjx.cn
http://VAmq14TE.sfhjx.cn
http://tKf3hdh5.sfhjx.cn
http://CCcW6gfM.sfhjx.cn
http://BjjvvfR2.sfhjx.cn
http://qSsQaVuM.sfhjx.cn
http://jpn8UWxg.sfhjx.cn
http://www.dtcms.com/a/380084.html

相关文章:

  • Java学习之——“IO流“的进阶流之打印流的学习
  • Vue 进阶实战:从待办清单到完整应用(路由 / 状态管理 / 性能优化全攻略)
  • 《用 Python 和 TensorFlow 构建你的第一个神经网络:从零开始识别手写数字》
  • 深入探索Vue.js:响应式原理与性能优化
  • 58.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--新增功能--图形验证码
  • 【Linux】基本指令 · 下
  • springboot+python+uniapp基于微信小程序的旅游服务系统景点信息展示 路线推荐 在线预约 评论互动系统
  • WebApp 的价值与实现:从浏览器架构到用户体验优化
  • 用户体验五大要点:从问题到解决方案的完整指南
  • 从ChatGPT家长控制功能看AI合规与技术应对策略
  • DeepSeek-VL 解析:混合视觉-语言模型如何超越传统计算机视觉方法
  • 从15kHz 到20MHz:为什么LTE带宽不能被子载波间隔整除?
  • Android SystemServer 系列专题【篇五:UserController用户状态控制】
  • Nature | 本周最新文献速递
  • Vuetify:构建优雅Vue应用的Material Design组件库
  • 6种A2A(智能体到智能体)的协议方案
  • 性能测试工具jmeter使用
  • [Windows] PDF 专业压缩工具 v3.0
  • kubectl常用命令
  • MinIO 分布式模式与纠删码
  • linux 宏 DEVICE_ATTR
  • 代码随想录刷题Day56
  • Ansible的 Playbook 模式详解
  • Qt 调用setLayout后,父对象自动设置
  • 现在中国香港服务器速度怎么样?
  • 用python的socket写一个局域网传输文件的程序
  • CentOS配置vsftpd服务器
  • 华为初级认证培训需要吗?HCIA考试考什么内容?自学还是报班?
  • 系统核心解析:深入操作系统内部机制——进程管理与控制指南(二)【进程状态】
  • KafKa02:Kafka配置文件server.properties介绍