数据结构Day 06:线性结构、库操作及 Makefile 完整学习笔记

1. 线性结构

线性结构的核心定义:数据元素之间仅存在 "一对一" 的前驱 - 后继关系,所有元素按线性顺序排列,可通过顺序存储或链式存储两种方式实现,是数据结构中最基础、最常用的结构类型。

a. 顺序存储

i. 顺序表 ------ 本质就是数组

  • 核心定义:以连续的物理内存空间存储数据元素,逻辑顺序与物理顺序完全一致,底层依托数组实现,是顺序存储的核心载体。
  • 底层特性:
    1. 内存连续:所有元素依次排列在一段整块的内存地址中,无间隔、无碎片(初始化时分配);
    2. 随机访问:支持通过下标(索引)直接定位任意元素,访问时间复杂度为 O (1),效率极高;
    3. 容量固定:初始化时需指定最大容量,后续扩容需重新申请更大的连续内存,拷贝原有元素,扩容开销大。
  • 核心操作(补全缺漏):
    1. 初始化:分配连续内存,初始化数组、容量、当前元素个数;
    2. 插入:头部 / 中间插入需平移后续所有元素,时间复杂度 O (n);尾部插入无需平移,时间复杂度 O (1);
    3. 删除:头部 / 中间删除需平移后续所有元素,时间复杂度 O (n);尾部删除无需平移,时间复杂度 O (1);
    4. 查找:无序顺序表遍历查找(O (n));有序顺序表可折半查找(O (log₂n));
    5. 销毁:释放分配的连续内存,避免内存泄漏(动态分配的顺序表必须销毁)。
  • 优缺点(补全细节):
    • 优点:访问速度快、遍历效率高、CPU 缓存友好(内存连续,可预加载)、空间利用率较高(无指针开销);
    • 缺点:插入 / 删除效率低(需平移元素)、容量固定(扩容麻烦)、易产生内部内存碎片(数组未占满时,剩余空间无法利用)。
  • 适用场景:查询、遍历操作频繁,插入 / 删除操作较少,数据量固定或波动小的场景(如学生成绩查询、固定长度的数据存储)。

b. 链式存储

链式存储的核心定义:以离散的物理内存空间存储数据元素,每个元素(结点)包含 "数据域" 和 "指针域",通过指针域关联前后元素,逻辑上连续、物理上离散,无需连续内存空间。

i. 单链表(补全细节、实现、常见问题)

  • 结构组成:每个结点包含 数据域(存储元素值) + 一个后继指针 next(指向后一个结点),链表头部为头结点(可空),尾部结点的 next 指针为 NULL(非循环单链表)。
  • 核心特性:
    1. 内存离散:无需连续内存,有新元素时动态申请结点,按需分配;
    2. 遍历限制:只能从前往后遍历(从头结点开始,通过 next 指针依次访问),无法反向回溯;
    3. 操作效率:插入 / 删除只需修改指针指向,无需移动元素,时间复杂度 O (1)(已知插入 / 删除位置时);查找需从头遍历,时间复杂度 O (n)。
  • 核心操作:
    1. 初始化:创建头结点(可空),初始化头指针、尾指针(可选)、元素个数;
    2. 插入:头插(在头结点后插入)、尾插(在尾部结点后插入)、中间插(在指定结点后插入);
    3. 删除:删除指定结点(需找到其前驱结点,修改前驱结点的 next 指针);
    4. 查找:从头遍历,匹配数据域,找到目标结点;
    5. 销毁:从头遍历,逐个释放每个结点的内存,避免内存泄漏。
  • 常见问题:
    • 空链表判断:头指针为 NULL 或头结点的 next 为 NULL;
    • 野指针问题:删除结点后,需将指针置为 NULL,避免悬挂指针;
    • 遍历终止条件:当指针为 NULL 时,遍历结束。

ii. 双链表

  • 结构组成:每个结点包含 数据域 + 前驱指针 prev(指向前一个结点) + 后继指针 next(指向后一个结点),头部有头结点,尾部结点的 next 为 NULL,头部结点的 prev 为 NULL(非循环双链表)。

  • 核心特性(补充与单链表的区别):

    1. 双向遍历:可从头往后遍历(通过 next),也可从后往前遍历(通过 prev),遍历灵活性远超单链表;
    2. 操作更便捷:插入 / 删除时,无需像单链表那样查找前驱结点(可通过 prev 直接获取),操作更高效;
    3. 空间开销略大:比单链表多一个 prev 指针,每个结点占用更多内存。
  • 核心操作:

    1. 初始化:创建头结点,头指针指向头结点,头结点的 prev 和 next 均为 NULL;
    2. 插入:头插、尾插、中间插(需同时修改前驱结点的 next 和后继结点的 prev);
    3. 删除:删除指定结点(同时修改前驱结点的 next 和后继结点的 prev,无需查找前驱);
    4. 双向遍历:正向遍历(next)、反向遍历(prev);
    5. 销毁:与单链表类似,逐个释放结点,注意指针置空。
  • 优势对比:双链表比单链表更适合需要频繁反向遍历、插入 / 删除位置不明确的场景(如操作系统中的进程链表、双向循环队列)。

  • 补充:单链表与双链表的共性

    1. 均不支持随机访问,只能线性遍历;
    2. 均动态分配内存,无固定容量限制;
    3. 插入 / 删除的核心是修改指针,无需移动元素;
    4. 均需注意内存释放,避免内存泄漏。

