C语言(Head First C)-7:数据结构与动态存储

2023-11-06

 该系列文章系个人读书笔记及总结性内容,任何组织和个人不得转载进行商业活动!

7:数据结构与动态存储:

 

 一个结构根本不够!

 

 本章内容:

 1)如何用结构指针吧自定义数据类型连接成复杂的大型数据结构;通过创建链表探索其中的基本原理;

 2)通过在堆上动态分配空间来学习如何让数据结构处理可变数量的数据,并在完成后释放空间;(如果嫌清理麻烦,可以学习一下使用valgrind)

 

 保存可变数量的数据:

     为了保存可变数量的数据,需要一样比数组更灵活的东西,即链表;

     链表是一连串的数据;

     链表是一种抽象数据结构;它是通用的,可以用来保存很多不同类型的数据,所以被称为抽象数据结构;

     链表保存了一条数据和一个链向另一条数据的链接;

     要知道链表从哪里开始,就可以遍历链表,可以从一条数据跳到另一条,直到链表的尾部;

 (P1)

 

 在链表中的插入数据:

     相比于数组,链表插入非常快;如果想在数组中插入一个值,就不得不将插入点后所有的数据后移一个位置;

     我们可以用链表来保存可变数量的数据;

 (P2)

 

 如何在C语言中创建链表?

 

 创建递归结构:

     链表中每个结构都需要与下一个结构相连;

     如果一个结构包含一个链向同种结构的链接,这种结构就称为递归结构;

     递归结构中含有指向同种结构的指针;

 场景:

     太平洋上诸多岛屿之间的飞机航线,受天气原因可能会临时变化,存储的结构可以设计如下:

 (Code7_1)

/*
 *
 */

#include <stdio.h>

//island
typedef struct island{//必须为这个结构体命名 因为递归定义的额时候要用
    char * name;
    char * opens;//岛上机场的营业时间
    char * closes;
    struct island * next;//这里不能用别名 必须用结构体名
}island;

int main() {
    
    island island1 = {"A","09:00","17:00",NULL};
    island island2 = {"B","09:00","17:00",NULL};
    island island3 = {"C","09:00","17:00",NULL};
    island island4 = {"D","09:00","17:00",NULL};
    island island5 = {"E","09:00","17:00",NULL};
    
    island1.next = &island2;
    island2.next = &island3;
    island3.next = &island4;
    island4.next = &island5;
    
    //2、3之间插入一个岛
    island island_insert = {"E","09:00","17:00",NULL};
    
    island_insert.next = island2.next;
    island2.next = &island_insert;
    
    return 0;
}

 这里要小心:

     递归结构要有名字;

     在递归结构中,需要包含一个相同类型的指针,C语言的语法不允许用typedef别名声明它,因此必须为结构起一个名字;

 

 接下来,参看Code7_1中实现的飞机航线上诸岛的链表示例:

     初始island的next字段值都设为NULL;C语言中,NULL的值实际上为0,NULL专门用来把某个指针设为0;

     我们需要小心地将每一个island的next变量设为下一个island的地址;

 

 在链表中插入值:

     通过修改指针就可以实现插入island,参看Code7_1;

     这里在中间位置声明了要插入的变量,这在C99和C11标准是可以的,在ANSI C中,必须在函数的顶部声明局部变量;

 

 示例:

     打印链表-航线上岛屿的名字;

 (Code7_2)

/*
 *
 */

#include <stdio.h>

//island
typedef struct island{//必须为这个结构体命名 因为递归定义的额时候要用
    char * name;
    char * opens;//岛上机场的营业时间
    char * closes;
    struct island * next;//这里不能用别名 必须用结构体名
}island;

int display(island * start){
    island * il = start;
    int i;
    for (i = 0 ; il->next != NULL; il = il->next , i++) {
        printf("Island name:%s\nOpen-Close:%s-%s\n",il->name,il->opens,il->closes);
    }
    return i;
}

