【C++初阶】:(11)list的功能介绍&&list迭代器模拟实现

1. list的介绍和使用

1.1 list的介绍

C++ 标准库中的 list是一个基于环状双向串行结构 实现的序列容器。如图所示,它的内部包含一个头结点(node),该结点由成员指针 M_node指向,作为整个数据结构的入口,其中并不存储实际数据。真实的节点首尾相连形成闭环,使得从任意节点出发都能正向或逆向遍历整个链表。对于使用者来说,list.begin()返回的是第一个实际存放数据的节点,而 list.end()指向头结点,表示范围的结束。这种设计使得 list支持高效的任意位置插入与删除操作(时间复杂度为 O(1)),但它不支持随机访问,且由于节点内存不连续,在进行排序等需要大量元素移动的操作时,通常不如 vector高效。

1.2 对于迭代器知识的补充

首先按照功能和性质对迭代器进行划分:

按照功能来划分的话,迭代器可以分为:

  • iterator
  • reverse_iterator
  • const_iterator
  • const_reserve_iterator

按照性质来划分的话,迭代器可以分为:

|-------|-------------------------------|-----------|
| 性质 | 举例 | 支持的运算 |
| 单向迭代器 | forward_list、unordered_map... | ++ |
| 双向迭代器 | list、map、set... | ++、-- |
| 随机迭代器 | vector、string、deque... | ++、--、+、- |

如果想要查看某种数据结构的迭代器到底是哪种性质的迭代器,可以在官方文档的Member types中进行查看,例如查看list的迭代器属于哪种性质的迭代器,我们可以查看到如下结果:

所以迭代器的性质取决于容器的底层结构,因为string,vector这种容器的底层物理存储空间的地址是连续的,所以相应的迭代器可以支持+/-运算,而list的底层物理存储空间的地址并不是连续的所以并不能支持+/-运算,而对于后续将会学习到的forward_list的底层结构采用的是单向链表,所以迭代器只能够单方向的进行遍历(向后遍历)。

同时还值得一提的一点是,容器的底层结构也决定了可以使用哪些算法。

以 std::sort为例,虽然它采用模板定义,表面上似乎能适配各类迭代器,但实际上它仅支持随机访问迭代器(RandomAccessIterator)。这是因为 std::sort底层通常采用快速排序(Quicksort)等算法,这些算法在执行过程中依赖随机访问特性(例如通过 +或 -运算符直接跳转到序列中的任意位置)。由于只有随机访问迭代器才能满足这种高效的跳跃式访问需求,因此 std::sort无法直接与仅支持顺序访问的容器(如 std::list)配合使用。

正是由于标准库全局的 std::sort算法要求迭代器必须是随机访问迭代(RandomAccessIterator),而 std::list仅提供双向迭代器(BidirectionalIterator),无法满足随机跳转的性能需求,因此 C++ 标准库特意为 std::list在类内部重载定义了专属的 sort成员函数。这种设计允许链表在不支持随机访问的情况下,依然能利用底层的归并排序(Merge Sort)算法实现高效的原地排序,同时也规避了将链表节点拷贝到连续内存的开销。

再举个例子,C++标准库中的reverse函数所要求的迭代器如下:

std::reverse函数要求迭代器类型为双向迭代器(BidirectionalIterator)。因此,std::forward_list​ 无法直接调用该函数,因为它仅支持单向迭代器;而 std::list​ 满足条件,可以正常使用。同时,std::vector​ 也能调用该函数,因为随机访问迭代器是一种特殊的双向迭代器,自然兼容该要求。同理,双向迭代器也是一种特殊的单向迭代器。

但是,可以发现C++标准库中还有一类函数所支持的迭代器比较特殊,比如find函数:

如上图所示,std::find的模板参数只需要 输入迭代器(InputIterator) 。相比之前的 std::sort(需要随机访问)和 std::reverse(需要双向访问),输入迭代器的限制是最小的,它仅要求能够单向遍历并读取元素即可。

因此,这类算法具有极高的通用性。无论是 vector、list这样的双向或随机访问容器,还是 forward_list(单向链表),甚至包括用于读取文件的流迭代器(如 std::istream_iterator),都可以使用 find函数进行查找操作。


