【Linux指南】动静态库系列(二):从源码复用到目标文件复用:为什么需要把 .o 打包成库

文章目录

    • 一、先写一个可以复用的小模块
    • 二、第一种复用方式:给源码和头文件
      • [1. 源码完全暴露](#1. 源码完全暴露)
      • [2. 文件越来越多后使用麻烦](#2. 文件越来越多后使用麻烦)
      • [3. 不利于模块独立维护](#3. 不利于模块独立维护)
    • [三、第二种复用方式:给 .o 和 .h](#三、第二种复用方式:给 .o 和 .h)
    • 四、头文件和目标文件分别负责什么
      • [1. 头文件负责告诉编译器"函数长什么样"](#1. 头文件负责告诉编译器“函数长什么样”)
      • [2. .o 文件负责提供真正的函数实现](#2. .o 文件负责提供真正的函数实现)
    • [五、只给 .o + .h 的优点](#五、只给 .o + .h 的优点)
      • [1. 可以隐藏源码实现](#1. 可以隐藏源码实现)
      • [2. 编译速度更快](#2. 编译速度更快)
      • [3. 接口和实现分离](#3. 接口和实现分离)
    • [六、但 .o 文件多了以后仍然麻烦](#六、但 .o 文件多了以后仍然麻烦)
    • [七、静态库的出现:把多个 .o 打成一个包](#七、静态库的出现:把多个 .o 打成一个包)
    • [八、为什么 .a 不需要手动解包](#八、为什么 .a 不需要手动解包)
    • 九、接口发布时应该给什么
    • [十、不要在 include 中写死路径](#十、不要在 include 中写死路径)
    • 十一、从源码到静态库的完整演进
    • 十二、总结

上一篇我们讲了库的本质:库是已经写好、成熟可复用的二进制代码。静态库和动态库虽然使用方式不同,但它们都离不开一个共同基础:目标文件 .o

这篇文章不急着直接讲 ar -rc libxxx.a *.o。我们先从最朴素的代码复用方式开始,看一个库是如何从"发源码"一步步演进到"发 .o + .h",最终发展到"把多个 .o 打包成 .a"的。

一、先写一个可以复用的小模块

假设我们写了一个简单的字符串工具模块,提供两个函数:

  • my_strlen:计算字符串长度;
  • my_strcmp:比较两个字符串。

头文件 my_string.h

c 复制代码
#pragma once

int my_strlen(const char *s);
int my_strcmp(const char *s1, const char *s2);

实现文件 my_string.c

c 复制代码
#include "my_string.h"

int my_strlen(const char *s)
{
    const char *end = s;
    while (*end) {
        end++;
    }
    return end - s;
}

int my_strcmp(const char *s1, const char *s2)
{
    while (*s1 && *s2 && *s1 == *s2) {
        s1++;
        s2++;
    }
    return *s1 - *s2;
}

使用者写一个 main.c

c 复制代码
#include <stdio.h>
#include "my_string.h"

int main()
{
    const char *s1 = "hello";
    const char *s2 = "hello";

    printf("len=%d\n", my_strlen(s1));
    printf("cmp=%d\n", my_strcmp(s1, s2));
    return 0;
}

这时最直接的编译方式是:

bash 复制代码
gcc main.c my_string.c -o main
./main

这就是最原始的复用:把源码文件直接给别人。

二、第一种复用方式:给源码和头文件

如果别人想使用我们的字符串工具,我们可以把下面两个文件发给他:

text 复制代码
my_string.h
my_string.c

使用者把自己的 main.cmy_string.c 一起编译即可:

bash 复制代码
gcc main.c my_string.c -o main

这种方式简单直观,但问题也很明显。

1. 源码完全暴露

如果这是你写的一个商业库,或者你不希望别人直接看到内部实现,发 .c 文件就不合适。

头文件本来就是给别人看的,因为它描述"怎么调用"。但实现文件不一定要给别人看。

2. 文件越来越多后使用麻烦

真实项目不会只有一个 my_string.c,可能还有:

text 复制代码
my_stdio.c
my_math.c
my_log.c
my_time.c
my_net.c

使用者每次编译时都要把这些 .c 文件全部写进命令里:

bash 复制代码
gcc main.c my_string.c my_stdio.c my_math.c my_log.c my_time.c -o main

这很容易漏掉某个文件,导致链接失败。

3. 不利于模块独立维护

如果库开发者和库使用者是两拨人,使用者其实只关心:

text 复制代码
有哪些函数可以调用?
参数是什么?
返回值是什么?

至于函数内部怎么实现,不应该成为使用者必须关心的问题。

这就引出了第二种方式:只给头文件和目标文件。

三、第二种复用方式:给 .o 和 .h

.c 文件经过编译后,会生成 .o 目标文件:

bash 复制代码
gcc -c my_string.c -o my_string.o

这里的 -c 表示只编译,不链接。

生成的 my_string.o 已经包含了 my_strlenmy_strcmp 的机器码实现。此时我们可以只把下面两个文件给使用者:

text 复制代码
my_string.h
my_string.o

使用者编译自己的代码:

bash 复制代码
gcc -c main.c -o main.o

再把自己的目标文件和我们的目标文件链接起来:

bash 复制代码
gcc main.o my_string.o -o main

这样程序照样能运行。

四、头文件和目标文件分别负责什么

这一步非常关键。很多链接错误,本质上都是没分清头文件和库文件的职责。

1. 头文件负责告诉编译器"函数长什么样"

main.c 中写:

c 复制代码
#include "my_string.h"

编译器能看到:

c 复制代码
int my_strlen(const char *s);

于是编译器知道:

text 复制代码
my_strlen 是一个函数;
它接收 const char *;
它返回 int。

所以 main.c 可以通过编译。

但是,头文件里只有声明,没有函数实现。

2. .o 文件负责提供真正的函数实现

my_string.o 中才有 my_strlen 的机器码。

链接阶段,链接器会发现:

text 复制代码
main.o 里引用了 my_strlen;
my_string.o 里定义了 my_strlen;
两者可以匹配。

于是最终可执行程序就能生成。

可以总结成一句话:

text 复制代码
.h 解决编译阶段"能不能认识这个函数"的问题;
.o/.a/.so 解决链接阶段"能不能找到这个函数实现"的问题。

五、只给 .o + .h 的优点

相比直接发源码,只发 .o + .h 有几个好处。

1. 可以隐藏源码实现

使用者只能看到头文件里的接口,看不到 .c 里的具体实现。

2. 编译速度更快

库开发者提前把库源码编译成 .o。使用者不需要再编译库源码,只需要链接 .o

3. 接口和实现分离

使用者只依赖头文件描述的接口,库开发者可以在不改变接口的情况下优化内部实现。

六、但 .o 文件多了以后仍然麻烦

如果一个库只有一个 .o 文件,直接给 .o + .h 还可以接受。

但真实库通常包含多个模块:

text 复制代码
my_stdio.o
my_string.o
my_math.o
my_log.o
my_time.o

使用者链接时就要写:

bash 复制代码
gcc main.o my_stdio.o my_string.o my_math.o my_log.o my_time.o -o main

这带来三个问题:

  1. 命令太长。
  2. 容易漏掉某个目标文件。
  3. 发布和传输不方便。

于是就需要一个"打包工具",把多个 .o 组织成一个文件。

这就是静态库 .a

七、静态库的出现:把多个 .o 打成一个包

静态库的本质可以非常直白地理解:

text 复制代码
静态库 = 多个 .o 文件的归档包

例如:

bash 复制代码
ar -rc libmyc.a my_stdio.o my_string.o my_math.o

这条命令会把多个目标文件归档到 libmyc.a 中。

之后使用者不需要关心里面有多少 .o,只需要链接这个库:

bash 复制代码
gcc main.c -L. -lmyc -o main

这里的 -lmyc 会让链接器寻找:

text 复制代码
libmyc.a

也就是自动补上:

text 复制代码
lib + myc + .a

这就是为什么库文件命名要遵守 libxxx.a 的规则。

八、为什么 .a 不需要手动解包

.a 是归档文件,但它不是普通压缩包。使用者不需要先把它解开,再一个个链接里面的 .o

链接器认识 .a 格式。它会自动扫描静态库,找到程序需要的目标文件,并把相关代码合并进最终可执行程序。

例如:

text 复制代码
libmyc.a 中有:
my_stdio.o
my_string.o
my_math.o

如果你的 main.c 只调用了 my_strlen,链接器会从库中找到包含 my_strlen 的目标文件,并把需要的部分纳入最终程序。

所以,静态库的价值不只是"少传几个文件",而是把一组目标文件变成一个可被链接器直接管理的整体。

九、接口发布时应该给什么

如果你要把一个库发布给别人,最少需要给两类文件:

text 复制代码
头文件:告诉别人怎么调用
库文件:提供真正实现

典型目录结构可以这样组织:

text 复制代码
myc/
├── include/
│   ├── my_stdio.h
│   └── my_string.h
└── lib/
    └── libmyc.a

使用者编译时写:

bash 复制代码
gcc main.c -I./myc/include -L./myc/lib -lmyc -o main

这里三个参数分别是:

text 复制代码
-I:告诉编译器去哪里找头文件
-L:告诉链接器去哪里找库文件
-l:告诉链接器要链接哪个库

这三个参数会在后续文章反复出现。

十、不要在 include 中写死路径

有些初学者可能会这样写:

c 复制代码
#include "./myc/include/my_string.h"

这样虽然可能暂时能编译,但不推荐。

原因是:

  1. 源码和目录结构强绑定,可移植性差。
  2. 换一个项目路径就可能失效。
  3. 正规做法应该通过 -I 指定头文件搜索路径。

推荐写法是:

c 复制代码
#include "my_string.h"

编译时指定:

bash 复制代码
gcc main.c -I./myc/include -L./myc/lib -lmyc -o main

这样项目结构更清晰,也更符合工程习惯。

十一、从源码到静态库的完整演进

我们把这篇文章的演进路线总结一下:

text 复制代码
阶段 1:给 .c + .h
优点:简单
缺点:暴露源码,文件多

阶段 2:给 .o + .h
优点:隐藏源码,使用者不必编译库源码
缺点:.o 多了以后链接麻烦

阶段 3:给 .a + .h
优点:多个 .o 打成一个库,方便发布和链接
缺点:库更新后程序需要重新链接

这就是静态库出现的真实背景。

十二、总结

.o 是理解库的关键。源文件先被编译成目标文件,目标文件再被链接成可执行程序或库文件。

头文件负责声明接口,目标文件负责提供实现。当目标文件越来越多时,就需要用 ar 把多个 .o 打包成静态库 .a

下一篇,我们正式进入静态库制作实战,手写 libmyc.a,并用 Makefile 完成从编译、归档到打包发布的完整流程。

相关推荐
码农学院1 小时前
GEO与SEO协同:从传统搜索到生成式搜索的平滑迁移路径
服务器·前端·python
步步精BBJconn1 小时前
从GPU服务器到数据中心:AI服务器高压连接器的应用与发展趋势
大数据·运维·服务器·人工智能·科技·物联网
码农学院3 小时前
基于运维监控体系的网络品牌推广方案:从架构设计到技术实现
运维·网络
爱写代码的森10 小时前
鸿蒙三方库 | harmony-utils之ImageUtil图片保存到本地详解
服务器·华为·harmonyos·鸿蒙·huawei
大耳朵-小飞象12 小时前
电力安全运维的智能密码:BACS如何破解设备全生命周期管理难题,让电网安全“看得见、管得住”?
运维·安全·智慧城市·能耗系统·楼宇智控·未来生活
极客侃科技13 小时前
制造企业 MES/APS 选型:SAP PP/DS 集成、ERP-MES 边界划分与一体化架构要点
运维·架构·制造
HLC++13 小时前
Linux的进程间通信
android·linux·服务器
华清远见IT开放实验室14 小时前
实验室建设案例 | 石家庄科技信息职业学院嵌入式实验室——从底层硬件到系统应用,一所应用型高校的嵌入式人才培养这样落地
linux·arm开发·stm32·嵌入式硬件·高校·实验室建设
白露与泡影15 小时前
Arthas 实战指南:从方法耗时定位到 JVM 变量热修改
服务器·jvm·c#