int main() {
    
    island island1 = {"A","09:00","17:00",NULL};
    island island2 = {"B","09:00","17:00",NULL};
    island island3 = {"C","09:00","17:00",NULL};
    island island4 = {"D","09:00","17:00",NULL};
    island island5 = {"E","09:00","17:00",NULL};
    
    island1.next = &island2;
    island2.next = &island3;
    island3.next = &island4;
    island4.next = &island5;
    
    printf("%i\n",display(&island1));
    
    return 0;
}

 log:

 Island name:A

 Open-Close:09:00-17:00

 Island name:B

 Open-Close:09:00-17:00

 Island name:C

 Open-Close:09:00-17:00

 Island name:D

 Open-Close:09:00-17:00

 4

 

 我们打印了该线路上所经过的岛屿,同时返回了岛屿的数量(没有打印出目的地岛屿,可以使用do-while打印);

 

 注意:

     不同于J其他语言,如Java,它有内置链表;C语言没有内置数据结构,必须自己创建;

     链表想遍历只能从头开始;这一点不比数组;

     递归定义的结构中使用的是指向其他结构的指针,它不能换成一个递归定义的结构,因为结构在存储器中大小确定,如果递归地赋值自己,两条数据将不一样大;

 

 程序需要动态存储:

 场景:

     从一个不断更新的文件中读取各个岛的信息,比如一个每行只有一个 island name的文件;我们不知道这个文件有多大;

 

 分析:

     前面程序我们是为每个岛都声明了一个变量,但现在我们不知道文件大小,又怎么能知道有多少变量呢?

     我们需要在需要的时候分配新的存储空间;

 

 所以,需要以某种方法创建动态存储:

     到目前为止,我们写过的所有程序都使用静态存储;

     每当想保存一样东西,都在代码中添加一个变量,通常保存在栈中;(栈式存储器中专门用来保存局部变量的区域)

     如果编译时知道需要保存多少数据,那没问题,但程序在运行前往往不知道自己需要多少存储空间;

 

 用堆进行动态存储:

     程序运行时很难在栈上分配大量空间,所以需要堆;

     堆是程序中用来保存长期使用数据的地方;堆上的数据不会自动清除;是保存数据的好地方,比如保存我们的链表;

 

 首先,用malloc()获取空间:

     如果程序在运行时发现有大量数据要保存,要申请一个大容量的存储空间,可以用malloc()函数来申请;

     告诉malloc()需要多少个存储,他会要求操作系统在堆上分配空间;然后会返回一个指针,指向堆上新分配的空间;

     指针就好比这块存储空间的钥匙,可以用它来访问存储器,并跟踪这块区域的使用;

 

 有用有还:

     堆存储器的优点是可以占用很长时间,缺点同样是这个;

     使用栈的时候,无需操心归还存储器,因为这个过程是自动进行的;离开函数事,局部变量会从栈中清除;

     堆则不同,一旦申请就不能再分配,直到告诉C标准库,你已用完;

     堆的空间有限,不断堆积,会造成存储器泄漏,这种错误很常见,却也很难追踪;

 

 调用free()释放存储器:

     malloc()函数分配空间并给出一个指向这块空间的指针;你需要用这个指针访问数据,用完之后,需要用free()函数释放存储器;

 

 看看malloc()和free()如何工作:

 

 用malloc()申请存储器:

     memory allocation(存储器分配)的意思;

     malloc()接收一个参数:所需要的字节数;通常我们不知道确切的字节数,因此malloc()经常与sizeof运算符一起使用;

 像这样:

 #include <stdlib.h>//包含stdlib.h头文件,以使用malloc()和free()函数;

 ...

 malloc(sizeof(island));//表示“给我足够大的空间来保存island结构”

 

 siziof告知某种数据类型在系统中占了多少字节;这种数据类型可以是结构,也可以是int或double这样的基本数据类型;

 malloc()函数为你分配一块存储器,然后返回一个指针;指针中保存了存储器块的起始地址;malloc()返回的是通用指针,即void*类型的指针;

 

 用free()释放存储器:

     如果在一个地方用malloc()分配了存储器,就应该在后面用free()释放他;

     free()需要接收malloc()创建的存储器地址;只要告诉C标准库存储器块从哪里开始,他就能查阅记录,知道要释放多少存储器;

 island * p = malloc(sizeof(island));

 free(p);//表示释放分配的存储器,从堆地址p开始;

 (Code7_3)

/*
 * 创建一条航线之后 打印输出
 */

#include <stdio.h>
#include <stdlib.h>//使用malloc()不要忘记导入头文件
#include <string.h>

//island
typedef struct island{//必须为这个结构体命名 因为递归定义的额时候要用
    char * name;
    char * opens;//岛上机场的营业时间
    char * closes;
    struct island * next;//这里不能用别名 必须用结构体名
}island;

int display(island * start){
    if (start == NULL) {
        return 0;
    }
    island * il = start;
    int i;
    for (i = 1 ; il->next != NULL; il = il->next , i++) {
        printf("Island name:%s  Open-Close:%s-%s\n",il->name,il->opens,il->closes);
    }
    printf("Island name:%s  Open-Close:%s-%s\n",il->name,il->opens,il->closes);
    return i;
}