为了更直观地理解各类迭代器的关系,我们可以参考标准库的定义:迭代器根据其提供的功能被划分为五大类别。正如下图所见,这五类迭代器构成了一个层层递进的层级结构。位于顶端的随机访问迭代器(Random Access)功能最强大,向下依次是双向迭代器(Bidirectional)和前向迭代器(Forward),而输入(Input)和输出(Output)迭代器则位于基础层,功能最为单一。

这种设计的精髓在于"继承性":如果一个容器支持更高级别的迭代器(例如 vector支持随机访问),那么它天然向下兼容,也能作为更低级别迭代器(如双向、前向、输入)被使用。这也就是为什么我们之前看到 vector能调用 find、reverse和 sort的原因。

1.3 list的使用

list 提供的常用成员函数(如 push_backpush_fronteraseinsert等)在接口设计和使用方式上与 vector、string 基本一致,因此不再逐一展开说明。这里仅特别介绍几个在 list 中需重点关注的函数或特性,其余函数的详细用法可查阅相关标准库文档。

https://cplusplus.com/reference/list/list/?kw=listhttps://cplusplus.com/reference/list/list/?kw=list

1.3.1 emplace_back

push_back 和**emplace_back** 都是向 list 尾部添加元素的方法,核心区别在于构造效率

push_back 接受一个已构造好的对象 (或临时对象),会调用拷贝构造或移动构造将其放入容器;而 emplace_back则直接接受构造对象所需的参数 ,在容器内部原地构造对象,避免了一次额外的拷贝或移动构造 ,效率更高。例如对于**list<A>push_back(A(2,2))** 会先构造临时对象再移动(或拷贝)到容器,而 emplace_back(3,3) 则直接在链表节点中构造 A对象,省去了临时对象的构造和转移开销。因此,在 C++11 之后,推荐优先使用**emplace_back**来提升性能。

测试代码如下:

cpp 复制代码
struct A
{
public:
	A(int a1 = 1, int a2 = 1)
		:_a1(a1)
		,_a2(a2)
	{}
	int _a1;
	int _a2;
};

void test_list2()
{
	list<A> lt;
	A aa1(1, 1);
	lt.push_back(aa1);
	lt.push_back(A(2, 2));
	//lt.push_back(3,3);-->会直接报错

	lt.emplace_back(aa1);
	lt.emplace_back(A(2, 2));
	//支持直接传构造A对象的参数
	lt.emplace_back(3, 3);
}

如果想要直观的看到每行调用中构造和拷贝构造的详情,可以改写代码如下:

cpp 复制代码
struct A
{
public:
	A(int a1 = 1, int a2 = 1)
		:_a1(a1)
		,_a2(a2)
	{
		cout << "A(int a1 = 1,int a2 = 1)" << endl;
	}
	A(const A& aa)
		:_a1(aa._a1)
		,_a2(aa._a2)
	{
		cout << "A(const A& aa)" << endl;
	}
	int _a1;
	int _a2;
};

void test_list2()
{
	list<A> lt;// 1. 创建一个空链表,准备存放A类对象
	A aa1(1, 1);// 2. 创建一个A对象aa1
	lt.push_back(aa1);
	lt.push_back(A(2, 2));
	//lt.push_back(3,3);

	lt.emplace_back(aa1);
	lt.emplace_back(A(2, 2));
	cout << endl;
	//支持直接传构造A对象的参数
	lt.emplace_back(3, 3);
}

输出结果为:

具体解读如下:

输出序号 对应的代码行 调用的函数 原因解释
A aa1(1, 1); A(int, int) 直接构造对象 aa1
lt.push_back(aa1); A(const A&) push_back 拷贝 aa1 进链表
lt.push_back(A(2, 2)); A(int, int) 先构造临时对象 A(2,2)
lt.push_back(A(2, 2)); A(const A&) push_back 拷贝那个临时对象
lt.emplace_back(aa1); A(const A&) 传了对象进去,emplace 只能拷贝它
lt.emplace_back(A(2, 2)); A(int, int) 先构造临时对象 A(2,2)
lt.emplace_back(A(2, 2)); A(const A&) emplace_back 拷贝那个临时对象
lt.emplace_back(3, 3); A(int, int) 最佳实践:直接在链表内用参数构造对象

