我从未做过 C 程序员,但时不时需要从源代码编译 C/C++ 程序。这对我来说有点棘手:很长一段时间里,我的做法基本上是“安装依赖项,运行make
,如果不行,要么尝试找个别人编译好的二进制文件,要么就放弃”。
当我运行 Linux 时,“希望其他人已经编译了它”非常有效,但自从过去几年我一直在使用 Mac 以来,我遇到了更多必须自己编译程序的情况。
那么,让我们来谈谈编译 C 程序可能需要做些什么!我会用几个我编译过的具体 C 程序的例子,并讨论一些可能出错的地方。以下是我们将要讨论的三个程序的编译:
步骤 1:安装 C 编译器
这非常简单:在 Ubuntu 系统上,如果我还没有 C 编译器,我会使用以下命令安装一个:
sudo apt-get install build-essential
这将安装gcc
、 g++
和make
。Mac 上的情况可能比较复杂,但大致类似于“安装 xcode 命令行工具”。
第 2 步:安装程序的依赖项
与一些较新的编程语言不同,C 没有依赖项管理器。因此,如果程序有任何依赖项,您需要自己查找。值得庆幸的是,C 程序员通常会将依赖项保持在极低的水平,并且通常无论您使用哪种包管理器,这些依赖项都可以找到。
几乎总有一个部分解释如何在 README 中获取依赖项,例如在paperjam的 README 中,它说:
要编译 PaperJam,您需要 libqpdf 和 libpaper 库的标题(通常以 libqpdf-dev 和 libpaper-dev 包的形式提供)。
您可能需要
a2x
(可在AsciiDoc中找到) 来构建手册页。
因此,在基于 Debian 的系统上,您可以像这样安装依赖项。
sudo apt install -y libqpdf-dev libpaper-dev
如果 README 文件给出了软件包的名称(例如libqpdf-dev
),我基本上总是会假设它们的意思是“在基于 Debian 的 Linux 发行版中”:如果你使用的是 Mac,那么brew install libqpdf-dev
将无法正常工作。我还没有完全掌握在 Mac 上进行开发的技巧,所以目前还没有太多这方面的技巧。我想在这种情况下,如果你使用 Homebrew,那么brew install qpdf
就更好了。
步骤 3:运行./configure
(如果需要)
有些 C 程序附带Makefile
,而有些则附带一个名为./configure
的脚本。例如,如果你下载了sqlite 的源代码,它里面有一个./configure
脚本,而不是 Makefile。
我对这个./configure
脚本的理解是:
- 你运行它,它会打印出很多难以理解的输出,然后它要么生成一个
Makefile
,要么因为缺少一些依赖而失败 ./configure
脚本是名为autotools的系统的一部分,除了“运行它来生成Makefile
”之外,我不需要学习任何有关它的知识。
我认为您可以传递一些选项来让./configure
脚本生成不同的Makefile
,但我从未这样做过。
步骤 4:运行make
下一步是运行make
来尝试构建一个程序。关于make
一些注意事项:
- 有时您可以运行
make -j8
来并行化构建并使其运行得更快 - 编译程序时,它通常会打印出无数个编译器警告。我总是直接忽略它们。我又没写这个软件!编译器警告跟我没关系。
编译器错误通常是依赖性问题
这是我在 Mac 上编译paperjam
时遇到的错误:
/opt/homebrew/Cellar/qpdf/12.0.0/include/qpdf/InputSource.hh:85:19: error: function definition does not declare parameters 85 | qpdf_offset_t last_offset{0}; | ^
多年来,我了解到最好不要过度思考这样的问题:如果它谈论的是qpdf
,那么有一个很好的变化,它只是意味着我在包含qpdf
依赖项的方式上做错了什么。
现在让我们讨论一下以正确方式包含qpdf
依赖项的一些方法。
世界上最简短的编译器和链接器介绍
在我们讨论如何解决依赖问题之前:构建 C 程序分为两个步骤:
- 将代码编译成目标文件(使用
gcc
或clang
) - 将这些目标文件链接到最终的二进制文件中(使用
ld
)
在构建 C 程序时了解这一点很重要,因为有时您需要将正确的标志传递给编译器和链接器,以告诉它们在哪里找到您正在编译的程序的依赖项。
make
使用环境变量来配置编译器和链接器
如果我在 Mac 上运行make
来安装paperjam
,我会收到此错误:
c++ -o paperjam paperjam.o pdf-tools.o parse.o cmds.o pdf.o -lqpdf -lpaper ld: library 'qpdf' not found
这并不是因为我的系统上没有安装qpdf
(实际上它已经安装了!)。而是编译器和链接器不知道如何找到qpdf
库。为了解决这个问题,我们需要:
- 将
"-I/opt/homebrew/include"
传递给编译器(告诉它在哪里找到头文件) - 将
"-L/opt/homebrew/lib -liconv"
传递给链接器(告诉它在哪里找到库文件并链接到iconv
)
我们可以让make
使用环境变量将这些额外的参数传递给编译器和链接器!要了解其工作原理,请在paperjam
的 Makefile 中看到一堆环境变量,例如下面的LDLIBS
:
paperjam: $(OBJS) $(LD) -o $@ $^ $(LDLIBS)
您放入LDLIBS
环境变量的所有内容都会作为命令行参数传递给链接器( ld
)。
秘密环境变量: CPPFLAGS
Makefiles
有时会定义自己的环境变量并将其传递给编译器/链接器,但make
还包含一些“隐式”环境变量,这些变量会自动传递给 C 编译器和链接器。 这里有一个完整的隐式环境变量列表,其中之一就是CPPFLAGS
,它会自动传递给 C 编译器。
(从技术上讲,使用CXXFLAGS
对此更为正常,但这个特定的Makefile
对CXXFLAGS
进行了硬编码,因此设置CPPFLAGS
是我发现的唯一一种无需编辑Makefile
即可设置编译器标志的方法)
顺便说一句:我花了很长时间才意识到 `make` 与 C/C++ 的紧密联系——我曾经认为 `make` 只是一个通用的构建系统(当然你可以用它做任何事情!)但它有很多用于构建 C/C++ 程序的功能,而这些功能在构建任何其他类型的程序时都是没有的。
如何使用CPPFLAGS
和LDLIBS
修复此编译器错误
现在我们已经讨论了如何将CPPFLAGS
和LDLIBS
传递给编译器和链接器,下面是我用来成功构建程序的最后一个咒语!
CPPFLAGS="-I/opt/homebrew/include" LDLIBS="-L/opt/homebrew/lib -liconv" make paperjam
这会将-I/opt/homebrew/include
给编译器,并将-L/opt/homebrew/lib -liconv
给链接器。
另外,我不想假装自己“神奇地”知道这些参数是正确的,因为搞清楚这些参数需要我费劲地谷歌搜索一番,而我在这篇文章中就略过了。我会说:
-
-I
编译器标志告诉编译器在哪个目录中查找头文件,例如/opt/homebrew/include/qpdf/QPDF.hh
-
-L
链接器标志告诉链接器在哪个目录中查找库,例如/opt/homebrew/lib/libqpdf.a
-
-l
链接器标志告诉链接器要链接哪些库,例如-liconv
表示“链接iconv
库”,或-lm
表示“链接math
”
提示:如何只构建一个特定文件: make $FILENAME
昨天我发现了一个很酷的工具,叫做qf ,你可以使用它从ripgrep
的输出中快速打开文件。
qf
位于一个包含各种工具的大目录中,但我只想编译qf
。所以我直接编译了qf
,如下所示:
make qf
基本上,如果您知道(或可以猜测)要构建的文件的输出文件名,则可以通过运行make $FILENAME
来告诉make
仅构建该文件
提示:查看其他打包系统如何构建相同的 C 程序
如果你在构建 C 程序时遇到困难,那么其他人可能也遇到了同样的问题!每个 Linux 发行版都为其构建的每个软件包提供了构建文件,所以即使你无法直接从该发行版安装软件包,也许你也可以从该发行版那里获得一些构建软件包的技巧。意识到这一点(感谢我的朋友 Dave)对我来说是一个巨大的顿悟。
例如, paperjam
的 nix 包中的这一行内容是:
env.NIX_LDFLAGS = lib.optionalString stdenv.hostPlatform.isDarwin "-liconv";
这基本上是在说“传递链接器标志-liconv
在 Mac 上构建它”,所以这是我们可以用来构建它的线索。
同一个文件还显示env.NIX_CFLAGS_COMPILE = "-DPOINTERHOLDER_TRANSITION=1";
。我不确定这是什么意思,但是当我尝试构建paperjam
包时,我确实收到了一个关于PointerHolder
的错误,所以我猜这与“PointerHolder 转换”有某种联系。
步骤 5:安装二进制文件
编译完程序后,你可能想把它安装到某个地方!有些Makefile
包含一个install
目标,让你可以用make install
将工具安装到系统上。我总是有点担心这个问题(文件该放哪儿?万一以后想卸载怎么办?),所以如果我编译的是一个非常简单的程序,我通常会手动复制二进制文件来安装它,就像这样:
cp qf ~/bin
第 6 步:也许制作您自己的包裹!
搞清楚了这些之后,我意识到我可以用新学到的make
知识为 Homebrew 贡献一个paperjam
软件包!这样以后我就能直接在以后的系统上brew install paperjam
。
好消息是,即使所有不同的打包系统的细节各不相同,它们从根本上来说都使用 C 编译器和链接器。
即使你不是 C 程序员,了解一些 C 语言也是很有用的
我认为所有这些都是一个有趣的例子,说明了解 C 程序工作原理的一些基础知识(例如“它们有头文件”)很有用,即使你一生中从未打算编写一个非平凡的 C 程序。
能够自己编译 C/C++ 程序感觉很好,尽管我对所有的编译器和链接器标志仍然不是完全有信心,而且我仍然计划除了“运行./configure
来生成Makefile
”之外,永远不学习任何有关自动工具如何工作的知识。
另外,我遗漏了一件重要的事情,那就是LD_LIBRARY_PATH / DYLD_LIBRARY_PATH
(使用它来告诉动态链接器在运行时在哪里找到动态链接文件),因为我不记得上次遇到LD_LIBRARY_PATH
问题并且找不到示例。
原文: https://jvns.ca/blog/2025/06/10/how-to-compile-a-c-program/