island * create(char * name){
    island * i = malloc(sizeof(island));
    i->name = name;
    i->opens = "09:00";
    i->closes = "17:00";
    i->next = NULL;
    
    return i;
}

int main() {
    char name[80];
    island * start = NULL;
    island * current = NULL;
    
    puts("输入岛名:(输入S结束)");
    while (scanf("%79s",name) == 1) {
        if (strlen(name) == 1 && (name[0] - 'S' == 0)){
            break;
        }
        island * il = create(name);
        if (start == NULL) {
            start = il;
        }
        if (current != NULL) {
            current->next = il;
        }
        current = il;
        
        puts("输入岛名:");
    }
    
    printf("%i\n",display(start));
    
    //free all
    return 0;
}

 这个程序中,我们定义了一个结构;一个遍历的方法;一个生成的方法;并在main函数中循环输入直到输入S;

 log:

 输入岛名:

 a

 a-输入岛名:

 b

 b-输入岛名:

 c

 c-输入岛名:

 d

 d-输入岛名:

 e

 e-输入岛名:

 f

 f-输入岛名:

 S

 S-Island name:S  Open-Close:09:00-17:00

 Island name:S  Open-Close:09:00-17:00

 Island name:S  Open-Close:09:00-17:00

 Island name:S  Open-Close:09:00-17:00

 Island name:S  Open-Close:09:00-17:00

 Island name:S  Open-Close:09:00-17:00

 6

 

 奇怪的事情发生了,我们用来存储name的字符数组在最后接收字符“S”之后,前面所有的岛的名字全都被莫名的修改了!

 

 问题在于:

     当代码记录岛名字的时候,并没有接收一份完整的name字符,而是记录了name字符串在存储器中的地址;

     这也导致,所有创建的岛都共享了同一个name字符串;一旦局部变量name更新了,前面的岛的名字也就没了;

 

 聚焦字符串复制:

     string.h头文件中创建了一个叫strdup()的函数,可以将字符串的所有字符复制到堆上;

     strdup()函数能够计算出字符串的长度,然后调用malloc()函数在堆上分配相应的空间;

     然后,strdup()函数把所有字符串复制到堆上的新空间;

     因为strdup()是把新字符串放在堆上,所以千万记得要用free()函数释放空间;

 

 用strdup()修改代码:

     i->name = name;    =>     i->name = strdup(name);

     opens和closes只所以不用修改,是因为他们根本就不会变;

 (Code7_4)

/*
 * 创建一条航线之后 打印输出
 */

#include <stdio.h>
#include <stdlib.h>//使用malloc()不要忘记导入头文件
#include <string.h>

//island
typedef struct island{//必须为这个结构体命名 因为递归定义的额时候要用
    char * name;
    char * opens;//岛上机场的营业时间
    char * closes;
    struct island * next;//这里不能用别名 必须用结构体名
}island;

int display(island * start){
    if (start == NULL) {
        return 0;
    }
    island * il = start;
    int i;
    for (i = 1 ; il->next != NULL; il = il->next , i++) {
        printf("Island name:%s  Open-Close:%s-%s\n",il->name,il->opens,il->closes);
    }
    printf("Island name:%s  Open-Close:%s-%s\n",il->name,il->opens,il->closes);
    return i;
}

island * create(char * name){
    island * i = malloc(sizeof(island));
    i->name = strdup(name);
    i->opens = "09:00";
    i->closes = "17:00";
    i->next = NULL;
    
    return i;
}

void release(island * start) {
    island * i = start;
    island * next = NULL;
    for (; i != NULL; i = next) {
        next = i->next;
        free(i->name);//首先需要释放strdup()创建的name字符串
        free(i);
    }
    
}

