
🔥个人主页:爱和冰阔乐
📚专栏传送门:《数据结构与算法》 、C++
🐶学习方向:C++方向学习爱好者
⭐人生格言:得知坦然 ,失之淡然

🏠博主简介

文章目录
前言
写 C/C++ 项目时,我们经常会直接调用现成接口,却很少停下来观察:头文件负责什么,.o 文件从哪里来,静态库又是怎样被链接进程序的。IDE 把这些步骤藏在“生成”按钮后面,方便是方便了,但真正遇到 undefined reference、找不到头文件或找不到库时,就容易分不清问题出在哪个阶段。
这篇文章从一套可以实际编译的源码开始,先实现一个简化的文件流模型和字符串函数,再完成静态库的制作、链接、目录整理、安装与打包。整篇只围绕一条主线展开:
源文件先编译成
.o目标文件,多个.o再由ar归档成静态库;调用方通过头文件完成编译,通过库文件完成链接。
本文主要解决下面几个问题:
- 头文件、目标文件和静态库分别承担什么工作?
- 为什么静态库使用
ar制作? -I、-L、-l分别在哪个阶段生效?- 为什么只写
-l仍然可能找不到库? - 一个库交付给别人时,目录应该怎样整理?
- 静态链接完成后,为什么
ldd看不到自定义静态库?
一、什么是库?
库可以理解为一组已经写好、经过测试、可以重复使用的代码。大型程序不可能所有功能都从零开始实现,字符串处理、输入输出、网络通信、图形显示等基础能力,通常都由库提供。
从文件形式上看,Linux 常见的库分为两类:
| 类型 | Linux | Windows | 主要特点 |
|---|---|---|---|
| 静态库 | .a | .lib | 链接时把需要的目标代码合并进可执行程序 |
| 动态库/共享库 | .so | .dll | 运行时加载,多个程序可以共享同一份库代码 |
日常使用 Windows 软件时,偶尔会遇到“缺少某某 DLL”的提示,这就是程序依赖的动态库没有被找到、版本不匹配,或者被误删导致的。

1.1 在系统中查看 C/C++ 动静态库
不同发行版安装位置可能不同,下面分别看 Ubuntu 和 CentOS 中常见的 C/C++ 标准库文件。
# Ubuntu:C 动态库与静态库
ls -l /lib/x86_64-linux-gnu/libc-2.31.so
ls -l /lib/x86_64-linux-gnu/libc.a
# Ubuntu:C++ 动态库与静态库
ls -l /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so
ls -l /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a
# CentOS:C 动态库与静态库
ls -l /lib64/libc-2.17.so
ls -l /lib64/libc.a
# CentOS:C++ 动态库与静态库
ls -l /lib64/libstdc++.so.6
ls -l /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a
这里先记住:
.a通常是多个可重定位目标文件的归档;.so是可以被动态装载的共享目标文件。二者都来自编译后的目标代码,但组织方式、链接方式和加载方式不同。
1.2 库里为什么通常不写 main?
main 是用户程序的入口。库的职责是提供函数、类型或数据,供不同程序调用。
假设库里已经定义了一个 main,用户程序里又有自己的 main,链接器就会发现两个同名的全局入口,产生重复定义错误。因此,一般的函数库不包含 main。
库通常向使用者提供两部分内容:
- 头文件
.h:声明接口,告诉编译器函数名、参数类型、返回值和类型定义。 - 库文件
.a或.so:保存编译后的函数实现,供链接器或动态加载器使用。
更准确地说,头文件不是库文件本体,但它是库对外发布的接口说明。使用者需要头文件完成编译,需要库文件完成链接或运行时加载。
二、准备一套可以制作成库的源码
为了让后面的实验有完整上下文,我们先实现两组函数:
- 简化版标准 I/O:
mfopen、mfwrite、mfflush、mfclose。 - 字符串长度函数:
my_strlen。
下面的
mFILE是为了理解FILE* + 用户态缓冲区 + fd之间关系而设计的教学模型,不是 glibc 的完整实现。
2.1 my_stdio.h
#pragma once
#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
typedef struct IO_FILE
{
int flag; // 刷新策略
int fileno; // 底层文件描述符
char outbuffer[SIZE]; // 用户态输出缓冲区
int cap; // 缓冲区容量
int size; // 当前已经使用的字节数
} mFILE;
mFILE *mfopen(const char *filename, const char *mode);
int mfwrite(const void *ptr, int num, mFILE *stream);
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);
2.2 my_stdio.c
下面沿用这套实现思路,并补上边界检查、部分写入和内存释放,让示例可以直接编译运行。
#include "my_stdio.h"
#include <errno.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
static int write_all(int fd, const char *buffer, int size)
{
int written = 0;
while (written < size)
{
ssize_t n = write(fd, buffer + written, (size_t)(size - written));
if (n > 0)
{
written += (int)n;
continue;
}
if (n < 0 && errno == EINTR)
{
continue;
}
return -1;
}
return 0;
}
mFILE *mfopen(const char *filename, const char *mode)
{
if (filename == NULL || mode == NULL)
{
return NULL;
}
int fd = -1;
if (strcmp(mode, "r") == 0)
{
fd = open(filename, O_RDONLY);
}
else if (strcmp(mode, "w") == 0)
{
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
}
else if (strcmp(mode, "a") == 0)
{
fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
}
else
{
return NULL;
}
if (fd < 0)
{
return NULL;
}
mFILE *stream = (mFILE *)malloc(sizeof(mFILE));
if (stream == NULL)
{
close(fd);
return NULL;
}
stream->fileno = fd;
stream->flag = FLUSH_LINE;
stream->size = 0;
stream->cap = SIZE;
return stream;
}
void mfflush(mFILE *stream)
{
if (stream == NULL || stream->size <= 0)
{
return;
}
if (write_all(stream->fileno, stream->outbuffer, stream->size) == 0)
{
stream->size = 0;
}
}
int mfwrite(const void *ptr, int num, mFILE *stream)
{
if (ptr == NULL || stream == NULL || num < 0)
{
return -1;
}
const char *data = (const char *)ptr;
int copied = 0;
while (copied < num)
{
int free_space = stream->cap - stream->size;
if (free_space == 0)
{
mfflush(stream);
free_space = stream->cap - stream->size;
if (free_space == 0)
{
return copied;
}
}
int current = num - copied;
if (current > free_space)
{
current = free_space;
}
memcpy(stream->outbuffer + stream->size, data + copied, (size_t)current);
stream->size += current;
copied += current;
if (stream->flag == FLUSH_LINE &&
stream->size > 0 &&
stream->outbuffer[stream->size - 1] == '\n')
{
mfflush(stream);
}
else if (stream->flag == FLUSH_FULL && stream->size == stream->cap)
{
mfflush(stream);
}
}
return copied;
}
void mfclose(mFILE *stream)
{
if (stream == NULL)
{
return;
}
mfflush(stream);
close(stream->fileno);
free(stream);
}
有些教学实现还会在 mfflush 中调用 fsync。这里需要区分两件事:
write:把用户态数据交给内核。fsync:要求内核尽量把对应文件的数据同步到持久化设备。
普通标准库的 fflush 主要解决用户态缓冲区刷新,不等于每次都调用 fsync。如果每次刷新都强制落盘,性能会明显下降。因此,上面的教学实现没有默认调用 fsync,需要强持久化时可以由调用方单独执行。
实验说明: 为了方便观察刷新过程,这里把
flag统一初始化为FLUSH_LINE。真实标准库会结合输出对象和运行环境选择缓冲策略,普通文件通常采用全缓冲,终端输出通常采用行缓冲。另外,mfopen虽然保留了"r"分支来展示打开模式与open标志位的对应关系,但本文没有实现mfread,所以后面的实际调用以"w"和"a"为主。

