双向链表及如何使用GLib的GList实现双向链表

news2024/11/23 8:36:28

双向链表是一种比单向链表更为灵活的数据结构,与单向链表相比可以有更多的应用场景,本文讨论双向链表的基本概念及实现方法,并着重介绍使用GLib的GList实现单向链表的方法及步骤,本文给出了多个实际范例源代码,旨在帮助学习基于GLib编程的读者较快地掌握GList的使用方法,本文程序在 ubuntu 20.04 下编译测试完成,gcc 版本号 9.4.0;本文适合初学者阅读。

1 双向链表及其实现

  • 在文章《单向链表以及如何使用GLib中的GSList实现单向链表》中,介绍了单向链表以及基于 GLib 实现单向链表的方法,建议阅读本文前先阅读这篇文章;

  • 在文章《使用GLib进行C语言编程的实例》中,简单介绍了 GLib,建议阅读本文前先阅读这篇文章;

  • 双向链表(Doubly Linked List)是一种链式数据结构,每个节点包含三个主要部分:

    1. 数据部分:存储节点的数据
    2. 前向指针:指向链表中的下一个节点
    3. 后向指针:指向链表中的上一个节点
  • 可以看出,和单向链表相比较,双向链表多了一个指向前一个节点的指针

  • 双向链表的基本特性

    1. 双向性:与单向链表不同,双向链表允许从两个方向遍历,可以从头节点向尾节点遍历,也可以从尾节点向头节点遍历;
    2. 动态大小:双向链表的大小可以动态增长或缩小,不需要提前定义大小;
    3. 节点插入和删除:在双向链表中,插入和删除节点操作相对简单,因为每个节点都有指向前后节点的指针;
  • 双向链表的节点结构:

    struct Node {
        int data;               // 数据部分
        struct Node *next;      // 指向下一个节点的指针
        struct Node *prev;      // 指向前一个节点的指针
    };
    
  • 双向链表的基本操作

    1. 插入节点:可以在链表的开头、结尾或任意位置插入节点;
    2. 删除节点:可以删除链表中的任意节点,操作相对简单,因每个节点都知道其前一个和后一个节点;
    3. 遍历链表:可以从头到尾遍历链表(正向遍历)或从尾到头遍历链表(反向遍历);
  • 与单向链表相比,双向链表有以下特点:

    1. 由于数据结构中增加了后向指针,使链表可以双向遍历,而单向链表仅能单向遍历;
    2. 通过后向指针可以直接访问前一个节点,与单向链表相比,可以简化节点删除操作的复杂度;
    3. 在插入节点时,比单向链表更快捷更灵活;
    4. 与单向链表相比,由于增加了后向指针,内存开销增加;
    5. 与单向链表相比,双向链表需要操作两个指针,其操作和维护的复杂度要高一些;
  • 总的来说,‌双向链表比单向链表更加灵活,‌适用场景也要多一些。;

  • 下面程序是一个简单的双向链表的 C 语言标准库实现,dllist-c.c(点击文件名下载源程序)

  • 编译:gcc -Wall -g dllist-c.c -o dllist-c

  • 运行:./dllist-c

  • 该程序实现了双向链表的插入、删除以及正向遍历;

  • 该程序首先建立一个双向链表,并在链表中加入 4 个节点,数据分别为:1、2、3、5,然后显示整个链表;

  • 在第 3 个节点(数据为 3,索引号为 2)的后面插入节点,数据为 4,然后显示整个链表;

  • 将第 3 个节点(数据为 3,索引号为 2)删除,然后显示整个链表;

  • 最后释放整个链表;

  • 运行截图:

    screenshot of dllist-c