c. 特殊线性结构

特殊线性结构的核心:底层仍是顺序表或链表,只是人为限制了插入、删除的操作位置,形成固定的操作规则(LIFO/FIFO),适配特定场景。

i. 只能在表的同一端插入、删除 ------ 栈

  1. 核心规则:LIFO(Last In First Out)后进先出
  • 核心定义:栈是操作受限的线性表,仅开放一个操作端口(栈顶),所有插入、删除、访问操作均只能在栈顶进行,另一端(栈底)固定不动,无法操作。
  • 关键概念(补全):
    • 栈顶(top):唯一可操作端,始终指向最后入栈的元素(最新元素),入栈时栈顶 "上移",出栈时栈顶 "下移";
    • 栈底(bottom):固定端,是栈的起始位置,无论栈是否为空,栈底位置不变;
    • 空栈:栈中无任何元素,此时栈顶与栈底重合;
    • 栈满:栈中元素个数达到最大容量(仅顺序栈有栈满,链式栈无栈满限制)。
  • 存储方式:
    1. 顺序栈:底层是数组,栈底固定为数组下标 0,栈顶用索引标记(空栈时 top=-1);
    2. 链式栈:底层是单链表,链表头部作为栈顶,链表尾部作为栈底,入栈时头插,出栈时头删。
  • 核心操作:
    1. 初始化:创建空栈,初始化栈顶、栈底指针,设置容量(顺序栈);
    2. 判空:判断栈是否为空(避免空栈出栈);
    3. 判满:判断栈是否已满(顺序栈,避免栈溢出);
    4. 入栈(push):将元素添加到栈顶,更新栈顶指针;
    5. 出栈(pop):删除栈顶元素,取出元素值,更新栈顶指针;
    6. 取栈顶(gettop):读取栈顶元素,不删除,栈状态不变;
    7. 销毁:释放栈占用的内存(链式栈需逐个释放结点)。
  • 典型应用:
    • 函数调用栈:操作系统为每个函数调用分配栈帧,存储局部变量、返回地址,函数执行完出栈;
    • 递归实现:递归过程中,每次递归调用的参数、返回地址入栈,递归结束后出栈回溯;
    • 括号匹配:遍历表达式,左括号入栈,右括号出栈匹配,判断括号是否成对;
    • 表达式求值:将中缀表达式转换为后缀 / 前缀表达式,借助栈实现计算;
    • 进制转换:十进制转二进制、八进制、十六进制(除基取余,逆序输出,用栈实现逆序)。

ii. 表的一端插入,另一端删除 ------ 队列

  1. 核心规则:FIFO(First In First Out)先进先出
  • 核心定义:队列是操作受限的线性表,开放两个独立端口,一端(队尾)仅负责插入,另一端(队头)仅负责删除,不允许中间操作,元素按进入顺序依次处理,完全模拟 "排队" 逻辑。
  • 关键概念:
    • 队尾(rear):插入端,只进不出,入队时队尾 "后移",指向新插入的元素;
    • 队头(front):删除端,只出不进,出队时队头 "后移",指向新的队头元素;
    • 空队列:队列中无任何元素,此时队头与队尾重合;
    • 队满:队列中元素个数达到最大容量(仅顺序队列有队满,链式队列无队满限制)。
  • 存储方式:
    1. 顺序存储:
      • 普通顺序队列:底层是数组,队头、队尾初始为 0,入队 rear++,出队 front++,存在 "假溢出" 问题(数组有空闲空间,但 rear 达到最大下标,无法入队);
      • 循环队列(补全核心,解决假溢出):将数组头尾相连,形成环形结构,队头、队尾指针移动到数组末尾时,通过取模运算自动回到开头,充分利用内存;
        • 判空条件:front == rear;
        • 判满条件:(rear + 1) % 最大容量 == front(预留一个空位置,区分队空和队满);
    2. 链式存储(lqueue):
      • 底层是单链表,链表头部作为队头(出队),链表尾部作为队尾(入队);
      • 无假溢出问题,动态扩容,无需指定容量;
      • 结构定义:队头指针 front(指向链表头部)、队尾指针 rear(指向链表尾部),每个结点包含数据域和 next 指针。
  • 核心操作:
    1. 初始化:创建空队列,初始化队头、队尾指针,设置容量(顺序队列);
    2. 判空:判断队列是否为空(避免空队列出队);
    3. 判满:判断队列是否已满(顺序队列,避免队列溢出);
    4. 入队(enqueue):将元素添加到队尾,更新队尾指针;
    5. 出队(dequeue):删除队头元素,取出元素值,更新队头指针;
    6. 取队头(getfront):读取队头元素,不删除,队列状态不变;
    7. 销毁:释放队列占用的内存(链式队列需逐个释放结点)。
  • 典型应用:
    • 任务调度:操作系统中,优先级相同的进程按队列排队,CPU 按 FIFO 顺序执行;
    • 消息缓冲:网络通信中,接收的消息按顺序入队,处理线程按顺序出队处理;
    • 打印机排队:多个打印任务按提交顺序入队,打印机依次执行;
    • IO 缓冲:键盘输入、文件读写时,数据按顺序入队,逐步处理,避免数据丢失;
    • 线程池任务队列:线程池中的任务按提交顺序排队,空闲线程依次取出任务执行。

2. 作业讲解

结合前面的线性结构(栈、队列、表达式),补全作业核心思路、详细步骤、常见错误、代码框架,确保能直接用于作业实操,避免踩坑。

