Linux C 语言编译链接全解析:静态库与动态库从原理到实战

上篇文章:

面试官灵魂拷问:Linux软链接与硬链接到底有什么区别?(附底层Inode级深度图解)

目录

1.回顾

目标文件

2.库的概念

3.静态库

3.1制作一个静态库

3.2静态库的使用

思考:

3.3从制作到使用全流程

总结:

4.动态库

4.1动态库的生成

4.2动态库使用

思考

4.3库运行搜索路径

第一种

第二种

第三种

第四种

5.总结与思考

5.1为什么C/C++要头源分离?

5.2如果同时存在动静态库呢?

第一种情况

第二种情况

第三种情况

6.使用外部库


1.回顾

目标文件

编译和链接这两个步骤,在Windows下被我们的IDE封装的很完美,我们一般都是一键构建非常方便,但一旦遇到错误的时候呢,尤其是链接相关的错误,很多人就束手无策了。在Linux下,我们之前也学习过如何通过gcc编译器来完成这一系列操作。

接下来我们深⼊探讨⼀下编译和链接的整个过程,来更好的理解动静态库的使用原理。
先来回顾下什么是编译呢?编译的过程其实就是将我们程序的源代码翻译成CPU能够直接运⾏的机器代码。
⽐如:在⼀个源⽂件 hello.c ⾥便简单输出"hello world!",并且调⽤⼀个run函数,⽽这个函数被定义在另⼀个原⽂件 code.c 中。这⾥我们就可以调⽤ gcc -c 来分别编译这两个原⽂件。

复制代码
// hello.c
#include<stdio.h>
void run();
int main() {
    printf("hello world!\n");
    run();
    return 0;
}

// code.c
#include<stdio.h>
void run() {
    printf("running...\n");
}

// 编译两个源⽂件
$ gcc -c hello.c
$ gcc -c code.c
$ ls
code.c code.o hello.c hello.o

可以看到,在编译之后会⽣成两个扩展名为 .o 的⽂件,它们被称作⽬标⽂件。要注意的是如果我们修改了⼀个原⽂件,那么只需要单独编译它这⼀个,⽽不需要浪费时间重新编译整个⼯程。目标⽂件是⼀个⼆进制的⽂件,⽂件的格式是 ELF ,是对⼆进制代码的⼀种封装。

复制代码
$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
## file命令⽤于辨识⽂件类型。

2.库的概念

库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个⼈的代码都从零开始,因此库的存在意义非同寻常。
本质上来说库是⼀种可执⾏代码的二进制形式,可以被操作系统载⼊内存执行(库默认存储在磁盘中 )
库有两种:

  • 静态库:.a [Linux]、.lib[windows]
  • 动态库:.so [Linux]、.dll[windows]

库文件的命名:libxxx.so/.a

本章内容我们使用之前文章中封装好的libc代码,在任意新增"库文件"。


相关文章:

用系统调用从零封装一个C语言标准I/O库 | 附源码


3.静态库

  • 静态库(.a):程序在编译链接的时候,把库的代码链接到可执行文件中,程序运行的时候将不再需要静态库。
  • 一个可执行程序可能用到许多的库,这些库运行时,有的是静态库,有的是动态库,而我们的编译器默认为动态库,只有在该库下找不到动态.so的时候才会采⽤同名静态库。我们也可以使用gcc的-static强转设置链接静态库。

3.1制作一个静态库

我们先创建一个Makefile:

ar是archiver(归档器),rc表示(replace and create)

复制代码
$ ar -tv libmystdio.a
rw-rw-r-- 1000/1000 2848 Oct 29 14:35 2024 my_stdio.o
rw-rw-r-- 1000/1000 1272 Oct 29 14:35 2024 my_string.o
  • t:列出静态库中的文件
  • v:verbose详细信息

