数据结构Day 01:数据结构开篇总览 + 顺序表超详细完整版(含原理 + 代码 + 注释 + 面试考点)
摘要
数据结构是计算机科学的核心基础,是连接 C 语言基础与实际项目开发、算法设计的关键桥梁。本文从数据结构学习总览入手,明确学习范围、目标、要求与收获,再深度拆解线性结构的开篇重点 —— 顺序表,涵盖ADT 抽象数据类型定义、核心原理、完整功能接口封装(初始化、增删改查、遍历、销毁)、内存操作函数详解、实战测试案例、常见错误排查、面试高频考点,全程结合底层原理、代码实现与注释,兼顾专业性与易懂性,适合 C 语言基础学习者入门数据结构、期末复习、笔试面试备考。
0. 数据结构学习总览(体系化梳理,避免盲目学习)
数据结构的核心是 “组织数据的方式”—— 如何高效地存储、访问、修改数据,是后续算法设计、项目开发(如操作系统、数据库、嵌入式开发)的基础。以下明确学习框架、目标与要求,帮你建立清晰的学习认知。
1. 我们到底学什么?(数据结构核心知识体系)
数据结构按 “元素之间的关系” 可分为三大类,本次先聚焦线性结构,后续逐步延伸至树型、图型结构,具体体系如下:
a. 线性结构(一对一关系,重点开篇)
线性结构的核心特征:数据元素之间前后相连、仅有一对一的逻辑关系,是最基础、最常用的数据结构,也是后续复杂结构的基础。
(1)顺序存储结构(连续内存)
- 核心实现:顺序表(基于数组)
- 底层原理:数据元素在物理内存中连续存放,通过 “起始地址 + 元素下标” 实现随机访问
- 核心特点:查找快(O (1) 时间复杂度)、遍历快;插入、删除慢(需移动大量元素,O (n) 时间复杂度)
- 适用场景:数据查找频繁、插入删除较少的场景(如学生成绩查询、静态数据存储)
(2)链式存储结构(非连续内存)
- 核心实现:单链表、双链表、循环链表、内核链表(Linux 内核通用链表)
- 底层原理:数据元素(节点)分散存储在内存中,通过 “指针” 连接前后元素,形成逻辑上的线性关系
- 核心特点:插入、删除快(无需移动元素,仅修改指针,O (1) 时间复杂度);查找慢(需从头遍历,O (n) 时间复杂度)
- 适用场景:数据插入删除频繁、查找较少的场景(如消息队列、动态链表管理)
(3)特殊线性结构(受限线性结构)
- 栈(Stack):遵循 “先进后出(FILO)” 原则,仅允许在一端(栈顶)进行插入、删除操作
- 应用:函数调用栈、表达式求值、括号匹配
- 队列(Queue):遵循 “先进先出(FIFO)” 原则,允许在一端(队尾)插入、另一端(队头)删除
- 应用:消息队列、任务调度、键盘输入缓冲区
b. 树型结构(一对多关系,进阶重点)
树型结构的核心特征:数据元素之间是层次化的一对多关系,适用于描述具有层级结构的数据。
- 基础树型结构:二叉树(每个节点最多有两个子节点)
- 核心操作:前序遍历、中序遍历、后序遍历、层序遍历(面试必考)
- 进阶树型结构:
- 二叉搜索树(BST):用于快速查找、插入、删除(左子树值 < 根节点 < 右子树值)
- 平衡二叉树(AVL):解决二叉搜索树失衡问题,保证查找效率稳定(左右子树高度差≤1)
- 红黑树:近似平衡,插入删除效率更高(Linux 内核、STL 容器底层常用)
- B + 树:多路平衡树,适合大量数据存储(数据库索引底层核心结构)
c. 图型结构(多对多关系,高阶内容)
图型结构的核心特征:数据元素之间是任意多对多的关系,是最复杂的数据结构。
- 核心概念:顶点、边、有向图、无向图、权重
- 应用:路径规划、社交网络、网络拓扑、推荐系统
2. 学习目标(学完能达到什么水平?)
学习数据结构,不是单纯记代码,而是提升 “数据组织与逻辑设计” 能力,具体目标如下:a.编程逻辑能力显著提升
- 从 “写简单语法” 升级为 “处理复杂逻辑、抽象问题”,能将实际需求转化为数据结构设计
- 培养 “模块化、结构化” 思维,不再写杂乱无章的代码
b.C 语言语法能力彻底巩固与深化
- 吃透指针、结构体、函数指针、动态内存管理(malloc/calloc/realloc/free)等核心难点
- 掌握通用代码封装技巧,能写可复用、可扩展的 C 语言代码
c.库封装能力实战掌握
- 能独立封装通用数据结构库(如顺序表、链表、栈、队列),支持任意数据类型(int、结构体等)
- 理解 “抽象数据类型(ADT)” 的思想,能隐藏底层实现,提供统一接口
d.具备面试核心竞争力
- 掌握校招 / 笔试 / 面试中数据结构的高频考点(如顺序表内存对齐、链表操作、二叉树遍历)
- 能独立手写核心代码(如顺序表增删改查、链表反转、二叉树遍历),应对面试手写题
3. 学习要求(门槛不高,循序渐进即可)
无需具备高深的 C 语言基础,满足以下条件即可轻松入门:
- 掌握 C 语言基础语法(变量、循环、分支、函数)
- 理解结构体的定义与使用(能定义结构体、访问成员)
- 掌握指针的基本用法(指针变量定义、解引用、取地址)
- 了解动态内存分配(知道 malloc/calloc/free 的作用,能简单使用)
- 愿意动手敲代码、调试代码(数据结构是 “练” 出来的,不是 “看” 出来的)
4. 学完能得到什么?(明确学习价值)
a.编程能力质的飞跃:从 “会写代码” 到 “会写高质量代码”,能处理更复杂的项目需求b.底层逻辑认知提升:理解计算机存储数据、处理数据的底层逻辑,为后续学习操作系统、数据库、嵌入式开发打下基础c.面试与就业优势:数据结构是校招、社招(尤其是后端、嵌入式、算法岗)的必考内容,掌握后能大幅提升面试通过率d.算法设计基础:数据结构是算法的载体,掌握数据结构后,能轻松入门算法设计(如排序、查找算法)
5. 今日核心内容:顺序表(线性结构开篇,面试高频)
顺序表是最基础、最易理解的数据结构,也是后续学习链表、栈、队列的基础。它的本质是 “用数组实现的线性表”,但通过封装,实现了通用、可扩展的操作接口,解决了普通数组固定大小、无法灵活增删的问题。
5.1 什么是顺序表?(底层原理详解)
5.1.1 定义
顺序表(Sequential List)是线性表的顺序存储结构,它将线性表中的所有元素,按逻辑顺序依次存储在一片连续的物理内存空间中,元素的逻辑顺序与物理存储顺序完全一致。
5.1.2 核心特征(必记,面试常考)
- 逻辑关系:元素之间是一对一的线性关系
- 物理存储:地址连续,元素在内存中紧密排列(类似数组,但比数组更灵活)
- 访问方式:支持随机访问(通过下标计算元素地址,O (1) 时间复杂度)
- 存储限制:需要提前分配固定大小的内存空间,容量固定(若要扩容,需重新分配内存)
5.1.3 顺序表与普通数组的区别(面试易错点)
很多人会把顺序表和数组混淆,二者核心区别如下:
表格
| 对比维度 | 普通数组 | 顺序表 |
|---|---|---|
| 容量 | 固定不变,定义时确定 | 可扩容(通过重新分配内存) |
| 操作接口 | 仅支持下标访问,无统一接口 | 封装了增删改查、遍历、销毁等统一接口 |
| 通用性 | 仅支持单一数据类型 | 支持任意数据类型(通过 void * 指针) |
| 内存管理 | 手动管理,易出现内存泄漏 | 封装销毁接口,自动释放内存,更安全 |
5.1.4 顺序表的优缺点(面试必背)
优点
- 随机访问效率高:通过起始地址 + 下标,可直接访问任意元素,时间复杂度 O (1)
- 存储密度高:元素之间无额外空间(除了必要的填充字节),内存利用率高
- 遍历效率高:连续内存可通过循环快速遍历所有元素,无需跳转指针
缺点
- 插入、删除效率低:插入 / 删除元素时,需移动后续所有元素,时间复杂度 O (n)
- 容量固定(初始):若初始容量分配过小,会频繁扩容;分配过大,会浪费内存
- 扩容成本高:扩容时需重新分配内存、复制原有数据,消耗系统资源
5.1.5 适用场景
- 数据量固定或变化不大,且以查找、遍历操作为主的场景
- 对访问效率要求高,对插入删除效率要求不高的场景(如学生成绩管理、静态数据查询)
5.2 顺序表 ADT 抽象数据类型定义(核心,封装思想)
ADT(Abstract Data Type,抽象数据类型)是指 “只关注数据的逻辑结构和操作接口,不关注底层实现” 的思想。我们封装顺序表时,采用 ADT 思想,隐藏底层数组实现,对外提供统一的操作接口,同时支持任意数据类型存储。
5.2.1 结构体定义(核心代码,带详细注释)
#include <stdio.h> #include <stdlib.h> #include <string.h> // 顺序表抽象数据类型(ADT)定义 struct seqlist_st { void *arr; // 顺序表存储空间的起始地址(void* 指针:支持任意数据类型存储) int size; // 每个元素的大小(单位:字节),用于计算元素地址偏移 int nmemb; // 当前顺序表中已存储的元素个数(有效元素数) int capacity; // 顺序表的最大容量(最多能存储的元素个数) };5.2.2 结构体成员详细解释(必懂,避免踩坑)
void *arr:- 通用指针(无类型指针),可以指向任意类型的数据(int、char、float、结构体等)
- 底层本质是一片连续的内存空间,通过地址偏移访问不同元素
- 为什么不用具体类型(如 int*)?为了实现通用性,让顺序表能存储任意类型数据
int size:- 每个元素的字节大小(如 int 是 4 字节,char 是 1 字节,结构体按内存对齐后的大小计算)
- 核心作用:计算元素的地址偏移(第 i 个元素地址 = arr + i * size)
int nmemb:- 记录当前已存储的有效元素个数,初始值为 0
- 用于判断顺序表是否已满(nmemb == capacity)、是否为空(nmemb == 0)
int capacity:- 顺序表的最大容量,即最多能存储的元素个数
- 初始化时确定,后续可通过扩容接口修改(本文后续补充扩容功能)
5.3 顺序表核心功能接口封装(完整代码 + 注释 + 原理)
顺序表的核心操作的是 “增、删、改、查、遍历、初始化、销毁”,以下接口全部封装为独立函数,支持任意数据类型,兼顾安全性与通用性,每一步都带底层原理解释。
5.3.1 1. 顺序表初始化(seqlist_init)
功能
初始化顺序表,分配结构体空间和数据存储空间,初始化所有成员变量。
函数原型
// 参数: // s:二级指针(要修改指针本身,因为要给指针分配内存) // size:每个元素的大小(字节) // capacity:顺序表的最大容量 // 返回值:0 成功;-1 失败(内存分配失败) int seqlist_init(struct seqlist_st **s, int size, int capacity);实现代码(带详细注释)
int seqlist_init(struct seqlist_st **s, int size, int capacity) { // 1. 参数合法性校验(避免无效参数) if (s == NULL || size <= 0 || capacity <= 0) { printf("初始化失败:参数非法!\n"); return -1; } // 2. 分配顺序表结构体本身的内存空间 // *s是指针,需要给指针指向的空间分配内存(结构体大小) *s = (struct seqlist_st *)malloc(sizeof(struct seqlist_st)); if (*s == NULL) // 内存分配失败判断(必须做,避免野指针) { perror("malloc seqlist_st failed"); // 打印错误原因 return -1; } // 3. 分配顺序表数据存储空间(arr指向的空间) // calloc:分配capacity个size大小的空间,并初始化为0(比malloc更安全) (*s)->arr = calloc(capacity, size); if ((*s)->arr == NULL) // 数据空间分配失败,需释放已分配的结构体空间(避免内存泄漏) { perror("calloc arr failed"); free(*s); // 释放结构体空间 *s = NULL; // 置空指针,避免野指针 return -1; } // 4. 初始化结构体成员变量 (*s)->size = size; // 赋值每个元素的大小 (*s)->capacity = capacity;// 赋值最大容量 (*s)->nmemb = 0; // 初始有效元素个数为0 printf("顺序表初始化成功!容量:%d,元素大小:%d字节\n", capacity, size); return 0; }原理补充(面试考点)
- 为什么用二级指针?因为要修改指针
s本身(给s分配内存),一级指针只能修改指针指向的内容,无法修改指针本身。 - 为什么用 calloc 而不用 malloc?calloc 会自动将分配的内存初始化为 0,避免数据脏读;malloc 分配的内存是随机值,需手动初始化。
- 内存分配失败的处理:必须释放已分配的空间,避免内存泄漏,这是高质量代码的基本要求(面试常考内存泄漏问题)。
5.3.2 2. 顺序表扩容(seqlist_expand)(补充,解决容量不足问题)
功能
当顺序表已满(nmemb == capacity)时,扩容为原来的 2 倍(行业通用扩容策略),避免频繁扩容。
函数原型
// 参数:s:顺序表指针;new_capacity:新容量(建议为原容量的2倍) // 返回值:0 成功;-1 失败 int seqlist_expand(struct seqlist_st *s, int new_capacity);实现代码
int seqlist_expand(struct seqlist_st *s, int new_capacity) { // 1. 参数校验 if (s == NULL || new_capacity <= s->capacity) { printf("扩容失败:参数非法或新容量不大于原容量!\n"); return -1; } // 2. 重新分配数据存储空间(realloc:扩容原有内存,保留原有数据) void *new_arr = realloc(s->arr, new_capacity * s->size); if (new_arr == NULL) { perror("realloc new_arr failed"); return -1; } // 3. 更新顺序表成员 s->arr = new_arr; // 指向新的内存空间 s->capacity = new_capacity;// 更新最大容量 printf("顺序表扩容成功!原容量:%d,新容量:%d\n", s->capacity/2, s->capacity); return 0; }面试考点
- 扩容策略:为什么扩容为 2 倍?避免频繁扩容(每次扩容成本高),同时平衡内存利用率(扩容过大浪费内存,过小频繁扩容)。
- realloc 的用法:realloc 会尝试在原有内存后面扩展空间,若空间不足,会分配新空间并复制原有数据,最后释放原空间。
5.3.3 3. 增加元素(插入,seqlist_insert)
功能
向顺序表的指定位置 pos 插入一个元素,插入后元素顺序保持线性。
函数原型
// 参数: // s:顺序表指针 // data:要插入的数据(const void* 避免修改原数据) // pos:插入位置(0 ≤ pos ≤ nmemb,pos=0插在头部,pos=nmemb插在尾部) // 返回值:0 成功;-1 失败(参数非法、顺序表已满) int seqlist_insert(struct seqlist_st *s, const void *data, int pos);实现代码(带详细注释)
int seqlist_insert(struct seqlist_st *s, const void *data, int pos) { // 1. 参数合法性校验 if (s == NULL || data == NULL) { printf("插入失败:参数非法(空指针)!\n"); return -1; } // 插入位置校验:pos不能小于0,也不能大于当前有效元素个数(否则会出现空隙) if (pos < 0 || pos > s->nmemb) { printf("插入失败:位置非法!\n"); return -1; } // 顺序表已满,先扩容(扩容为原容量的2倍) if (s->nmemb >= s->capacity) { if (seqlist_expand(s, s->capacity * 2) != 0) { printf("插入失败:扩容失败!\n"); return -1; } } // 2. 计算数据起始地址(void*不能直接运算,强制转换为char*,按字节偏移) char *base = (char *)s->arr; // 3. 移动元素:从pos位置开始,后续所有元素向后移动一个位置(避免覆盖) // 从最后一个元素开始移动,直到pos位置(若从pos开始移动,会覆盖后面的元素) for (int i = s->nmemb; i > pos; i--) { // 复制:将第i-1个元素,复制到第i个位置 // 源地址:base + (i-1)*s->size // 目的地址:base + i*s->size // 复制长度:s->size(每个元素的大小) memcpy(base + i * s->size, base + (i - 1) * s->size, s->size); } // 4. 插入新元素:将data复制到pos位置 memcpy(base + pos * s->size, data, s->size); // 5. 有效元素个数+1 s->nmemb++; printf("插入成功!当前有效元素个数:%d\n", s->nmemb); return 0; }原理补充(面试考点)
- 为什么要将 void转换为 char?因为 void指针不能直接进行算术运算(不知道每个元素的大小),char是 1 字节,可按字节偏移计算元素地址。
- 元素移动方向:必须从后向前移动,若从前向后移动,pos 位置后面的元素会被覆盖,导致数据丢失(面试常考错误点)。
- 插入位置的范围:pos 可以等于 nmemb(插在尾部),但不能大于 nmemb(否则会出现内存空隙,导致访问越界)。
5.3.4 4. 删除元素(seqlist_delete)
功能
删除顺序表指定位置 pos 的元素,删除后后续元素向前移动,保持线性关系。
函数原型
// 参数:s:顺序表指针;pos:删除位置(0 ≤ pos < nmemb) // 返回值:0 成功;-1 失败(参数非法、顺序表为空) int seqlist_delete(struct seqlist_st *s, int pos);实现代码(带详细注释)
int seqlist_delete(struct seqlist_st *s, int pos) { // 1. 参数合法性校验 if (s == NULL) { printf("删除失败:顺序表为空指针!\n"); return -1; } // 顺序表为空,无法删除 if (s->nmemb == 0) { printf("删除失败:顺序表为空!\n"); return -1; } // 删除位置校验:pos不能小于0,也不能大于等于有效元素个数(否则越界) if (pos < 0 || pos >= s->nmemb) { printf("删除失败:位置非法!\n"); return -1; } // 2. 计算数据起始地址 char *base = (char *)s->arr; // 3. 移动元素:从pos+1位置开始,后续所有元素向前移动一个位置,覆盖pos位置的元素 for (int i = pos; i < s->nmemb - 1; i++) { memcpy(base + i * s->size, base + (i + 1) * s->size, s->size); } // 4. 有效元素个数-1 s->nmemb--; printf("删除成功!当前有效元素个数:%d\n", s->nmemb); return 0; }面试考点
- 删除后的元素处理:删除后无需手动清空 pos 位置的元素,后续元素会覆盖该位置,且 nmemb 减少,该位置不再被视为有效元素。
- 边界条件:顺序表为空时不能删除,pos 不能等于 nmemb(否则访问越界)。
5.3.5 5. 修改元素(seqlist_modify)
功能
修改顺序表指定位置 pos 的元素,用新数据覆盖原有数据。
函数原型
// 参数: // s:顺序表指针 // pos:修改位置(0 ≤ pos < nmemb) // data:新数据(const void* 避免修改原数据) // 返回值:0 成功;-1 失败(参数非法、顺序表为空) int seqlist_modify(struct seqlist_st *s, int pos, const void *data);实现代码
int seqlist_modify(struct seqlist_st *s, int pos, const void *data) { // 1. 参数合法性校验 if (s == NULL || data == NULL) { printf("修改失败:参数非法(空指针)!\n"); return -1; } if (s->nmemb == 0) { printf("修改失败:顺序表为空!\n"); return -1; } if (pos < 0 || pos >= s->nmemb) { printf("修改失败:位置非法!\n"); return -1; } // 2. 计算pos位置的元素地址,用新数据覆盖 char *base = (char *)s->arr; memcpy(base + pos * s->size, data, s->size); printf("修改成功!\n"); return 0; }5.3.6 6. 查询元素(seqlist_find)
功能
根据自定义的比较规则,查找顺序表中与目标数据匹配的元素,返回其下标。
函数原型
// 参数: // s:顺序表指针 // data:目标数据(要查找的数据) // cmp:比较函数指针(自定义比较规则,返回0表示匹配,非0表示不匹配) // 返回值:匹配元素的下标(≥0);-1 失败(未找到、参数非法) int seqlist_find(struct seqlist_st *s, const void *data, int (*cmp)(const void*, const void*));实现代码(带详细注释)
int seqlist_find(struct seqlist_st *s, const void *data, int (*cmp)(const void*, const void*)) { // 1. 参数合法性校验 if (s == NULL || data == NULL || cmp == NULL) { printf("查找失败:参数非法!\n"); return -1; } if (s->nmemb == 0) { printf("查找失败:顺序表为空!\n"); return -1; } // 2. 遍历顺序表,逐个比较元素 char *base = (char *)s->arr; for (int i = 0; i < s->nmemb; i++) { // 调用自定义比较函数,比较第i个元素和目标数据 // 第i个元素地址:base + i*s->size if (cmp(base + i * s->size, data) == 0) { printf("查找成功!元素下标:%d\n", i); return i; // 返回匹配元素的下标 } } // 3. 未找到匹配元素 printf("查找失败:未找到目标元素!\n"); return -1; }原理补充(面试考点)
- 为什么用函数指针?因为顺序表支持任意数据类型,不同类型的比较规则不同(如 int 比较、结构体比较),用函数指针让用户自定义比较规则,实现通用性。
- 比较函数的设计:返回 0 表示匹配,非 0 表示不匹配,符合 C 语言库函数的设计习惯(如 strcmp 函数)。
5.3.7 7. 遍历顺序表(seqlist_traverse)
功能
遍历顺序表中的所有有效元素,调用自定义的打印函数,实现不同类型元素的打印(通用性)。
函数原型
// 参数: // s:顺序表指针(const修饰,避免修改顺序表内容) // pri:打印函数指针(自定义打印规则,适配不同数据类型) // 返回值:无 void seqlist_traverse(const struct seqlist_st *s, void (*pri)(const void *data));实现代码
void seqlist_traverse(const struct seqlist_st *s, void (*pri)(const void *data)) { // 1. 参数合法性校验 if (s == NULL || pri == NULL) { printf("遍历失败:参数非法!\n"); return; } if (s->nmemb == 0) { printf("遍历失败:顺序表为空!\n"); return; } // 2. 遍历所有有效元素,调用打印函数 char *base = (char *)s->arr; printf("顺序表元素:"); for (int i = 0; i < s->nmemb; i++) { pri(base + i * s->size); // 回调自定义打印函数 } printf("\n"); }面试考点
- const 修饰的作用:const 修饰顺序表指针,防止在遍历过程中修改顺序表的内容,提升代码安全性(面试常考 const 的用法)。
- 函数指针的回调机制:遍历函数不关心元素的具体类型和打印方式,将打印逻辑交给用户,实现 “解耦”,这是模块化编程的核心思想。
5.3.8 8. 销毁顺序表(seqlist_destroy)
功能
释放顺序表占用的所有内存(数据空间 + 结构体空间),避免内存泄漏。
函数原型
// 参数:s:二级指针(要修改指针本身,置空为NULL) // 返回值:无 void seqlist_destroy(struct seqlist_st **s);实现代码(带详细注释)
void seqlist_destroy(struct seqlist_st **s) { // 1. 参数合法性校验 if (s == NULL || *s == NULL) { printf("销毁失败:顺序表已为空!\n"); return; } // 2. 先释放数据存储空间(arr指向的空间) free((*s)->arr); (*s)->arr = NULL; // 置空指针,避免野指针 // 3. 再释放顺序表结构体空间 free(*s); *s = NULL; // 置空指针,避免野指针 printf("顺序表销毁成功!\n"); }面试考点(高频)
- 销毁顺序的顺序:必须先释放数据空间(arr),再释放结构体空间,否则会导致 arr 指针成为野指针,无法释放数据空间(内存泄漏)。
- 为什么用二级指针?因为要将指针
s置空(*s = NULL),一级指针无法修改指针本身,只能修改指针指向的内容。 - 内存泄漏的危害:长期运行的程序(如服务器、嵌入式设备),内存泄漏会导致内存耗尽,程序崩溃。
5.4 内存操作三函数详解(顺序表核心依赖,必掌握)
顺序表的增、删、改操作,核心依赖 C 语言标准库中的三个内存操作函数:memcpy、memmove、memset,这三个函数是 C 语言面试高频考点,必须吃透其用法、区别与底层原理。
5.4.1 1. memcpy(内存复制函数)
函数原型
#include <string.h> void *memcpy(void *dest, const void *src, size_t n);功能
从源地址src开始,复制n个字节的数据到目标地址dest,返回目标地址dest。
核心特点
- 不检查源地址和目标地址是否重叠
- 复制速度快,适合地址不重叠的内存复制
- 若地址重叠,复制结果未定义(可能出现数据覆盖、复制错误)
示例(顺序表插入 / 修改中使用)
// 将src指向的数据,复制n个字节到dest memcpy(dest, src, n);面试考点
- memcpy 与 strcpy 的区别(高频):
- strcpy 只能复制字符串,会自动在末尾添加 '\0' 终止符;memcpy 可以复制任意类型数据(int、结构体等),按字节复制。
- strcpy 不需要指定复制长度,以 '\0' 为终止标志;memcpy 需要指定复制长度
n。 - strcpy 若源字符串没有 '\0',会导致越界访问;memcpy 只要
n指定正确,不会越界。
5.4.2 2. memmove(内存移动函数)
函数原型
#include <string.h> void *memmove(void *dest, const void *src, size_t n);功能
与 memcpy 完全一致:从源地址src复制n个字节到目标地址dest,返回dest。
核心特点
- 支持源地址和目标地址重叠,复制结果定义明确(不会出现数据错误)
- 实现逻辑比 memcpy 复杂,复制速度略慢
- 推荐场景:当源地址和目标地址可能重叠时(如顺序表插入 / 删除时的元素移动)
示例(顺序表元素移动推荐使用)
// 即使dest和src地址重叠,也能正确复制 memmove(dest, src, n);面试考点(高频)
- memcpy 与 memmove 的区别:
- 地址重叠处理:memcpy 不支持重叠,memmove 支持重叠。
- 速度:memcpy > memmove(memmove 需要判断地址重叠,多一步逻辑)。
- 安全性:memmove > memcpy(避免地址重叠导致的错误)。
5.4.3 3. memset(内存初始化函数)
函数原型
#include <string.h> void *memset(void *s, int c, size_t n);功能
将地址s开始的n个字节,全部设置为字符c(注意:c是 int 类型,但实际只使用低 8 位,即 ASCII 码),返回s。
核心特点
- 用于初始化内存(如将内存清空为 0、设置为指定字符)
- 按字节初始化,适合初始化连续内存空间
- 不能用于初始化非字符类型的复杂数据(如 int 数组设置为 1,会出错)
示例
// 将arr指向的100个字节,全部设置为0(常用初始化方式) memset(arr, 0, 100); // 将arr指向的10个字节,全部设置为'a'(ASCII码97) memset(arr, 'a', 10);面试易错点
- 错误用法:
memset(arr, 1, 10*sizeof(int)),想将 int 数组初始化为 1。- 原因:memset 按字节初始化,int 是 4 字节,每个字节都会被设置为 1,最终每个 int 值为 0x00000001(即 1),看似正确?不,若要设置为其他值(如 2),会变成 0x00000002,正确;但如果是负数(如 - 1),会变成 0xffffffff,正确。但注意:memset 只能初始化 “每个字节相同” 的值,不能初始化任意 int 值(如 5)。
5.5 顺序表完整实战测试案例(可直接运行)
以下案例测试顺序表的所有功能,分别测试 int 类型和结构体类型(验证通用性),带详细注释,可直接复制编译运行。
5.5.1 测试 1:int 类型顺序表
// 1. 自定义打印函数(int类型) void print_int(const void *data) { printf("%d ", *(int *)data); // 强制转换为int*,解引用获取值 } // 2. 自定义比较函数(int类型) int cmp_int(const void *a, const void *b) { return *(int *)a - *(int *)b; // 相等返回0,不相等返回差值 } // 3. 测试主函数 int main() { struct seqlist_st *s = NULL; int ret; // 1. 初始化顺序表(int类型,每个元素4字节,初始容量5) ret = seqlist_init(&s, sizeof(int), 5); if (ret != 0) { printf("初始化失败,程序退出!\n"); return -1; } // 2. 插入元素(插在尾部) int a = 10, b = 20, c = 30, d = 40, e = 50, f = 60; seqlist_insert(s, &a, 0); // 插在头部(pos=0) seqlist_insert(s, &b, 1); // 插在pos=1 seqlist_insert(s, &c, 2); // 插在pos=2 seqlist_insert(s, &d, 3); // 插在pos=3 seqlist_insert(s, &e, 4); // 插在尾部(pos=4) seqlist_insert(s, &f, 5); // 容量已满,触发扩容(扩容为10) // 3. 遍历顺序表 seqlist_traverse(s, print_int); // 输出:10 20 30 40 50 60 // 4. 查找元素(查找30) int find_data = 30; int pos = seqlist_find(s, &find_data, cmp_int); // 5. 修改元素(将pos位置的元素改为300) int modify_data = 300; seqlist_modify(s, pos, &modify_data); seqlist_traverse(s, print_int); // 输出:10 20 300 40 50 60 // 6. 删除元素(删除pos=2的元素) seqlist_delete(s, 2); seqlist_traverse(s, print_int); // 输出:10 20 40 50 60 // 7. 销毁顺序表 seqlist_destroy(&s); return 0; }5.5.2 测试 2:结构体类型顺序表(验证通用性)
// 定义一个学生结构体 struct student { int id; // 学号 char name[20]; // 姓名 float score; // 成绩 }; // 1. 自定义打印函数(结构体类型) void print_stu(const void *data) { struct student *stu = (struct student *)data; printf("学号:%d,姓名:%s,成绩:%.2f ", stu->id, stu->name, stu->score); } // 2. 自定义比较函数(按学号比较) int cmp_stu(const void *a, const void *b) { struct student *stu1 = (struct student *)a; struct student *stu2 = (struct student *)b; return stu1->id - stu2->id; // 按学号相等返回0 } // 3. 测试主函数 int main() { struct seqlist_st *s = NULL; int ret; // 1. 初始化顺序表(结构体类型,每个元素大小为struct student的大小,初始容量3) ret = seqlist_init(&s, sizeof(struct student), 3); if (ret != 0) { printf("初始化失败,程序退出!\n"); return -1; } // 2. 定义学生数据 struct student stu1 = {101, "张三", 88.5}; struct student stu2 = {102, "李四", 76.0}; struct student stu3 = {103, "王五", 92.5}; struct student stu4 = {104, "赵六", 59.0}; // 用于测试扩容 // 3. 插入学生数据 seqlist_insert(s, &stu1, 0); seqlist_insert(s, &stu2, 1); seqlist_insert(s, &stu3, 2); seqlist_insert(s, &stu4, 3); // 扩容为6 // 4. 遍历学生信息 printf("\n学生信息:\n"); seqlist_traverse(s, print_stu); // 5. 查找学生(查找学号102的学生) struct student find_stu = {102, "", 0.0}; int pos = seqlist_find(s, &find_stu, cmp_stu); // 6. 修改学生成绩(将学号102的学生成绩改为80.0) struct student modify_stu = {102, "李四", 80.0}; seqlist_modify(s, pos, &modify_stu); printf("\n修改后学生信息:\n"); seqlist_traverse(s, print_stu); // 7. 删除学生(删除学号103的学生) struct student delete_stu = {103, "", 0.0}; pos = seqlist_find(s, &delete_stu, cmp_stu); seqlist_delete(s, pos); printf("\n删除后学生信息:\n"); seqlist_traverse(s, print_stu); // 8. 销毁顺序表 seqlist_destroy(&s); return 0; }5.6 常见错误排查(新手必看,面试常考)
错误 1:使用一级指针初始化顺序表,导致初始化失败
// 错误写法 struct seqlist_st *s; seqlist_init(s, sizeof(int), 5); // 传递一级指针 // 正确写法 struct seqlist_st *s = NULL; seqlist_init(&s, sizeof(int), 5); // 传递二级指针原因:初始化函数需要修改指针s本身(给s分配内存),一级指针无法修改指针本身,只能修改指针指向的内容。
错误 2:插入 / 删除时,元素移动方向错误(从前向后移动)
// 错误写法(插入时,从前向后移动) for (int i = pos; i < s->nmemb; i++) { memcpy(base + i * s->size, base + (i - 1) * s->size, s->size); }原因:从前向后移动,pos 位置后面的元素会被覆盖,导致数据丢失,正确方向是从后向前移动。
错误 3:忘记释放内存,导致内存泄漏
// 错误写法:只初始化,不销毁 struct seqlist_st *s; seqlist_init(&s, sizeof(int), 5); // 不调用seqlist_destroy(&s);原因:顺序表的结构体和数据空间都是动态分配的,不销毁会导致内存泄漏,长期运行会耗尽内存。
错误 4:使用 memcpy 处理地址重叠的内存
// 错误写法:地址重叠时用memcpy char arr[10] = "123456789"; memcpy(arr+2, arr, 5); // 源地址arr和目标地址arr+2重叠原因:memcpy 不支持地址重叠,会导致复制结果错误,应使用 memmove。
错误 5:顺序表扩容时,新容量小于等于原容量
// 错误写法 seqlist_expand(s, s->capacity); // 新容量等于原容量原因:扩容的目的是增加容量,新容量必须大于原容量,否则扩容无意义。
5.7 面试高频考点总结(必背)
- 顺序表的定义、特点、优缺点(必考)。
- 顺序表的内存对齐(结合结构体大小计算,如顺序表结构体的大小)。
- 顺序表插入、删除的时间复杂度(O (n)),查找的时间复杂度(O (1))。
- 顺序表扩容策略(为什么扩容为 2 倍?)。
- memcpy 与 memmove 的区别(地址重叠处理、速度、安全性)。
- 顺序表初始化、销毁时,为什么用二级指针?
- 如何实现通用顺序表(支持任意数据类型)?(void * 指针 + 函数指针)。
- 顺序表与链表的区别(面试高频对比题)。
- 顺序表的内存泄漏问题(如何避免?)。
- 手写顺序表的核心接口(插入、删除、遍历,面试手写题高频)。
本章整体总结
- 数据结构的核心是 “组织数据的方式”,线性结构是基础,顺序表是线性结构的开篇重点。
- 顺序表本质是 “连续内存的线性表”,支持随机访问,插入删除慢,适合查找频繁的场景。
- 通用顺序表的实现核心:void * 指针(支持任意类型)+ 函数指针(自定义比较、打印规则)。
- 核心操作接口:初始化、扩容、增、删、改、查、遍历、销毁,必须掌握每个接口的实现原理和代码。
- 内存操作三函数(memcpy、memmove、memset)是顺序表的核心依赖,必须吃透其用法和区别。
- 学习顺序表的关键:理解 ADT 抽象数据类型思想,掌握模块化、通用化的代码封装技巧,多动手敲代码、调试错误。