1.3.2 insert

因为list底层是双向链表,其迭代器不支持算术运算(如 +、-)及随机访问,这意味着无法像 vector那样通过加减偏移量来定位任意位置。

因此,若要在指定位置插入元素,必须先通过迭代器(如 begin())逐个遍历移动到目标位置,然后再调用 insert方法完成插入。

cpp 复制代码
void test_list3()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
	auto it = lt.begin();
	int k = 3;
	while (k--)
	{
		++it;
	}
	lt.insert(it, 30);
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}

输出结果:
1 2 3 4
1 2 3 30 4

1.3.3 erase

使用代码演示:

cpp 复制代码
void test_list3()
{
	list<int> lt;
	lt.push_back(1);
	lt.push_back(2);
	lt.push_back(3);
	lt.push_back(4);
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
	//删除操作
	int x = 0;
	cin >> x;
	it = find(lt.begin(), lt.end(), x);
	if (it != lt.end())
	{
		lt.erase(it);
	}
	for (auto e : lt)
	{
		cout << e << " ";
	}
	cout << endl;
}
输出结果:
1 2 3 4
输入x=3
输出:
1 2 4

这里面进行删除操作的if判断条件之所以为it != lt.end(),是因为库中find函数的算法实现如下,看一下就很好理解了:

1.4 vector 排序 VS list 排序

在实际开发中,当我们面对链表(list)的排序需求时,一个常见的经验是:将链表数据转移到 vector 中排序,再转移回链表,比直接对链表排序更快。这是为什么呢?

性能对比实验

下面通过两组测试来验证这个结论:

实验1:直接排序对比

cpp 复制代码
void test_op1()
{
    const int N = 1000000;
    list<int> lt1;
    vector<int> v;
    
    // 初始化相同数据
    for (int i = 0; i < N; ++i) {
        auto e = rand() + i;
        lt1.push_back(e);
        v.push_back(e);
    }
    
    int begin1 = clock();
    sort(v.begin(), v.end());  // vector排序
    int end1 = clock();
    
    int begin2 = clock();
    lt1.sort();  // list排序
    int end2 = clock();
    
    printf("vector sort: %dms\n", end1 - begin1);
    printf("list sort: %dms\n", end2 - begin2);
}

结果示例

  • vector 排序:约 150ms

  • list 排序:约 3000ms

vector 排序速度是 list 的 20 倍左右。

实验2:转移排序方案

cpp 复制代码
void test_op2()
{
    const int N = 1000000;
    list<int> lt1;
    list<int> lt2;
    
    // 初始化相同数据
    for (int i = 0; i < N; ++i) {
        auto e = rand() + i;
        lt1.push_back(e);
        lt2.push_back(e);
    }
    
    int begin1 = clock();
    // 1. 从list拷贝到vector
    vector<int> v(lt2.begin(), lt2.end());
    
    // 2. 在vector中排序
    sort(v.begin(), v.end());
    
    // 3. 从vector拷贝回list
    lt2.assign(v.begin(), v.end());
    int end1 = clock();
    
    int begin2 = clock();
    lt1.sort();  // 直接对list排序
    int end2 = clock();
    
    printf("拷贝+vector排序+拷贝回: %dms\n", end1 - begin1);
    printf("list直接排序: %dms\n", end2 - begin2);
}

结果示例

  • 拷贝+vector排序+拷贝回:约 350ms

  • list 直接排序:约 3000ms

即使加上两次拷贝的开销,vector 方案仍然比 list 直接排序快 8-9 倍

原因分析

1. 缓存局部性

vector 在内存中是连续存储的,CPU 缓存能够高效预加载相邻元素。排序过程中频繁的随机访问在 vector 上表现极佳。

list 是离散存储的,每个元素分布在堆内存的不同位置,每次访问都可能引发缓存未命中(cache miss),严重影响性能。

2. 算法实现

std::sort使用的快速排序/内省排序依赖随机访问,复杂度为 O(NlogN)。

list::sort使用的是归并排序的变体,虽然也是 O(NlogN),但由于指针操作频繁和缓存不友好,常数项极大。

3. 拷贝开销相对较小