作业 1:栈的应用 ------ 十进制转二进制(argv [1] 输入十进制,输出二进制)

核心原理

  • 转换规则:除 2 取余,逆序输出,利用栈的 LIFO 特性实现 "逆序";
  • 特殊情况:输入十进制数为 0 时,直接输出 0(避免空栈或逻辑错误);输入负数时,可提示 "请输入正整数",或补充负数二进制(补码)转换逻辑(可选)。

详细步骤

  1. 命令行参数处理:判断 argc 是否大于 1(确保用户输入 argv [1]),若未输入,提示 "请输入十进制正整数";
  2. 数据预处理:将 argv [1](字符串类型)转换为整数类型(int 或 long long,避免数值溢出),判断是否为正整数(排除非数字、负数、小数);
  3. 初始化栈:创建顺序栈或链式栈(优先顺序栈,实现简单),初始化栈顶、栈底指针;
  4. 除 2 取余,余数入栈:
    • 若输入数为 0,直接将 0 入栈;
    • 若输入数为正整数,循环执行:① 计算余数 rem = 输入数 % 2;② 将 rem 入栈;③ 输入数 = 输入数 / 2;④ 直到输入数为 0,停止循环;
  5. 余数出栈,输出二进制:循环执行出栈操作,将栈中所有余数依次输出,直到栈为空,输出的序列即为二进制数;
  6. 销毁栈:释放栈占用的内存,避免内存泄漏;
  7. 异常处理:处理非数字输入、负数、数值过大等情况,给出友好提示。

常见错误

  1. 未处理 argv [1] 缺失,导致数组越界;
  2. 未处理输入为 0 的情况,导致空栈,无输出;
  3. 顺序栈未判满,导致栈溢出;
  4. 出栈前未判空,导致空栈出栈,程序崩溃;
  5. 链式栈未释放结点,导致内存泄漏。

代码思路(C 语言框架)

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 顺序栈定义
#define MAX_SIZE 100
typedef struct {
    int data[MAX_SIZE];
    int top;
} SeqStack;

// 栈初始化
void InitStack(SeqStack *s) {
    s->top = -1;
}

// 判空
int IsEmpty(SeqStack *s) {
    return s->top == -1;
}

// 判满
int IsFull(SeqStack *s) {
    return s->top == MAX_SIZE - 1;
}

// 入栈
int Push(SeqStack *s, int num) {
    if (IsFull(s)) return 0;
    s->top++;
    s->data[s->top] = num;
    return 1;
}

// 出栈
int Pop(SeqStack *s, int *num) {
    if (IsEmpty(s)) return 0;
    *num = s->data[s->top];
    s->top--;
    return 1;
}

int main(int argc, char *argv[]) {
    // 处理命令行参数
    if (argc != 2) {
        printf("请输入正确的参数:./a.out 十进制数\n");
        return 1;
    }
    // 转换为整数
    int n = atoi(argv[1]);
    if (n < 0) {
        printf("请输入十进制正整数\n");
        return 1;
    }

    SeqStack s;
    InitStack(&s);

    // 除2取余,入栈
    if (n == 0) {
        Push(&s, 0);
    } else {
        while (n != 0) {
            int rem = n % 2;
            Push(&s, rem);
            n = n / 2;
        }
    }

    // 出栈,输出二进制
    printf("二进制结果:");
    int num;
    while (!IsEmpty(&s)) {
        Pop(&s, &num);
        printf("%d", num);
    }
    printf("\n");

    return 0;
}

作业 2:表达式转换 ------ 中缀表达式转前缀表达式(argv [1] 输入中缀,输出前缀)

核心原理

  • 中缀表达式:运算符在操作数中间,需考虑优先级和括号(人类习惯写法);
  • 前缀表达式:运算符在操作数前面,无需括号、无需优先级,计算机可直接计算;
  • 转换核心:利用栈存储运算符,通过 "逆序处理、优先级判断、括号翻转、二次逆序" 实现转换。

详细步骤(必背)

  1. 预处理中缀表达式:
    • 去除表达式中的所有空格(如 "1 + 2 * 3" → "1+2*3");
    • 处理异常情况:表达式为空、括号不匹配(如 "(1+2""1+2)")、运算符位置错误(如 "+1+2""1++2"),提示错误信息;
    • 处理多位数(可选):若操作数为多位数(如 10、20),需判断连续数字,作为一个整体处理。
  2. 中缀表达式逆序:将预处理后的中缀表达式整体逆序,同时将括号翻转("("→")",")"→"("),因为逆序后括号的作用会反转;
  3. 初始化工具:创建一个空栈(存储运算符),创建一个结果字符串(存储中间结果);
  4. 遍历逆序后的字符串,逐个处理字符:
    • 情况 1:当前字符是操作数(0-9,或多位数):直接追加到结果字符串,多位数需连续读取;
    • 情况 2:当前字符是右括号 ")"(原中缀的左括号):直接入栈(括号优先级最高,等待左括号匹配);
    • 情况 3:当前字符是左括号 "("(原中缀的右括号):循环出栈,将栈中所有运算符追加到结果字符串,直到遇到右括号 ")",然后将右括号出栈(不追加到结果);
    • 情况 4:当前字符是运算符(+、-、*、/):
      1. 定义优先级:*、/ 优先级为 2,+、- 优先级为 1;
      2. 循环判断:若栈不为空,且栈顶运算符的优先级 ≥ 当前运算符的优先级,则出栈并追加到结果;
      3. 直到栈为空,或栈顶运算符优先级 < 当前运算符,将当前运算符入栈;
  5. 遍历结束后,若栈不为空,将栈中剩余的所有运算符依次出栈,追加到结果字符串;
  6. 结果字符串逆序:将得到的中间结果再次逆序,即为原中缀表达式对应的前缀表达式;
  7. 输出前缀表达式:注意操作数之间添加空格,便于阅读(如 "+ 1 * 2 3")。

