Bootstrap

【C++】list模拟实现(完结)

1.普通迭代器(补充)

1.1 后置++和后置--

我们迭代器里面实现了前置++和前置--,还需要实现后置++后置--

 在list.h文件的list_iterator类里面实现。

//后置++/--
Self& operator++(int)
{
	Self tem(*this);//保存原来的值
	_node = _node->_next;
	return tem;
}
Self& operator--(int)
{
	Self tem(*this);//保存原来的值
	_node = _node->_prev;
	return tem;
}

1.2 重载->

因为迭代器模拟的是指针的行为,所以这里我们还要重载一下->。

T* operator->()
{
	return &_node->_data;
}

这个->的应用场景如下。

现在有个AA的类或结构体。

struct AA
{
	int _a = 1;
	int _b = 2;
};

list里面存这个AA类型的数据。

void test5()
{
	list<AA> la;

}

然后给里面随便存几个数据。

list<AA> la;
la.push_back(AA());
la.push_back(AA());
la.push_back(AA());
la.push_back(AA());

再用迭代器遍历,此时就要用->来引出AA里面的_a或者_b,不能用.引出。

list<AA>::iterator ia = la.begin();
while (ia != la.end())
{
	cout << ia->_a << " " << ia->_b << endl;
	++ia;
}

完整地写法如下。

cout << ia.operator->()->_a << " " << ia.operator->()->_b << endl;

第一个箭头是运算符重载的->,第二个_a前面的->就是没有重载的解引用符号。

为了可读性,省略了一个箭头,这里省略的也是第二个->。 

2.const迭代器

2.1 const_iterator

const修饰迭代器,我们不可以对迭代器指向的内容进行修改,但是迭代器可以改变自己的指向

我们如何实现迭代器可以改变自己指向而不让改变指向内容?

我们修改迭代器指向的内容通过什么修改?通过迭代器类里面的两个运算符重载,operator*operator->, 如果我们现在把operator*和operator->的返回值用const修饰,就不能通过他们两个修改指向内容了。

const T& operator*() 
{
	return _node->_data;
}

const T* operator->()
{
	return &_node->_data;
}

但是我们不可以直接把list_iterator里面的这两个成员函数给改了,改了的话普通迭代器就不能改了,我们只要const迭代器不能改。所以,我们重新实现一个类,叫list_const_iterator,这个类除了前面两个运算符重载函数变,其他成员函数一律不变

list.h文件的namespace里实现。

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 T& operator*() 
	{
		return _node->_data;
	}

	const T* operator->()
	{
		return &_node->_data;
	}

	//前置++/--
	Self& operator++() //重载++
	{
		_node = _node->_next;//加到下一个节点
		return *this;//返回自己
	}
	Self& operator--() //重载--
	{
		_node = _node->_prev;//减到前一个节点
		return *this;//返回自己
	}

	//后置++/--
	Self& operator++(int)
	{
		Self tem(*this);//保存原来的值
		_node = _node->_next;
		return tem;
	}
	Self& operator--(int)
	{
		Self tem(*this);//保存原来的值
		_node = _node->_prev;
		return tem;
	}

	bool operator!=(const Self& s) const
	{
		return _node != s._node;
	}
	bool operator==(const Self& s) const
	{
		return _node == s._node;
	}
};

同时,我们要在list类里面,public部分加上下面这句话。

typedef list_const_iterator<T> const_iterator;

以及迭代器的beginend接口。

const_iterator begin() const
{
	return _head->_next;
}
const_iterator end() const
{
	return _head;
}

 const迭代器部分的测试我们和接下来要说的打印函数一起测。

2.2 print_container 打印

这个函数就是为了方便我们打印数据。

template<class container>
void print_container(const container& c)
{
	for (auto e : c)
	{
		cout << e << " ";
	}
	cout << endl;
}

test.cpp中对const迭代器和print_container一起测试。

void test6()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	lt.print_container(lt);
}

3.优化两种迭代器的实现

很容易发现,我们前面的实现方式代码过于冗余,list_iterator 和 list_const_iterator两个类重叠部分太多。于是,我们就需要用到类模板,来优化一下。

list_iterator类的基础上优化。

我们增加两个模板参数,Ref和Ptr。

Ref是引用,或者是const引用;Ptr是地址,或者const地址。

 然后把operator*返回值类型换成Ref,operator->返回值类型换成Ptr。

Ref operator*() 
{
	return _node->_data;
}

Ptr operator->()
{
	return &_node->_data;
}