解释上述代码:

  • target=libmyc.a定义最终生成的目标文件,即静态库名称。Linux 静态库的命名规范为libxxx.a,这里库名为myc
  • src=$(wildcard *.c)调用 Make 内置函数wildcard,自动匹配当前目录下所有.c 后缀的 C 源文件 ,将文件名列表赋值给src变量。例如当前目录有a.cb.csrc的值就是a.c b.c
  • obj=$(src:.c=.o)Make 的变量后缀替换语法 :把src里所有.c后缀的文件名,替换为.o后缀,赋值给obj变量。.o是 C 语言编译后的目标文件,是生成静态库的中间文件。
  • cc=gcc -c定义编译命令:gcc是 GNU C 编译器,-c参数表示只编译、不链接,作用是把.c 源文件生成对应的.o 目标文件。
  • ar=ar -rc定义静态库打包命令:ar是 Linux 下创建静态库的专用工具,-rc是核心参数:r表示替换库中已有的目标文件,c表示库不存在时自动创建。

output 目标(第 12-18 行)

作用是把静态库、头文件整理归档,完成发布打包。

  • 命令前的@:表示静默执行,执行时不会把命令本身打印到终端,不加 @则会先打印命令、再执行。
  • mkdir -p:递归创建目录,目录已存在时不会报错,这里创建存放库文件的myc/lib和存放头文件的myc/include
  • cp *.h myc/include:把当前目录所有头文件复制到归档目录,静态库给第三方使用时,必须配套头文件声明函数接口。
  • tar czf myc.tgz myc:把归档目录myc打包压缩成myc.tgz,方便发布传输。c创建包、z用 gzip 压缩、f指定包名。

3.2静态库的使用

在用户目录下新建main.c文件,引入库头文件:

cpp 复制代码
E>  1 #include "my_stdio.h"
E>  2 #include "my_string.h"
    3 #include <stdio.h>
    4 
    5 int main()
    6 {
    7 
E>  8     My_FILE *fp = Myfopen("./log.txt", "w");
    9     Myfclose(fp);
   10 
   11     const char *msg = "hello world";
E> 12     printf("msg len: %d\n", my_strlen(msg));
   13                                                                                                            
   14     return 0;
   15 }

将所有.o文件,归档在静态库中,我们对于文件的命名,不论是静态库还是动态库,都是以lib开头,.a表示静态,.os表示动态。

此时将main.c变为.o文件,发现系统找不到对应的头文件,我们通过 -l 来确定头文件,在Linux中,包含头文件后,只需要再添加上文件名称即可,如此时的名称:myc

但,此时依旧提示系统找不到路径,我们接着通过-L.表示当前路径:

思考:

但是,再以前C语言的库方法在使用时,编译的时候从来没加过这些选项,这次为什么要加呢?

因为gcc是专门用来编译C语言的,它会到/lib64 或者 /usr/lib64中搜索库文件,凡是第三方库在使用时,都要明确的告诉gcc库的名字和路径。

而我们将我们自己的库拷贝到/lib64路径下时,就只需要添加库的名称就可以了,这种行为也被称为:库的安装

3.3从制作到使用全流程

之后,给其他用户使用时,解压缩即可:(注意,我这里没有删除之前的文件,模拟其他用户,应该没有其余.c或.h或.a文件)

当我们将mian.c转换为.o文件时,却依旧找不到头文件,我们需要指明路径:

.o文件转换为可执行程序是同样的道理:

以上两个操作要想一步到位,就需要这样做:(把库当作项目的一部分)

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

把库安装到系统中,其本质是,将include中的头文件安装到/usr/include中,把libmyc.a安装到lib64中。

**注意:**库中不能出现main函数,不然库编译到.o,就不会再链接了。

总结:

-L:指定库路径

-I(大写i) :指定头文件搜索路径

-l(小写l):指定库名

测试目标文件生成后,静态库删掉,程序照样可以运行。

库文件名称和引入库的名称:去掉前缀lib,去掉后缀.so,.a,如:libc.so -> c

4.动态库

  • 动态库(.so):程序在运行时才去链接动态库的代码,多个程序共享使用库的代码。
  • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
  • 在可执行文件开始运行之前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
  • 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制,允许物理内存中的一份动静态库要用到该库的所有进程共用,节省了内存和磁盘空间。

