Linux-【动静态库】

库 = 预先写好、可直接复用的二进制代码。我们写的 C/C++ 程序,几乎都离不开系统库(libc、libstdc++),自己封装工具代码做成库,能实现:

  • 代码复用,不用重复造轮子
  • 二进制发布,不暴露源码
  • 模块化开发,大型项目必备

一、什么是库

库是写好的 , 现有的 、 成熟的 、 可以复用的代码的二进制形式 。 每个程序都要依赖很多底层库 , 比如C标准库 libc , C++标准库 libstdc++ 。 库的存在极大地提高了开发效率和代码复用性 。

本质上 , 库是一种可执行代码的二进制形式 , 可以被操作系统载入内存执行 。 根据链接时机不同 , 库分为两种:

  • 静态库 :在Linux中后缀为.a(archive)在Windows中为 .lib。程序在编译链接时,会将库中的代码直接拷贝到最终的可执行文件中。因此,程序运行时不再需要静态库。

  • 动态库 :在Linux中后缀为 .so(shared object),在Windows中为 .dll程序在运行时才去链接动态库的代码,多个程序可以共享同一个动态库。

核心区分动静态库:

  • 静态库 :编译时复制代码到可执行文件 → 运行不需要库
  • 动态库 :编译时只记地址 → 运行必须依赖库

可以看到,系统同时提供了静态库(.a)和动态库(.so)。默认情况下,gcc优先使用动态链接,只有当动态库不存在时才会考虑静态库,或者使用 -static 选项强制静态链接。

情景解释: 如果突然某一天,电脑报错 , 出现dll(window下的动态库)缺失 , 可能是由于软件更新 或者 电脑的杀毒软件更新毒源 , 病毒库 ,把dll文件看成病毒, 干掉了

demo:

为了深入理解,我们来实现一个迷你的标准I/O库,包含文件打开、写入、刷新、关闭等功能,以及一个字符串长度函数。这个示例将贯穿全文。

mystdio.h:

复制代码
#pragma once

#define SIZE 1024

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

struct IO_FILE
{
    int flag;          // 刷新方式
    int fileno;        // 文件描述符
    char outbuffer[SIZE];
    int cap;           // 缓冲区容量
    int size;          // 当前缓冲区中数据大小
    // TODO 可扩展
};

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);

mystdio.c:

复制代码
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include "mystdio.h"

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;
}

void mfflush(mFILE *stream)
{
    if(stream->size > 0)
    {
        write(stream->fileno, stream->outbuffer, stream->size);
        fsync(stream->fileno);   // 强制刷新到磁盘
        stream->size = 0;
    }
}

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

    // 2. 检测是否要刷新(行缓冲且遇到换行符)
    if(stream->flag == FLUSH_LINE && stream->size > 0 && stream->outbuffer[stream->size-1] == '\n')
    {
        mfflush(stream);
    }
    return num;
}

void mfclose(mFILE *stream)
{
    if(stream->size > 0)
    {
        mfflush(stream);
    }
    close(stream->fileno);
    free(stream);
}

mystring.h:

复制代码
#pragma once
int my_strlen(const char *s);

my_string.c:

复制代码
#include "mystring.h"

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

usercode.c

复制代码
#include "mystdio.h"
#include <string.h>
#include <unistd.h>
#include "mystring.h"
#include <stdio.h>

int main()
{
    MYFILE * filep = MyFopen("./log.txt","a");
    if(!filep)
    {
        printf("fopen error!\n");
        return 1;
    }

    int cnt = 10;
    while(cnt--)
    {
        char *msg = (char*)"helllo myfile!!!";
        MyFwrite(filep,msg,strlen(msg));
        MyFflush(filep);
        printf("buffer:%s\n",filep->outbuffer);
        sleep(1);
    }

    MyClose(filep);//FILE *fp

    const char* str = "hello bit\n";
    printf("strlen: %d\n",my_strlen(str));
    return 0;
}

以上代码模拟了标准I/O库的缓冲机制和文件操作。我们将以此为基础制作静态库和动态库。

如果现在,我写了很大的一个工程项目 , 里面有很多个 .c 和 .h文件 , 别人想要购买我这个项目 , 但是我希望隐藏源代码 (.c) 文件 , 仅提供给别人 (.o) 文件 和接口说明(.h) ,供他人使用 。引出我们接下来讲的动静态库!