int main() {
    char name[80];
    island * start = NULL;
    island * current = NULL;
    
    puts("输入岛名:(输入S结束)");
    while (scanf("%79s",name) == 1) {
        if (strlen(name) == 1 && (name[0] - 'S' == 0)){
            break;
        }
        island * il = create(name);
        if (start == NULL) {
            start = il;
        }
        if (current != NULL) {
            current->next = il;
        }
        current = il;
        
        puts("输入岛名:");
    }
    
    printf("%i\n",display(start));
    
    //free all
    release(start);
    
    return 0;
}

 log:

 输入岛名:(输入S结束)

 a

 输入岛名:

 b

 输入岛名:

 c

 输入岛名:

 d

 输入岛名:

 e

 输入岛名:

 f

 输入岛名:

 S

 Island name:a  Open-Close:09:00-17:00

 Island name:b  Open-Close:09:00-17:00

 Island name:c  Open-Close:09:00-17:00

 Island name:d  Open-Close:09:00-17:00

 Island name:e  Open-Close:09:00-17:00

 Island name:f  Open-Close:09:00-17:00

 6

 

 上述示例中:

     使用字符指针相比于字符数组更好,因为字符数组需要提前决定字符串的长度;

 值得注意的是:

     我们添加了一个新的函数release(),用于链表的释放;

     其中,需要释放使用strdup()函数创建的name字符串;

     之后,再释放相应节点的结构;

     用完后调用释放存储器调用release(start);即可;

 

 有了动态分配存储器,就能在运行时创建需要的存储器,使用malloc()和free(),可以访问动态堆存储器;

 

 小结:

     堆之所以叫堆,因为计算机不会自动组织它,他只是一大堆数据而已;

     垃圾收集:一些语言会跟踪你在堆上分配的数据,当不再使用时,就释放它们;C语言是没有的;

     虽然操作系统会在程序结束时回收所有存储器,但最后显示的释放自己创建的每样东西;

 

 要点:

 -可以用动态数据结构保存可变数量的数据项;

 -可以很方便地在链表这种数据结构中插入数据项;

 -在C语言中,动态数据结构通常用递归结构来定义;

 -递归结构中有一个或多个指向相同结构的指针;

 -栈用来保存局部变量,他由计算机管理;

 -堆用来保存长期使用的数据,可以用malloc()分配堆空间;

 -sizeof运算符告诉你一个结构需要多少空间;

 -数据会一直留在堆上,直到用free()释放它;

 

 使用结构我们还可以创建很多其他形式的数据结构:

 (P3)

 

 数据结构很有用,但要小心使用:

     当用C语言创建这些数据结构时需要非常小心,如果没有记录好保存的数据,就可能把不同的数据留在堆上;时间久了,会消耗存储器,程序也会因为存储器错误而崩溃;

     我们可以使用valgrind工具追查代码中的存储器泄露:

         gcc -g xxx.c ... //-g开关会告诉编译器要记录要编译代码的行数

         valgrind --leak-check=full ./xxx

 

 C语言工具箱:

 -链表比数组更容易扩展;

 -在链表中插入数据很方便;

 -链表是动态数据结构;

 -动态数据结构使用递归结构;

 -递归结构包含一个或多个链向相同结构数据的链接;

 -malloc()在堆上分配存储器;

 -free()释放堆上的存储器;

 -与栈不同堆存储器不会自动释放;

 -栈用来保存局部变量;

 -strdup()会把字符串复制到堆上;

 -存储器内存泄漏是指存储器分配出去以后,你再也访问不到;

 -valgrind可以帮助跟踪存储器泄露;(可自行查阅)

 

 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C语言(Head First C)-7:数据结构与动态存储 的相关文章