2 GLib 中双向链表结构 GList

  • GLib API version 2.0 手册 (点击查看手册)
  • GLib API 手册中 GList 部分 (点击查看手册)
  • 在 GLib 中,‌双向链表是通过 GList 结构体实现的,GList 是一个简单的双向链表结构,‌用于存储各种类型的数据;
  • GSList 定义如下:
    struct GList {
        gpointer data;
        GList *next;
        GList *prev;
    }
    
  • data 为双向链表的数据指针,可以指向任何类型或结构的数据;
  • next 为指向该双向链表当前节点的下一个节点的指针;
  • prev 为指向该双向链表当前节点的前一个节点的指针;
  • GLib 为双向链表结构 GList 的操作提供了大量的函数,本文仅就其中的一部分函数进行简单介绍;
  1. 添加、插入新节点

    • g_list_append() 在双向链表的最后添加一个新节点;
      GList *g_slist_append(GList *list, gpointer data)
      
      • list - 指向双向链表的指针
      • data - 指向添加节点的数据
      • 返回指向双向链表的起始指针;
      • 说明:在双向链表的最后添加节点,必须要遍历整个链表才能找到链表的尾部,这种做法效率很低,通常的做法是使用 g_list_prepend() 在链表的起始位置添加节点,当所有节点添加完毕后,再使用 g_list_reverse() 将整个链表反转;
    • g_list_prepend() 在双向链表的最前面添加一个新节点;
      GList *g_list_prepend(GList *list, gpointer data)
      
      • list - 指向双向链表的指针
      • data - 指向添加节点的数据
      • 返回指向双向链表的指针,在双向链表的开头添加一个节点,双向链表的指针是肯定会变化的;
    • g_list_insert() 在双向链表的中间插入一个新节点;
      GList *g_list_insert(GList *list, gpointer data, gint position)
      
      • list - 指向双向链表的指针
      • data - 指向添加节点的数据
      • position - 插入节点的位置,如果是负数或者超过了该双向链表的节点的数量,新节点将插到双向链表的最后;
      • 返回该双向链表的起始指针;
    • g_list_insert_before() 在包含指定数据的节点之前插入一个新节点;
      GList *g_list_insert_before(GList *list, GSList *sibling, gpointer data)
      
      • list - 指向双向链表的指针
      • data - 指向添加节点的数据
      • sibling - 指向一个节点的指针,将在这个节点前插入新节点
      • 返回该双向链表的起始指针;
  2. 删除节点

    • g_list_remove_link() 从双向链表中删除一个节点,但并不释放该节点占用的内存
      GList *g_list_remove_link(GList *list, GList *llink_)
      
      • list - 指向双向链表的指针;
      • llink_ - 指向双向链表中一个节点的指针,该节点将被删除;
      • 返回该双向链表的起始指针;
      • 该函数并不释放被删除的节点内存,被删除的节点的 next 和 prev 指针将指向 NULL,所以可以认为被删除的节点变成了一个只有一个节点的新的双向链表;
    • g_list_delete_link() 从双向链表中删除一个节点,并释放该节点占用的内存;
      GList *g_list_delete_link(GList *list, GList *link_)
      
      • list - 指向双向链表的指针;
      • link_ - 指向双向链表中一个节点的指针,该节点将被删除;
      • 返回该双向链表的起始指针;
      • 该函数与 g_list_remove_link() 的唯一区别是该函数在删除节点后释放了被删除节点占用的内存;
    • g_list_remove() 从双向链表中删除指定数据的一个节点,如果链表中有指定数据的节点有多个,将只删除第一个;
      GList *g_list_remove(GList *list, gconstpointer data)
      
      • list - 指向双向链表的指针
      • data - 指向要删除节点的数据
      • 返回该双向链表的起始指针;
    • g_list_remove_all() 从双向链表中删除指定数据的所有节点;
      GList *g_list_remove_all(GList *list, gconstpointer data)
      
      • list - 指向双向链表的指针
      • data - 指向要删除节点的数据
      • 返回该双向链表的起始指针;
  3. 遍历链表

    • g_list_foreach() 遍历双向链表,每个节点都会调用一个指定函数;
      void g_list_foreach(GList *list, GFunc func, gpointer user_data)
      
      • list - 指向双向链表的指针
      • func - 一个指向函数的指针,遍历到双向链表的每个节点时,都会调用这个函数;
      • GFunc 的定义如下:
      void (* GFunc) (gpointer data, gpointer user_data)
      
      • GFunc 的定义表明,传递给 func 的参数有两个,一个是 data - 指向当前节点的节点数据指针,另一个就是指向自定义参数 user_data 的指针
      • user_data - 指针指向调用 func 时传递的用户参数;
  4. 查找节点

    • g_list_find() 查找链表中包含给定数据的节点;
      GList *g_list_find(GList *list, gconstpointer data)
      
      • list - 指向双向链表的指针
      • data - 指向要查找节点的数据
      • 返回在双向链表中找到的节点的指针,如果没有找到相应节点,返回 NULL;
    • g_list_index() 获取包含给定数据的节点的位置(从 0 开始);
      gint g_list_index(GList *list, gconstpointer data)
      
      • list - 指向双向链表的指针;
      • data - 指向要查找节点的数据;
      • 返回数据为 data 的节点在双向链表中的位置(从 0 开始),如果没找到相应节点,则返回 -1;
    • g_list_position() 获取给定节点在链表中的位置(从 0 开始);
      gint g_list_position(GList *list, GList *llink)
      
      • list - 指向双向链表的指针;
      • llink - 指向双向链表中的一个节点的指针;
      • 返回 llink 指向的节点在双向链表中的位置(从 0 开始),如果没找到相应节点,则返回 -1;
  5. 释放链表

    • g_list_free() 释放链表使用的所有内存,该函数不会释放节点中动态分配的内存;
      void g_list_free(GList *list)
      
      • list - 指向双向链表的指针;
      • 该函数仅释放 GList 占用的内存,并不释放双向链表中各个节点动态申请的内存,如果链表中有动态申请内存,考虑使用 g_list_free_full() 或手动释放内存;
    • g_list_free_full() 释放链表使用的所有内存,并对每个节点的数据调用指定的销毁函数
      void g_list_free_full(GList *list, GDestroyNotify free_func)
      
      • list - 指向双向链表的指针;
      • free_func - 销毁函数,对双向链表中的每个节点数据将调用该函数,可用于释放节点中动态分配的内存;
      • GDestroyNotify 的定义如下:
      void (* GDestroyNotify) (gpointer data)
      
      • 所以在调用 free_func 时会将指向节点数据的指针传递给该函数;
  6. 其它

    • g_list_length() 获取双向链表的长度;
      guint g_list_length(GList *list)
      
      • list - 指向双向链表的指针;
      • 返回双向链表中节点的数量。
    • g_list_last() 获取双向链表的最后一个节点;
      GList *g_list_last(GList *list)
      
      • list - 指向单向链表的指针;
      • 返回双向链表的最后一个节点的指针,如果双向链表没有节点,则返回 NULL;
    • g_list_concat() 连接两个双向链表;
      GList *g_list_concat(GList *list1, GList *list2)
      
      • list1 - 指向第 1 个双向链表的指针;
      • list2 - 指向准备连接到第 1 个双向链表后面的双向链表的指针;
      • 返回连接好的双向链表的指针,
    • g_list_reverse() 反转整个双向链表
      GList *g_list_reverse(GList *list)
      
      • list - 指向双向链表的指针;
      • 返回该双向链表的起始指针;

