嵌入式Linux学习笔记 | 数据结构(Day01)线性结构开篇 + 顺序表完整实现
点赞+关注私信免费领取全部代码资料~
📝 学习阶段:数据结构筑基第 1 天💡 主题:数据结构总览 + 顺序表(顺序存储结构)从理论到代码封装(严格贴合当日内容,不延申未讲知识点)适合人群:有 C 语言基础(结构体、指针、动态内存),准备系统学习数据结构、适配嵌入式开发的同学⚠️ 注意:本文仅围绕 “今日内容” 展开,不涉及后续链表、栈、队列、二叉树等未讲解知识点,重点夯实顺序表核心逻辑与代码实现。
一、数据结构学什么?(整体框架总览)
数据结构本质就是数据与数据之间的关系 + 数据的组织存储方式,是编程进阶的核心必修课,也是嵌入式、后端开发的底层基础。今日先明确整体学习框架,重点聚焦 “线性结构” 中的 “顺序表”。
1. 线性结构(核心:元素之间是一对一关系)
线性结构的核心特征:除了首尾元素,每个元素有且只有一个前驱元素和一个后继元素,就像排队一样,逐个衔接,无分支、无嵌套。
- (今日重点)顺序存储结构:数据元素在内存中是连续存储的,最典型的就是「顺序表」,本质是用数组实现的线性表。
- (后续学习,今日不展开)链式存储结构:数据元素在内存中不连续,通过指针链接,包括:
- 单链表
- 双链表
- 循环链表
- 内核链表(Linux 嵌入式重点,后续详解)
- (后续学习,今日不展开)特殊线性结构:基于线性结构衍生,有特殊的操作规则,包括:
- 栈(先进后出)
- 队列(先进先出)
2. 树形结构(后续学习,今日仅提及框架)
树形结构的核心特征:元素之间是一对多关系,像大树的分支一样,有一个根节点,多个子节点。
- 二叉树(树形结构的基础)
- 遍历(前序、中序、后序、层序,后续详解)
- 二叉平衡树(后续详解)
二、学习目标(明确当日及整体学习方向,不超纲)
学习数据结构,核心是提升 “编程硬实力”,而非单纯记概念,今日及整体学习目标明确为 3 点,贴合 C 语言 + 嵌入式开发需求:
- 编程逻辑提升:摆脱 “只会写简单代码” 的局限,学会用 “数据组织” 的思维设计逻辑,比如如何高效存储、操作一组数据,而非零散处理单个变量。
- 语法能力提升:熟练运用 C 语言核心语法,重点强化「指针」「结构体」「函数指针」「动态内存分配」的用法 —— 这些语法在数据结构封装中高频使用,也是嵌入式开发的核心语法。
- 库封装能力:学会设计通用、可复用的 C 语言数据结构库(比如今日的顺序表),能让代码更规范、可移植,适配嵌入式项目中 “多场景复用” 的需求(比如一个顺序表,既能存 int 类型,也能存结构体类型)。
三、学习要求(明确入门门槛,贴合当日学习内容)
数据结构的学习需要一定的 C 语言基础,今日学习顺序表,必须具备以下基础,否则会影响理解:
- 熟练掌握 C 语言基础语法(变量、循环、条件判断、函数定义与调用);
- 会使用结构体(能定义结构体、访问结构体成员);
- 理解指针的基本用法(指针变量定义、取值、取地址,能通过指针访问内存);
- 掌握动态内存分配函数(malloc、calloc、free)的用法,知道如何申请和释放内存;
- 能理解函数指针的基本概念(后续遍历顺序表会直接用到,今日重点讲解用法)。
四、能得到什么?(贴合学习目标,不夸大、不延申)
坚持学习数据结构,尤其是结合 C 语言实现,能直接获得 3 点核心提升,适配嵌入式 / 编程入门需求:
- 编程能力的质的提升:从 “会写代码” 变成 “会设计代码”,能独立解决 “数据存储、数据操作” 相关的问题;
- 夯实底层基础:能看懂嵌入式开发中常用的数据结构逻辑(比如 STM32、Linux 内核中用到的顺序表、链表);
- 提升代码规范性:学会封装通用接口,写出的代码更简洁、可复用,为后续项目开发、笔试面试打下基础。
今日核心内容:顺序表(重中之重,完整实现 + 细节拆解)
一、顺序表的核心定义(吃透本质,不延申)
顺序表是「线性结构」中「顺序存储结构」的核心实现,也是最基础、最常用的数据结构之一,核心要点如下(严格贴合当日笔记,不添加额外知识点):
- 元素关系:数据元素之间是一对一的关系(每个元素除首尾外,有且只有一个前驱和一个后继);
- 存储特点:所有元素的内存地址是连续的(和数组一致,本质就是对数组的封装和规范化操作);
- 本质:用数组实现的线性表,但比普通数组更规范 —— 能记录元素大小、当前元素个数、最大容量,支持增删改查等标准化操作;
- 核心优势:随机访问速度快(因为地址连续,可通过 “首地址 + 偏移量” 直接访问任意元素)、遍历效率高;
- 核心局限:插入、删除操作效率低(需要移动大量元素)、需要连续的内存空间(内存不足时无法扩容,今日暂不讲解扩容)。
二、顺序表 ADT 抽象数据类型定义(通用版,企业级封装)
ADT(抽象数据类型)的核心是 “不关注具体存储的是什么数据,只关注数据的操作规则”,所以我们要封装一个可以存储任意类型数据的顺序表(int、char、结构体等都能存),适配多场景使用,严格按照笔记中的结构实现,补充细节说明:
// 顺序表核心结构体(通用封装,可存储任意类型数据)
struct seqlist_st
{
void *arr; // 顺序表存储空间的起始地址(void* 通用指针,适配任意类型)
int size; // 每个元素的大小(单位:字节),比如int是4,char是1,结构体是其总大小
int nmemb; // 当前表中已有的元素个数(初始为0,插入元素时递增,删除时递减)
int capacity; // 顺序表最多能容纳的元素个数(固定值,今日暂不讲解扩容)
};
结构体成员详细解析(必吃透,避免后续踩坑)
void *arr:通用指针(无类型指针),这是顺序表 “通用” 的核心 —— 它不指定具体存储的数据类型,后续可通过强制转换,存储 int、char、结构体等任意类型数据;size:每个元素的字节数,比如存储 int 类型时 size=4,存储 char 类型时 size=1,存储自定义结构体时 size=sizeof (结构体名),用于计算每个元素的内存偏移量;nmemb:记录当前顺序表中实际有多少个数据,初始值为 0,插入元素时 + 1,删除元素时 - 1,用于判断顺序表是否为空、是否已满;capacity:顺序表的最大容量,即最多能存储的元素个数,由初始化时传入的参数决定,今日暂不讲解扩容,所以 capacity 一旦确定,暂时不可修改。
三、顺序表标准接口设计(严格按照笔记中的接口,完整实现 + 细节拆解)
顺序表的核心操作的是 “增、删、改、查、遍历、初始化、销毁”,今日笔记中明确提及的接口,全部实现并补充细节,未提及的操作(增、删、改、查的具体实现)不延申,仅给出接口定义,留到后续讲解。
1. 初始化顺序表(核心接口,必须掌握)
初始化是顺序表使用的第一步,核心作用是:申请内存(结构体本身 + 数据存储区)、初始化结构体成员,笔记中给出的接口是 int seqlist_init(struct seqlist_st **s, int size, int capacity),完整实现 + 逐行解析:
#include <stdlib.h> // 包含malloc、calloc、free函数的头文件
// 顺序表初始化函数
// 参数说明:
// s:二级指针,用于将初始化好的顺序表地址返回给主函数(因为要修改指针本身的值)
// size:每个元素的大小(字节)
// capacity:顺序表的最大容量(最多能存多少个元素)
// 返回值:0表示成功,-1表示结构体内存申请失败,-2表示数据区内存申请失败
int seqlist_init(struct seqlist_st **s, int size, int capacity)
{
// 1. 先判断参数合法性(避免无效参数,嵌入式开发中必加的容错判断)
if (s == NULL || size <= 0 || capacity <= 0)
{
return -1; // 参数错误,初始化失败
}
// 2. 分配顺序表结构体本身的内存(结构体是用来存储顺序表的基本信息的)
*s = (struct seqlist_st *)malloc(sizeof(struct seqlist_st));
if (*s == NULL) // 判断内存申请是否成功(避免malloc失败导致野指针)
{
return -1; // 结构体内存申请失败
}
// 3. 分配顺序表的数据存储区内存(用来存储实际的数据元素)
// calloc:申请capacity个大小为size的连续内存,并且自动初始化为0(推荐用于顺序表初始化)
(*s)->arr = calloc(capacity, size);
if ((*s)->arr == NULL) // 判断数据区内存申请是否成功
{
free(*s); // 数据区申请失败,先释放之前申请的结构体内存,避免内存泄漏
*s = NULL; // 将指针置空,避免野指针
return -2; // 数据区内存申请失败
}
// 4. 初始化结构体的所有成员(严格按照笔记要求)
(*s)->size = size; // 赋值每个元素的大小
(*s)->capacity = capacity; // 赋值顺序表的最大容量
(*s)->nmemb = 0; // 初始时顺序表为空,元素个数为0
return 0; // 初始化成功
}
关键注意点(嵌入式开发必避坑)
- 为什么用二级指针?因为我们要在函数内部修改主函数中指针变量的值(将申请好的结构体地址返回给主函数),一级指针只能修改指针指向的内容,不能修改指针本身;
- 内存申请失败的处理:必须判断 malloc、calloc 的返回值是否为 NULL,若失败,要释放已申请的内存,避免内存泄漏;
- calloc 和 malloc 的区别:calloc 会自动将申请的内存初始化为 0,malloc 不会,顺序表初始化用 calloc,能避免数据区出现随机值。
2. 增、删、改、查接口(仅给出笔记中的接口定义,不实现,不延申)
笔记中明确提及 “增、删、改、查” 为顺序表的核心操作,今日暂不实现具体逻辑,仅给出标准接口定义,贴合笔记要求:
- 增:
int seqlist_add(struct seqlist_st *s, const void *data, int pos);(笔记中函数名笔误,修正为 seqlist_add,避免与初始化函数重名)- 参数说明:s 是顺序表指针,data 是要插入的数据(void* 通用指针),pos 是插入位置(从 0 开始);
- 删:
int seqlist_del(struct seqlist_st *s, int pos);(笔记未给出函数名,补充标准命名,贴合接口规范)- 参数说明:s 是顺序表指针,pos 是要删除的元素位置;
- 改:
int seqlist_modify(struct seqlist_st *s, const void *data, int pos);(补充标准接口,贴合笔记逻辑)- 参数说明:s 是顺序表指针,data 是修改后的数据,pos 是要修改的元素位置;
- 查:
int seqlist_find(struct seqlist_st *s, const void *data, int (*cmp)(const void *, const void *));(补充标准接口,需用函数指针实现通用查找)- 参数说明:s 是顺序表指针,data 是要查找的数据,cmp 是比较函数指针(判断两个元素是否相等)。
3. 遍历函数(笔记重点,完整实现 + 细节拆解)
遍历是顺序表最常用的操作之一,笔记中给出两个接口,核心区别是 “是否修改顺序表内容”,完整实现并解析,重点讲解函数指针的用法(贴合 C 语言基础,不延申):
接口 1:不修改顺序表(推荐,用 const 修饰,避免误修改)
#include <stdio.h> // 若打印函数需要用到printf,需包含此头文件
// 顺序表遍历函数(不修改顺序表内容,const修饰指针,更安全)
// 参数说明:
// s:const修饰的顺序表指针,禁止在函数内修改顺序表的任何内容
// pri:函数指针,指向一个“打印单个元素”的函数(因为顺序表可存任意类型,打印方式不同,需自定义)
// 函数指针格式:返回值void,参数是const void*(指向要打印的元素)
void seqlist_traval(const struct seqlist_st *s, void (*pri)(const void *data))
{
// 容错判断:顺序表为空,或函数指针为空,直接返回
if (s == NULL || pri == NULL || s->nmemb == 0)
{
return;
}
// 遍历顺序表,逐个打印元素
for (int i = 0; i < s->nmemb; i++)
{
// 计算第i个元素的内存地址:首地址 + i * 每个元素的大小
// 强制转换为char*:因为char是1字节,偏移量计算更精准(避免不同类型指针偏移量不同的问题)
void *ptr = (char *)s->arr + i * s->size;
pri(ptr); // 调用传入的打印函数,打印当前元素
}
}
接口 2:可修改顺序表(不推荐,仅贴合笔记给出实现)
// 顺序表遍历函数(可修改顺序表内容,无const修饰)
// 参数说明与接口1一致,仅去掉const修饰,允许在遍历过程中修改元素
void seqlist_traval(struct seqlist_st *s, void (*pri)(const void *data))
{
if (s == NULL || pri == NULL || s->nmemb == 0)
{
return;
}
for (int i = 0; i < s->nmemb; i++)
{
void *ptr = (char *)s->arr + i * s->size;
pri(ptr);
}
}
函数指针用法解析(必懂,今日重点)
- 为什么用函数指针?因为顺序表可存储任意类型数据(int、char、结构体),不同类型的打印方式不同(比如 int 用 % d,char 用 % c),无法在遍历函数中固定打印格式;
- 如何使用?主函数中自定义一个打印函数(比如打印 int 类型、打印结构体类型),将该函数的地址传入 seqlist_traval,遍历函数会调用这个自定义打印函数,实现 “通用遍历”;
- 示例(打印 int 类型元素,贴合今日内容):
// 自定义打印int类型元素的函数(符合函数指针的格式要求) void print_int(const void *data) { // 将void* 强制转换为int*,再取值打印 printf("%d ", *(int *)data); } // 主函数中调用遍历 int main() { struct seqlist_st *s = NULL; seqlist_init(&s, sizeof(int), 10); // 初始化:存储int类型,容量10 // 后续插入int类型元素后,调用遍历 seqlist_traval(s, print_int); // 传入打印函数地址 return 0; }
4. 销毁顺序表(核心接口,避免内存泄漏,完整实现 + 细节拆解)
顺序表使用完毕后,必须销毁,释放申请的所有内存(数据区 + 结构体本身),否则会导致内存泄漏,笔记中给出的接口是 void seqlist_destroy(struct seqlist_st **s),完整实现 + 解析:
#include <stdlib.h>
// 顺序表销毁函数
// 参数说明:s是二级指针,用于将主函数中的顺序表指针置空,避免野指针
void seqlist_destroy(struct seqlist_st **s)
{
// 容错判断:指针为空,直接返回
if (s == NULL || *s == NULL)
{
return;
}
// 1. 先释放数据存储区的内存(因为数据区是结构体的成员,先释放成员,再释放结构体)
free((*s)->arr);
(*s)->arr = NULL; // 将数据区指针置空,避免野指针
// 2. 再释放顺序表结构体本身的内存
free(*s);
*s = NULL; // 将结构体指针置空,避免主函数中使用野指针
}
关键注意点
- 释放顺序:必须先释放结构体的成员(data 区),再释放结构体本身,否则释放结构体后,就无法访问 data 区,导致内存泄漏;
- 置空操作:释放内存后,必须将指针置空,避免后续误操作野指针(野指针会导致程序崩溃,嵌入式开发中必做)。
四、内存空间操作三剑客(顺序表必备,严格贴合笔记,补充用法细节)
顺序表的插入、删除操作,本质是 “内存数据的移动、拷贝”,笔记中提及的 3 个内存操作函数(memcpy、memmove、memset)是核心,必须掌握,不延申其他内存函数,仅讲解笔记中提及的用法:
1. memcpy (3)(man 3 memcpy 可查看系统手册)
- 核心功能:内存地址空间的数据拷贝,将源地址空间的数据,拷贝到目标地址空间;
- 函数原型:
void *memcpy(void *dest, const void *src, size_t n);- 参数:dest(目标地址)、src(源地址)、n(要拷贝的字节数);
- 关键特点:不处理源地址和目标地址重叠的情况;
- 适用场景:顺序表中 “非重叠区域” 的数据拷贝(比如将一个元素拷贝到另一个不重叠的位置);
- 注意:若源地址和目标地址重叠,使用 memcpy 可能导致拷贝结果错误,此时推荐用 memmove。
2. memmove (3)(man 3 memmove 可查看系统手册)
- 核心功能:和 memcpy 一样,用于内存数据拷贝;
- 函数原型:
void *memmove(void *dest, const void *src, size_t n);- 参数:和 memcpy 完全一致;
- 关键特点:能处理源地址和目标地址重叠的情况(内部做了地址判断,会先将数据临时存储,再拷贝);
- 适用场景:顺序表的插入、删除操作(必然会出现地址重叠,比如插入元素时,需要将 pos 位置及后面的元素向后移动,此时源地址和目标地址重叠,必须用 memmove);
- 注意:memmove 的效率略低于 memcpy,但安全性更高,顺序表中优先使用 memmove。
3. memset (3)(man 3 memset 可查看系统手册)
- 核心功能:向指定的存储空间填充指定的数据,通常用于内存初始化、清空缓冲区;
- 函数原型:
void *memset(void *s, int c, size_t n);- 参数:s(要填充的内存起始地址)、c(要填充的字符 / 字节值,以 ASCII 值传入)、n(要填充的字节数);
- 适用场景:
- 顺序表初始化时,若用 malloc 申请数据区,可通过 memset 将其初始化为 0(calloc 已自动初始化,可省略);
- 清空顺序表中的某个元素、某个区域的数据;
- 示例(贴合顺序表):
// 将顺序表的数据区全部初始化为0(假设s已初始化) memset(s->arr, 0, s->capacity * s->size); - 注意:memset 是按 “字节” 填充,若填充 int 类型(4 字节),不能直接用 memset 填充非 0 值(会导致每个字节都为该值,比如 memset (arr, 1, 4),结果是 0x01010101,而非 1)。
今日学习总结(严格贴合当日内容,不延申)
- 明确数据结构的整体框架:分为线性结构(一对一)和树形结构(一对多),今日重点学习线性结构中的顺序存储结构 —— 顺序表;
- 掌握顺序表的核心本质:地址连续、元素一对一,本质是封装后的通用数组,可存储任意类型数据;
- 吃透顺序表的 ADT 定义:掌握 struct seqlist_st 结构体中每个成员的含义,理解 void* arr 实现 “通用存储” 的原理;
- 掌握顺序表的核心接口:能独立写出初始化(seqlist_init)、遍历(seqlist_traval)、销毁(seqlist_destroy)的完整代码,理解二级指针、函数指针的用法;
- 掌握 3 个核心内存操作函数:memcpy(非重叠拷贝)、memmove(重叠拷贝,顺序表必备)、memset(内存初始化),明确各自的适用场景。
更多推荐


所有评论(0)