常见错误

  1. 未去除表达式中的空格,导致字符处理错误;
  2. 逆序后未翻转括号,导致括号匹配错误、优先级判断错误;
  3. 运算符优先级判断错误(如把 +、- 优先级设为 2,*、/ 设为 1);
  4. 遍历结束后,未将栈中剩余运算符出栈,导致结果缺失;
  5. 未处理多位数,导致多位数被拆分为单个数字(如 10 被拆分为 1 和 0);
  6. 括号不匹配,未做异常处理,导致程序崩溃。

代码思路(C 语言框架)

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

// 栈定义(存储运算符)
#define MAX_STACK 100
typedef struct {
    char data[MAX_STACK];
    int top;
} OpStack;

// 初始化栈
void InitOpStack(OpStack *s) {
    s->top = -1;
}

// 判空
int OpIsEmpty(OpStack *s) {
    return s->top == -1;
}

// 判满
int OpIsFull(OpStack *s) {
    return s->top == MAX_STACK - 1;
}

// 入栈
int OpPush(OpStack *s, char op) {
    if (OpIsFull(s)) return 0;
    s->top++;
    s->data[s->top] = op;
    return 1;
}

// 出栈
char OpPop(OpStack *s) {
    if (OpIsEmpty(s)) return '\0';
    return s->data[s->top--];
}

// 取栈顶
char OpGetTop(OpStack *s) {
    if (OpIsEmpty(s)) return '\0';
    return s->data[s->top];
}

// 运算符优先级判断
int GetPriority(char op) {
    if (op == '*' || op == '/') return 2;
    if (op == '+' || op == '-') return 1;
    return 0; // 括号优先级单独处理
}

// 字符串逆序
void ReverseStr(char *str) {
    int len = strlen(str);
    for (int i = 0; i < len / 2; i++) {
        char temp = str[i];
        str[i] = str[len - 1 - i];
        str[len - 1 - i] = temp;
    }
}

// 中缀转前缀
void InfixToPrefix(char *infix, char *prefix) {
    OpStack s;
    InitOpStack(&s);
    int len = strlen(infix);
    int idx = 0;

    // 1. 逆序中缀表达式,翻转括号
    char reversed[100] = {0};
    for (int i = 0; i < len; i++) {
        if (infix[i] == '(') reversed[i] = ')';
        else if (infix[i] == ')') reversed[i] = '(';
        else reversed[i] = infix[i];
    }
    ReverseStr(reversed);

    // 2. 遍历逆序后的字符串
    for (int i = 0; i < strlen(reversed); i++) {
        char c = reversed[i];
        // 情况1:操作数(数字)
        if (isdigit(c)) {
            // 处理多位数
            while (isdigit(reversed[i])) {
                prefix[idx++] = reversed[i++];
            }
            prefix[idx++] = ' '; // 操作数之间加空格
            i--; // 回退一位,避免跳过非数字字符
        }
        // 情况2:右括号(原左括号)
        else if (c == ')') {
            OpPush(&s, c);
        }
        // 情况3:左括号(原右括号)
        else if (c == '(') {
            char op;
            while ((op = OpPop(&s)) != ')') {
                prefix[idx++] = op;
                prefix[idx++] = ' ';
            }
        }
        // 情况4:运算符
        else if (c == '+' || c == '-' || c == '*' || c == '/') {
            while (!OpIsEmpty(&s) && GetPriority(OpGetTop(&s)) >= GetPriority(c)) {
                prefix[idx++] = OpPop(&s);
                prefix[idx++] = ' ';
            }
            OpPush(&s, c);
        }
    }

    // 3. 弹出栈中剩余运算符
    while (!OpIsEmpty(&s)) {
        prefix[idx++] = OpPop(&s);
        prefix[idx++] = ' ';
    }

    // 4. 逆序结果,得到前缀表达式
    prefix[idx - 1] = '\0'; // 去掉最后一个空格
    ReverseStr(prefix);
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        printf("请输入正确的参数:./a.out 中缀表达式\n");
        return 1;
    }

    char infix[100] = {0};
    strcpy(infix, argv[1]);
    char prefix[200] = {0};

    InfixToPrefix(infix, prefix);
    printf("前缀表达式:%s\n", prefix);

    return 0;
}

补充作业:队列的应用

  • 作业需求:实现循环队列,完成入队、出队、遍历操作;
  • 核心思路:基于数组实现循环队列,利用取模运算解决假溢出,实现判空、判满、入队、出队、遍历等操作;
  • 代码框架可参考前面的循环队列定义,补全操作函数,此处不再赘述。

3. 动态库和静态库

库(Library)是模块化、可复用的代码集合,将常用功能封装成库,供多个程序调用,减少重复开发、提高编译效率、保护源码,是工程开发中不可或缺的工具。

a. 库 library