1. 目标文件(.o文件)
通过 gcc -c将.c源文件预处理、编译、汇编生成,包含机器代码但未链接。
2. 头文件(.h文件)
本质是方法声明文档,包含函数原型、结构体定义等,供调用者参考。
3. 静态库(.a文件)
将多个.o文件打包为单个归档文件(.a),便于分发和管理。

二、静态库

  • 静态库本质上是一个归档文件,它将多个目标文件(.o)打包成一个文件 ,并建立了索引以便链接器快速查找。
  • .a静态库 :**本质是一种归档文件,不需要使用者解包,**而用gcc/g++直接进行链接即可!!!!
  • 一个可执行程序可能用到很多库 , 这些库运行有的是静态库 , 有的是动态库,而我们的编译的时候默认是动态库 , 只有在该库下找不到动态库 (以.so )的时候,才会采用同名的静态库!!!我们也可以使用gcc的**-static** 强转设置链接静态库
  • 编译链接时,把库中代码直接拷贝进可执行文件
    • 优点:运行不依赖库,移植方便。
    • 缺点:可执行文件大,多个程序复用浪费磁盘 / 内存

生成静态库的步骤:

  1. 编译源文件为目标文件gcc -c my_stdio.c mystring.c

  2. 使用 ar 工具归档ar -rc libmyc.a

2.1 静态库生成

Makefile:

复制代码
libmyc.a: mystudio.o mystring.o
	ar -rc $@ $^

mystudio.o: mystudio.c
	gcc -c $<

mystring.o: mystring.c
	gcc -c $<

.PHONY: output
output:
	mkdir -p lib/include
	mkdir -p lib/mylib
	cp -f *.h lib/include
	cp -f *.a lib/mylib
	tar czf lib.tgz lib

.PHONY: clean
clean:
	rm -f *.o libmyc.a lib.tgz
	rm -rf lib

逐行解释Makefile:

  • libmyc.a: mystudio.o mystring.o:目标 libmyc.a依赖于两个 .o 文件。

  • ar -rc $@ $^ar 是归档工具,-r 表示替换或插入文件-c 表示创建归档$@ 是目标文件名(libmyc.a),$^ 是所有依赖文件(mystudio.o mystring.o)。@ 前缀表示不显示命令本身。

  • %.o: %.c:模式规则,任何 .o 文件都由对应的 .c 文件通过 gcc -c 生成。

  • clean 伪目标:删除所有生成的文件。

  • output 伪目标:创建目录结构,将头文件和静态库拷贝到 lib/includelib/mylib,然后打包成 lib.tgz,方便分发。

执行 make 后,会生成 libmyc.a。我们可以用 ar -tv 查看库中包含的目标文件:

2.2 静态库使用

库的名称 :去掉前缀 lib + 后缀 .a (静态库) 或者 .so (动态库), 剩下的就是 库的名称

  • 通过静态库 , 我们开发者在给别人可执行项目的时候 ,就可以不给源码,给相对应的.o 文件以及 .h 文件即可!
  • 测试目标文件生成后 , 静态库删掉,程序照样可以运行 , ,因为库代码已经被整合到可执行文件中。
  • 关于 -static 选项:如果我们希望强制使用静态链接(即使系统中有动态库)
  • 静态链接的可执行文件,体积更大,但不依赖任何外部动态库。

三、动态库

动态库在程序运行时才被加载 ,多个程序可以共享内存中的同一份库代码,节省内存和磁盘空间。

3.1 动态库生成

复制代码
libmyc.so: mystudio.o mystring.o
	gcc -shared -o $@ $^

mystudio.o: mystudio.c
	gcc -fPIC -c $<

mystring.o: mystring.c
	gcc -fPIC -c $<

.PHONY: output
output:
	mkdir -p lib/include
	mkdir -p lib/mylib
	cp -f *.h lib/include
	cp -f *.so lib/mylib
	tar czf lib.tgz lib

.PHONY: clean
clean:
	rm -f *.o libmyc.so lib.tgz
	rm -rf lib
  • libmyc.so :将 mystudio.omystring.o 打包为动态共享库,-shared 选项指定生成动态库。
  • .o 目标 :**编译 .c 文件时添加 -fPIC 选项,生成位置无关代码,**这是生成动态库的必要条件。
  • output 伪目标 :创建目录结构,拷贝头文件和动态库,最后将整个 lib 目录打包为 lib.tgz
  • clean 伪目标:清理所有编译生成的中间文件、动态库、压缩包和目录。

