C++ - 结构体模拟链表
从一段代码说起:C++ 结构体指针与链表的实现
先来看你提供的这段代码(略有整理):
#include<bits/stdc++.h>
using namespace std;
struct Node {
int data;
Node *pre = nullptr;
Node *next = nullptr;
};
int main() {
Node *head = new Node;
head->data = 10;
Node *current = head;
for (int i = 0; i < 10; ++i) {
Node *an = new Node;
an->data = i;
current->next = an;
an->pre = current;
current = an;
}
Node *it = head;
while (it != nullptr) {
cout << it->data << endl;
it = it->next;
}
return 0;
}
这段代码用结构体 Node 定义了一个双向链表的节点,在 main 函数中创建了一个含有 11 个节点的链表(头结点数据为 10,后面依次为 0,1,...,9),最后从头到尾遍历输出。虽然功能简单,但已经包含了动态内存分配、指针操作、链表链接等核心概念。下面我们从结构体指针讲起,再深入理解链表。
一、C++ 结构体与指针
1.1 结构体的本质
结构体是一种将不同数据组合在一起的自定义类型。在 C++ 中,结构体与类的区别很小(默认访问权限不同),完全可以拥有成员变量、成员函数、构造函数等。
上面的 Node 结构体包含:
- 一个整数
data存储数据。 - 两个指向
Node类型的指针pre和next,分别指向前一个和后一个节点。
这里的关键是:结构体内部可以包含指向自身类型(或其它结构体类型)的指针。这是实现链表、树等动态数据结构的基础。
1.2 结构体指针的定义与使用
Node *head = new Node; // 动态分配一个 Node 对象,返回其地址,存入指针 head
Node *是指向Node类型变量的指针类型。new Node在堆上创建一个Node对象,调用默认构造函数(这里没有显式定义构造函数,但 C++ 会生成一个默认的,将pre和next初始化为nullptr,data未初始化,不过我们在下一行赋值为 10 了)。- 通过
->运算符访问指针所指向对象的成员:head->data = 10;等价于(*head).data = 10;。
1.3 为什么需要结构体指针?
- 动态性:链表的节点数量在程序运行时才确定,无法用静态数组固定大小。使用
new动态创建节点,用指针将它们串联。 - 节省内存:不需要预先分配一大块连续内存,按需分配,删除节点时也可释放内存。
- 高效插入/删除:数组中间插入需要移动大量元素,而链表只需改变相邻节点的指针指向,这得益于指针的间接寻址能力。
二、用结构体实现一个简单的链表
链表是由一系列节点组成的数据结构,每个节点至少包含数据域和指向下一个节点的指针。双向链表还包含指向前一个节点的指针。
2.1 单链表 vs 双向链表
- 单链表:只有
next指针,只能从前往后遍历,插入/删除时需要知道前驱节点。 - 双向链表:同时有
prev和next,可以双向遍历,删除任意节点更方便(无需额外找前驱)。
你的代码实现的是双向链表,但只演示了向后链接和正向遍历。
2.2 代码分步解析
第一步:创建头节点
Node *head = new Node;
head->data = 10;
head 是链表的第一个节点,它的 pre 为 nullptr,next 也为 nullptr。
第二步:用循环添加后续节点
Node *current = head;
for (int i = 0; i < 10; ++i) {
Node *an = new Node;
an->data = i;
current->next = an; // 当前节点的 next 指向新节点
an->pre = current; // 新节点的 pre 指向当前节点
current = an; // 移动 current 到新节点,为下一次添加做准备
}
current始终指向链表的最后一个节点(尾节点)。- 每次新建一个节点
an,将其链接到current的后面,然后更新current。 - 循环结束后,链表结构为:
head(10) <-> node(0) <-> node(1) <-> ... <-> node(9)。注意:头节点后面的第一个节点数据是 0,接着是 1,依此类推。
第三步:遍历并输出
Node *it = head;
while (it != nullptr) {
cout << it->data << endl;
it = it->next;
}
- 从头节点开始,沿着
next指针一直走到链表末尾(nullptr)。 - 输出结果依次为 10, 0, 1, …, 9。
2.3 常见注意事项
-
内存泄漏:代码中没有释放动态分配的节点。实际编程中,应在使用完毕后用
delete逐个释放:Node *tmp; while (head != nullptr) { tmp = head; head = head->next; delete tmp; } -
指针未初始化:C++ 中局部指针变量的初值是不确定的(野指针)。幸运的是,
new Node会调用默认构造函数,而我们在结构体定义中已经给了pre和next的类内初始值nullptr,所以每个新节点的两个指针都是安全的。 -
双向链表的优势未体现:代码只使用了
next遍历,没有用到pre。若需要逆向遍历,可从尾节点开始用pre指针。 -
头节点的处理:本示例中头节点也存储有效数据(10)。有时会使用“哑头节点”(dummy head)不存数据,以简化插入/删除操作,避免特殊处理空链表。
三、扩展思考:结构体指针与类的对比
C++ 的 struct 完全可以像 class 一样拥有构造函数、析构函数,实现 RAII 管理资源。例如:
struct Node {
int data;
Node *pre, *next;
Node(int val = 0) : data(val), pre(nullptr), next(nullptr) {}
};
这样在 new Node(10) 时就会自动初始化 data 为 10,pre 和 next 为 nullptr。使用构造函数可以避免忘记赋初值的问题。
另外,使用智能指针(如 std::unique_ptr<Node>)可以自动管理内存,但环形引用会使析构变得复杂,因此原始指针在链表实现中仍然很常见。

浙公网安备 33010602011771号