i. 定义:将多个具有相关功能的.c 文件(模块)封装成一个独立的文件(库文件),隐藏源码实现细节,只提供调用接口(头文件),供其他程序复用。ii. 库的分类(补全):

  • 静态库(.a 后缀,Windows 下为.lib):编译时融入可执行文件,运行时独立;
  • 动态库(.so 后缀,Windows 下为.dll):运行时动态加载,编译时不融入可执行文件;
  • 核心区别:编译时是否将库代码拷贝到可执行文件中。iii. 作用(补全细节,拓展更多作用):
  1. 可供复用:多个程序可共用同一个库,无需重复编写相同功能的代码,提高开发效率;
  2. 可共享:动态库可被多个程序共享加载到内存,节省内存空间(静态库不可共享);
  3. 减少编译开销:修改库代码后,动态库无需重新编译依赖它的程序,静态库需重新编译;
  4. 保护源码:只提供库文件和头文件,不提供.c 源码,避免源码泄露;
  5. 模块化开发:将大型项目拆分为多个模块,每个模块封装成库,便于团队协作、维护和升级;
  6. 简化部署:多个程序依赖同一个库时,只需部署一次库文件(动态库),减少部署体积。

b. 动态库

i. 核心定义:运行期间动态加载到内存的库,程序编译时,仅记录库的依赖关系,不将库代码拷贝到可执行文件中;程序启动时,系统会将动态库加载到内存,供程序调用,运行结束后,动态库从内存中释放。ii. 核心特性:编译时不嵌入可执行文件,运行时依赖库文件存在,多个程序可共享同一个动态库。iii. 优势(补全细节,拓展更多优势):

  1. 目标文件(可执行文件)体积小:仅包含自身代码和库的依赖信息,不包含库代码,节省磁盘空间;

  2. 可无缝升级:库文件更新后,依赖它的程序无需重新编译,直接运行即可使用新功能(只需保证接口不变);

  3. 内存共享:多个程序同时运行时,只需加载一次动态库到内存,供所有程序共享,节省内存资源;

  4. 开发效率高:修改库功能后,无需重新编译所有依赖程序,只需重新编译动态库;

  5. 灵活部署:可单独部署动态库,根据需求更新或替换库文件,不影响主程序。iv. 缺点(补全细节,拓展更多缺点):

  6. 移植性差:程序移植到其他机器时,必须确保目标机器上存在对应的动态库(版本一致),否则程序无法运行("缺少.so 文件" 错误);

  7. 运行依赖:运行时必须依赖动态库文件,若库文件丢失、损坏或版本不兼容,程序会崩溃;

  8. 加载开销:程序启动时,需要动态加载库文件,比静态库多了一次加载过程,启动速度略慢;

  9. 接口依赖:若库的接口(函数名、参数、返回值)修改,依赖它的程序必须重新编译,否则会调用失败。v. 创建过程(补全细节、参数说明、常见错误):

  10. 核心命令:

    复制代码
    gcc -fPIC --shared -o libxxxx.so llist.c lstack.c
  11. 参数详解:

    • -fPIC:生成位置无关代码(Position-Independent Code),确保库文件加载到内存的任意地址都能正常运行,避免地址冲突(动态库必须添加此参数);
    • --shared:指定生成动态库(共享库),告诉编译器将多个.c 文件打包成动态库文件;
    • -o libxxxx.so:指定输出的动态库文件名,必须以 "lib" 开头,以 ".so" 结尾(系统默认识别此格式),xxxx 为自定义库名(如 libmystack.so);
    • llist.c lstack.c:需要打包成动态库的.c 文件(多个文件用空格分隔),可包含多个模块。
  12. 常见错误:

    • 忘记加-fPIC:生成的动态库无法正常加载,提示 "不是位置无关代码";
    • 文件名不符合规范(未以 lib 开头、.so 结尾):链接库时无法找到库文件;
    • 多个.c 文件未全部写入:库中缺少对应的函数,调用时提示 "未定义的引用"。vi. 链接库(编译主程序时链接动态库,补全细节、参数说明):
  13. 核心命令:

    复制代码
    gcc -o a.out main.c -lxxxx -Lpath
  14. 参数详解:

    • -o a.out:指定生成的可执行文件名(自定义,如 main、test 等);
    • main.c:主程序的.c 文件(依赖动态库的程序代码);
    • -lxxxx:指定链接的动态库名,去掉 "lib" 和 ".so"(如链接 libmystack.so,只需写 - lmystack);
    • -Lpath:指定动态库所在的路径(path 为库文件的绝对路径或相对路径);
      • 若动态库和主程序在同一目录,可写-L./(./ 表示当前目录);
      • 若不指定 - L,系统会默认去 /usr/lib、/lib 等系统库路径查找,找不到则链接失败。
  15. 常见错误:

    • -lxxxx写错库名:提示 "无法找到 - lxxxx";
    • 未指定 - Lpath,库不在系统路径:提示 "无法找到库文件";
    • 库文件权限不足:无法读取库文件,修改权限(chmod 755 libxxxx.so)。vii. 查看目标文件链接的所有库(补全细节、用法):
  16. 核心命令:

    复制代码
    ldd a.out
  17. 作用:查看可执行文件 a.out 所依赖的所有动态库,包括系统库和自定义动态库;

  18. 输出解读:

    • 若显示 "libxxxx.so => 库路径":表示找到对应的动态库,可正常加载;

    • 若显示 "libxxxx.so => not found":表示未找到动态库,程序无法运行,需检查库路径或环境变量。viii. 指定环境变量(让系统找到自定义动态库,补全两种方式、细节):动态库链接成功后,运行可执行文件时,系统会默认去系统路径查找动态库,自定义动态库需配置环境变量,两种常用方式:方式 1:临时环境变量(仅当前终端有效,重启终端失效)

      export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:你的动态库所在路径

