栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C++ list容器模拟实现

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C++ list容器模拟实现

文章目录
    • 何为list?
    • list的模拟实现
      • 节点
      • 迭代器
      • list类
        • 类的整体框架
        • 构造和析构
        • 修改
        • 迭代器定义

何为list?

list是一个容器,list的底层是带头双向循环链表,支持在任意位置插入与删除,并且时间复杂度为O(1)。list的使用与vector类似,这里就不再赘述,这篇博客是对list的模拟实现,以提升对其理解。

list的模拟实现 节点

list的底层是带头双向循环链表,链表的节点单独作为一个类,节点有指针域和数据域,指向前节点的指针和指向后节点的指针,节点保存的数据,三个成员变量,需要写一个默认构造函数,根据形参创建节点,将两个指针置空。

template 
struct list_node
{
	list_node(const T data = T());
	T _data;
	list_node* prev;
	list_node* next;
};

template 
myList::list_node::list_node(T data)
{
	_data = data;
	prev = nullptr;
	next = nullptr;
}
迭代器

之前模拟实现的string和vector的迭代器都是原生指针,使用起来很方便,但链表的物理结构是分散的,空间不连续导致不能像string和vector一样使用原生指针作为迭代器。list的迭代器需要对指针进行封装,使其成为一个类,重载++,–,*,等操作。所以list迭代器是一个类,该类只有一个成员变量,就是指向节点的指针,节点有数据域和指针域,对迭代器解引用要返回节点的数据域,++,–要使指针指向前或后的节点。

template 
struct __list_iterator
{
	typedef list_node Node;
	// 迭代器有三个模板参数,第一个是节点数据域中的数据类型,Ref是该数据的引用,Ptr是指向该数据的指针
	typedef __list_iterator self;
	
	self(Node* node)
	{
		_node = node;
	}

	self& operator++()
	{
		_node = _node->_next; // 将迭代器的成员,节点的指针指向下一个节点,再返回该指针
		return *this;
	}
	
	self& operator--()
	{
		_node = _node->_prev;
		return *this;
	}
	
	self& operator++(int)
	{
		self tmp(_node);
		_node = _node->_next;
		return tmp;
	}

	self& operator--(int)
	{
		self tmp(_node);
		_node = _node->_prev;
		return tmp;
	}

	Ref operator*() // 对迭代器的解引用操作,返回数据的引用
	{
		return _node->_data;
	}
	
	// 如果数据域存储的数据是自定义的一个类,类的成员是内置类型的,此时可以对迭代器->操作,去访问自定义类型中的数据
	// 但重载的->返回的是数据域中数据的指针,需要再一次解引用(it->->xxx)
	// 但不用连续写两个操作符,编译器会自动优化
	Ptr operator->() // 对迭代器的解引用操作,返回数据的指针
	{
		return &(_node->_data);
	}

	bool operator==(const self& it)
	{
		return it._node == _node; // 判断两迭代器是否相等的操作,根据迭代器的成员_node是否相等为依据进行判断
	}

	bool operator!=(const self& it)
	{
		return it._node != _node;
	}
	
	Node* _node; // 迭代器唯一的成员,指向节点的指针
};

至于迭代器的拷贝构造,赋值要不要实现,答案是不用,因为迭代器不管理资源,因此编译器自动生成的浅拷贝就够用了。

list类

list类只有一个成员变量,就是链表的头节点,该头节点不存储任何数据,所以一个list对象创建后,需要对头节点分配空间

类的整体框架
template 
class list
{
	typedef list_node Node;
	typedef __list_iterator iterator;
	typedef __list_iterator const_iterator;
	// 迭代器有三个模板参数,根据创建的list对象是否有const修使,生成对应的模板类
	
	// 构造和析构
	list();
	template 
	list(InputIterator first, InputIterator last); // 用迭代器指向的头尾区间构造list
	list(const list& x);
	void swap(list& x);
	list& operator=(list x);
	~list();

	// 修改
	iterator insert(iterator pos, T val);
	iterator erase(iterator pos);
	void push_back(T val);
	void push_front(T val);
	void pop_back();
	void pop_front();
	void clear();

	// 迭代器
	iterator begin();
	const_iterator begin() const;

	iterator end();
	const_iterator end() const;

private:
	Node* _head;
};
构造和析构


list的构造:为头节点分配空间,并将前后指针指向自己

template 
myList::list::list()
{
	_head = new Node;
	_head->_prev = _head;
	_head->_next = _head;
}