3 如何使用 GList 实现双向链表

  • 文章的一开始有一个使用标准 C 语言函数库的双向链表的实例,使用 GLib 的 GList 操作双向链表要容易得多;

  • 下面程序是使用 C 语言,基于 GLib 实现的双向链表,dllist-glib.c(点击文件名下载源程序)

  • 该程序实现的功能与文章开头的程序 dllist-c.c 完全一样,但程序看上去要简洁很多,我们不妨把源程序列在这里

  • 该程序与文章《单向链表以及如何使用GLib中的GSList实现单向链表》中使用 GLib 实现单向链表的程序非常相似

    #include <stdio.h>
    #include <glib.h>
    
    void print_node(gpointer data, gpointer user_data) {
        printf("%d -> ", GPOINTER_TO_INT(data));
    }
    void print_list(GList *list) {
        g_list_foreach(list, &print_node, NULL);
        printf("NULL\n");
    }
    
    int main() {
        GList *list = NULL;
    
        printf("Append 4 nodes, the data are 1, 2, 3, 5.\n");
        list = g_list_append(list, GINT_TO_POINTER(1));
        list = g_list_append(list, GINT_TO_POINTER(2));
        list = g_list_append(list, GINT_TO_POINTER(3));
        list = g_list_append(list, GINT_TO_POINTER(5));
        print_list(list);
    
        printf("Insert a new node after node with the data 3.\n");
        list = g_list_insert(list, GINT_TO_POINTER(4), 3);
        print_list(list);
    
        printf("Remove node with the data 3.\n");
        list = g_list_remove(list, GINT_TO_POINTER(3));
        print_list(list);
    
        // Free the list
        g_list_free(list);
    
        return 0;
    }
    
  • 该程序中涉及到的两个宏:GINT_TO_POINTER(value)GPOINTER_TO_INT(p),在文章《单向链表以及如何使用GLib中的GSList实现单向链表》中有比较详细的介绍;

  • 编译:

    gcc -Wall -g dllist-glib.c -o dllist-glib `pkg-config --cflags --libs glib-2.0`
    
  • 其中,pkg-config --cflags --libs glib-2.0 的含义在文章《使用GLib进行C语言编程的实例》中做过介绍;

  • 运行:./dllist-glib

  • 该程序实现了双向链表的插入、删除、遍历;

  • print_list() 中使用 g_list_foreach() 对链表进行遍历,对链表中的每个节点数据,将调用函数 print_node()

  • 运行截图:

    screenshot of dllist-glib

