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