从 list 拷贝到 vector 是 O(N) 的线性操作,虽然需要额外内存,但连续的写入操作非常高效。排序的 O(NlogN) 占据主导,拷贝开销相对较小。

结论

在 C++ STL 中,list 的直接排序是性能陷阱。**"拷贝到 vector → 排序 → 拷贝回 list"**​ 虽然看似多了一步,但由于现代 CPU 缓存架构的特性,反而能获得数量级的性能提升。这是空间换时间的典型范例,也是理解数据结构内存布局重要性的生动案例。

2. list迭代器的实现原理

2.1 迭代器设计的背景与挑战

在前面的章节中,我们已经学习了vector和string的迭代器实现,它们的特点是底层物理存储空间连续。在这种连续存储结构中,迭代器本质上就是原生指针的简单封装,通过指针的自增/自减就能遍历整个容器。

然而,list(双向链表)的情况完全不同。list的节点在内存中是离散分布 的,每个节点通过指针相互连接。如果使用原生指针作为迭代器,++操作将无法正确地移动到下一个节点,因为下一个节点的地址与当前节点的地址在物理上不连续。

2.2 迭代器的封装设计

设计思想

为了让用户能够使用统一的方式操作不同类型的容器,list采用了迭代器封装模式 。我们创建一个专门的迭代器类,在内部重载运算符,使得++--*等操作符在list的上下文中具有正确的语义。

代码实现框架

cpp 复制代码
namespace YJ
{
    // 链表节点结构
    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)
        {}
    };

    // 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;
        }

        // 前置自增运算符重载
        Self& operator++()
        {
            _node = _node->_next;  // 移动到下一个节点
            return *this;
        }

        // 前置自减运算符重载
        Self& operator--()
        {
            _node = _node->_prev;  // 移动到前一个节点
            return *this;
        }
        
        // 不等运算符重载
        bool operator!=(const Self& s) const
        {
            return _node != s._node;  // 比较节点指针
        }

        // 相等运算符重载
        bool operator==(const Self& s) const
        {
            return _node == s._node;  // 比较节点指针
        }
    };
}

补充: 在 C++ 中,classstruct的唯一区别在于默认访问权限:struct默认为 public,而 class默认为 private,除此之外二者在功能上完全相同。这里选择使用 struct来定义链表的节点和迭代器,是因为这些内部结构中的成员变量(如 _data_next_prev等)需要被外层容器类频繁访问和操作,将其默认设为 public可以简化代码,避免反复书写 public:访问说明符,使实现更为清晰和直接。

2.3 迭代器在list类中的集成

迭代器类型定义

在list类中,我们将**list_iterator<T>** 定义为公有类型**iterator** ,这样用户可以通过**list<T>::iterator**访问迭代器类型。

cpp 复制代码
template<class T>
class list
{
    typedef list_node<T> Node;
public:
    typedef list_iterator<T> iterator;  // 迭代器类型定义

    // 获取起始迭代器
    iterator begin()
    {
        // 三种等效写法:
        // 1. 创建临时对象
        // iterator it(_head->_next);
        // return it;
        
        // 2. 显式构造
        // return iterator(_head->_next);
        
        // 3. 隐式转换(最简洁)
        return _head->_next;  // Node*隐式转换为iterator
    }

    // 获取结束迭代器
    iterator end()
    {
        return _head;  // 头节点作为end标志
    }
    
private:
    Node* _head;    // 头节点(哨兵节点)
    size_t _size;   // 元素个数
};

2.4 关键操作的实现原理

1. 构造函数

list采用带头节点的双向循环链表实现,初始化时创建头节点并让其前后指针都指向自己,形成空链表。

cpp 复制代码
list()
{
    _head = new Node;           // 创建头节点
    _head->_next = _head;       // 后继指向自己
    _head->_prev = _head;       // 前驱指向自己
    _size = 0;                  // 大小为0
}

2. 插入操作

insert操作展示了迭代器的实际应用。通过迭代器可以精确定位插入位置。

cpp 复制代码
void insert(iterator pos, const T& x)
{
    Node* cur = pos._node;      // 当前位置节点
    Node* prev = cur->_prev;    // 前一个节点
    
    Node* newnode = new Node(x); // 创建新节点
    
    // 重新链接指针
    newnode->_next = cur;
    cur->_prev = newnode;
    newnode->_prev = prev;
    prev->_next = newnode;
    
    ++_size;  // 更新大小
}

