代码库的概念
为什么要使用 "库"
在上一节中, 我们引入了 "多文件" 这样的方法, 来减少代码的重复, 提高代码的复用性.
而当我们把一类经常会被使用到的代码整理在一起, 就成了一个代码库.
而要理解库的概念, 建议从 "源代码" 和 "编译后" 两个层面进行.
提取代码成库
从源代码的层面来理解很直观, 就是把代码有条理的整理在一起.
接下来将通过一个例子, 来介绍之后的内容, 包括代码的提取整理成库, 以及之后的使用. 该示例所使用的代码也可以在 gitlab.com/kLiHz/cpp-multi-file-demo 查看.
假设我们现在有这样一个 C++ 文件 single_file_demo.cpp, 发现其中的 Date
相关代码有复用的价值, 因此计划将其拆分出来, 单独成库.
#include <iostream>
auto is_leap(int y) {
return (y % 4 ==0 && y % 100 != 0) || y % 400 == 0;
}
struct Date
{
int y;
int m;
int d;
Date(int y_, int m_, int d_) : y(y_), m(m_), d(d_) {}
};
auto calc_days(Date const & date) {
int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (is_leap(date.y)) {
days[2] = 29;
}
auto cnt = 0;
for (int i = 1; i < date.m; ++i) {
cnt += days[i];
}
cnt += date.d;
return cnt;
}
int main() {
int y, m, d;
std::cin >> y >> m >> d;
Date d1(y, m, d);
std::cout << calc_days(d1);
}
具体来说, 可以将代码中相关的 is_leap
和 calc_days
等函数, 变成 Date
类的成员方法. 结果如下:
// Date.hpp
// 头文件需要进行保护,目的是避免重复包含
#ifndef DATE_HPP
#define DATE_HPP
// 头文件中不能包含具体的定义
// 由于头文件可能会被多个 cpp (编译单元)包含
// 如果头文件中包含具体的定义,这些编译单元在最后链接过程中,会出现多个重复的符号
// 如果确需在头文件中包含具体函数的定义,需要声明为 inline(内联)
class Date {
int y;
int m;
int d;
bool is_leap(int year) {
return (year % 4 ==0 && year % 100 != 0) || year % 400 == 0;
}
public:
// 类的声明中直接书写的函数是 inline 的
Date(int y_, int m_, int d_) : y(y_), m(m_), d(d_) {}
// 也可以只写声明,再在 cpp 文件中实现这个函数
int get_days();
};
#endif // DATE_HPP
// Date.cpp
// 由于需要实现 Date.hpp 中声明的函数,需要包含 Date.hpp
#include "Date.hpp"
int Date::get_days()
{
int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
if (is_leap(this->y)) {
days[2] = 29;
}
auto cnt = 0;
for (int i = 1; i < this->m; ++i) {
cnt += days[i];
}
cnt += this->d;
return cnt;
}
如此, 我们便得到了一个库 (或者说是库的源代码/源代码形式的库), 该库由 Date.cpp
和 Date.hpp
组成. 使用该库的用户, 可以调用该库中的功能, 而不必重新编写代码.
库的分发形式
我们可以将这个库分享给其他用户使用, 一种方式便是以源代码的形式.
对于使用以源代码形式分发的库的用户来说, 一般会是像上一节中介绍的方式那样, 在需要使用库的地方, 引入头文件中的声明, 之后将自己的源文件和库的源文件均编译得到目标文件, 最后链接得到完整的可执行程序.
除此之外, 假如我们不希望公开库的源代码, 或者希望该库可以更方便地进行分发, 我们还可以将这个库构建为 (某个操作系统平台上的) 库文件. 构建成库文件的过程像是把多个目标文件 (假如库中包含多个源文件) 打包在一起, 但是不完全如此; 虽然目标文件中也是编译后的二进制代码, 但是它们更像是构建的中间产物, 而库文件可能会经过一些专门的优化, 更像是完善的商品.
相较于以源文件形式分发的库, 使用 (编译后的) 库文件不必花时间重新编译, 并且可以享受来自发布者的优化; 缺点则是, 使用过程中很可能遇到平台兼容性的问题.
详情可以参考 "ABI (应用程序二进制接口) 兼容性".
对于 C++ 来说, 有一种特殊的 "头文件库 (header-only libraries)". 顾名思义, 这种库只有头文件组成, 库中包含的代码都 (以源代码形式) 存储在头文件里, 因此用户只需在自己的程序中引用头文件即可使用库中的功能. 这种库的好处是携带使用起来都很方便, 缺点是会增加编译的时间.
此外, 对于以解释型语言 (比如 Python, JavaScript 等) 写成的库, 通常不存在编译这样的说法, 因为代码均需要解释器来执行, 所以分发时一般都是人类可以阅读 (但是不一定方便阅读) 的文本形式.
但是有几种情况涉及到 "编译" 或者类似的概念.
一是该库由一种更 "高级" 的语言写成, 需要编译器处理, 才能得到能够直接被解释器解释的代码;
二是为了便于传输, 代码会经过一种叫做 Minification (最小化) 的过程, 简单来说就是在不影响代码功能的情况下, 尽可能减少不必要的字符 (如空白, 换行等等), 缩短函数或变量的名字等等, 从而减小文件的大小, 便于传输.
类似的, 在 Obfuscation (混淆) 这一技术中, 也会涉及到函数和变量的重命名. 不过二者的目标不尽相同, 混淆的目的主要是为了增加用户破解软件的难度, 除了重命名之外, 可能还会增加一些无意义的干扰代码.