2.3 my_string.h 与 my_string.c
// my_string.h
#pragma once
int my_strlen(const char *s);
// my_string.c
#include "my_string.h"
int my_strlen(const char *s)
{
const char *end = s;
while (*end != '\0')
{
end++;
}
return (int)(end - s);
}
完成这些源文件后,我们就有了可以制作成库的目标代码。
三、静态库的制作与使用
3.1 静态库到底是什么?
源文件不能直接被链接器使用,需要先经过编译和汇编,形成 .o 目标文件:
gcc -c my_stdio.c
gcc -c my_string.c
此时目录里会得到:
my_stdio.o
my_string.o
静态库的核心,就是把多个 .o 目标文件组织成一个归档文件。它不是普通的 tar 压缩包,而是链接器能够直接识别的目标文件归档。
可以把多个
.o想象成零散纸张,静态库就是把这些纸张装订成一本册子。使用者不需要手动拆开,链接器会从归档中挑出需要的目标模块。
3.2 使用 ar 生成静态库
# 先生成所有目标文件
gcc -c *.c
# 将目标文件归档成静态库
ar -rcs libmystdio.a my_stdio.o my_string.o
选项含义:
| 选项 | 含义 |
|---|---|
r | 将目标文件插入归档;同名成员已经存在时替换 |
c | 归档不存在时创建,同时不输出创建提示 |
s | 建立或更新符号索引,方便链接器快速查找符号 |
t | 列出归档中的成员 |
v | 显示详细信息 |
查看静态库中包含哪些目标文件:
ar -tv libmystdio.a

静态库的命名规则可以直接记成:
lib + 库名 + .a
例如 libmystdio.a,传给 -l 的库名就是 mystdio,需要去掉开头的 lib 和结尾的 .a。
3.3 第一次链接:为什么只写 -l 还不够?
假设调用方有一个 main.c:
#include "my_stdio.h"
#include "my_string.h"
#include <stdio.h>
int main(void)
{
const char *text = "abcdefg\n";
printf("length = %d\n", my_strlen(text));
mFILE *fp = mfopen("./log.txt", "a");
if (fp == NULL)
{
return 1;
}
mfwrite(text, my_strlen(text), fp);
mfwrite(text, my_strlen(text), fp);
mfwrite(text, my_strlen(text), fp);
mfclose(fp);
return 0;
}
先只编译调用方:
gcc -c main.c
如果直接链接:
gcc -o main main.o
链接器会提示 my_strlen、mfopen 等符号未定义,因为它只看到了函数声明,没有找到函数实现。

