• 动态库:Linux 下后缀是.so,Windows 下是.dll。程序编译时只记录函数入口地址,运行时才去加载库文件并调用函数。多个程序可以共享同一个动态库,实现 “一份代码,多处使用”。

我们可以用ls命令直观查看系统中的动静态库。比如 Linux 系统下的 C 标准库:

代码语言:javascript

AI代码解释

# 查看C标准动态库
ls -l /lib/x86_64-linux-gnu/libc-2.31.so
# 输出:-rwxr-xr-x 1 root root 2029592 May 1 02:20 /lib/x86_64-linux-gnu/libc-2.31.so

# 查看C标准静态库
ls -l /lib/x86_64-linux-gnu/libc.a
# 输出:-rw-r--r-- 1 root root 5747594 May 1 02:20 /lib/x86_64-linux-gnu/libc.a

C++ 标准库同理,动态库通常是软链接形式,指向具体版本:

代码语言:javascript

AI代码解释

ls -l /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so
# 输出:lrwxrwxrwx 1 root root 40 Oct 24 2022 /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so -> ../../../x86_64-linux-gnu/libstdc++.so.6

接下来,我们用一个自定义的mystdio库(模拟标准 IO 功能)作为案例,一步步演示动静态库的制作和使用。

二、静态库 ——“打包带走” 的代码

静态库的核心特点是编译时嵌入,运行时独立。就像你点外卖时把餐具打包带走,吃饭的时候不需要再回餐馆拿 —— 程序一旦编译完成,静态库就可以删除,完全不影响运行。

2.1 准备源码 —— 造一个 “迷你标准库”

我们先写两个基础文件:my_stdio.h(头文件,声明函数)和my_stdio.c(源文件,实现函数),再加上一个字符串处理文件my_string.c

2.1.1 头文件:my_stdio.h

这个头文件定义了模拟的文件结构体和 IO 函数,类似标准库的FILEfopenfwrite等函数。

代码语言:javascript

AI代码解释

#pragma once

#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2

// 模拟标准库的FILE结构体
struct IO_FILE
{
    int flag;      // 刷新模式
    int fileno;    // 文件描述符
    char outbuffer[SIZE]; // 输出缓冲区
    int cap;       // 缓冲区容量
    int size;      // 缓冲区已使用大小
};

typedef struct IO_FILE 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.1.2 源文件:my_stdio.c

实现文件的打开、写入、刷新和关闭功能,底层调用 Linux 系统调用openwrite等。

代码语言:javascript

AI代码解释

#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>

// 打开文件,模拟fopen
mFILE *mfopen(const char *filename, const char *mode)
{
    int fd = -1;
    if (strcmp(mode, "r") == 0)
    {
        fd = open(filename, O_RDONLY);
    }
    else if (strcmp(mode, "w") == 0)
    {
        fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0666);
    }
    else if (strcmp(mode, "a") == 0)
    {
        fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
    }

    if (fd < 0) return NULL;

    mFILE *mf = (mFILE *)malloc(sizeof(mFILE));
    if (!mf)
    {
        close(fd);
        return NULL;
    }

    mf->fileno = fd;
    mf->flag = FLUSH_LINE;
    mf->size = 0;
    mf->cap = SIZE;

    return mf;
}

// 刷新缓冲区,模拟fflush
void mfflush(mFILE *stream)
{
    if (stream->size > 0)
    {
        write(stream->fileno, stream->outbuffer, stream->size);
        fsync(stream->fileno); // 强制写入磁盘
        stream->size = 0;
    }
}

// 写入数据,模拟fwrite
int mfwrite(const void *ptr, int num, mFILE *stream)
{
    // 数据拷贝到缓冲区
    memcpy(stream->outbuffer + stream->size, ptr, num);
    stream->size += num;

    // 行缓冲:遇到换行符刷新
    if (stream->flag == FLUSH_LINE && stream->size > 0 && stream->outbuffer[stream->size - 1] == '\n')
    {
        mfflush(stream);
    }

    return num;
}

// 关闭文件,模拟fclose
void mfclose(mFILE *stream)
{
    if (stream->size > 0)
    {
        mfflush(stream);
    }
    close(stream->fileno);
    free(stream);
}
2.1.3 字符串处理:my_string.c

实现一个简单的my_strlen函数,模拟标准库的strlen

代码语言:javascript

AI代码解释

#include "my_string.h"

// 计算字符串长度
int my_strlen(const char *s)
{
    const char *end = s;
    while (*end != '\0') end++;
    return end - s;
}

对应的头文件my_string.h

代码语言:javascript

AI代码解释

#pragma once

int my_strlen(const char *s);
2.2 制作静态库 —— 把代码 “打包” 成.a 文件