list类里面的如下部分也要改。

就可以了,迭代器的类模板就实现好了。 

我们用前面的测试样例在test.cpp测试一下。

4.迭代器失效问题

list的迭代器失效在insert插入数据是按理来说应该是不会发生的,因为这里没有扩容的的问题。

但是删除数据就有迭代器失效的问题,迭代器失效问题更加详细具体的分析在【C++】vector模拟实现、迭代器失效问题(超详解)

list这里如果删除一个节点,迭代器绝对就是类似野指针了。

解决方案还是对erase进行改造,让erase有返回值,erase返回的是删除结点的下一个节点位置。

iterator erase(iterator pos)
{
	assert(pos != end());//不可以删哨兵位头节点
	Node* prev = pos._node->_prev;//存pos前后节点
	Node* next = pos._node->_next;
	prev->_next = next;//链接pos前后节点
	next->_prev = prev;
	delete pos._node;//释放pos节点
	--_size;
	return next;
}

test.cpp中测试一下。我们删除所有偶数。

list<int> lt;
lt.push_back(1);
lt.push_back(2);
lt.push_back(3);
lt.push_back(4);
lt.push_back(5);
lt.push_back(6);
auto it = lt.begin();
while (it != lt.end())
{
	if (*it % 2 == 0)
	{
		it = lt.erase(it);
	}
	else
	{
		++it;
	}
}
lt.print_container(lt);

 所以list的迭代器失效问题就简单很多。

但是为了和库里面保持一致,我们还是对insert也改进一下,让他也有返回值。

iterator insert(iterator pos, const T& x)
{
	Node* cur = pos._node;
	Node* prev = cur->_prev;
	Node* newnode = new Node(x);
	//连接起来
	newnode->_next = cur;
	newnode->_prev = prev;
	prev->_next = newnode;
	cur->_prev = newnode;
	++_size;
	return newnode;//返回插入的节点
}

5.析构函数

5.1 clear

清空所有数据,但是不清除哨兵位头节点。

void clear()
{
	auto it = begin();
	while (it != end())
	{
		it = erase(it);
	}
}

5.2 析构函数

析构函数可以套用clear,clear不清除哨兵位,析构这里另外清除一下就可以了。

~list()
{
	clear();
	delete _head;
	_head = nullptr;
}

6.构造函数(优化)

6.1 空链表初始化

我们重新写一个函数,用来初始化空的list。空的list就是只有哨兵位头节点

void empty_init()
{
	_head = new Node();
	_head->_next = _head;
	_head->_prev = _head;
    _size = 0;
}

这个代码和list默认构造代码一模一样。这样实现主要是为了方便后面的拷贝构造。

6.2 默认构造

我们现在默认构造的代码就直接复用前面写的empty_init。

list()
{
	empty_init();
}

6.3 拷贝构造

这里的拷贝构造依然是复用push_back实现。

list(const list<T>& lt)
{
	for (auto& e : lt)
	{
		push_back(e);
	}
}

比如说我们用lt1拷贝构造lt2 

但是此时的lt2连哨兵位都没有,这样写肯定有问题。所以我们要加上前面写的empty_init。

//lt2(lt1)
list(const list<T>& lt)
{
	empty_init();
	for (auto& e : lt)
	{
		push_back(e);
	}
}

 

 在test.cpp里对前面的三个一起测试。

void test8()
{
	list<int> lt1;
	lt1.push_back(1);
	lt1.push_back(2);
	lt1.push_back(3);
	lt1.push_back(4);
	list<int> lt2(lt1);//拷贝构造
	lt1.print_container(lt1);
	lt2.print_container(lt2);
}

 

7.operator= 赋值运算符重载

这里的赋值运算符重载,也是要深拷贝,所以我们还是采用现代写法,现代写法详解在【C++拓展】深拷贝现代写法 

首先自己写一个swap。

void swap(list<T>& lt)
{
	std::swap(_head, lt._head);
	std::swap(_size, lt._size);
}

然后再交换。

list<T>& operator=(list<T> lt)
{
	swap(lt);
	return *this;
}

test.cpp中测试一下。

list<int> lt1, lt2;
lt1.push_back(1);
lt1.push_back(2);
lt2.push_back(3);
lt2.push_back(4);
lt1.print_container(lt1);
lt2.print_container(lt2);
lt2 = lt1; //赋值
lt1.print_container(lt1);
lt2.print_container(lt2);

 

list的内容就结束了,感谢观看,下篇再见~

;