3. 删除操作

erase操作同样通过迭代器定位要删除的元素。

cpp 复制代码
void erase(iterator pos)
{
    assert(pos != end());  // 不能删除end()
    
    Node* prev = pos._node->_prev;
    Node* next = pos._node->_next;
    
    // 跳过要删除的节点
    prev->_next = next;
    next->_prev = prev;
    
    delete pos._node;  // 释放节点
    --_size;          // 更新大小
}

4. 完整代码

cpp 复制代码
namespace YJ
{
	template<class T>
	struct list_node
	{
		T _data;
		list_node<T>* _prev;
		list_node<T>* _next;

		//构造函数
		list_node(const T& data = T())
			:_data(data)
			,_prev(nullptr)
			,_next(nullptr)
		{}

	};

	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;
		}

		Self& operator++()
		{
			_node = _node->_next;
			return *this;
		}

		Self& operator--()
		{
			_node=_node->prev;
			return *this;
		}

		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
	{
		//内接口
		typedef list_node<T> Node;
	public:
		//外接口
		typedef list_iterator<T> iterator;
		list()
		{
			_head = new Node;
			_head->_prev = _head;
			_head->_next = _head;
			_size = 0;
		}

		iterator begin()
		{
			iterator it(_head->_next);//哨兵位_head的下一个节点就是头节点    begin()指向第一个有效元素
			return it;
		}
		iterator end()
		{
			return _head;//end()指向最后一个有效元素的下一个位置
		}

		//void push_back(const T& x)
		//{
		//	Node* newnode = new Node(x);
		//	Node* tail = _head->_prev;
		//	tail->_next = newnode;
		//	newnode->_prev = tail;
		//	newnode->_next = _head;
		//	_head->prev = newnode;
		//	++_size;
		//}

		void insert(iterator pos, const T& x)
		{
			Node* cur = pos._node;
			Node* prev = cur->_prev;
			Node* newnode = new Node(x);
			//prev  newnode  cur
			newnode->_next = cur;
			cur->_prev = newnode;
			newnode->_prev = prev;
			prev->_next = newnode;
			++_size;
		}

		//可以直接复用insert对push_backde的实现进行改写
		void push_back(const T& x)
		{
			insert(end(), x);
		}

		//前插同理
		void push_front(const T& x)
		{
			insert(begin(), x);
		}

		void erase(iterator pos)
		{
			assert(pos != end());
			Node* prev = pos._node->prev;
			Node* next = pos._node->next;
			prev->_next = next;
			next->_prev = prev;
			delete pos._node;
			--_size;
		}
		void pop_back()
		{
			erase(--end());
		}
		void pop_front()
		{
			erase(begin());
		}
		size_t size() const
		{
			return _size;
		}
		bool empty() const
		{
			return _size == 0;
		}


	private:
		Node* _head;
		size_t _size;
	};
}

2.5 测试示例

cpp 复制代码
void test_list_iterator()
{
    list<int> lt;
    lt.push_back(1);
    lt.push_back(2);
    lt.push_back(3);
    lt.push_back(4);
    
    // 使用迭代器遍历
    list<int>::iterator it = lt.begin();
    while (it != lt.end())
    {
        cout << *it << " ";  // 解引用获取数据
        ++it;                // 移动到下一个元素
    }
    cout << endl;  // 输出: 1 2 3 4
}

2.6 设计要点总结

  1. 3.节点封装:每个元素存储于独立的节点中,节点包含数据、前驱和后继指针。

  2. 迭代器封装:通过自定义迭代器类,重载运算符实现链表特有的遍历逻辑。

  3. 隐式转换 :利用构造函数实现Node*iterator的隐式转换,简化代码。

  4. 哨兵节点 :使用头节点简化边界条件处理,begin()返回头节点的下一个节点,end()返回头节点本身。

  5. 运算符重载

    • operator*():返回节点数据的引用

    • operator++():移动到后继节点

    • operator--():移动到前驱节点

    • operator!=()operator==():比较底层节点指针

这种设计使得list的迭代器在使用方式上与vector、string的迭代器完全一致,用户无需关心底层实现差异,体现了C++STL的抽象之美