方式 2:永久环境变量(补全细节,推荐)

  1. 编辑.bashrc 文件:

    复制代码
    vim ~/.bashrc
  2. 在文件末尾添加:

    复制代码
    export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/xxx/mylib  # 替换为你的动态库路径
  3. 保存退出(vim 中按 Esc,输入:wq);ix. 更新环境变量配置(让配置立即生效,补全):

    source ~/.bashrc

  • 作用:无需重启终端,立即加载新的环境变量配置;

  • 若未执行此命令,环境变量配置不会生效,系统仍找不到动态库。x. 加载最新配置,分配地址(补全细节、作用):

    sudo ldconfig

  • 作用:更新系统动态库缓存,让系统识别新添加的动态库,分配内存地址,避免 "库文件存在但无法加载" 的问题;

  • 注意:必须加 sudo(管理员权限),否则无法更新系统缓存;

  • 补充:若配置环境变量后,运行程序仍提示 "找不到动态库",执行此命令即可解决。xi. 补充:动态库的运行测试(补全)配置完成后,运行可执行文件,测试是否能正常调用动态库:

    ./a.out

  • 若正常输出结果,说明动态库创建、链接、配置成功;

  • 若提示 "缺少.so 文件",检查环境变量、库路径、ldconfig 命令是否执行。


c. 静态库

i. 核心定义:编译时直接将库代码拷贝到目标文件(可执行文件)中的库,程序运行时,不依赖任何外部库文件,完全独立,可直接运行。ii. 核心特性:编译时嵌入可执行文件,运行时与库文件完全分离,不依赖库文件存在。iii. 优势(补全细节,拓展更多优势):

  1. 移植性更强:可执行文件包含了所有需要的库代码,拿到可执行文件后,无需安装任何库,直接在任意支持的机器上运行;

  2. 运行速度快:无需在运行时动态加载库文件,减少加载开销,运行效率比动态库略高;

  3. 无依赖风险:运行时不依赖任何外部库文件,不会出现 "缺少库文件""库版本不兼容" 的问题;

  4. 接口稳定:库代码直接嵌入可执行文件,即使库文件被修改或删除,也不影响可执行文件的运行;

  5. 适合小型程序:小型程序使用静态库,可避免动态库的加载开销,简化部署。iv. 缺点(补全细节,拓展更多缺点):

  6. 目标文件(可执行文件)体积大:包含了自身代码和所有库代码,比动态库链接的可执行文件大很多,占用更多磁盘空间;

  7. 无法无缝升级:若静态库升级(修改功能、修复 bug),依赖它的所有程序必须重新编译(重新将新的库代码拷贝到可执行文件中),否则无法使用新功能;

  8. 内存浪费:多个程序使用同一个静态库时,每个程序的可执行文件中都会包含一份库代码,导致多份拷贝,浪费内存资源;

  9. 编译效率低:修改静态库后,所有依赖它的程序都需要重新编译,大型项目中会大幅降低开发效率。v. 生成步骤(补全细节、参数说明、常见错误):静态库生成分为两步:生成.o 目标文件 → 打包成静态库,步骤固定,必须严格执行。

  10. 第一步:生成.o 目标文件(将.c 文件编译为二进制目标文件)

    复制代码
    gcc -c -o xxx.o xxxx.c
    • 参数详解:
      • -c:只编译,不链接,生成.o 目标文件(不生成可执行文件);

      • -o xxx.o:指定生成的.o 文件名,与.c 文件名对应(如 llist.c → llist.o);

      • 多个.c 文件需分别生成.o 文件,如:

        复制代码
        gcc -c -o llist.o llist.c
        gcc -c -o lstack.o lstack.c
  11. 第二步:将.o 文件打包成静态库

    复制代码
    ar -cr libxxxx.a xxx.o yyy.o
    • 参数详解:
      • ar:打包命令,用于将多个.o 文件打包成静态库;
      • -c:创建静态库文件(若库文件不存在,自动创建;若存在,覆盖);
      • -r:将.o 文件添加到静态库中,若库中已有同名.o 文件,替换为新的;
      • libxxxx.a:静态库文件名,必须以 "lib" 开头,以 ".a" 结尾(系统默认识别格式),xxxx 为自定义库名;
      • xxx.o yyy.o:需要打包的.o 文件(多个文件用空格分隔)。
  12. 常见错误:

    • 忘记执行第一步(生成.o 文件):直接打包,提示 "无.o 文件";
    • 文件名不符合规范(未以 lib 开头、.a 结尾):链接时无法找到库文件;
    • 遗漏.o 文件:库中缺少对应的函数,调用时提示 "未定义的引用"。vi. 补充:静态库的链接(补全,与动态库对比)静态库的链接命令与动态库类似,无需配置环境变量(因为库代码已嵌入可执行文件):
      • 说明:-lxxxx(去掉 lib 和.a)、-Lpath(指定静态库路径)的用法与动态库一致;

      • 区别:静态库链接后,可执行文件不依赖任何库文件,直接运行即可,无需配置环境变量、无需执行 ldconfig。

        gcc -o a.out main.c -lxxxx -Lpath

