[c] _ c中的单链表与文件读写

我们知道,链表是一种经典的数据结构,在很多地方都会使用到.而链表中最基础且经典的就是但链表和双链表了.今天总结一下链表在c语言中的实现.

  • 链表的出现

我们知道,在c语言中,有基本数据类型,自定义类型和数组..其中数组用来存储同类型数据的集合.但是数组确是十分不方便的,数组的大小在定义时要事先给出.编译系统会更具数组元素类型和数组元素的大小静态的分配一片连续的内存空间.这片内存空间却不能在程序执行过程中进行调整.所以我们常常以最大需求来定义数组.这样会导致内存空间的浪费或空间不足的尴尬.

所以,为了达到动态内存分配的目的,存储同种类型数据的集合.链表的形式出现了.链表将连续的和非连续的内存空间联系起来(指针域或者地址域).基本思路就是:在存放当前数据的时候,顺便存储下一条数据的首地址这样就实现了将各个不连续的数据串成"链".如此,就可实现把不同的数据存放在非连续或者连续的内存区域.

  • 单链表
    正如链表的概念:
图片.png

单链表的每个数据由两部分组成,数据域以及指针域(地址域),数据域用来存放各种数据,指针域用来指向下一条数据.当然,一张单链表必须具备一个头指针才能让我们找到他们,所以通常的,我们都会为链表添加一个头指针来指向链表的开始,也就是头结点.头指针抛去了数据域,只存放头指针的地址,只是起到指向的作用,这一点,在数组,字符数组中就有体现,他们在c语言中的读取与操作都是通过其首地址实现的.我们在使用调试的时候就会发现,他们的首个数据是带有一个地址信息的.

c语言中链表是用结构体来实现的.我们知道,c语言不像高级语言,不支持面向对象,可以自定义类和对象,而是直接通过结构体来定义自定义类型,C语言通过这种方式来描述众多的复杂类型数据.解决实际问题的.

  • 单链表的定义:

我们知道c语言中是使用指针变量来存放一种类型数据的首地址的,所以这里单链表的结构体定义,有点类似与递归的定义,在结构体中定义一个结构体指针变量,来指向下一个结构体指针的首地址.

// 定义学生结构体
struct Student
{
    char id[20]; //id
    char name[20]; //姓名
    char sex[3]; // 性别
    int age; // 年纪
    int Grade; // 成绩
};

// 定义一个学生单链表类型,当然数据域你也可以不嵌套结构体直接写也没有问题.
struct StuLinklist
{
    Student student; //数据域
    StuLinklist* next;  // 指针域
};
  • 双链表
图片.png

很容易理解,根据链表的概念,我们可以看出单链表与双链表的最根本的区别就是,双链表使用了两个指针域分别用来指向了前一个数据,和后一个数据.双链表的出现是显而易见的.使用单链表的时候,我们只能向下索取,而不能向上索取,只能通过循环遍历来获取对应位置的数据,这无疑是十分不方便的,所以双链表出现了.这使得我们在操作一个双链表数据的时候非常的方便.

  • 双链表的定义:
    相同的,我们只需要在结构体中多声明一个结构体指针指向上一个结构体数据就可以了.
// 双链表
struct Humanity
{
    // 数据域
    char name[20];
    int age;
    char sex;
    // 指针域
    Humanity* previous;
    Humanity* nxet;
};

当然,这里只是简单的了解下双链表,比较在使用上还是单链表较为常用.特别对于c语言来说,初学链表.以单链表为切入点,更加容易.也能够提高你对双链表的理解.毕竟它们原理类似.

在本章,只结合结构体,单链表,文件读取,总结下单链表在c语言中的使用,和单链表结合文件存取的使用.如何将链表式数据保存到文件中,如何从文件中读取单链表数据.基于这几点,了解通透了的话,基本就可以解决绝大部分基于文件操作的控制台信息管理系统的程序设计了.


  • 单链表的使用

单链表的使用最基本,最常用的无非就是初始化,增删改查,求表长,销毁等操作.
需要注意的是单链表的单向访问性,注定了对单链表子节点的访问必须从头开始.

  • 初始化

相当于创建一个空节点(数据域为空,指针域也为空).

// 创建一个空节点
StuLinklist* mcreate(){
    // 创建一个单链表节点.
    StuLinklist* H = (StuLinklist*)malloc(sizeof(StuLinklist));
    if (!H)
    {
        printf("创建失败");
        getchar();
        exit(0);
    }
    else { // 初始化
        H->next = NULL;
    }
    return H;
}

初始化了一个带头指针(H)的空的头结点.

  • 插入

我们根据单链表的性质.插入只需要三步,创建一个新节点.将插入前一个节点指向新节点,将新节点的指针域指向后一个节点.

// 在第i个位置插入一个新的节点
bool minsert(StuLinklist* p, int i,Student stu){
    int j = 0;
    // 定义temp指针变量循环遍历指针,初始化为被插入链表p.
    StuLinklist* temp=p;
    //循环遍历找到i-1的位置
    while (temp && j<i-1)
    {
        temp = temp->next;
        j++;
    }
    // 判断i为空或i<1(i=0)
    if (!temp || j>i-1)
    {
        return 0; // 插入失败
    }
    else {
        // 创建一个新节点
        StuLinklist* s = mcreate();
        // 更新数据域为传入的stu
        s->student = stu;
        // 更改节点的指针域插入节点s
        s->next = temp->next;
        temp->next = s;
        return 1; //插入成功
    }
}

  • 删除第i个节点
    道理和插入是相同的,指针域的更改不同,并且要释放掉内存空间而已:
