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 类型的指针 prenext,分别指向前一个和后一个节点。

这里的关键是:结构体内部可以包含指向自身类型(或其它结构体类型)的指针。这是实现链表、树等动态数据结构的基础。

1.2 结构体指针的定义与使用

Node *head = new Node;   // 动态分配一个 Node 对象,返回其地址,存入指针 head
  • Node * 是指向 Node 类型变量的指针类型。
  • new Node 在堆上创建一个 Node 对象,调用默认构造函数(这里没有显式定义构造函数,但 C++ 会生成一个默认的,将 prenext 初始化为 nullptrdata 未初始化,不过我们在下一行赋值为 10 了)。
  • 通过 -> 运算符访问指针所指向对象的成员:head->data = 10; 等价于 (*head).data = 10;

1.3 为什么需要结构体指针?

  • 动态性:链表的节点数量在程序运行时才确定,无法用静态数组固定大小。使用 new 动态创建节点,用指针将它们串联。
  • 节省内存:不需要预先分配一大块连续内存,按需分配,删除节点时也可释放内存。
  • 高效插入/删除:数组中间插入需要移动大量元素,而链表只需改变相邻节点的指针指向,这得益于指针的间接寻址能力。

二、用结构体实现一个简单的链表

链表是由一系列节点组成的数据结构,每个节点至少包含数据域和指向下一个节点的指针。双向链表还包含指向前一个节点的指针。

2.1 单链表 vs 双向链表

  • 单链表:只有 next 指针,只能从前往后遍历,插入/删除时需要知道前驱节点。
  • 双向链表:同时有 prevnext,可以双向遍历,删除任意节点更方便(无需额外找前驱)。

你的代码实现的是双向链表,但只演示了向后链接和正向遍历。

2.2 代码分步解析

第一步:创建头节点

Node *head = new Node;
head->data = 10;

head 是链表的第一个节点,它的 prenullptrnext 也为 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 常见注意事项

  1. 内存泄漏:代码中没有释放动态分配的节点。实际编程中,应在使用完毕后用 delete 逐个释放:

    Node *tmp;
    while (head != nullptr) {
        tmp = head;
        head = head->next;
        delete tmp;
    }
    
  2. 指针未初始化:C++ 中局部指针变量的初值是不确定的(野指针)。幸运的是,new Node 会调用默认构造函数,而我们在结构体定义中已经给了 prenext 的类内初始值 nullptr,所以每个新节点的两个指针都是安全的。

  3. 双向链表的优势未体现:代码只使用了 next 遍历,没有用到 pre。若需要逆向遍历,可从尾节点开始用 pre 指针。

  4. 头节点的处理:本示例中头节点也存储有效数据(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,prenextnullptr。使用构造函数可以避免忘记赋初值的问题。

另外,使用智能指针(如 std::unique_ptr<Node>)可以自动管理内存,但环形引用会使析构变得复杂,因此原始指针在链表实现中仍然很常见。

posted @ 2026-05-27 08:55  一亩食堂  阅读(14)  评论(0)    收藏  举报