d. 动态库与静态库的核心对比

对比维度 动态库(.so) 静态库(.a)
编译方式 不拷贝库代码,仅记录依赖 拷贝库代码到可执行文件
运行依赖 依赖动态库文件存在 不依赖任何库文件
可执行文件体积
移植性 差(需携带库文件) 强(可直接运行)
升级方式 无缝升级,无需重新编译程序 需重新编译程序
内存占用 多个程序共享,节省内存 多个程序多份拷贝,浪费内存
运行速度 略慢(需动态加载) 略快(无加载开销)
编译效率 高(修改库无需重新编译程序) 低(修改库需重新编译程序)

e. 头文件

头文件(.h)是库的 "接口说明文件",包含函数声明、宏定义、结构体定义等,主程序通过包含头文件,才能调用库中的函数,头文件的正确包含是库调用成功的前提。

i. 自己编写的头文件(非系统头文件)如何包含?(补全 3 种方式,细节拉满)

  1. 方式 1:直接写头文件路径(相对路径 / 绝对路径,最常用、最安全,推荐)

    • 语法:

      复制代码
      #include "./head.h"  // 相对路径,头文件在当前目录
      #include "../include/head.h"  // 相对路径,头文件在上一级目录的include文件夹
      #include "/home/xxx/include/head.h"  // 绝对路径,头文件在指定绝对路径
    • 优势:无需配置环境变量,直接指定路径,避免与系统头文件冲突;

    • 注意事项:路径必须正确,否则编译器无法找到头文件,提示 "没有那个文件或目录"。

  2. 方式 2:使用 #include <头文件名>(系统默认查找路径,补全细节、不推荐原因)

    • 语法:

      复制代码
      #include <head.h>
    • 原理:编译器会默认去系统头文件路径查找头文件,系统默认路径包括:

      • /usr/include(系统标准头文件路径);
      • /usr/local/include(第三方库头文件路径);
    • 实现方式:a. 方式一(不鼓励,不推荐):将自己的头文件拷贝到系统头文件路径(如 /usr/include),命令:

      复制代码
      sudo cp head.h /usr/include
      • 弊端:① 容易覆盖系统同名头文件,导致系统程序出错;② 只有 root 用户有权限操作;③ 头文件更新后,需重新拷贝,维护麻烦。b. 方式二(推荐,补全):配置环境变量 C_INCLUDE_PATH,让编译器在指定路径中查找头文件(无需拷贝到系统目录)。
  3. 方式 3:配置环境变量 C_INCLUDE_PATH(推荐,补全细节、步骤)

    • 作用:指定头文件的查找路径,编译器会在环境变量指定的路径中,查找 #include <头文件名> 包含的头文件;

    • 操作步骤:

      1. 编辑.bashrc 文件(永久生效):

        复制代码
        vim ~/.bashrc
      2. 在文件末尾添加(替换为自己的头文件路径):

        复制代码
        export C_INCLUDE_PATH=$C_INCLUDE_PATH:/home/xxx/include
        • 说明:$C_INCLUDE_PATH 表示保留原有环境变量,冒号后面添加新的头文件路径,多个路径用冒号分隔;
      3. 保存退出,更新环境变量:

        复制代码
        source ~/.bashrc
    • 优势:无需拷贝头文件到系统目录,不破坏系统文件,头文件更新后无需重新配置,多个头文件路径可灵活添加。

ii. 头文件包含的常见错误(补全,避免踩坑)

  1. 路径错误:#include "./head.h" 中路径写错(如多写 / 少写目录、文件名拼写错误),提示 "没有那个文件或目录";

  2. 混淆 #include "" 和 #include <>:将自己的头文件用 #include <> 包含,且未配置环境变量,导致编译器无法找到;

  3. 头文件重复包含:多个.c 文件或头文件中重复包含同一个头文件,导致 "重复定义" 错误;

    • 解决方法:在头文件中添加 "防止重复包含" 的宏,如:

      复制代码
      #ifndef HEAD_H
      #define HEAD_H
      
      // 头文件内容(函数声明、结构体定义等)
      
      #endif
  4. 头文件与库文件不匹配:头文件中的函数声明与库文件中的函数实现不一致(如参数个数、返回值类型不同),导致 "未定义的引用" 或运行错误;

  5. 环境变量配置错误:配置 C_INCLUDE_PATH 时,路径写错,或未执行 source ~/.bashrc,导致编译器无法找到头文件。


4. Makefile

Makefile 是管理大型项目的自动化编译脚本,通过定义 "目标 - 依赖 - 规则",自动判断哪些文件被修改过,只编译修改的文件,避免重复编译,大幅提高编译效率,是 C/C++ 工程开发的必备工具。

a. make 命令(补全细节、用法)

i. make main:执行 Makefile 中,以 main 为目标的编译规则(若 Makefile 中无 main 目标,会执行第一个目标);ii. make :默认执行 Makefile 中的第一个目标(通常是可执行文件目标,如 a.out、main);iii. make clean:执行 Makefile 中定义的 clean 目标,用于清理编译生成的.o 文件、可执行文件等,避免冗余文件;iv. make -f 文件名:若 Makefile 文件名不是默认名称(如 makefile1、myMakefile),用此命令指定要执行的 Makefile 文件,如:

复制代码
make -f myMakefile

