4、实战案例:基于C语言实现一个数学计算库

好了,理论讲了一大堆,是时候动手了。这一章我们直接撸代码,做一个简单的数学计算库。我会分别用静态库和动态库两种方式来实现,让你亲眼看看它们到底有什么区别。

我个人习惯,做这种小项目时,先把目录结构搭好。这样后面写代码、编译、测试,思路都清晰很多。

4.1 项目目录结构

先看看我打算怎么组织文件:

mathlib/
├── include/
│   └── mathlib.h          # 头文件
├── src/
│   ├── add.c              # 加法
│   ├── sub.c              # 减法
│   ├── mul.c              # 乘法
│   └── div.c              # 除法
├── build.sh               # 编译脚本(静态库)
├── build_dynamic.sh       # 编译脚本(动态库)
└── test/
    └── test_math.c        # 测试程序

嗯,这里要注意:头文件和源文件分开存放,这是工程化的基本素养。你想想看,如果所有文件都堆在一个目录里,项目大了以后找东西得累死。

4.2 头文件设计

头文件是库的“门面”。用户能看到的就是它,所以设计要干净、规范。

// include/mathlib.h
#ifndef MATHLIB_H
#define MATHLIB_H

#ifdef __cplusplus
extern "C" {
#endif

/**
 * @brief 加法运算
 * @param a 加数
 * @param b 加数
 * @return a + b
 */
double add(double a, double b);

/**
 * @brief 减法运算
 * @param a 被减数
 * @param b 减数
 * @return a - b
 */
double sub(double a, double b);

/**
 * @brief 乘法运算
 * @param a 乘数
 * @param b 乘数
 * @return a * b
 */
double mul(double a, double b);

/**
 * @brief 除法运算
 * @param a 被除数
 * @param b 除数(不能为0)
 * @return a / b
 */
double div_op(double a, double b);

#ifdef __cplusplus
}
#endif

#endif /* MATHLIB_H */

这里我加了个 extern "C" 的宏判断。为什么?因为C++编译器会对函数名做名字修饰(name mangling),如果不加这个,C++程序链接C库时会找不到符号。我在项目中遇到过好几次这种坑,所以现在写头文件都会顺手加上。

4.3 源文件编写

每个函数一个文件,简单明了。我选几个给你看看:

// src/add.c
#include "mathlib.h"

double add(double a, double b) {
    return a + b;
}
// src/div.c
#include "mathlib.h"

double div_op(double a, double b) {
    if (b == 0.0) {
        // 实际项目中应该用错误码或异常处理
        return 0.0;
    }
    return a / b;
}

其他几个文件(sub.c、mul.c)结构完全一样,我就不贴了。说白了,数学库的核心就是这些四则运算,但麻雀虽小五脏俱全。

4.4 编译脚本——静态库方式

先来静态库的编译脚本。我习惯用 build.sh 来命名:

#!/bin/bash
# build.sh - 编译静态库

# 1. 编译所有源文件为目标文件
gcc -c src/add.c -I include -o add.o
gcc -c src/sub.c -I include -o sub.o
gcc -c src/mul.c -I include -o mul.o
gcc -c src/div.c -I include -o div.o

# 2. 打包成静态库
ar rcs libmath.a add.o sub.o mul.o div.o

# 3. 编译测试程序并链接静态库
gcc test/test_math.c -I include -L . -lmath -o test_static

# 4. 清理中间文件
rm -f *.o

echo "静态库编译完成!运行: ./test_static"

这里有个细节:-I include 指定头文件路径,-L . 指定库文件路径,-lmath 表示链接 libmath.a。我曾经见过新手把 -lmath 写成 -llibmath,结果链接失败,折腾了半天才发现是命名问题。

4.5 编译脚本——动态库方式

动态库的编译脚本稍有不同:

#!/bin/bash
# build_dynamic.sh - 编译动态库

# 1. 编译位置无关代码的目标文件
gcc -c -fPIC src/add.c -I include -o add.o
gcc -c -fPIC src/sub.c -I include -o sub.o
gcc -c -fPIC src/mul.c -I include -o mul.o
gcc -c -fPIC src/div.c -I include -o div.o

# 2. 生成动态库
gcc -shared -o libmath.so add.o sub.o mul.o div.o

# 3. 编译测试程序并链接动态库
gcc test/test_math.c -I include -L . -lmath -o test_dynamic

# 4. 清理中间文件
rm -f *.o

echo "动态库编译完成!运行前请设置 LD_LIBRARY_PATH"
echo "export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH"
echo "运行: ./test_dynamic"

看到 -fPIC 这个选项了吗?它表示生成位置无关代码(Position Independent Code)。动态库加载时地址不确定,所以必须用PIC。我记得有一次忘了加这个,编译没报错,但运行时直接段错误,排查了好久才发现是这个问题。

4.6 测试程序

测试程序用来验证库的功能:

// test/test_math.c
#include <stdio.h>
#include "mathlib.h"

int main() {
    double a = 10.0, b = 3.0;

    printf("a = %.2f, b = %.2f\n", a, b);
    printf("add: %.2f + %.2f = %.2f\n", a, b, add(a, b));
    printf("sub: %.2f - %.2f = %.2f\n", a, b, sub(a, b));
    printf("mul: %.2f * %.2f = %.2f\n", a, b, mul(a, b));
    printf("div: %.2f / %.2f = %.2f\n", a, b, div_op(a, b));

    // 测试除零情况
    printf("div by zero: %.2f / 0.0 = %.2f\n", a, div_op(a, 0.0));

    return 0;
}

4.7 核心逻辑流程图

下面我用一张SVG图来展示整个库的构建和链接流程:

数学计算库构建与链接流程 add.c sub.c mul.c div.c add.o sub.o mul.o div.o ar rcs libmath.a gcc -shared -o libmath.so libmath.a (静态库) libmath.so (动态库)

核心要点:静态库在链接时被完整复制到可执行文件中,动态库则在运行时加载。前者生成的文件大但独立,后者生成的文件小但依赖外部库文件。

4.8 运行测试

编译完成后,运行测试程序看看效果:

# 静态库测试
$ ./test_static
a = 10.00, b = 3.00
add: 10.00 + 3.00 = 13.00
sub: 10.00 - 3.00 = 7.00
mul: 10.00 * 3.00 = 30.00
div: 10.00 / 3.00 = 3.33
div by zero: 10.00 / 0.00 = 0.00

# 动态库测试(需要先设置 LD_LIBRARY_PATH)
$ export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
$ ./test_dynamic
a = 10.00, b = 3.00
add: 10.00 + 3.00 = 13.00
sub: 10.00 - 3.00 = 7.00
mul: 10.00 * 3.00 = 30.00
div: 10.00 / 3.00 = 3.33
div by zero: 10.00 / 0.00 = 0.00

输出结果完全一致。但如果你把 libmath.so 删掉,再运行 test_dynamic,就会报错说找不到共享库。而 test_static 则完全不受影响。这就是两者最直观的区别。

小技巧:用 ldd test_dynamic 命令可以查看可执行文件依赖了哪些动态库。如果某个库显示 "not found",说明运行时加载路径有问题。

注意:动态库的搜索路径优先级是:LD_LIBRARY_PATH 环境变量 > /etc/ld.so.cache 缓存 > /lib、/usr/lib 等默认路径。生产环境中建议将动态库安装到系统标准路径,或者使用 rpath 编译选项硬编码路径。

好了,这个实战案例就到这里。你可以在自己的机器上动手试试,把代码敲一遍,感受一下静态库和动态库在编译、链接、运行时的差异。只有亲手做过,才能真正理解这些概念。

可可精品资料库