4 双向链表的应用场景

  • 双向链表是一种数据结构,它的每个节点包含对前一个节点和后一个节点的引用;这种结构在许多应用场景中非常有用,以下是一些常见的应用场景:
  1. 浏览器历史记录:

    双向链表可以用来实现浏览器的“后退”和“前进”按钮,用户可以在历史记录中前后移动当前指针;

  2. 音乐播放器:

    在音乐播放器中,双向链表可以用于管理播放列表,允许用户在歌曲之间前后切换;

  3. 文本编辑器:

    在实现撤销和重做功能时,双向链表可用于存储编辑历史,方便在不同操作间切换;

  4. LRU缓存:

    在实现最近最少使用(LRU)缓存时,双向链表可以高效地维护访问顺序,以便快速找到和删除最少使用的项;

  5. 操作系统中的进程调度:

    在某些调度算法中,双向链表可用于管理就绪队列,使得进程可以方便地添加和移除;

  6. 图形界面中的组件布局:

    在某些图形用户界面(GUI)框架中,双向链表用于管理组件的顺序和关系,使得组件之间的插入和删除变得灵活;

  7. 实现栈和队列:

    双向链表可以作为基础结构来实现栈和队列,提供灵活的插入和删除操作。

5 基于 GLib 的 GList 模拟终端命令的历史记录

  • 当我们在 Linux 终端上输入命令时,终端应用程序会记录你输入的命令并形成历史记录,可以使用 history 命令来查看这个历史记录;

  • 在终端上也可以使用上、下箭头键来翻看曾经输入过的前一个或者后一个历史命令,这个命令历史记录给使用终端带来了一定的便利;

  • 本实例模拟了终端输入命令并使用双向链表生成命令的历史记录,按上下箭头键可以查看上一条或下一条命令;

  • 源程序 cmd-history.c(点击文件名下载源程序) 基于 GLib 的 GList 模拟了终端历史记录;

  • 该程序首先建立了一个双向链表队列,然后模拟输入命令,链表中的每个节点存储一条命令,命令输入完成后显示最后一条命令,然后按上下箭头键可以从链表中取出上一条命令或者下一条命令并显示在屏幕上;

  • 很显然,使用单向链表实现命令历史记录是不方便的,但使用双向链表就很方便;

  • 编译:

    gcc -Wall -g cmd-history.c -o cmd-history `pkg-config --cflags --libs glib-2.0`
    
  • 其中,pkg-config --cflags --libs glib-2.0 的含义在文章《使用GLib进行C语言编程的实例》中做过介绍;

  • 运行:./cmd-history

  • 运行截图:

    screenshot of cmd-history在这里插入图片描述

  • 该程序涉及到终端的操作,使用了结构 struct termios、函数 tcgetattr()tcsetattr(),这些并不在 C 标准库 libc 中,需要启用 GNU 扩展库,所以在程序的开始有 #define _GNU_SOURCE

  • 有关终端操作的相关数据结构、宏定义以及相关函数,并不在本文的讨论之内,请自行参考其它资料;

  • 该程序中还涉及到了使用 ESC 转义符对终端屏幕进行清屏操作,有关 ESC 转义符的含义,请参考另一篇文章《ANSI的ESC转义序列》

  • 该程序中还涉及到了从键盘缓冲区读取上、下箭头键的方法,上箭头键返回的编码为 ESC [ A,下箭头键返回的编码为 ESC [ B,这里说明一下有助于读者更快地读懂程序。


email: hengch@163.com

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2230342.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

web——warmup——攻防世界

这道题还是没有做出来。。&#xff0c;来总结一下 1.ctrlU显示源码 2.看见body里有source.php 打开这个source.php 看见了源码 highlight_file(FILE); 这行代码用于高亮显示当前文件的源码&#xff0c;适合调试和学习&#xff0c;但在生产环境中通常不需要。 class emmm 定义…

【MATLAB代码】三个CT模型的IMM例程,各CT旋转速率不同,适用于定位、导航、目标跟踪

三个CT模型&#xff0c;各CT模型下的运动旋转速率不同&#xff0c;适用于定位、导航、目标跟踪 文章目录 代码构成运行结果源代码代码讲解概述代码结构1. 初始化2. 仿真参数设置3. 生成量测数据4. IMM迭代5. 绘图 主要功能函数部分1. 卡尔曼滤波函数2. 模型综合函数3. 模型概率…

sklearn 实现随机森林分类器 - python 实现

python sklearn 实现随机森林分类器 from sklearn.ensemble import RandomForestClassifier from sklearn.datasets import load_iris # 加载数据集 irisload_iris() x,yiris.data,iris.target print("x y shape:",x.shape,y.shape) # 创建并训练模型 model Random…

GetX的一些高级API

目录 前言 一、一些常用的API 二、局部状态组件 1.可选的全局设置和手动配置 2.局部状态组件 1.ValueBuilder 1.特点 2.基本用法 2.ObxValue 1.特点 2.基本用法 前言 这篇文章主要讲解GetX的一些高级API和一些有用的小组件。 一、一些常用的API GetX提供了一些高级…

WPF+MVVM案例实战(十一)- 环形进度条实现

文章目录 1、运行效果2、功能实现1、文件创建与代码实现2、角度转换器实现3、命名空间引用3、源代码下载1、运行效果 2、功能实现 1、文件创建与代码实现 打开 Wpf_Examples 项目,在Views 文件夹下创建 CircularProgressBar.xaml 窗体文件。 CircularProgressBar.xaml 代码实…

SYN590RH

一般描述 SYN590RH是SYNOXO全新开发设计的一款宽电压范围&#xff0c;低功耗&#xff0c;高性能&#xff0c;无需外置AGC电容&#xff0c;灵敏度达到典型-110 dBm,400MHz~450MHz频率范围应用的单芯片ASK或00 K射频接收器。 SYN590RH是一款典型的即插即用型单片高…

kafka里的consumer 是推还是拉?

大家好&#xff0c;我是锋哥。今天分享关于【kafka里的consumer 是推还是拉&#xff1f;】面试题&#xff1f;希望对大家有帮助&#xff1b; kafka里的consumer 是推还是拉&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在Kafka中&#xff0c;消费者&…

C语言的数组地址 数组的遍历与练习

1.int main(void) { int a[5] { 10,20,30,40,50 };//数组间的元素地址相连的 int* p; printf("%d\n", &a[0]); printf("%d\n", &a[1]); printf("%d\n", &a[2]); printf("%d\n", &a[3]); …

基于springboot + vue的网上订餐系统的设计与实现(附源码)

一、项目背景 随着互联网技术的飞速发展和智能手机的普及&#xff0c;人们的生活方式发生了翻天覆地的变化&#xff0c;其中之一便是网上订餐系统的兴起。这种系统通过在线平台连接消费者和餐饮服务提供商&#xff0c;使得用户可以随时随地浏览菜单、下单并支付&#xff0c;极…

Redis安装及运维

源码安装 Redis安装前建议要进行残留数据检查&#xff0c;排除后期存在的各种隐患 官网&#xff1a;https://redis.io/&#xff0c;Linux版本只会提供源码&#xff0c;不提供二进制安装包&#xff0c;因此需要编译源码进行安装&#xff0c;本次使用CentOS8 VMware虚拟机进行安…

windows10 安装 达梦数据库DM8

一. 前期工作 下载 https://www.dameng.com/list_103.html 通过百度网盘分享的文件&#xff1a;达梦数据库 链接&#xff1a;https://pan.baidu.com/s/1mJcT3UiwojeWIhXpAwh-RA 提取码&#xff1a;jyzi 点我: 想要 解压 双击iso文件 二. 安装步骤 1 .双击setup.ext安装 …

机器人大模型GR2——在大规模视频数据集上预训练且机器人数据上微调,随后预测动作轨迹和视频(含GR1详解)

前言 上个月的24年10.9日&#xff0c;我在朋友圈看到字节发了个机器人大模型GR2&#xff0c;立马去看了下其论文(当然了&#xff0c;本质是个技术报告) 这次也是我头一次看paper&#xff0c;不看正文&#xff0c;而是直奔其References&#xff0c;​看有没有我预想中的文献&a…

PCB电源层布线信号

在印刷电路板(PCB)的设计过程中,电源层通常被视为电源分配网络(PDN)的核心。电源层和接地层通常是通过平面铜层来实现的,旨在确保系统稳定性。然而,随着电路板复杂性的增加,尤其是在多层电路板中,设计师可能面临在电源层上布置信号线路的需求。虽然这种做法可以节省空…

【大数据学习 | kafka】producer的参数与结构

1. producer的结构 producer&#xff1a;生产者 它由三个部分组成 interceptor&#xff1a;拦截器&#xff0c;能拦截到数据&#xff0c;处理完毕以后发送给下游&#xff0c;它和过滤器不同并不是丢弃数据&#xff0c;而是将数据处理完毕再次发送出去&#xff0c;这个默认是不…

Java基本语法和基础数据类型——针对实习面试

目录 Java基本语法和基础数据类型标识符和关键字有什么区别&#xff1f;Java关键字有哪些&#xff1f;Java基本数据类型有哪些&#xff1f;什么是自动装箱和拆箱&#xff1f;自动装箱&#xff08;Autoboxing&#xff09;自动拆箱&#xff08;Unboxing&#xff09; 自动装箱和拆…

自动化测试工具Ranorex Studio(十九)-其他编辑选项

失败继续运行和禁用 表中列出的每个Action条目&#xff0c;都可以设置为禁用或 “失败继续运行”。 设置action条目为“失败继续运行”时&#xff0c;如果遇到错误&#xff0c;模块不会停在那个位置&#xff0c;而是继续执行。 您可以通过右键菜单或属性窗口设置这两个选项。 设…

知识见闻 - Workday公司介绍

人力资源“一哥”Workday的前世今生 01 Duffield 既然要聊Workday&#xff0c;我们首先要认识一个人。David Duffield&#xff0c;又一位企业软件服务行业的绝对大神。 大卫杜菲尔德&#xff08;David Duffield&#xff09;出生于1941年。 40岁&#xff0c;很多职场人都已经认命…

VS2022配置调试Qt源代码

需要保证源代码和项目使用的版本匹配&#xff0c;符号需要注意是64位还是32位&#xff0c;并且用msvc。 1. 设置源代码路径 2. 设置调试PDB路径 这里最好把4个地方都加进去&#xff0c;防止某些不常用PDB被漏掉。 D:\Qt\5.15.2\msvc2019_64\bin D:\Qt\5.15.2\msvc2019_64\lib…

利用腾讯元器构建商业化AI智能体——【快递100 AI智能体实战教学】

写在开头 随着人工智能技术的不断进步&#xff0c;腾讯元器作为一项强大的工具&#xff0c;使得构建商业化的AI智能体变得更加便捷和高效。本文将带你深入了解如何利用腾讯元器搭建快递100 AI智能体的全过程&#xff0c;从前期规划到最终实现&#xff0c;为你提供一份详尽的实…

「C/C++」C++ 设计模式 之 单例模式(Singleton)

✨博客主页何曾参静谧的博客&#x1f4cc;文章专栏「C/C」C/C程序设计&#x1f4da;全部专栏「VS」Visual Studio「C/C」C/C程序设计「UG/NX」BlockUI集合「Win」Windows程序设计「DSA」数据结构与算法「UG/NX」NX二次开发「QT」QT5程序设计「File」数据文件格式「PK」Parasoli…