解锁C++ list:从入门到精通的全攻略

目录

一、list 初相识

二、list 的基本操作

2.1 声明与初始化

2.2 元素的插入与删除 

2.3 元素访问

2.4 遍历 list

2.5 其他常用操作

三、迭代器失效问题


一、list 初相识

在 C++ 的标准模板库(STL)中,list是一个非常重要的容器,它以双向链表的形式存在,这赋予了它独特的性质和广泛的应用场景。​
从数据结构的角度来看,双向链表是由一系列节点组成,每个节点包含三个部分:数据域,用于存储实际的数据;前驱指针(prev),指向该节点的前一个节点;后继指针(next),指向该节点的后一个节点。这种结构使得list在插入和删除操作上具有无与伦比的优势。

二、list 的基本操作

2.1 声明与初始化

在 C++ 中,list的声明和初始化方式丰富多样,以满足不同的编程需求。最基础的是创建一个空的list,就像准备一个空的收纳盒,等待存放物品。例如:

#include<iostream>
#include<list>
int main()
{
	std::list<int> mylist1;
	std::list<int> mylist2(5, 10);
	std::list<int> mylist3 = { 2,36,38,99,20 };
	std::list<int> mylist4 = mylist3;
	return 0;
}

上面mylist1是一个空链表,mylist2则是有五个元素,每个里面值都是10,mylist3则是则是有五个值里面分别存着2,36,38,99,20。当STL中也允许将一个链表用于初始化另一个链表,于是有了mylist4。

2.2 元素的插入与删除 

push_front函数用于在list的开头插入一个元素,就像在收纳盒的最上面放上一个新物品。

push_back函数则是在list的末尾插入元素,类似于在收纳盒的最下面添加物品。

pop_front函数用于删除list的第一个元素,如同从收纳盒的最上面拿走一个物品。例如:

pop_back函数删除list的最后一个元素,即从收纳盒的最下面拿走物品

insert函数用于在任意位置插入一个新元素。它需要一个迭代器来指定插入位置,然后将元素插入到该位置之前。

erase函数用于删除list中指定位置的元素,通过迭代器指定要删除的元素位置。

如图一开始mylist3中有五个元素2,36,38,99,20,执行完push_front后最前面多了一个4。

执行完两次push_back()后在最后面又多了7,9。

后面执行完pop_back和pop_front,之后第一个元素和最后一个元素则被删除。

这里先通过begin函数获取myList的起始迭代器,然后使用std::advance函数将迭代器移动到第二个位置,最后使用insert函数插入元素 29。

后面执行一下erase函数,刚刚插入的元素又被删除了。

#include<iostream>
#include<list>
int main()
{
	std::list<int> mylist1;
	std::list<int> mylist2(5, 10);
	std::list<int>mylist3 = { 2,36,38,99,20 };
	std::list<int>mylist4 = mylist3;
	mylist3.push_front(4);
	mylist3.push_back(7);
	mylist3.push_back(9);
	mylist3.pop_back();
	mylist3.pop_front();
	auto it = mylist3.begin();
	advance(it, 1);
	mylist3.insert(it, 29);
	mylist3.erase(it);

	return 0;
}

2.3 元素访问

在list中,虽然不能像数组那样通过下标直接访问元素,但可以使用front和back函数来获取第一个和最后一个元素的引用。这就好比我们可以直接拿到收纳盒最上面和最下面的物品。

front函数返回list的第一个元素的引用,back函数返回list的最后一个元素的引用。

#include<iostream>
#include<list>
int main()
{
	std::list<int> mylist={ 1,2,3,4,5 };
	int first = mylist.front();
	int last = mylist.back();
	std::cout << "first: " << first << " last: " << last << std::endl;
	return 0;
}

2.4 遍历 list

遍历list是常见的操作,就像依次查看收纳盒里的每一个物品。在 C++ 中,有两种主要的方式来遍历list:使用迭代器和范围 for 循环

使用迭代器遍历list时,需要先获取list的起始迭代器和结束迭代器。起始迭代器就像指向收纳盒最上面物品的指针,结束迭代器则指向收纳盒最下面物品的下一个位置(实际上这个位置并不存在元素,只是作为遍历结束的标志)。例如:

#include<iostream>
#include<list>
int main()
{
	std::list<int> myList = { 1, 2, 3, 4, 5 };
	for (auto it = myList.begin(); it != myList.end(); ++it)
	{
		std::cout << *it << " ";
	}
	return 0;
}

在这段代码中,auto关键字让编译器自动推断it的类型为std::list<int>::iterator。通过begin函数获取起始迭代器,end函数获取结束迭代器,然后在for循环中,使用*it来访问迭代器指向的元素,并将其输出。每次循环中,++it将迭代器移动到下一个元素,直到it等于end迭代器时,循环结束。

