我创建了一个简单的静态库,包含在
.a
文件中。我可能会在各种项目中使用它,其中一些项目 90% 根本不需要它。例如,如果我想在 AVR 微型计算机上使用神经网络(它是我的库的一部分),我可能不需要大量其他东西,但是将其链接到我的代码中可能会生成相当大的文件吗?
我打算编译这样的程序:
g++ myProg.cpp myLib.a -o prog
G++ 只会从您的库中提取所需的目标文件,但这意味着如果使用单个目标文件中的一个符号,则该目标文件中的所有内容都会添加到您的可执行文件中。
一个源文件变成一个目标文件,因此只有当确定需要一起使用时,才可以将它们逻辑地组合在一起。
这种做法因编译器(实际上是链接器)而异。例如,Microsoft 链接器会将目标文件分开,只包含实际需要的部分。
您还可以尝试将您的库分成独立的较小部分,并仅链接您真正需要的部分。
当您链接到静态库时,链接器会提取解析代码其他部分中使用的名称的内容。一般来说,如果不使用该名称,则不会链接到其中。
GNU 链接器将从您在目标文件上指定的库中逐个目标文件提取所需的内容。就 GNU 链接器而言,目标文件是原子单元。它不会将它们分开。如果目标文件定义了一个或多个未解析的外部引用,则链接器将引入该目标文件。该目标文件可能有外部引用。链接器将尝试解决这些问题,但如果无法解决,链接器会将它们添加到需要解决的引用集中。
有一些陷阱可能会导致可执行文件比所需的大得多。我所说的大于需要,是指一个可执行文件包含永远不会被调用的函数,在程序执行期间永远不会被检查或修改的全局对象。您将拥有无法访问的二进制代码。
当目标文件包含大量函数或全局对象时,就会出现这些陷阱之一。您的程序可能只需要其中之一,但您的可执行文件会获取所有这些,因为目标文件对于链接器来说是原子单元。这些额外的函数将无法访问,因为没有从
main
到这些函数的调用路径,但它们仍然在您的可执行文件中。确保这种情况不会发生的唯一方法是使用“每个源文件一个函数”规则。我自己并不遵守这条规则,但我确实理解它的逻辑。
使用多态类时会出现另一组陷阱。构造函数包含自动生成的代码以及构造函数本身的主体。该自动生成的代码调用父类的构造函数,插入指向对象中类的 vtable 的指针,并根据初始值设定项列表初始化数据成员。这些父类构造函数、vtable 和处理初始化列表的机制可能是链接器需要解析的外部引用。如果父类构造函数位于较大的头文件中,则您只需将所有这些内容拖到可执行文件中即可。
虚表呢? GNU 编译器选择一个关键成员函数作为存储 vtable 的位置。该关键函数是类中第一个没有内联定义的成员函数。即使您不调用该成员函数,您也会获得可执行文件中包含该成员函数的目标文件 - 并且您将获得该目标文件拖入的所有内容。
再次将源文件保持在较小的大小有助于“看看猫拖进来了什么!”问题。最好特别注意包含该关键成员函数的文件。保持该源文件较小,至少就猫将拖入的内容而言。我倾向于在该源文件中放置小型的、独立的成员函数。不可避免地会拖入一堆其他东西的功能不应该放在那里。
vtable 的另一个问题是它包含指向类的所有虚函数的指针。这些指针需要指向真实的东西。您的可执行文件将包含定义为类定义的每个虚拟函数的目标文件,包括您从未调用的虚拟函数。而且您还将获得这些虚拟函数拖入的所有内容。
这个问题的一个解决方案是避免创建大的类。他们倾向于拖入“一切”。神类在这方面尤其存在问题。另一个解决方案是认真思考一个函数是否真的需要是虚拟的。不要仅仅因为您认为有一天有人需要重载函数而将其设为虚拟函数。这就是推测的普遍性,对于虚函数来说,推测的普遍性会带来很高的成本。