一、库文件基础认知
1. 库文件与头文件的核心区别
| 类型 | 存储内容 | 作用说明 |
|---|---|---|
| 库文件 | 仅存储函数的实现,不存储声明 | 预先编译好的二进制文件,包含大量函数实现、数据结构,供程序调用(无需重新编写实现代码) |
| 头文件 | 仅存储函数的声明,不存储实现 | 告诉编译器函数的名称、参数类型、返回值类型,相当于 "工具使用说明",方便程序调用库中的函数 |
2.库文件的分类与标识
| 库类型 | 别称 | 系统标识(Linux) | Windows 标识 | 示例(绿色标注) |
|---|---|---|---|---|
| 静态库 | 静态链接库 | libxx.a(前缀lib,后缀.a) |
libxx.lib |
libfoo.a、libmath.a |
| 共享库 | 动态链接库 / 动态库 | libxx.so(前缀lib,后缀.so) |
libxx.dll |
libadd.so、libfoo.so |
3.库文件的核心特性
- 库中函数实现已完成编译(二进制格式),本质是存储函数实现代码的二进制文件
- 使用库时需 "去前缀去后缀":如库文件全名为
libfoo.a,使用时仅需写foo - 系统默认优先使用共享库(若静态库和共享库同时存在)
- 库是 "预先编译好的方法集合",可重复使用,减少重复开发
二、静态库(libxx.a)的创建与使用
1. 创建静态库的 3 个步骤(含示例)
步骤 1:将多个源文件编译为目标文件(.o)
-
指令:
gcc -c 源文件名1.c 源文件名2.c ...(仅编译不链接,生成二进制目标文件)bashgcc -c add.c # 生成 add.o gcc -c max.c # 生成 max.o -
作用:相当于 "打包函数实现的第一步",将高级语言转为二进制目标文件
步骤 2:用ar指令打包生成静态库
-
指令:
ar rcs lib[库名].a 目标文件1.o 目标文件2.o ...r:插入文件到库中c:创建库文件
-
s:生成索引bashar rcs libmath.a add.o max.o -
作用:相当于 "最终打包",将多个目标文件合并为静态库文件
步骤 3**:验证静态库(可选)**
- 可通过
ls查看生成的静态库:ls -l libmath.a(确认文件存在) - 核心指令格式:
gcc -o 可执行文件名 源文件.c -L[库路径] -l[库名]-L:指定库文件所在路径(若库在当前目录,可省略-L,直接写-L.明确当前目录)-l:指定库名(去前缀去后缀,如libmath.a对应-lmath)
2. 链接使用静态库(编译时链接)
-
核心指令格式:
gcc -o 可执行文件名 源文件.c -L[库路径] -l[库名]-
-L:指定库文件所在路径(若库在当前目录,可省略-L,直接写-L.明确当前目录) -
-l:指定库名(去前缀去后缀,如libmath.a对应-lmath)
示例 1:当前目录下使用libmath.a,编译main.c生成可执行文件mainbash
bashgcc -o main main.c -L. -lmath # -L. 表示当前目录,-lmath 对应 libmath.a示例 2:库文件在
/home/stu/lib目录下,使用libfoo.abashgcc -o main main.c -L/home/stu/lib -lfoo # -L指定库路径,-lfoo对应libfoo.a3. 静态库的核心特性(重点)
-
拷贝机制:链接时,会将程序所需的函数代码拷贝一份到最终的可执行程序中
-
独立性:可执行程序生成后,删除静态库,程序依然能正常运行(已包含所需函数代码)
-
静态性(非实时性):若库中函数代码修改,需重新创建静态库并重新编译链接程序,才能生效(代码是 "调用时瞬间的拷贝版本",具有过去性)
-
体积较大:可执行程序包含了库中的函数代码,占用更多磁盘空间和内存
-
无需依赖库文件:运行时不依赖静态库,可独立部署
三、共享库(libxx.so)的创建与使用
1. 创建共享库的 3 个步骤(含示例)
步骤 1:将源文件编译为位置无关目标文件(.o)
-
指令:
gcc -c -fPIC 源文件名1.c 源文件名2.c ... -
-fPIC:生成位置无关代码(保证共享库在内存中仅加载一份,供多个程序共享)bashgcc -c -fPIC add.c步骤 2:用
gcc -shared生成共享库 -
指令:
gcc -shared -fPIC -o lib[库名].so 目标文件1.o 目标文件2.o ...(可直接合并步骤 1 和步骤 2) -
简化指令(合并编译与打包):
gcc -shared -fPIC -o lib[库名].so 源文件名1.c 源文件名2.c ... -
示例 1:单独执行步骤 2,将
add.o打包为共享库libadd.sobashgcc -shared -fPIC -o libadd.so add.o -
示例 2:合并步骤 1 和步骤 2,直接从
add.c生成libadd.sobashgcc -shared -fPIC -o libadd.so add.c -
作用:相当于 "打包函数实现",生成可被多个程序共享的动态库文件
步骤 3:验证共享库(可选)
-
用
ls查看生成的共享库:ls -l libadd.so(确认文件存在) -
核心指令格式:与静态库一致,
gcc -o 可执行文件名 源文件.c -L[库路径] -l[库名] -
示例:当前目录下使用
libadd.so,编译main.c生成可执行文件mainbashgcc -o main main.c -L. -ladd # -L. 指定当前目录,-ladd 对应 libadd.so
2. 链接使用共享库(编译时链接)
- 核心指令格式:与静态库一致,
gcc -o 可执行文件名 源文件.c -L[库路径] -l[库名] - 示例:当前目录下使用
libadd.so,编译main.c生成可执行文件main
bashgcc -o main main.c -L. -ladd # -L. 指定当前目录,-ladd 对应 libadd.so3. 设置共享库搜索路径(运行时必备)
- 问题:编译成功后,运行程序可能提示 "找不到共享库"(系统不知道共享库所在路径)
- 指令:
export LD_LIBRARY_PATH=[库路径]:$LD_LIBRARY_PATH(临时设置,终端关闭后失效) - 示例:共享库在当前目录(.),设置搜索路径
bashexport LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH- 作用:告诉系统去哪里查找运行程序所需的共享库
4. 共享库的核心特性(重点)
- 标记机制(不拷贝):链接时,仅在可执行程序中标记函数的引用地址,不拷贝函数代码到程序中
- 依赖性:可执行程序生成后,若删除共享库,程序无法运行(运行时需加载共享库)
- 动态性(实时性):若库中函数代码修改 / 升级,无需重新编译可执行程序,直接运行即可生效(链接的是 "在线库",具有实时性)
- 体积小巧:可执行程序仅包含标记信息,不包含函数代码,节省磁盘空间和内存
- 共享性:共享库在内存中仅加载一份,所有使用该库的应用程序共享这份代码,提高资源利用率
四、库使用的注意事项(关键要求)
- 两个必不可少的步骤(不同阶段)
- 编译前:在主程序中
#include "头文件名.h"(如#include "math.h"),不可省略,用于告诉编译器函数声明 - 编译时:通过
-L指定库路径、-l指定库名(如-L. -lmath),用于告诉编译器去哪里找库文件
- 编译前:在主程序中
- 系统层面 :系统默认不知道自定义库的位置,必须通过
-L指定路径,否则无法找到库文件 - 目标文件(.o):是二进制文件,是编译库和程序的中间产物,不可直接运行
五、查看库依赖:ldd 指令
1. 功能
查看可执行文件或共享库所依赖的共享库信息 (静态库无需依赖,已拷贝代码,故
ldd不识别静态库依赖)2. 指令格式与示例
- 格式:
ldd 可执行文件名/共享库文件名 - 示例 1:查看可执行文件
main依赖的共享库
bashldd main # 输出:会列出 main 依赖的所有 libxx.so 及路径- 示例 2:查看共享库
libadd.so依赖的其他共享库
bashldd main # 输出:会列出 main 依赖的所有 libxx.so 及路径六、静态库与共享库的核心区别(重点对比)
对比维度 静态库(libxx.a) 共享库(libxx.so) 链接机制 拷贝所需函数代码到可执行程序(静态拷贝) 仅标记函数引用地址,不拷贝代码(动态标记) 程序独立性 独立运行,删除静态库不影响程序执行 依赖共享库,删除共享库程序无法运行 代码更新 库代码修改后,需重新编译链接程序才生效 库代码修改后,无需重新编译,直接运行程序即可 可执行程序体积 体积较大(包含库函数代码) 体积小巧(仅包含标记信息) 内存利用率 较低,多个程序使用同一库会重复加载代码 较高,内存中仅加载一份库代码,多程序共享 运行效率 略高(无需运行时加载库) 略低(运行时需动态加载库) 部署难度 简单(无需携带库文件) 复杂(需同时部署可执行程序和共享库) 七、tar 指令补充(打包 / 解包)
1. 打包(仅打包,不压缩)
- 指令:
tar cvf [包名].tar 要打包的文件1 要打包的文件2 ...c:创建打包文件v:显示打包详情f:指定打包文件名
- 示例:将
a.txt和passwd文件打包为my.tar
-
bash
tar cvf my.tar a.txt passwd
2. 解包 / 解压缩(gzip 格式)
- 指令:
tar zxf [压缩包名].tar.gz(一次完成解压缩 + 解包)z:处理 gzip 格式压缩包x:释放 / 解包文件f:指定压缩包文件名
- 示例:解压
my.tar.gz到当前目录
bash
tar zxf my.tar.gz
八、printf 缓冲区问题
1. 缓冲区现象
-
示例:以下代码执行后,不会立即打印
abc和hello,而是等待缓冲区满足条件后才输出cpp#include <stdio.h> int main() { printf("abc"); printf("hello"); sleep(3); // 阻塞3秒 return 0; } -
原因:
printf输出的内容会先存入stdout 缓冲区(默认是行缓冲),而非直接打印到屏幕
2. 缓冲区刷新的 3 种情况
| 刷新条件 | 说明 | 示例 |
|---|---|---|
| 缓冲区满 | 当缓冲区存储的数据达到上限时,自动刷新到屏幕(内核负责输出) | 批量输出大量文本时触发 |
| 强制刷新缓冲区 | 用fflush(stdout)指令,或输出换行符\n(行缓冲特性) |
fflush(stdout);、printf("abc\n"); |
| 程序正常结束时 | 程序执行到return 0或exit(0)时,会自动刷新缓冲区后退出 |
上述示例中,3 秒后程序结束,才打印内容 |
3. 相关函数区别(刷新行为)
| 函数 | 功能说明 | 缓冲区处理 |
|---|---|---|
exit(n) |
终止程序,n为退出码(Linux:0 = 成功,1/2/3 = 失败) |
先刷新缓冲区,再退出 |
_exit(n) |
强制终止程序,n为退出码 |
不刷新缓冲区,直接退出 |
fflush(stdout) |
强制刷新标准输出缓冲区(stdout代表屏幕) |
主动触发缓冲区刷新 |
九、主函数参数(argc、argv、envp)
1. 主函数完整格式
cpp
#include <stdio.h>
int main(int argc, char* argv[], char* envp[]) {
// 业务逻辑
return 0;
}
2. 各参数说明(含示例)
| 参数名 | 数据类型 | 功能说明 | 示例(运行./main 123 hello) |
|---|---|---|---|
argc |
整型(int) | 命令行传入主函数的参数个数,最低为 1(默认包含可执行程序自身路径) | argc=3(./main、123、hello) |
argv |
字符指针数组 | 存储命令行参数的具体内容 ,argv[0]是可执行程序路径,argv[1]开始是传入的自定义参数 |
argv[0]="./main"、argv[1]="123"、argv[2]="hello" |
envp |
字符指针数组 | 存储系统环境变量 (如路径、用户信息、系统配置等),个数与argc无关 |
envp[0]="PATH=/usr/bin"、envp[1]="USER=stu" |
3. 核心特性
argc >= 1:永远不会为 0,因为默认包含可执行程序的路径argv是 "字符串数组",每个元素对应一个命令行参数envp继承当前终端的环境变量,可通过它获取系统配置信息
十、进程进阶(fork () 函数)(十分重点)
1. fork () 函数基础信息
- 头文件:
#include <unistd.h> - 功能:创建一个新进程(子进程),从当前进程(父进程)复制而来
- 返回值(关键:一次调用,两个返回值):
- 父进程中:返回子进程的 PID(大于 0 的整数)
- 子进程中:返回
0 - 失败时:返回
-1(如内存不足、进程数达到上限)
2. fork () 进程复制的细节
- 复制内容:父进程的内存空间(代码、数据、缓冲区、环境变量等)会完整拷贝到子进程
- 执行逻辑 :
- 子进程继承父进程的运行状态,从
fork()函数下一行代码开始执行 - 父子进程是彼此独立的进程,互不影响(一个进程终止,不影响另一个进程运行)
- 父子进程同时并发执行,执行顺序由操作系统调度决定
- 子进程继承父进程的运行状态,从
- 完成标志 :
fork()函数执行完毕,才算完成父进程到子进程的完整拷贝 - 特殊复制 :
printf缓冲区的信息也会被拷贝到子进程中
3. 父子进程的区分与 ID 获取
(1)区分方式:通过 fork () 返回值
cpp
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t pid = fork(); // 创建子进程
if (pid > 0) {
// 父进程:pid是子进程ID
printf("我是父进程,PID=%d,子进程PID=%d\n", getpid(), pid);
} else if (pid == 0) {
// 子进程:pid=0
printf("我是子进程,PID=%d,父进程PID=%d\n", getpid(), getppid());
} else {
// 失败
perror("fork失败");
return 1;
}
sleep(1);
return 0;
}
(2)获取进程 ID 的函数
| 函数 | 功能说明 | 示例 |
|---|---|---|
getpid() |
获取当前进程的 PID | pid_t my_pid = getpid(); |
getpptd() |
获取当前进程的父进程的 PID | pid_t parent_pid = getppid(); |
4. 进程的 3 种基本状态
- 就绪状态:进程已准备就绪,等待操作系统分配 CPU 资源(具备运行条件)
- 运行状态:进程正在 CPU 上执行指令(占用 CPU 资源)
- 阻塞状态:进程因等待某个事件(如 sleep、IO 操作)而暂停,暂时无法执行(释放 CPU 资源)
5. 进程的并发与并行
| 执行方式 | 核心特点 | 示例 |
|---|---|---|
| 并行 | 多个进程在多个 CPU 核心上同时执行,真正的 "一起运行" | 双核 CPU 上,进程 A 在核心 1 运行,进程 B 在核心 2 运行 |
| 并发 | 多个进程在单个 CPU 核心上交替执行,通过时间片轮转实现 "看似同时运行" | 单核 CPU 上,进程 A 运行 10ms,切换到进程 B 运行 10ms,循环交替 |
6. 僵死进程(僵尸进程)
(1)定义
子进程先于父进程结束,父进程未及时获取子进程的退出码(未回收子进程资源),子进程的 PCB 仍保留在系统中,成为僵死进程。
(2)产生条件
- 子进程先终止
- 父进程未调用
wait()/waitpid()等函数回收子进程资源 - 父进程未终止(若父进程终止,子进程会被 init 进程收养并回收)
(3)危害
占用系统的 PID 资源和 PCB 资源,若大量僵死进程存在,会导致无法创建新进程。
7. fork () 习题解题技巧
- 推荐方法:类比细胞分裂(父进程分裂为父进程和子进程,各自独立后续分裂)
- 不推荐方法:树状图(复杂场景下易混乱)
- 核心逻辑:每调用一次
fork(),进程数翻倍(忽略失败情况)
十一、内存地址相关概念
1. 逻辑地址(相对地址)
- 定义:程序在编写和运行时所使用的内存地址,是 "相对地址",与实际物理内存地址无关
- 特性:
- 程序不直接访问物理地址,仅通过逻辑地址操作内存
- 父子进程中,同一个变量的逻辑地址相同,但实际物理地址不同(内存空间独立)
- 我们通过程序看到的地址,本质都是逻辑地址
2. 物理地址
- 定义:硬件(内存)上的实际存储地址,是 "绝对地址",对应内存芯片的具体存储单元
- 映射关系:逻辑地址通过操作系统内核的 "内存映射机制" 转换为物理地址,实现内存访问
3. Linux 32 位系统虚拟地址空间(4G)
| 地址范围 | 内存区域 | 说明 |
|---|---|---|
0x00000000 - 0xBFFFFFFF |
用户空间(3G) | 包含代码段、数据段、堆、栈(中间有内核与栈区的空隙,范围约0xC0000000-0xBFFFFFFF) |
0xC0000000 - 0xFFFFFFFF |
内核空间(1G) | 操作系统内核使用,存放内核代码、数据,用户进程无法直接访问 |
十二、操作系统基础
1. 操作系统的核心功能
- 管理计算机的硬件资源(CPU、内存、硬盘、外设等)
- 为用户和应用程序提供交互接口(命令行、图形界面、系统调用)
- 实现进程调度、内存管理、文件管理等核心功能
2. 计算机五大部件
- 运算器:负责算术运算(加减乘除)和逻辑运算(与或非)
- 控制器:控制计算机各部件协调工作,读取指令并执行
- 存储器:分为内存(临时存储)和硬盘(持久存储),存放数据和指令
- 输入设备:向计算机输入数据(键盘、鼠标、扫描仪等)
- 输出设备:将计算机处理结果输出(屏幕、打印机、音箱等)
3. 进程控制块(PCB)补充
- 别称:进程描述符(Linux 中)
- 本质:一个结构体,存储进程的 PID、状态、内存地址、优先级等核心信息
- 管理方式:操作系统通过双向链表管理所有进程的 PCB(创建进程 = 添加节点,终止进程 = 删除节点)
- 每个进程对应一个唯一的 PCB,是操作系统管理进程的核心依据