概述:
list是一个双向链表,如果只在头尾插入删除,效率是非常高的,并且内存地址不连续可以动态改变内存。而vector是一个数组,在内存中地址连续,虽然也可以叫动态开辟内存,但是每次都是先创建新的内存,将上一次的内容拷贝到新内存,再释放原内存,这样的拷贝对于内存改变大的效率低下。
所以list主要用于头尾插入删除。vertor用于随机删除更加适当。
1 list的遍历
非常简单,不详解。这里注意一下,当list存string内容时,可以使用string的data成员函数去遍历取值。
void test01() {
list<int> l;
for (list<int>::iterator it = l.begin(); it != l.end(); it++){
cout << *it << " ";
}
cout << endl;
}
2 list的插入
2.1 list的插入简单使用
void test02() {
list<int> l;
//头插
l.push_front(1);
l.push_front(2);
//尾插
l.push_back(7);
l.push_back(8);
for (auto it = l.begin(); it != l.end(); it++) {
cout << *it << " ";
}
cout << endl;
}
结果:
2.2 测试list的插入是否会造成迭代器失效
测试代码:
//测试链表的插入是否对迭代器失效
void test03() {
list<int> l;
//头插
l.push_front(1);
l.push_front(2);
//尾插
l.push_back(7);
l.push_back(8);
for (auto it = l.begin(); it != l.end(); it++) {
if (*it == 1) {
//l.push_front(3);
//l.push_back(9);
l.insert(it, 0);
}
}
cout << endl;
}
2.2.1 先测试push_front,push_back
1)首先,先记录首次迭代器的首地址。
2)我们随便让它再某个位置插入后,再观察迭代器的首地址是否改变(即再使用该迭代器是否会报错),即可验证是否失效。
可以看到,即使插入后,迭代器的首地址是不会改变的,继续使用迭代器也不会报错。并且这里我们可以得出一个结论:C++迭代器遍历容器是使用it+n的方法去遍历,it始终指向首地址。而非it的指向不断改变。
2.2.2 测试list的insert
1)迭代器首地址。
2)调用insert后,可以看到,迭代器并未失效,继续使用迭代器也是正常的。
只不过需要注意的是list的insert是插入到迭代器的前面。使用这里的结果是2,0,1,7,8。
2.3 总结list的插入是否会造成迭代器失效
- 1)list的插入不会对迭代器失效,原因在于list的首地址是固定的,即使内存不够也只是再链表中增加关系上的连接,并不会影响到首地址。而vector内存不足会导致整片内存更改,故首地址会变。
- 2)所以list的插入不需要更新迭代器,vector需要。虽然list不需要更新迭代器,但是多线程下该加锁加锁。
3 list的删除
3.1 list的删除
list的删除主要用到pop_front和pop_back,erase极少用到,因为list的这种用法被更高效的vector代替(主要是遍历时使用erase嘛)。
//链表的删除
void test04() {
list<int> l;
l.push_back(1);
l.push_back(2);
l.push_back(7);
l.push_back(8);
l.pop_front();
l.pop_back();
l.erase(l.begin());
for (auto it = l.begin(); it != l.end(); it++) {
cout << *it << " ";
}
cout << endl;
}
结果:
3.2 测试list的删除是否会造成迭代器失效
测试代码。
//测试list的删除是否会造成迭代器失效
void test05() {
list<int> l;
l.push_back(1);
l.push_back(2);
l.push_back(7);
l.push_back(8);
for (auto it = l.begin(); it != l.end(); it++) {
if (*it == 2) {
//l.pop_front();
//l.pop_back();
l.erase(it);
}
}
cout << endl;
}
3.2.1 先测试pop_front
1)同样,先记录迭代器首地址。
2)运行完pop_front后,并且删除的是链表的首个元素,但是结果可以看到,迭代器首地址仍然不变,继续使用迭代器也正常,这说明pop_front的删除不会造成迭代器失效。所以我们这里可以猜想,list内部的pop_front删除操作只是对该元素进行删除,并且list.size减1,而实际上该内存并未释放。
3.2.2 测试pop_back
1)迭代器首地址。
2)调用pop_back后,地址仍一样,故迭代器没有失效。
3.2.3 测试erase
1)迭代器首地址。
2)调用erase函数后。可以看到,虽然迭代器仍指向该地址,但是指向的值已经被改变即地址被系统回收了,故调用erase会造成迭代器失效。
总结list的删除:
- 1)pop_front,pop_back函数不会使迭代器失效,原因是它们只对容器的元素进行删除,而实际的内存并未被系统回收,仍然有效。
- 2)而erase会使迭代器失效,因为该地址会被系统回收,后续不能再使用该迭代器即it++,所以必须更新迭代器。
4 list的其它成员函数
4.1 构造函数
//构造函数
void test06() {
list<int> l1;
list<int> l2(10, 5);//有参构造,创建10个5
list<int> l3(l2);//拷贝构造
list<int> l4(l2.begin(), l2.end());
for (list<int>::iterator it = l4.begin(); it != l4.end(); it++){
cout << *it << " ";
}
cout << endl;
}
4.2 resize函数
/*
list.resize():
若容器变长,则以elem值填充新位置。
如果容器变短,则末尾超出容器长度的元素被删除。
*/
void test07() {
list<int> l;
l.push_back(1);
l.push_back(2);
l.push_back(3);
l.push_back(4);
l.push_back(5);
l.push_back(6);
l.resize(5, 0);//重置大小为5,默认以0填充
for (list<int>::iterator it = l.begin(); it != l.end(); it++) {
cout << *it << " ";
}
l.resize(10, 555);//重置大小为10,默认以555填充
for (list<int>::iterator it = l.begin(); it != l.end(); it++) {
cout << *it << " ";
}
}
结果,可以看到6被删除,重置大小为10时默认以555填充。
4.3 assign函数
/*
list.assign:重载函数。
可以将另一个list容器的迭代器区间或者将多个值赋给本容器。
*/
void test08(){
list<int> l;
l.push_back(10);
l.push_back(20);
l.push_back(30);
l.push_front(100);
l.push_front(200);
l.push_front(300);
for (list<int>::iterator it = l.begin(); it != l.end(); it++) {
cout << *it << " ";
}
list<int> l2;
l2.assign(10, 100);
for (list<int>::iterator it = l2.begin(); it != l2.end(); it++) {
cout << *it << " ";
}
l2.assign(l.begin(), l.end());
for (list<int>::iterator it = l2.begin(); it != l2.end(); it++) {
cout << *it << " ";
}
}
4.4 list.sort排序自定义对象类型
以该对象的年龄进行排序。
代码。
//3.5.3案例 给葫芦娃高级排序
class Person {
public:
Person(string name, int age, int height) {
m_name = name;
m_age = age;
m_height = height;
}
//list容器调用remove(p1)时用到了 "==",所以要重载类对象的==操作符,且参数必须是引用,因为它传过来的对象就是引用。必须要匹配
bool operator==(const Person &p1) {
if (m_name == p1.m_name && m_age == p1.m_age && m_height == p1.m_height) {
return true;
}
return false;
}
string m_name;
int m_age;
int m_height;
};
//自定义排序的回调函数
bool MyCompare(Person &p1, Person &p2) { //因为对Person类排序,所以两个参数是Person类
//使用年龄做比较,相同则用身高,高的排前面(我下面用了身高矮的排前面)
if (p1.m_age == p2.m_age) {
return p1.m_height < p2.m_height;
}
return p1.m_age < p2.m_age;
}
void test09() {
list<Person> L;
Person p1("大娃", 30, 170);
Person p2("二娃", 28, 160);
Person p3("三娃", 24, 150);
Person p4("四娃", 24, 166);
Person p5("五娃", 24, 158);
Person p6("爷爷", 90, 200);
Person p7("蛇精", 999, 999);
L.push_back(p1);
L.push_back(p2);
L.push_back(p3);
L.push_back(p4);
L.push_back(p5);
L.push_back(p6);
L.push_back(p7);
cout << "===原始数据===" << endl;
for (list<Person>::iterator it = L.begin(); it != L.end(); it++) {
cout << "姓名:" << it->m_name << ' ' << "年龄:" << it->m_age << ' ' << "身高:" << it->m_height << endl;
}
//使用list自带的sort()进行排序,又因为是自定义排序,所以必须写回调函数
cout << "===排序后的数据===" << endl;
L.sort(MyCompare);
for (list<Person>::iterator it = L.begin(); it != L.end(); it++) {
cout << "姓名:" << it->m_name << ' ' << "年龄:" << it->m_age << ' ' << "身高:" << it->m_height << endl;
}
//删除大娃后打印
cout << "===删除大娃的数据===" << endl;
L.remove(p1);
for (list<Person>::iterator it = L.begin(); it != L.end(); it++) {
cout << "姓名:" << it->m_name << ' ' << "年龄:" << it->m_age << ' ' << "身高:" << it->m_height << endl;
}
}
结果:
5 多线程测试
下面我们测试多线程下,一个线程不断访问迭代器,另一个线程不断调用插入函数进行插入,看它是否仍满足上面单线程的结果。
这里不加上删除,是因为你在多线程下又写又读的,很容易出现问题,最好单独验证,并且我也不建议这种写法,因为list的读写不是原子操作,有风险。你想同时验证的话加锁是最安全的做法。
再次强调,再项目中不能多个线程不加锁同时对全局的list进行操作。这里只是为了演示,并且我这里的例子是一个负责写,一个负责读,并未同时写,例子本身只是用于测试迭代器是否失效。
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
#include <list>
#include <algorithm>
#include <string>
#include <thread>
list<int> g_list;
void List_Foreach() {
while (true){
for (list<int>::iterator it = g_list.begin(); it != g_list.end(); it++) {
cout << "Hello,i=" << *it << endl;
}
//this_thread::sleep_for(chrono::milliseconds(500));
}
}
void List_Insert() {
int i = 0;
while (true){
//this_thread::sleep_for(chrono::milliseconds(500));
g_list.push_front(i);
g_list.push_back(i);
g_list.insert(g_list.begin(), i);
i++;
}
}
int main() {
thread th1(List_Foreach);
thread th2(List_Insert);
th1.join();
th2.join();
return 0;
}
多次运行上面结果,可以看到一个线程不断插入,另一个线程不断循环使用迭代器是没有问题的。