你说得对,让我具体分析"隐式转换"在代码中的体现。

3.隐式转换的具体体现

1. 转换构造函数的作用

cpp 复制代码
// 在list_iterator类中
list_iterator(Node* node)  // 单参数构造函数
    :_node(node)
{}

这个构造函数是隐式转换 的关键。它是一个单参数构造函数,C++编译器允许它作为隐式转换的桥梁。

2. 代码中的具体应用

在list类的begin()函数中:

cpp 复制代码
iterator begin()
{
    return _head->_next;  // 这里发生隐式转换
}

3. 隐式转换的步骤分解

实际上,编译器看到return _head->_next;时:

  1. _head->_next的类型是Node*(节点指针)

  2. 函数的返回类型是iterator

  3. 编译器检查是否存在从Node*iterator的转换路径

  4. 发现list_iterator有一个接受Node*的构造函数

  5. 编译器隐式调用 构造函数:return list_iterator(_head->_next);

4. 等效的显式写法对比

cpp 复制代码
// 显式写法1:临时对象
iterator begin()
{
    iterator it(_head->_next);  // 显式构造
    return it;
}

// 显式写法2:显式转换
iterator begin()
{
    return iterator(_head->_next);  // 显式调用构造函数,匿名对象写法
}

// 隐式写法(实际使用的简洁形式)
iterator begin()
{
    return _head->_next;  // 自动隐式转换
}

5. 更多的隐式转换例子

insert()erase()函数中也有体现:

cpp 复制代码
void insert(iterator pos, const T& x)
{
    Node* cur = pos._node;  // iterator → Node*
    // ...
}

void erase(iterator pos)
{
    Node* cur = pos._node;  // iterator → Node*(通过成员访问)
    // ...
}

6. 隐式转换的便利性对比

假设没有隐式转换,遍历代码会变得很冗长:

cpp 复制代码
// 没有隐式转换的繁琐写法
list<int> lt;
// ... 插入数据

// 需要显式构造迭代器
list<int>::iterator it = list_iterator<int>(lt.get_head()->_next);
while (it != list_iterator<int>(lt.get_head()))  // 每次比较都要构造
{
    cout << *it << " ";
    it = list_iterator<int>(it._node->_next);  // 每次赋值都要构造
}
cpp 复制代码
// 有隐式转换的简洁写法
list<int>::iterator it = lt.begin();  // 自动转换
while (it != lt.end())  // 自动转换
{
    cout << *it << " ";
    ++it;  // 通过重载的++操作符
}

7. 隐式转换的原理图

cpp 复制代码
隐式转换过程
      ┌─────────────────────┐
Node* │   _head->_next      │
      └──────────┬──────────┘
                 │
                 ▼
      ┌─────────────────────┐
      │编译器自动调用构造函数│
      │list_iterator(Node*)  │
      └──────────┬──────────┘
                 │
                 ▼
      ┌─────────────────────┐
iterator │   _node = _head->_next │
      └─────────────────────┘
相关推荐
清水白石0081 小时前
Python 可变对象与不可变对象深度解析:为什么 `tuple` 里可以放 `list`?
开发语言·python·list
源图客1 小时前
【亚马逊 SP-API 实战】Java 实现单体商品 Listing 创建 + 图片上传完整教程(亲测可用)
开发语言·亚马逊电商
Irissgwe1 小时前
二、信号与槽
c++·qt·信号与槽
不会C语言的男孩2 小时前
C++ Primer 第3章:字符串、向量和数组
开发语言·c++
兰令水2 小时前
leecodecode【反前后指针】【2026.5.31打卡-java版本】
java·开发语言
Dovis(誓平步青云)3 小时前
《QT学习第四篇:常见事件与UDP、TCP、文件系统、(锁、信号量、条件变量》
c语言·开发语言·汇编·qt
code monkey.3 小时前
【Linux之旅】Linux 应用层自定义协议与序列化:从粘包问题到网络计算器
linux·网络·c++
草莓熊Lotso3 小时前
【Linux网络】深入理解 HTTP 协议(二):从协议格式到手写工业级 HTTP 服务器
linux·运维·服务器·网络·c++·http
isyangli_blog11 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php