制作静态库的步骤很简单:编译生成.o 目标文件 → 用 ar 工具打包成.a 文件

我们可以写一个Makefile来自动化这个过程,避免手动敲命令。

2.2.1 编写 Makefile

代码语言:javascript

AI代码解释

# 目标:静态库libmystdio.a
libmystdio.a: my_stdio.o my_string.o
	@ar -rc $@ $^
	@echo "build $^ to $@ ... done"

# 编译生成.o文件(-c表示只编译不链接)
%.o: %.c
	@gcc -c $<
	@echo "compling $< to $@ ... done"

# 清理目标文件和库文件
.PHONY: clean
clean:
	@rm -rf *.a *.o stdc*
	@echo "clean ... done"

# 打包头文件和库文件,方便分发
.PHONY: output
output:
	@mkdir -p stdc/include
	@mkdir -p stdc/lib
	@cp -f *.h stdc/include
	@cp -f *.a stdc/lib
	@tar -czf stdc.tgz stdc
	@echo "output stdc ... done"
2.2.2 关键命令解释

  • gcc -c *.c:把.c文件编译成.o目标文件,不进行链接
  • ar -rc libmystdio.a my_stdio.o my_string.oar是 GNU 归档工具,rc表示 “创建并替换”。libmystdio.a是静态库名,必须以lib开头,后缀是.a
  • ar -tv libmystdio.a:查看静态库中的文件列表,t是列出内容,v是显示详细信息。

执行make命令,就能生成libmystdio.a静态库:

代码语言:javascript

AI代码解释

make
# 输出:
# compling my_stdio.c to my_stdio.o ... done
# compling my_string.c to my_string.o ... done
# build my_stdio.o my_string.o to libmystdio.a ... done
2.3 使用静态库 —— 链接成可执行程序

静态库制作完成后,我们写一个main.c来测试它的功能。

2.3.1 测试程序:main.c

代码语言:javascript

AI代码解释

#include "my_stdio.h"
#include "my_string.h"
#include <stdio.h>

int main()
{
    const char *s = "hello static library!\n";
    // 测试my_strlen
    printf("%s: length = %d\n", s, my_strlen(s));

    // 测试文件写入
    mFILE *fp = mfopen("./log.txt", "a");
    if (fp == NULL) return 1;

    mfwrite(s, my_strlen(s), fp);
    mfwrite(s, my_strlen(s), fp);
    mfwrite(s, my_strlen(s), fp);

    mfclose(fp);
    return 0;
}
2.3.2 编译链接 —— 三种场景

链接静态库的核心是告诉编译器头文件在哪库文件在哪库名是什么。对应的 gcc 参数是:

  • -I:指定头文件搜索路径
  • -L:指定库文件搜索路径
  • -l:指定库名(去掉lib前缀和.a后缀)
场景 1:头文件和库文件在系统路径

如果把my_stdio.hmy_string.h拷贝到/usr/include,把libmystdio.a拷贝到/usr/lib,可以直接编译:

代码语言:javascript

AI代码解释

gcc main.c -lmystdio -o main

-lmystdio就是链接libmystdio.a,编译器会自动去系统路径找。

场景 2:头文件和库文件在当前目录

头文件、库文件和main.c在同一文件夹下:

代码语言:javascript

AI代码解释

gcc main.c -L. -lmystdio -o main

-L.表示库文件在当前目录(.代表当前路径)。

场景 3:头文件和库文件在自定义路径

如果头文件在./include,库文件在./lib

代码语言:javascript

AI代码解释

gcc main.c -I./include -L./lib -lmystdio -o main
2.3.3 验证静态库的独立性

编译生成可执行文件main后,我们删除静态库,再运行程序:

代码语言:javascript

AI代码解释

rm -f libmystdio.a
./main
# 输出:
# hello static library!
# : length = 22
cat log.txt
# 输出3行hello static library!

程序正常运行!这就是静态库的优势 ——编译后脱离库文件,可独立运行

2.4 静态库的优缺点 —— 优点缺点同样明显

优点

  1. 运行独立:可执行文件包含了库的代码,不需要依赖外部文件。
  2. 运行速度快:没有运行时的动态链接开销,执行效率高。
  3. 部署方便:发给别人时只需要一个可执行文件,不需要附带库。

缺点

  1. 可执行文件体积大:每个程序都会复制一份库代码,多个程序会造成冗余。比如 10 个程序都用libmystdio.a,就会有 10 份相同的代码。
  2. 更新麻烦:如果库有 bug 需要修复,所有使用该库的程序都要重新编译链接。

三、动态库 ——“共享使用” 的代码