// 删除第i个位置上的节点,且暂时保存其中的数据.
bool mdeletelist(StuLinklist* p, int i, Student* stu) {
    int j = 0;
    
    // 定义temp指针变量循环遍历指针,初始化为被插入链表p.
    StuLinklist* temp = p;
    //循环遍历找到i的位置
    while (temp->next && j < i-1)
    {
        temp = temp->next;
        j++;
    }
    // 判断i为空或i<1(i=0)
    if (!temp || j > i - 1)
    {
        return 0; // 插入失败
    }
    else {
        // 创建一个新节点
        StuLinklist* s = mcreate();
        // 使s指向第i个位置(要删除的位置)
        s = temp->next;
        //用stu参数暂时保存即将删除的数据.
        *stu = s->student;
        //让temp.next指向i后面的一位.
        temp->next = s->next;
        //删除节点,释放掉s指向的第i个位置的节点空间
        free(s);
        return 1; //删除成功
    }
}
  • 遍历输出和获取链表长度
    通过插入和删除,我们明白了链表的核心,就是遍历.我们对链表的操作基本都是基于遍历的基础之上的.
// 遍历链表输出
void printlist(StuLinklist* p) {
    StuLinklist* temp;
    temp = p;
    while (temp)
    {
        Student s = temp->student;
        printf("%s%s%s%d%d\n", s.id, s.name, s.sex, s.age, s.Grade);
        temp = temp->next;
    }
}

// 遍历获取链表长度
int mgetlength(StuLinklist* p) {
    int j = 0;
    StuLinklist* temp=p;
    while (temp)
    {
        temp = temp->next;
        j++;
    }
    return j;
}

当然,如果你想把功能写全,还可以设计查找函数,匹配函数.等等.这些同样,都是基于链表的遍历的.只要你理解了链表的遍历,相信这些函数你都可以很轻松的实现.


  • 向指定文件中写入数据块

文件读写的详细操作和原理,流程.都将在另一篇中进行总结.本篇只总结其中两种最常用的,基于数据块(也就是结构体,数组,等)两种读写方式:

  • 写入数据块: fwrite函数
/* 
    r只读(打开),r+读写(不存在返回空,读写从头开始,写时覆盖).
    w只写(打开或建立文件,内容全部消失).w+读写(打开或新建,读写可以通过位置函数定义)
    a追加数据.(打开或新建),a+(读写,位置可以定义) 以上都对文本文件进行操作.

    rb,rb+,wb,wb+,ab,ab+都是对二进制文件操作.
*/ 
//结构体写入文件:fwrite.
bool mwritefile(const char* file,StuLinklist* p) {
    // 声明一个文件指针
    FILE* fp;
    // 二进制文件追加的方式打开文件.
    fopen_s(&fp,file,"ab");
    if (!p)
    {
        return 0; //写入失败.
    }
    else{
        fwrite(p,sizeof(StuLinklist),1,fp);
        fclose(fp); // 关闭文件流
        return 1;
    }
}
  • 读取数据块:fread函数
// 结构体读取文件:fread
bool mreadfile(const char* file, StuLinklist* p) {
    // 声明一个文件指针
    FILE* fp;
    // 二进制文件追加的方式打开文件.
    fopen_s(&fp, file, "rb");
    if (!p)
    {
        return 0; //读取失败.
    }
    else {
        fread(p, sizeof(StuLinklist), 1, fp);
        fclose(fp); // 关闭文件流
        return 1; // 读取成功
    }
}

主函数:

int main()
{
    // 初始化一个学生
    Student student = {
        "1001","小明","男",14,87
    };
    // 创建并初始化一个空节点(其实就是为p分配内存空间)
    StuLinklist* p = mcreate();
    // 将变量student赋值给空节点.
    p->student = student;
    //定义一个新的空学生.初始化为p节点的student
    Student student2 = p->student;
    // 尝试直接输出student
    // printf("%s%s%c%d%d\n", p->student); 错误,嵌套的坏处也就意味着不能直接访问嵌套结构体里的成员了.
    printf("%s%s%s%d%d\n", student2.id, student2.name, student2.sex, student2.age, student2.Grade);

    //-------//

    // 初始化第三个学生.
    Student student3 = {
        "1001","小王","女",13,89
    };
    if (minsert(p,1,student3)){
        printf("插入成功");
    }
    else {
        printf("插入失败");
    }   
    // 遍历输出链表
    printlist(p);
    // 声明一个student用来暂存删除节点的数据
     Student stu2;
    // 删除一个节点,再次遍历输出
    mdeletelist(p, 1,&stu2);
    printlist(p);
    // 输出暂存的stu2
    printf("%s%s%s%d%d\n", stu2.id, stu2.name, stu2.sex, stu2.age, stu2.Grade);
    // 获取链表长度
    printf("%d",mgetlength(p));
    
    // -------------//
    // 声明读写文件的位置
    const char* file = "linktest.bin";

    // 写入文件
    if (mwritefile(file, p)) {
        printf("写入成功");
    }
    else {
        printf("写入失败");
    }

    // 声明一个单链表变量
    StuLinklist s;
    Student st;
    // 读取文件
    if (mreadfile(file,&s))
    {
        printf("读取成功");
        st = s.student;
        printf("%s%s%s%d%d\n", st.id, st.name, st.sex, st.age, st.Grade);
    }
    else {
        printf("读取失败");
    }
}

结果:


图片.png

2019.12.25
16:00

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容