告诉链接器需要 mystdio 库:
gcc -o main main.o -lmystdio
-l 的意思是指定库名,但此时仍可能提示找不到 libmystdio.a。

原因是链接器默认不会把所有当前目录都当成库目录。还需要用 -L 指定库文件所在路径:
gcc -o main main.o -L. -lmystdio

3.4 -I、-L、-l 分别负责什么?
这三个选项经常一起出现,但作用阶段不同:
| 选项 | 作用 | 面向阶段 | 示例 |
|---|---|---|---|
-I | 增加头文件搜索目录 | 预处理/编译 | -I./lib/include |
-L | 增加库文件搜索目录 | 链接 | -L./lib |
-l | 指定要链接的库名 | 链接 | -lmystdio |
完整命令:
gcc main.c \
-I./lib/include \
-L./lib/mylib \
-lmystdio \
-o main
-I解决“头文件在哪里”,-L解决“库文件去哪里找”,-l解决“具体找哪个库”。
如果代码直接写相对路径:
#include "./lib/include/my_stdio.h"
#include "./lib/include/my_string.h"
确实可以不写 -I,但这样会把项目目录结构硬编码到源代码里。更规范的做法通常是保持简洁的 #include,把搜索路径交给构建系统管理。
3.5 设计一个可以交付给别人的目录结构
库设计者一般不会只丢一个 .a,而是把头文件、库文件和构建说明整理好:
推荐把交付目录整理成下面三层:
lib/include/my_stdio.hmy_string.h
mylib/libmystdio.a
这样一眼就能分清:头文件放在 include,库文件放在 mylib。

打包:
tar czf lib.tgz lib
使用者解压:
cp ../lib.tgz .
tar xzf lib.tgz

编译时告诉 GCC 头文件和库文件位置:
gcc main.c \
-I./lib/include \
-L./lib/mylib \
-lmystdio \
-o main

3.6 把库“安装到系统”是什么意思?
头文件常见搜索位置包括:
ls /usr/include
ls /usr/local/include

库文件常见位置包括:
ls /lib64
ls /usr/lib64
ls /usr/local/lib64

最直接的安装方式,是把头文件和库文件复制到系统默认搜索目录:
cp lib/include/* /usr/include/
cp lib/mylib/libmystdio.a /lib64/
安装后可以省略 -I 和 -L:
gcc main.c -lmystdio -o main

不过,实际项目不建议随意往 /usr/include 和 /lib64 复制自定义文件,因为容易污染系统目录、覆盖同名文件,也不方便卸载。更常见的选择是:
- 安装到
/usr/local/include和/usr/local/lib。 - 使用项目私有目录,通过
-I、-L或 CMake 管理。 - 制作 RPM、DEB 等安装包,让包管理器记录文件。
3.7 静态链接后为什么 ldd 看不到自定义库?
查看可执行程序依赖:
ldd ./main
如果 libmystdio.a 是静态链接进去的,ldd 不会显示它。

原因是链接器已经从静态库中取出需要的目标模块,把代码合并进了可执行文件。程序运行时不再需要原来的 .a 文件。
静态库并不是整个文件无条件复制进去。链接器通常只提取解决当前未定义符号所需要的归档成员。
3.8 用 Makefile 自动完成静态库制作与打包
TARGET := libmystdio.a
OBJECTS := my_stdio.o my_string.o
$(TARGET): $(OBJECTS)
ar -rcs $@ $^
%.o: %.c
gcc -Wall -Wextra -O2 -c $< -o $@
.PHONY: output
output: $(TARGET)
mkdir -p lib/include
mkdir -p lib/mylib
cp -f *.h lib/include
cp -f $(TARGET) lib/mylib
tar czf lib.tgz lib
.PHONY: clean
clean:
rm -rf *.o $(TARGET) lib lib.tgz
执行:
make
make output
make clean

总结
这篇文章把静态库的完整流程走了一遍:先编写头文件和源文件,再使用 gcc -c 生成 .o,最后通过 ar -rcs 把多个目标文件归档成 .a。调用方在编译和链接时,需要分别解决三个问题:
-I:头文件在哪里;-L:库文件去哪里找;-l:具体要链接哪个库。
静态链接完成后,链接器会从归档中取出当前程序真正需要的目标模块,并把代码组织进可执行文件。因此程序运行时不再依赖原来的 .a,ldd 也不会显示这个自定义静态库。
静态库的核心不是“把源码压缩起来”,而是把已经编译好的目标文件整理成链接器可以识别的归档。
到这里,静态库这条线已经走通。下一篇继续处理动态库、运行时查找、Visual Studio 配置,以及目标文件和 ELF 加载之间的关系。
资源分享
【Linux】Ext 文件系统篇:从 inode 到路径解析,搞懂挂载与软硬链接
【Linux】Ext 文件系统:从机械磁盘到 CHS/LBA,搞懂扇区、块与分区
【Linux】文件描述符背后的底层逻辑:重定向、VFS 与用户态缓冲区

2662

被折叠的 条评论
为什么被折叠?