ii. makefile 的默认规则(补全,必懂)

  • 默认规则 1:若未指定目标,make 会自动执行 Makefile 中的第一个目标;
  • 默认规则 2:若目标文件不存在,或目标文件的修改时间晚于依赖文件,make 会执行规则命令,重新生成目标;
  • 默认规则 3:若依赖文件不存在,make 会自动查找是否有以依赖文件为目标的规则,递归生成依赖文件;
  • 默认规则 4:默认的编译命令(若未指定规则命令):对于.c 文件,默认执行gcc -c -o 目标.o 依赖.c,生成.o 文件;对于.o 文件,默认执行gcc -o 目标 依赖.o,生成可执行文件。

b. Makefile 是什么?

  • 定义:Makefile 是一个文本格式的自动化编译脚本,包含一系列 "目标 - 依赖 - 规则",用于描述项目的编译流程、文件依赖关系,告诉 make 命令如何编译、链接项目。
  • 核心作用:
    1. 自动化编译:无需手动输入冗长的 gcc 命令,只需输入 make,即可自动完成编译、链接,生成可执行文件;
    2. 增量编译:自动判断哪些文件被修改过,只编译修改的文件,未修改的文件不重新编译,节省编译时间(大型项目尤为重要);
    3. 管理项目结构:将大型项目的多个模块、多个文件的编译规则集中管理,便于维护、修改和团队协作;
    4. 简化命令:可将复杂的编译命令、清理命令、测试命令等,封装到 Makefile 中,只需输入简单的 make 命令即可执行。
  • 优势:
    • 高效:增量编译,避免重复编译;
    • 灵活:可自定义编译规则、目标,适配不同项目需求;
    • 可维护:集中管理编译规则,修改时只需修改 Makefile,无需修改每个编译命令;
    • 跨平台:可在 Linux、Unix、Windows 等系统中使用(Windows 需安装 make 工具)。

c. 文件名

i. make 识别的文件名(三种均可):

  • Makefile:最规范、最常用,推荐使用(首字母大写,区分于普通文件);
  • makefile:小写,make 也能识别,但不规范;
  • MAKEFILE:全大写,make 能识别,但不常用;ii. 注意事项:
  • 若文件名不是以上三种,make 会提示 "没有规则可制作目标",需用 make -f 文件名指定;
  • 一个项目中通常只有一个 Makefile,放在项目根目录下,统一管理所有编译规则。

d. 语法

i. 核心思想:倒推方式(依赖倒推)

  • 倒推逻辑:要生成最终的目标文件(如可执行文件 a.out),需要先生成哪些依赖文件(如.o 文件);要生成.o 文件,需要先有对应的.c 文件和头文件;以此类推,直到找到最基础的文件(.c、.h),然后按顺序执行编译命令,生成目标文件。

ii. 核心语法格式:

复制代码
目标文件: 依赖文件1 依赖文件2 ...
	规则命令(必须以Tab键开头,不能用空格)
  • 目标文件(target):要生成的文件(如 a.out、main.o、libxxx.a),也可以是 "伪目标"(如 clean,不生成文件,仅执行命令);
  • 依赖文件(prerequisites):生成目标文件所需要的文件(如.c、.h、.o 文件),若依赖文件不存在或被修改,会先生成依赖文件;
  • 规则命令(commands):生成目标文件的具体命令(如 gcc 编译命令),必须以 Tab 键开头(这是 Makefile 语法的硬性要求,用空格会报错);
  • 注释:用 #开头,注释内容不会被 make 执行,用于说明规则、标注注释。
  1. 规则详解:

    • 规则逻辑:如果 "依赖文件的修改时间晚于目标文件",或者 "目标文件不存在",make 会执行规则命令,重新生成目标文件;
    • 若依赖文件不存在,make 会自动查找 Makefile 中,以该依赖文件为目标的规则,递归生成依赖文件;
    • 一个目标可以有多个依赖文件,一个依赖文件可以被多个目标使用;
    • 一个目标可以有多个规则命令,每个命令占一行,均以 Tab 开头,按顺序执行。
  2. 伪目标:

    • 定义:不生成具体文件,仅用于执行特定命令(如清理文件、测试程序)的目标,通常用.PHONY 声明,避免与同名文件冲突;

    • 常用伪目标:clean(清理编译产物)、test(测试程序)、all(生成所有目标);

    • 示例(clean 伪目标):

      复制代码
      .PHONY: clean  # 声明clean为伪目标,避免与clean文件冲突
      clean:
相关推荐
雨田大大1 小时前
Windows11下IDEA运行后端时,端口被占用的解决方法
linux·运维·服务器
xqqxqxxq1 小时前
Maven 完整配置与使用技术笔记
java·笔记·maven
砍材农夫1 小时前
物联网 基于netty理解粘包/拆包
java·物联网·struts
Counter-Strike大牛1 小时前
Nacos源码修改tomcat版本方法
java·tomcat
IKun-bug1 小时前
CentOS 7 安装 Claude Code 指南
linux·运维·centos
念越1 小时前
HTTPS 安全内核:对称与非对称加密的博弈,数字证书一战定局
java·网络·网络协议·安全·https
Anastasiozzzz1 小时前
深入研究Java Agent生态:SpringAI 与 SpringAIAlibaba核心能力、架构演进与全场景对比研究
java·开发语言·架构
kdxiaojie1 小时前
U-Boot分析【学习笔记】(8)
linux·笔记·学习
彭于晏Yan1 小时前
JSONObject 使用文档(Java/Android原生)
java·spring boot·后端