3.2 动态库使用

3.3 库运行搜索路径

3.3.1 问题

可执行程序都摆在这儿了,为什么又打不开 ??? 凭啥???

ldd 可执行程序 : 查看可执行程序关联了那个库!!!

  • 你告诉谁了 ? 你只告诉了gcc !但是系统不知道,不知道去哪里找动态库(运行时错误) ;
  • 编译时 :你用 -L ./lib/mylib/告诉了 gcc 链接器库文件在哪里,所以能成功生成可执行文件。
  • 运行时系统动态链接器(ld-linux.so)不知道这个路径,它只会去系统默认库路径(如 /lib/usr/lib)和 LD_LIBRARY_PATH 里找,找不到 libmyc.so 就会报错。

简单说:-L 只管编译链接,不管运行时加载。

  • 静态库没有这个问题 , 为什么? 静态库链接的时候,是把程序拷贝到可执行程序里,一旦可执行程序创建成功,就不再依赖静态库了;只要编译成功,就一定能运行

3.3.2 解决方案

方法1:将库文件拷贝到系统库目录(需要root权限)

方法2:在系统库目录下建立软链接

取消软连接

方法3:设置环境变量 LD_LIBRARY_PATH

这种方法只对当前终端有效,适合临时测试。

复制代码
$ export LD_LIBRARY_PATH=.   # 将当前目录加入搜索路径
$ ./a.out

export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/home/zs/linux/lesson23/zs/lib/mylib/

  • export 命令设置的环境变量,只在当前终端会话(进程)中生效 ,属于临时生效
  • 一旦关闭终端,这个环境变量就会被系统回收,下次打开新终端时,LD_LIBRARY_PATH 会恢复为空或默认值,程序就又找不到动态库了。
  • LD_LIBRARY_PATH内存级的临时环境变量,仅当前终端进程可见,关闭终端后配置就会丢失。
  • 想要有效,就需要写到配置文件里

方法4:修改 /etc/ld.so.conf 配置文件

/etc/ld.so.conf.d/ 下新建一个配置文件,如 my.conf,内容为库所在目录的绝对路径,然后执行 sudo ldconfig

复制代码
$ sudo sh -c "echo /absolute/path/to/lib > /etc/ld.so.conf.d/my.conf"
$ sudo ldconfig

3.4 动态库的优点

  • 节省磁盘空间:多个程序共享同一份库文件,而不是每个程序都拷贝一份。

  • 节省内存:操作系统通过虚拟内存机制,物理内存中只保留一份动态库,被所有使用该库的进程共享。

  • 易于更新:只需替换库文件,无需重新链接所有程序(前提是接口保持不变)。

3.5 相关知识补充

【库里面能不能不动态库和静态库都同时存在 】

可以,在linux系统下,默认情况安装的大部分库,都是动态库

  • 如果非要使用静态链接使用 --static 选项
  • 如果只存在静态库 ,可执行程序就只能使用静态链接了!!!
  • 库 : 应用程序 = 1 : n
  • vs 不仅仅可以形成可执行程序 , 也能形成动静态库

四、使用外部库

实际开发中我们经常使用第三方库,比如图形库 ncurses。下面以一个简单的进度条程序为例,展示如何使用外部库。

4.1 安装ncurses开发包

在CentOS上

复制代码
$ sudo yum install -y ncurses-devel

在Ubuntu上:

复制代码
$ sudo apt install -y libncurses-dev

4.2 示例程序:进度条

以下代码使用ncurses库在终端显示一个动态进度条

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

#define PROGRESS_BAR_WIDTH 30
#define BORDER_PADDING 2
#define WINDOW_WIDTH (PROGRESS_BAR_WIDTH + 2 * BORDER_PADDING + 2) // 加边框的宽度
#define WINDOW_HEIGHT 5
#define PROGRESS_INCREMENT 3
#define DELAY 300000 // 微秒 (300毫秒)