4.1动态库的生成

Makefile:

cpp 复制代码
                                                                               ?? buffers 
  1 target=libmyc.so
  2 src=$(wildcard *.c)
  3 obj=$(src:.c=.o)
  4 cc=gcc
  5 
  6 $(target):$(obj)
  7     $(cc) -shared -o $@ $^
  8 %.o:%.c
  9     $(cc) -fPIC -c $<
 10 
 11 .PHONY:output
 12 output:
 13     @mkdir -p myc/lib
 14     @mkdir -p myc/include
 15     @cp *.h myc/include
 16     @cp *.so myc/lib                                                                                         
 17     @tar czf myc.tgz myc
 18 
 19 .PHONY:clean
 20 clean:
 21     rm -rf *.o $(target) myc myc.tgz
 22 
 23 debug:
 24     @echo $(target)
 25     @echo $(src)
 26     @echo $(obj)

上述代码解释:

shared:表示生成共享库格式

fPIC:产生位置无关码(position independent code)

库名规则:libxxxx.so

4.2动态库使用

前提操作:

此时,切换到用户目录,模拟使用:

上述操作暂时与静态库相同,将myc生成可执行程序也可以用一下命令,一步到位:

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

但是,此时运行myexe,却发现运行报错,原因是找不到动态库文件!

根据错误再回看我们的命令,我们是将路径和库名告诉给了编译器,但是运行可执行程序和操作系统有关。

通过ldd命令(查看一个可执行程序的动态库)

思考

那么为什么静态库为什么不存在这个问题?

因为动态链接的可执行程序,除了要运行你自己的程序,也要加载动态库!!!

4.3库运行搜索路径

操作系统查找动态库的路径在/usr/lib64或/lib64下,那么我们有四种方法让操作系统找到动态库:

第一种

将自己的动态库拷贝到/lib64文件下。使用命令:

