C/C++ 数据结构(五)链表的应用、对象池

本篇 核心知识 :带头尾结点双向链表完整实现、链表节点操作步骤、代码编码规范、程序调试技巧、链表实战场景、内存池 / 对象池思想、游戏怪物管理案例、随机数与概率应用


一、带头尾结点双向链表(完整结构)

概念

双向链表每个节点包含数据域 、前驱指针prev、后继指针next;额外增设头结点尾结点,两个结点均不存储有效业务数据,仅作为边界标识,统一空表、增删操作的代码逻辑。

特性
  1. 空表状态:头结点headnext指向尾结点tail,尾结点tailprev指向头结点;两者prev/next无野指针。

  2. 优势:无论链表为空、仅有一个节点、多节点,增删逻辑完全一致,无需单独判断头部 / 尾部边界。

  3. 节点访问:遍历从head->next(第一个有效节点)开始,到tail终止(尾结点不遍历)。

  4. 编码规范:做等值判断时常量写在左侧、变量写在右侧 (例:nullptr == p),避免漏写等号导致逻辑错误,编译器可直接报错。

代码示例(基础结构定义)

复制代码
#include <iostream>
using namespace std;
​
// 双向链表节点模板
template<typename T>
struct Node
{
    T data;         // 数据域
    Node<T>* prev;  // 前驱指针
    Node<T>* next;  // 后继指针
    // 构造函数:初始化指针为空
    Node(const T& val) : data(val), prev(nullptr), next(nullptr) {}
};
​
// 带头尾结点双向链表类
template<typename T>
class DoubleList
{
private:
    Node<T>* head;  // 头结点(无有效数据)
    Node<T>* tail;  // 尾结点(无有效数据)
public:
    // 构造函数:初始化空链表
    DoubleList()
    {
        head = new Node<T>(T());
        tail = new Node<T>(T());
        // 空表:head <-> tail 互相指向
        head->next = tail;
        tail->prev = head;
    }
​
    // 析构函数:释放所有节点
    ~DoubleList()
    {
        Node<T>* cur = head->next;
        while (cur != tail)
        {
            Node<T>* temp = cur;
            cur = cur->next;
            delete temp;
        }
        // 最后释放头尾结点
        delete head;
        delete tail;
        head = tail = nullptr;
    }
};

拓展

普通双向链表仅设头结点,而头尾双结点在尾部操作时无需遍历全链表查找尾节点,尾部增删效率更高。


二、双向链表尾部添加节点(核心操作)

概念

在链表末尾(尾结点tail之前)插入新节点,是双向链表最常用操作之一,操作本质是修改四处指针指向。

特性
  1. 操作顺序原则:先保存后续节点地址,再修改指针,防止断链、节点丢失;

  2. 核心四步指针修改(新节点记为newNode):

    1. 原尾结点的前驱节点(tail->prev)的next指向新节点;

    2. 新节点的prev指向原尾结点的前驱节点;

    3. 新节点的next指向尾结点tail

    4. 尾结点tailprev指向新节点。

  3. 头尾结点全程不移动,仅作为边界标记。

代码示例(尾插函数)

复制代码
template<typename T>
void DoubleList<T>::push_back(const T& val)
{
    Node<T>* newNode = new Node<T>(val);
    // 分步修改指针,严格遵循顺序防止断链
    newNode->prev = tail->prev;
    tail->prev->next = newNode;
    newNode->next = tail;
    tail->prev = newNode;
}

易错点解析

若颠倒指针修改顺序,会直接丢失原最后一个有效节点,造成内存泄漏与逻辑错误;编写代码时建议结合手绘链表图辅助分析。


三、链表遍历实现

概念

从第一个有效节点开始,依次访问每个节点数据,至尾结点终止。

特性
  1. 遍历起点:head->next(跳过空头结点);

  2. 遍历终止条件:当前节点!= tail(尾结点不参与遍历);

  3. 遍历过程仅读取数据,不修改指针指向。

代码示例(遍历打印)

复制代码
template<typename T>
void DoubleList<T>::print() const
{
    Node<T>* cur = head->next;
    while (cur != tail)
    {
        cout << cur->data << " ";
        cur = cur->next;
    }
    cout << endl;
}

四、程序调试核心技巧(开发必备)

概念

调试是定位代码逻辑错误、内存错误的核心手段,本节课基于 VS 编译器讲解常用调试快捷键与使用场景。

特性 & 快捷键

  1. F9:添加 / 取消断点,断点处程序会暂停,用于定位可疑代码行;

  2. F5:启动调试,运行至第一个断点处;

  3. F10(逐过程) :单步执行,不进入自定义函数内部,适合快速遍历代码;

  4. F11(逐语句) :单步执行,进入自定义函数内部,用于排查函数内部 bug。

调试用途

  1. 查看指针地址、节点prev/next指向,判断链表断链、野指针问题;

  2. 观察变量实时值,定位赋值、逻辑判断错误;

  3. 跟踪构造 / 析构执行流程,排查内存泄漏。

拓展