int main() {
    initscr();                      // 初始化ncurses模式
    start_color();                  // 启用颜色
    init_pair(1, COLOR_GREEN, COLOR_BLACK); // 已完成部分:绿色前景,黑色背景
    init_pair(2, COLOR_RED, COLOR_BLACK);   // 剩余部分:红色前景(仅为演示)
    cbreak();                        // 禁用行缓冲
    noecho();                        // 不显示输入字符
    curs_set(FALSE);                 // 隐藏光标

    int max_y, max_x;
    getmaxyx(stdscr, max_y, max_x);  // 获取终端大小
    int start_y = (max_y - WINDOW_HEIGHT) / 2;
    int start_x = (max_x - WINDOW_WIDTH) / 2;

    WINDOW *win = newwin(WINDOW_HEIGHT, WINDOW_WIDTH, start_y, start_x);
    box(win, 0, 0);                  // 加边框
    wrefresh(win);

    int progress = 0;
    int max_progress = PROGRESS_BAR_WIDTH;

    while (progress <= max_progress) {
        werase(win);                  // 清除窗口内容

        // 计算已完成的进度和剩余的进度
        int completed = progress;
        int remaining = max_progress - progress;

        // 进度条位置
        int bar_y = 1;
        int bar_x = BORDER_PADDING + 1;

        // 显示已完成部分(绿色)
        attron(COLOR_PAIR(1));
        for (int i = 0; i < completed; i++) {
            mvwprintw(win, bar_y, bar_x + i, "#");
        }
        attroff(COLOR_PAIR(1));

        // 显示剩余部分(红色背景,但只打印空格模拟)
        attron(COLOR_PAIR(2));
        for (int i = completed; i < max_progress; i++) {
            mvwprintw(win, bar_y, bar_x + i, " ");
        }
        attroff(COLOR_PAIR(2));

        // 显示百分比
        char percent_str[10];
        snprintf(percent_str, sizeof(percent_str), "%d%%", (progress * 100) / max_progress);
        int percent_x = (WINDOW_WIDTH - strlen(percent_str)) / 2;
        mvwprintw(win, WINDOW_HEIGHT - 2, percent_x, percent_str);

        wrefresh(win);                // 刷新窗口

        progress += PROGRESS_INCREMENT;
        usleep(DELAY);                // 延迟
    }

    // 清理并退出ncurses模式
    delwin(win);
    endwin();
    return 0;
}

编译时需要链接ncurses库:

复制代码
$ gcc progress.c -lncurses -o progress
$ ./progress

使用外部库的一般步骤:

  1. 安装开发包(通常包含头文件和库文件)。

  2. 在代码中包含相应头文件。

  3. 编译时用 -l 指定库名(如 -lncurses),必要时用 -L 指定库路径。

五、从源文件到目标文件

当我们执行 gcc -c hello.c 时,会生成一个目标文件 hello.o目标文件是二进制格式,但还不是可执行文件,它包含了编译后的机器指令和数据,但其中的外部符号地址尚未确定。

查看目标文件的类型:

复制代码
$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

这表明 hello.o 是一个ELF格式的可重定位文件(relocatable file)。

把 .c 编译形成 .o 文件 , 与其他文件没有任何关系 , 只有在链接的时候会和其他文件形成关系;如果存在一个 .c 文件出错 , 修改之后重新生成 .o 文件 , 只会修改同名的 .o 文件,不影响其他的 .o 文件 !!!!

-> 以较小的成本,把代码进行重新编译!!!!

相关推荐
深圳市恒讯科技2 小时前
云服务器怎么选?从CPU、内存到IOPS的零基础选型手册
运维·服务器
艾莉丝努力练剑3 小时前
【脉脉】AI创作者崛起:掌握核心工具,在AMA互动中共同成长
运维·服务器·c++·人工智能·安全·企业·脉脉
九皇叔叔4 小时前
CentOS 7.5/RHEL 7.x 配置 YUM 源(阿里云镜像+本地源双方案)
linux·阿里云·centos
chinesegf5 小时前
DNS 验证验证SSL证书
linux·服务器·网络
未佩妥剑,已入江湖5 小时前
docker Windows下安装
运维·windows·docker·容器
试试勇气6 小时前
Linux学习笔记(十七)--线程概念
linux·笔记·学习
LXY_BUAA6 小时前
《嵌入式操作系统》_高级字符设备驱动_20260316
linux·运维·服务器·驱动开发
顶妙WMS海外仓管理系统6 小时前
Shopify卖家破910万,海外仓如何对接Shopify独立站?
运维·产品运营
优美的赫蒂7 小时前
香橙派5plus单独编译内核安装时的报错记录
linux·rk3588·orangepi