动态库的核心特点是编译时记录地址,运行时加载共享。就像你去餐馆吃饭,餐具是餐馆提供的,多个顾客共享同一套餐具 —— 多个程序可以共享同一个动态库,大大节省内存和磁盘空间。

3.1 制作动态库 —— 生成.so 文件

动态库的制作和静态库类似,但需要两个关键参数:-fPIC-shared

  • -fPIC:生成位置无关代码(Position Independent Code)。动态库加载到内存的地址是不固定的,PIC 保证代码在任意地址都能正常运行。
  • -shared:生成共享库(动态库)。
3.1.1 编写 Makefile

代码语言:javascript

AI代码解释

# 目标:动态库libmystdio.so
libmystdio.so: my_stdio.o my_string.o
	gcc -o $@ $^ -shared
	@echo "build $^ to $@ ... done"

# 编译生成位置无关的.o文件
%.o: %.c
	gcc -fPIC -c $<
	@echo "compling $< to $@ ... done"

# 清理
.PHONY: clean
clean:
	@rm -rf *.so *.o stdc*
	@echo "clean ... done"

# 打包分发
.PHONY: output
output:
	@mkdir -p stdc/include
	@mkdir -p stdc/lib
	@cp -f *.h stdc/include
	@cp -f *.so stdc/lib
	@tar -czf stdc.tgz stdc
	@echo "output stdc ... done"

执行make命令,生成libmystdio.so动态库:

代码语言:javascript

AI代码解释

make
# 输出:
# compling my_stdio.c to my_stdio.o ... done
# compling my_string.c to my_string.o ... done
# build my_stdio.o my_string.o to libmystdio.so ... done
3.2 使用动态库 —— 编译链接 + 运行时加载

动态库的编译链接命令和静态库完全一样,但运行时需要让系统找到动态库文件。

3.2.1 编译测试程序

还是用之前的main.c,编译命令:

代码语言:javascript

AI代码解释

gcc main.c -I. -L. -lmystdio -o main

编译成功后,直接运行./main会报错:

代码语言:javascript

AI代码解释

./main
# 输出:
# ./main: error while loading shared libraries: libmystdio.so: cannot open shared object file: No such file or directory

原因是系统找不到动态库文件。Linux 下动态库的搜索路径是有优先级的,我们需要把libmystdio.so加入搜索路径。

3.2.2 解决动态库找不到的问题 —— 四种方案
方案 1:拷贝动态库到系统路径

libmystdio.so拷贝到/usr/lib/lib64(系统默认搜索路径):

代码语言:javascript

AI代码解释

sudo cp libmystdio.so /usr/lib
./main
# 正常运行
方案 2:设置环境变量 LD_LIBRARY_PATH

临时添加库路径,关闭终端后失效:

代码语言:javascript

AI代码解释

export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH
./main
# 正常运行

如果想永久生效,可以把这行命令写入~/.bashrc~/.zshrc,然后source ~/.bashrc

方案 3:添加软链接到系统路径

和方案 1 类似,但用软链接,方便更新库:

代码语言:javascript

AI代码解释

sudo ln -s $(pwd)/libmystdio.so /usr/lib/libmystdio.so
方案 4:配置 /etc/ld.so.conf.d

创建自定义配置文件,添加库路径:

代码语言:javascript

AI代码解释

sudo echo $(pwd) > /etc/ld.so.conf.d/mystdio.conf
sudo ldconfig # 更新缓存

这种方式适合长期使用的自定义库。

3.2.3 查看程序依赖的动态库

ldd命令可以查看可执行程序依赖的所有动态库:

代码语言:javascript

AI代码解释

ldd main
# 输出:
# linux-vdso.so.1 =>  (0x00007fffacbbf000)
# libmystdio.so => ./libmystdio.so (0x00007f8917335000)
# libc.so.6 => /lib64/libc.so.6 (0x00007f8916f67000)
# /lib64/ld-linux-x86-64.so.2 (0x00007f8917905000)

可以看到main依赖libmystdio.so和系统的libc.so.6

3.3 动态库的优缺点 —— 空间换时间的典范

优点

  1. 节省空间:多个程序共享一个动态库,磁盘和内存占用大大减少。比如 10 个程序用同一个动态库,内存中只需要一份库代码。
  2. 更新方便:修复库的 bug 后,不需要重新编译程序,直接替换动态库文件即可。
  3. 版本灵活:可以同时存在多个版本的动态库,满足不同程序的需求。

缺点

  1. 运行依赖库:程序运行时必须找到对应的动态库,否则无法启动。
  2. 运行时有开销:动态链接需要在运行时解析函数地址,比静态库多了一点性能损耗(现代系统中这个损耗可以忽略不计)。
  3. 部署麻烦:发给别人时需要附带动态库文件,或者配置库路径。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