cpp 复制代码
sudo cp myc/lib/* /lib64 -rf

第二种

使用绝对路径建立软链接:

第三种

导出环境变量LD_LIBRARY_PATH=$LD_LIBRARY_PATH:XXXX(XXXX指库的路径,不需要库名)

在我们Linux操作系统下,有一个环境变量:LD_LIBRARY_PATH,当然,同样的操作系统下,它有可能不存在,只需要自己建立即可。

但是,导入的环境变量只是临时的,当我们重启Linux后,他会初始化,那么为了让它变得永久,我们将其配置到对应的配置文件中。

配置文件,如家目录中的:

第四种

系统级配置

将库路径加入到系统动态链接器配置,永久生效,全局可用,比修改环境变量更规范、更稳定。

步骤:

新建系统配置文件:

复制代码
sudo vim /etc/ld.so.conf.d/myc.conf

在文件中写入你的库的绝对路径:

复制代码
/home/xxx404/linux-learning/test4_21/user/myc/lib

保存退出后,更新动态链接器缓存:

复制代码
sudo ldconfig

编译 & 运行:编译时只需要-I-L-l三个参数,无需额外配置,全局任何用户都能正常使用。

5.总结与思考

5.1为什么C/C++要头源分离?

C/C++ 采用头文件(.h/.hpp)+ 源文件(.c/.cpp)分离 的设计,核心是为了解决编译效率、代码复用、接口封装三大问题,是软件工程中 "模块化设计" 的经典实践。

核心价值:

  • :分离编译,改一点只编一点,大项目构建速度快。
  • :接口复用,一份头文件,多个源文件可以用。
  • :避免重复定义,符合 ODR 规则。
  • 好维护:接口与实现分离,改实现不影响使用者,代码结构清晰。

5.2如果同时存在动静态库呢?

main.c:

第一种情况

运行,发现系统优先使用动态库,进行动态链接:

第二种情况

要想使用静态链接,加static,(前提:必须提供静态库,不然报错):

第三种情况

要是只有静态库,没有动态库,不加static,连接形成的依旧是动态链接。

因为程序不仅仅依赖你的库,也依赖C/C++和其他的库,而其他库都是动态链接,属于局部静态,整体动态的方式,所以将提供的静态库中它能用到的作为静态,其他的都是动态。

6.使用外部库

到目前为止,我们并没有接触过太多库,只碰到过C/C++的标准库,这里我们使用一个好玩的图形库:ncurses

cpp 复制代码
// 安装
// Centos
$ sudo yum install -y ncurses-devel
// ubuntu
$ sudo apt install -y libncurses-dev

下载完成后,结合本篇文章,我们对于这些文件已经有了更加深入的认识:

它们本质上就是一堆头文件和动态库。

系统中其实有很多的库,它们通常由一组互相关联的用来完成某项常见工作的函数构成。比如:用来处理屏幕显示情况的函数(ncurses库)

测试代码(C++):

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

int main() {
    // 1. 初始化 ncurses
    initscr();          // 启动 ncurses 模式
    cbreak();           // 禁用行缓冲,输入立即生效
    noecho();           // 不回显用户输入
    keypad(stdscr, TRUE); // 启用功能键(如方向键、F1-F12)

    // 2. 检查终端是否支持颜色
    if (has_colors() == FALSE) {
        endwin();
        printf("你的终端不支持颜色!\n");
        return 1;
    }
    start_color(); // 启动颜色功能

    // 3. 定义颜色对(前景色,背景色)
    init_pair(1, COLOR_CYAN, COLOR_BLACK);  // 青色文本,黑色背景
    init_pair(2, COLOR_YELLOW, COLOR_BLUE); // 黄色文本,蓝色背景

    // 4. 主循环:处理用户输入
    int ch;
    char *msg = "按 'q' 退出,按方向键移动光标";
    int x = 0, y = 0; // 光标坐标

    while (1) {
        clear(); // 清屏

        // 显示标题(使用颜色对 1)
        attron(COLOR_PAIR(1));
        mvprintw(0, 0, "=== Ncurses 简单示例 ===");
        attroff(COLOR_PAIR(1));

        // 显示提示信息(使用颜色对 2)
        attron(COLOR_PAIR(2));
        mvprintw(2, 0, msg);
        attroff(COLOR_PAIR(2));

        // 显示当前光标位置
        mvprintw(4, 0, "当前光标位置:(x=%d, y=%d)", x, y);

        // 移动光标并刷新
        move(y, x);
        refresh();

        // 获取用户输入
        ch = getch();
        switch (ch) {
            case KEY_LEFT:  x--; break;
            case KEY_RIGHT: x++; break;
            case KEY_UP:    y--; break;
            case KEY_DOWN:  y++; break;
            case 'q':       goto end_loop; // 退出循环
        }

        // 防止光标超出屏幕边界
        if (x < 0) x = 0;
        if (y < 0) y = 0;
        if (x >= COLS) x = COLS - 1;
        if (y >= LINES) y = LINES - 1;
    }

end_loop:
    // 5. 清理并退出 ncurses
    endwin();
    return 0;
}

编译代码:

本章完。

相关推荐
她叫我大水龙2 小时前
Docker 安装和常用命令
运维·docker·容器
**蓝桉**2 小时前
Nginx 负载均衡策略详解
运维·nginx·负载均衡
GuiltyFet2 小时前
opencode+skill自动化渗透测试系列
运维·自动化
wanhengidc2 小时前
云手机 云端运行托管
运维·服务器·网络·安全·web安全·智能手机
Lazionr2 小时前
【链表经典OJ-中】
c语言·数据结构·链表
橙子也要努力变强2 小时前
信号捕捉底层机制-进程与OS
linux·服务器·c++
青瓦梦滋2 小时前
Linux线程
linux·运维·c++
oLLI PILO2 小时前
在linux(Centos)中Mysql的端口修改保姆级教程
linux·mysql·centos
埃伊蟹黄面2 小时前
网络层 IP 协议
服务器·网络·tcp/ip