随机推荐

  • Java学习笔记-锁

    Java学习笔记 锁 Lock 锁 从JDK5 0开始 Java提供了更强大的线程同步机制 通过显式定义同步锁对象来实现同步 同步锁使用Lock对象充当 java util concurrent locks Lock接口是个控制多线程对共享
  • linux磁盘空间不足的实用技巧

    1 删除日志 rm rf var log 安装了Anaconda后 发现这个文件越来越大 以下列一些删除不需要文件的方法 2 Anaconda 删除没有被硬依赖到其他地方的包 conda clean p 3 Anaconda 清理缓存的压缩
  • MySQL中视图与表的区别

    1 MySQL中视图和表的区别以及联系 1 视图是已经编译好的SQL语句 是基于SQL语句的结果集的可视化的表 而表不是 2 视图没有实际的物理记录 而表有 3 表是内存 视图是窗口 4 表占用物理存储空间而视图不占用物理存储空间 视图只是
  • undefined reference to ‘mysql_init@4‘ error: ld returned 1 exit status<-报错信息,用codeblocks连接MySQL时的报错

    解决方案 本文引用的是别人的文章 我自己总结一下 你能走到这一步出错说明你前面看了其他博主的文章 那么你只用利用文中提供网盘资源来代替你本来需要添加的include头文件的引用和lib库文件的链接即可 剩下的参考其他文章实现会更简单易懂 由
  • React组件之间如何通信?

    父组件向子组件传递 父组件在调用子组件的时候 在子组件标签内传递参数 子组件通过props属性就能接收父组件传递过来的参数 子组件向父组件传递 父组件向子组件传一个函数 然后通过这个函数的回调 拿到子组件传过来的值 兄弟组件之间的通信 父组
  • Spring源码分析(四)Bean生命周期源码分析2:合并BeanDefinition、FactoryBean

    Spring容器启动 扫描得到所有BeanDefinition之后 就会先实例化所有非懒加载的单例Bean的 入口 Spring容器启动刷新的方法里 org springframework context support AbstractA
  • 烂怂if-else代码优化方案

    0 问题概述 代码可读性是衡量代码质量的重要标准 可读性也是可维护性 可扩展性的保证 因为代码是连接程序员和机器的中间桥梁 要对双边友好 Quora 上有一个帖子 What are some of the most basic things
  • 【神经网络】图解LSTM和GRU

    图解LSTM和GRU 1 问题 循环神经网络 RNN Recurrent Neural Network 受到短期记忆的影响 如果一个序列足够长 就很难将早期产生的信息带到后续的步骤中来 因此 如果试图处理一段文字来做预测 RNN可能会从一开
  • 使用rke搭建k8s集群

    1 下载rke 选择合适的版本 https github com rancher rke 下载后上传到服务器 修改为rke 且移动号可执行目录 mv rke linux amd64 usr local bin rke 修改为可执行权限 ch
  • markdown+CSDN+Typora+知乎+微信公众号最佳偷懒写作方法

    目录 markdown 思想 适用范畴 基本语法 重点语法 高级语法 Typora 补充工具 公式编辑 保存和导出 导入 定制化主题 图片拖拽 超链接 列表 LaTex 公式 表格 生成目录 编辑模式 导出与公众号 md与CSDN 导入 导
  • 用户留存分析—SQL

    SELECT log day 日期 count user id day0 新增数量 count user id day1 count user id day0 次日留存率 count user id day3 count user id d
  • 星罗棋布:关于VPS测试脚本集锦内容

    2021 09 25 更新 优化排版 引言 莫忧世事兼身事 须著人间比梦间 勿埋我心 当我们获得一个服务器后 会想知道它的性能如何 宝塔自带跑分的应用 但是测试的数据比较片面 所以就有了各种各样的测试脚本 勿埋我心带你了解一下有哪些常用的V
  • VMware安装win10系统的心路历程

    友情提示 尽量使用原版或别人用过的 很早之前使用VMware装过win2008和centos7 都是傻瓜安装 没想到这次安装win10一点都不傻瓜 软件版本 VMware Workstation 15 Pro 1 按照常识先下载iso文件
  • hive建表

    https blog csdn net wgyzzzz article details 107446435 一 hive建表语法 二 hive外部表 1 准备测试数据 放入虚拟机 data目录下 2 创建外部表 3 装载数据 4 查询tes
  • unity使用setTrigger需注意地方

    使用上可以参考另一篇文章 RPG游戏主角状态机 Trigger触发器 注意都是从Any State开始的 否则你在使用Aniamtor的Trigger参数触发时没反应
  • 网页开发基础常见html、css

    目录 HTML基础 一 段落 行内和换行标签 二 文本样式标签 三 表格标签 四 表单标签 五 多行文本标签 六 列表标签 七 超链接标签 八 图像标签 HTML基础 html语言基本格式 常用的HTML标签 一 段落 行内和换行标签 二
  • 我为什么不在乎人工智能

    有人听说我想创业 给我提出了一些 忽悠 的办法 他们说 既然你是程序语言专家 而现在人工智能 AI 又非常热 那你其实可以搞一个 自动编程系统 号称可以自动生成程序 取代程序员的工作 节省许许多多的人力支出 这样就可以趁着 AI 热 拉到投
  • 记录一下:使用 python -m SimpleHTTPServer 快速搭建http服务

    为什么80 的码农都做不了架构师 gt gt gt 在 Linux 服务器上或安装了 Python 的机器上 Python自带了一个WEB服务器 SimpleHTTPServer 我们可以很简单的使用 python m SimpleHTTP
  • WinForm显示3D图(Sharpgl)

    总述 Sharpgl是 NET平台的Opengl 可以用来绘画 展示3D图 本文将介绍如何显示SOlidWorks等软件制作的3D模型 安装Sharpgl 下载SharpGL vsix文件并点击安装 VS中就会有相应的项目出现了 之后创建工
  • C语言(Head First C)-7:数据结构与动态存储

    该系列文章系个人读书笔记及总结性内容 任何组织和个人不得转载进行商业活动 7 数据结构与动态存储 一个结构根本不够 本章内容 1 如何用结构指针吧自定义数据类型连接成复杂的大型数据结构 通过创建链表探索其中的基本原理 2 通过在堆上动态分配