用迭代器区间构造list容器,用first迭代器遍历区间,将解引用迭代器得到的数据尾插到list容器中。但插入数据前头节点还没分配空间,没有头节点就进行尾插,程序会崩溃,所以在尾插前要为头节点开辟空间,并使其前后指针指向自己。

template 
template 
myList::list::list(InputIterator first, InputIterator last)
{
	_head = new Node;
	_head->_prev = _head;
	_head->_next = _head;
	while (first != last)
	{
		push_back(*first);
		first++;
	}
}

拷贝构造:利用迭代器区间的构造函数,用拷贝对象的迭代器区间构造一个tmp临时对象,此时的tmp是拷贝对象的复制,只要交换当前容器和tmp容器就能完成拷贝构造,但前提是要对头节点分配空间,否则交换后tmp的释放会崩溃(不像vector和string的拷贝构造只要把指针置空就行,vector和string的析构是delete指针,对于空指针是不会进行释放的,但list要怎么判空?头节点的下一个节点还是头节点为空,所以要为头节点分配空间。

template 
myList::list::list(const list& x)
{
	_head = new Node;
	_head->next = _head;
	_head->prev = _head;

	list tmp(x.begin(), x.end());
	swap(tmp);
}

void myList::list::swap(list& x)
{
	std::swap(_head, x._head); // 交换两容器的头节点
}

赋值运算符的重载:形参不加引用,使得要形成形参就要调用拷贝构造函数,此时的形参是赋值对象的拷贝,将当前容器和形参交换,完成赋值

template 
myList::list& myList::list::operator=(list x)
{
	swap(x);
	return *this;
}

析构:复用clear,clear函数的作用是释放所有节点,只留下头节点,所以调用完clear后只要删除释放头节点,将其置空就行

template 
myList::list::~list()
{
	clear();
	delete _head;
	_head = nullptr;
}
修改


实现了在任意位置的插入和删除,其他的修改复用这两个函数即可。

insert:在传入的迭代器位置处插入一个节点,先保存迭代器指向的节点和前一个节点,剩下就简单了,注意函数返回新插入节点的迭代器

template 
typedefname myList::list::iterator myList::list::insert(iterator pos, T val)
{
	Node* curr = pos._node;
	Node* prev = curr->prev;
	Node* new_node = new Node(val);

	prev->_next = new_node;
	new_node->_prev = prev;
	new_node->_next = curr;
	curr->_prev = new_node;
	
	return iterator(new_node);
}

erase:任意位置的删除,删除该迭代器指向的节点,这里有个迭代器失效的问题,因为从外面传入的迭代器指向的节点被释放了,外面无法继续使用这个迭代器了,所以erase的返回值是删除位置的下一个迭代器,这样外面就能更新迭代器了。

template 
typedefname myList::list::iterator myList::list::erase(iterator pos)
{
	Node* curr = pos._node;
	Node* prev = curr->_prev;
	Node* next = curr->_next;
	
	prev->_next = next;
	next->_prev = prev;
	delete curr; // 最后别忘了释放当前节点
	
	return iterator(next);
}

剩下的尾插头删啥的都是复用这两个接口,没啥好说的

template 
void myList::list::push_back(T val)
{
	insert(end(), val);
}

template 
void myList::list::pop_back()
{
	erase(end()->_prev);
}

template 
void myList::list::push_front(T val)
{
	insert(begin(), val);
}

template 
void myList::list::pop_front()
{
	erase(begin());
}

template 
void myList::list::clear()
{
	if (_head) // 头节点为nullptr不删除
	{
		iterator it = begin();
		while (it != end())
		{
			it = erase(it); // 注意迭代器失效
		}
	}
}
迭代器定义


begin函数返回头节点的下一个节点,end函数返回头节点,也没啥好说的

template 
typename myList::list::iterator myList::list::begin()
{
	return iterator(_head->next);
}

template 
typename myList::list::const_iterator myList::list::begin() const
{
	return const_iterator(_head->next);
}

template 
typename myList::list::iterator myList::list::end()
{
	return iterator(_head);
}

template 
typename myList::list::const_iterator myList::list::end() const
{
	return const_iterator(_head);
}

总结:在学习过链表的基础上,模拟实现list就比较简单,有几个要注意的。

1.拷贝构造list需要对头指针进行初始化,否则交换后的析构会崩溃
2.erase的迭代器失效问题,外面需要更新迭代器
3.迭代器的设计需要时间理解,const对象需要用const迭代器,不是实现两个类模板,而是添加模板参数,const对象传const T*和const T&作为非const对象的区分
转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1033325.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号