当前位置: 首页 > 科技观察

如何组织和构建一个多文件的C语言程序(一)

时间:2023-03-15 01:26:32 科技观察

准备好你喜欢的饮料、编辑器和编译器,放上音乐,开始构建一个由多个文件组成的C语言程序。人们常说,计算机编程的艺术部分是处理复杂性,部分是命名事物。另外,我认为“有时您需要添加情节”在很大程度上是正确的。在这篇文章中,我将编写一个小的C程序,列举一些东西,并处理一些复杂的问题。程序的结构大致按照我在《如何写一个好的 C 语言 main 函数》中讨论的内容。但是,这次做一些不同的事情。准备好您最喜欢的饮料、编辑器和编译器,放上一些音乐,让我们一起编写一个有趣的C程序。一个好的Unix程序的哲学首先,你需要知道这个C程序是一个Unix命令行工具。这意味着它运行在(或可以移植到)那些提供UnixC运行时环境的操作系统上。当贝尔实验室发明Unix时,它从一开始就充满了设计理念。用我自己的话说:程序只做一件事,而且做得很好,对文件进行一些操作。虽然“只做一件事,并把它做好”是有道理的,但“用文件做某事”部分似乎有点不合时宜。原来Unix中的抽象“文件”很强大。Unix文件是一个以文件结束(EOF)标记终止的字节流。就这样。文件中的任何其他结构都是由应用程序强加的,而不是由操作系统强加的。操作系统提供系统调用,允许程序对文件执行一组标准操作:打开、读取、写入、寻址和关闭(还有其他操作,但越来越复杂)。对文件的标准化访问允许不同的程序共享相同的抽象并协同工作,即使它们是由不同的人用不同的语言编写的。拥有共享文件接口使得构建可组合程序成为可能。一个程序的输出可以是另一个程序的输入。Unix家族的操作系统在执行程序时默认提供了三个文件:标准输入(stdin)、标准输出(stdout)和标准错误(stderr)。其中两个文件是只写的:stdout和stderr。stdin是只读的。当我们在Bash等普通shell中使用文件重定向时,我们可以看到它的效果。$ls|grepfoo|sed-e's/bar/baz/g'>ack这条命令可以简单描述为:ls的结果写入标准输出,重定向到grep的标准输入,grep的标准输出重定向到标准sed的输入,sed的标准输出被重定向到当前目录下名为ack的文件。我们希望我们的程序能够在这个灵活而令人敬畏的生态系统中运行良好,所以让我们编写一个可以读写文件的程序。喵喵喵:流编码器/解码器的概念当我还是一个学习计算机科学的没牙小孩的时候,我学到了很多关于编码方案的知识。其中一些用于压缩文件,一些用于打包文件,还有一些没有用,因此只是愚蠢的。最后一个案例的例子:moomoo编码方案。为了给我们的程序一个目的,我用21世纪的概念对其进行了更新,并实现了一个称为“喵喵喵”编码方案的概念(毕竟每个人都喜欢互联网上的猫)。这里的基本思想是获取文件并用文本“meow”对每个半字节(半字节)进行编码。小写字母代表0,大写字母代表1。因为用32位代替了4位,所以增加了文件的大小。是的,这没有意义。但是想象一下人们看到这样编码的东西时的惊讶程度。$cat/home/your_sibling/.super_secret_journal_of_my_innermost_thoughtsMeOWmeOWmeowMEoW...这太棒了。最终实现的完整源代码可以在GitHub上找到,但我会在编写程序时写下我的想法。目的是说明如何组织和构造多文件C语言程序。既然我决定写一个程序来对“喵喵”格式的文件进行编解码,那么我在shell中执行了如下命令:$mkdirmeowmeow$cdmeowmeow$gitinit$touchMakefile#编译程序的方法$touchmain.c#处理命令行选项$touchmain.h#“全局”常量和定义$touchmmencode.c#实现喵喵文件的编码$touchmmencode.h#描述编码API$touchmmdecode.c#实现喵喵文件的解码$touchmmdecode.h#描述解码API$touchtable.h#定义编码查找表$touch.gitignore#这个文件中的文件名会被git忽略$gitadd.$gitcommit-m"initialcommitofemptyfiles"简单的说,我创建了一个全是空文件的目录,提交给git。即使这些文件中没有任何内容,您仍然可以从文件名中推断出每个文件的用途。如果您不明白,我已经简要描述了每个触摸命令。通常,程序从一个简单的main.c文件开始,其中只有两个或三个问题解决函数。然后程序员轻率地向他的朋友或老板展示程序,文件中的函数数量爆炸式增长以支持所有新的“特性”和“需求”。“程序俱乐部”的第一条规则就是不谈“程序俱乐部”,第二条规则是尽量减少单个文件中的函数数量。老实说,C编译器不关心程序中的所有函数是否都在一个文件中。但我们不是为计算机或编译器编写程序,我们是为其他人(有时是我们)编写程序。我知道这可能很奇怪,但这是事实。程序包含计算机用来解决问题的一组算法。当问题的参数发生意外变化时,确保人们能够理解它们非常重要。当人们修改程序并在文件中找到2049个函数时,他们会诅咒你。因此,优秀的程序员会将函数分开,将类似的函数分组到不同的文件中。这里我使用了三个文件main.c、mmencode.c和mmdecode.c。也许对于这样一个小程序来说,这似乎有点矫枉过正。然而,让一个小程序保持小是很困难的,所以规划Gothic扩展是一个“好主意”。但是那些.h文件呢?稍后我将解释一般术语,但简单来说它们称为头文件,它们可以包含C语言类型定义和C预处理指令。头文件中不应包含任何函数。您可以将头文件视为提供应用程序编程接口(API)定义的.c文件,其他.c文件可以使用该接口。但是什么是Makefile?我知道下一个轰动一时的应用程序是由你们这些好孩子使用“UltimateCodeShredder3000”IDE编写的,构建项目是一系列复杂的Ctrl-Meta-Shift-Alt-Super-B等键的混搭。但是现在(也就是现在),使用Makefile可以在构建C程序时帮助做很多有用的工作。Makefile是一个包含如何处理文件的文本文件,程序员可以使用它从源代码自动构建二进制程序(以及其他内容!):以这个小东西为例:00#Makefile01TARGET=my_sweet_program02$(TARGET):main.c03cc-omy_sweet_programmain.c#符号后的文本是注释,例如第00行。第01行是一个变量赋值,它将字符串my_sweet_program赋给TARGET变量。按照惯例,所有Makefile变量都是大写的,单词之间用下划线分隔。第02行包含步骤配方要创建的文件的名称以及它所依赖的文件。在此示例中,构建目标target是my_sweet_program,其依赖项是main.c。最后一行03使用制表符而不是四个空格。这是将要执行以创建目标的命令。本例中,我们使用C编译器前端cc作为my_sweet_program进行编译链接。使用Makefile非常简单。$makecc-omy_sweet_programmain.c$lsMakefilemain.cmy_sweet_program构建我们喵喵编码器/解码器的Makefile比上面的例子更复杂,但是基本结构是一样的。我将在另一篇文章中将其分解为Barney风格。形式服从功能我的想法是程序从一个文件中读取,转换它,并将转换后的结果存储在另一个文件中。下面是我在使用程序命令行交互时的想象:解析和处理输入/输出流。我们需要一个函数来编码流并将结果写入另一个流。最后,我们需要一个函数来解码流并将结果写入另一个流。等一下,我们说的是如何写程序,但是在上面的例子中,我调用了两条指令:meow和unmeow?我知道您可能认为这会导致越来越复杂。次要内容:argv[0]和ln指令回想一下C中的main函数有如下结构:intmain(intargc,char*argv[])其中argc是命令行参数的个数,argv是一个字符指针(字符串)列表。argv[0]是包含正在执行的程序的文件的路径。Unix系统上很多功能互补的程序(例如:压缩和解压)看起来像是两条命令,但实际上在文件系统中它们是一个具有两个名字的程序。诀窍是使用ln命令创建指向两个名称的文件系统链接。我的笔记本电脑中的/usr/bin示例如下:$ls-li/usr/bin/git*3376-rwxr-xr-x。113rootroot1.5M2018年8月30日/usr/bin/git3376-rwxr-xr-x。113rootroot1.5MAug302018/usr/bin/git-receive-pack...这里git和git-receive-pack是同一个文件,只是名字不同。我们说它们是同一个文件,因为它们具有相同的inode值(第一列)。Inode是Unix文件系统的一个特性,它的介绍超出了本文的范围。好的或懒惰的程序可以使用Unix文件系统的这个特性来编写更少的代码并交付两倍的程序。首先,我们编写一个程序,根据其argv[0]的值进行更改,然后我们确保创建一个指向导致该行为的名称的链接。在我们的Makefile中,unmeow链接是通过以下方式创建的:#Makefile...$(DECODER):$(ENCODER)$(LN)-f$<$@...我倾向于把所有内容都参数化,“裸”字符串很少使用。我将所有定义放在Makefile的顶部,以便可以轻松找到和更改它们。当您尝试将程序移植到新平台时需要将cc更改为某个cc时,这很方便。除了两个内置变量$@和$<之外,分步食谱看起来相对简单。第一个是步骤目标的快捷方式,在本例中为$(DECODER)(我记得这个是因为@符号看起来像一个目标)。第二个$<是规则依赖项,在本例中解析为$(ENCODER)。事情肯定会变得复杂,但它仍在管理中。