手写链表、指针相关代码必须依赖调试;仅靠静态阅读代码很难发现隐性内存问题。


五、容器选型实战

概念

结合业务场景选择vector(顺序表)或list(双向链表),核心依据是操作类型、数据量、执行效率

场景分析

  1. 场景 1:游戏怪物管理(频繁增删)

    业务特点:怪物不断被击杀(删除)、刷新(新增),增删位置随机;

    选型:优先list

    原因:list任意位置增删仅修改指针((O(1))),vector中间删除会批量挪动元素,效率极低。

  2. 场景 2:海量数据 + 频繁查询

    业务特点:数据量大,以查询、尾部操作为主,极少中间增删;

    选型:优先vector

    原因:vector支持下标随机访问((O(1))),查询速度远超链表。

总结

随机访问、尾部操作多 → vector

任意位置频繁增删 → list


六、内存池 & 对象池(性能优化拓展)

概念

针对资源频繁申请 / 释放 场景的优化方案,避免反复调用new/delete带来的性能损耗,游戏、服务器项目高频使用。

1. 对象池(以游戏怪物为例)

特性
  1. 设计思路:划分活动链表闲置(死亡)链表两个双向链表;

  2. 运行逻辑:

    怪物血量归 0:将节点从活动链表 转移至闲置链表不释放内存

    刷新新怪物:优先从闲置链表取出节点、重置数据;闲置链表为空时,再新建节点;

  3. 优势:减少new/delete调用,大幅提升运行效率,避免内存碎片。

2. 内存池

特性

提前一次性申请大块连续内存,后续对象直接从预分配内存中取用,运行结束统一释放;适用于大量小型对象频繁创建销毁的场景。

代码示例(简易对象池思路)

复制代码
// 活动链表:当前存活怪物
list<Monster> activeList;
// 闲置链表:死亡怪物(复用内存)
list<Monster> idleList;
​
// 怪物被击杀,移入闲置链表
void monsterDie(Monster m)
{
    activeList.remove(m);
    idleList.push_back(m);
}
​
// 刷新新怪物,优先复用闲置对象
Monster createMonster()
{
    if(!idleList.empty())
    {
        Monster m = idleList.front();
        idleList.pop_front();
        m.reset(); // 重置属性
        return m;
    }
    return Monster(); // 无闲置则新建
}

拓展

同类技术:线程池、连接池,核心思想均为资源复用、减少频繁创建销毁开销


七、游戏怪物管理综合案例

需求说明

基于双向链表 + 对象池实现游戏怪物管理,结合随机数、概率逻辑,完整覆盖链表操作、面向对象思想。

核心功能

  1. 怪物基础属性:血量、基础属性,支持掉血逻辑;

  2. 自动刷新规则:场景怪物数量低于最小值时,随机刷新新怪物;

  3. 怪物击杀:怪物血量归零,移入闲置链表;

  4. 遍历输出:打印所有活动怪物的血量;

  5. 随机数 / 概率应用:控制怪物刷新数量、刷新种类。

关键知识点(随机数 & 概率)

  1. 随机数范围公式:利用取模运算限定数值区间,遵循前闭后开规则;

  2. 概率判断:通过随机数阈值实现概率逻辑(例:随机数 < 80 代表 80% 概率触发)。

代码示例(怪物基础类)

复制代码
#include <cstdlib>
#include <ctime>
// 怪物类
class Monster
{
private:
    int hp;    // 血量
public:
    Monster() : hp(100) {}
    // 怪物掉血
void hurt(int val)
    {
        if(hp > val) hp -= val;
        else hp = 0;
    }
    // 重置属性(对象池复用)
    void reset()
    {
        hp = 100;
    }
    int getHp() const { return hp; }
};
​
// 初始化随机数种子
int main()
{
    srand((unsigned int)time(nullptr));
    // 后续怪物管理逻辑...
    return 0;
}
相关推荐
2601_961845151 小时前
花生十三网课网盘|百度网盘|下载
数据结构·算法·链表·贪心算法·排序算法·线性回归·动态规划
三品吉他手会点灯1 小时前
C语言学习笔记 - 48.流程控制2 - 什么是流程控制
c语言·开发语言·笔记·学习
John_ToDebug1 小时前
Windows客户端热修复技术:从原理到工程实践
c++·经验分享·hook
凡人叶枫1 小时前
Effective C++ 条款37:绝不重新定义继承而来的缺省参数值
linux·c++·windows
王老师青少年编程1 小时前
2022年CSP-X复赛真题及题解(T4:摧毁)
c++·真题·csp·信奥赛·复赛·csp-x·摧毁
梓䈑1 小时前
C++大模型统一接入引擎(第三篇):模型管理、会话持久化与SDK门面封装的完整实现
数据库·c++
王燕龙(大卫)2 小时前
使用实时调度策略和无锁队列踩坑记录
c++
赴生-2 小时前
C++进阶 智能指针
开发语言·c++
AI thought2 小时前
C语言、C++与C#深度研究报告:从底层控制到现代企业级开发的演进
c语言·c++·c·内存管理·编译模型