范围 for 循环是 C++11 引入的一种更简洁的遍历方式,它可以自动处理容器的起始和结束位置。例如:

#include<iostream>
#include<list>
int main()
{
	std::list<int> myList = { 1, 2, 3, 4, 5 };
	for (auto it = myList.begin(); it != myList.end(); ++it)
	{
		std::cout << *it << " ";
	}
	std::cout<<std::endl;
	for (int element : myList) 
	{
		std::cout << element << " ";
	}
	return 0;
}

这里element会依次绑定到myList中的每一个元素,就像依次取出收纳盒里的每一个物品,然后将其输出。​需要注意的是,list的迭代器不支持随机访问,所以在判断迭代器是否到达末尾时,只能使用!=来比较,而不能像数组那样使用<。这是因为list的元素在内存中不是连续存储的,就像收纳盒里的物品不是整齐排列的,不能通过简单的下标计算来访问。

2.5 其他常用操作

empty函数用于检查list是否为空,就像检查收纳盒是否是空的。例如

std::list<int> myList;
if (myList.empty()) {
    std::cout << "The list is empty." << std::endl;
}

如果myList中没有元素,empty函数将返回true,输出相应的提示信息。​
size函数返回list中元素的个数,即收纳盒里物品的数量。示例如下:

std::list<int> myList = {1, 2, 3, 4, 5};
std::cout << "The size of the list is: " << myList.size() << std::endl;

这段代码将输出The size of the list is: 5,显示myList中元素的数量。​
clear函数用于清空list中的所有元素,就像把收纳盒里的物品全部清空。例如:

myList.clear();
if (myList.empty()) {
    std::cout << "The list has been cleared." << std::endl;
}

执行clear函数后,myList将变为空,empty函数返回true,输出提示信息。​
reverse函数用于反转list中元素的顺序,就像把收纳盒里的物品倒过来摆放。例如

std::list<int> myList = {1, 2, 3, 4, 5};
myList.reverse();
for (int element : myList) {
    std::cout << element << " ";
}

执行reverse函数后,myList中的元素顺序变为 5, 4, 3, 2, 1,并通过范围 for 循环输出。​
sort函数用于对list中的元素进行排序,默认是按照升序排列,就像把收纳盒里的物品按照从小到大的顺序整理。例如:

std::list<int> myList = {5, 3, 1, 4, 2};
myList.sort();
for (int element : myList) {
    std::cout << element << " ";
}

执行sort函数后,myList中的元素将按照升序排列为 1, 2, 3, 4, 5,并输出。如果需要按照降序排列,可以自定义比较函数作为参数传递给sort函数。​

三、迭代器失效问题

在使用list的迭代器时,迭代器失效是一个需要特别关注的问题。迭代器失效,简单来说,就是迭代器所指向的节点变得无效,无法再正确地访问元素。在list中,由于其底层是双向循环链表结构,迭代器失效主要发生在删除元素的操作中。​
当我们从list中删除一个元素时,指向被删除节点的迭代器就会失效。这是因为被删除节点从链表中移除,其内存可能被释放或重新分配,导致迭代器指向的位置不再有效。例如,考虑以下代码:

#include <iostream>
#include <list>

int main()
{
    std::list<int> myList = { 1, 2, 3, 4, 5 };
    auto it = myList.begin();
    while (it != myList.end()) {
        if (*it == 3) {
            myList.erase(it);
        }
        ++it;
    }
    return 0;
}

在这段代码中,当it指向元素 3 时,执行myList.erase(it)删除该元素,此时it就失效了。如果继续执行++it,就会导致未定义行为,因为it已经不再指向有效的节点。

为了避免这种情况,我们需要正确处理迭代器失效问题。list的erase函数会返回一个迭代器,它指向被删除元素的下一个有效节点。因此,我们可以利用这个返回值来更新迭代器,确保其始终指向有效的位置。

#include <iostream>
#include <list>
int main()
{
    std::list<int> myList = { 1, 2, 3, 4, 5 };
    auto it = myList.begin();
    while (it != myList.end()) 
    {
        if (*it == 3) {
            it = myList.erase(it);
        }
        else {
            ++it;
        }
    }
    return 0;
}

在这个修正后的代码中,当删除元素 3 时,it = myList.erase(it)会将it更新为指向元素 4 的迭代器,从而避免了迭代器失效带来的问题,确保后续的遍历操作能够正确进行。

好了,今天的分享就到这里了,下次我将带来list 的模拟实现!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值