我将在本系列的第二部分深入探讨由多个文件组成的C程序的结构。在第一个中,我设计了一个名为MeowMeow的多文件C程序,它实现了一个玩具编解码器。我在程序设计中也提到了Unix的哲学,就是一开始就创建多个空文件,并建立一个好的结构。最后,我创建了一个Makefile文件夹并解释了它的作用。在这篇文章中转向另一个方向:我现在将介绍一个简单但有启发性的喵喵编解码器实现。看完我的《如何写一个好的 C 语言 main 函数》,你会觉得喵喵编解码器的main.c文件的结构很熟悉,它的主要结构如下:/*main.c-MeowMeowMeowStreamingCodec*//*00系统包含文件*//*01项目包含文件*//*02外部声明*//*03定义*//*04类型定义*//*05全局变量声明(不要使用)*//*06附加函数原型*/intmain(intargc,char*argv[]){/*07变量声明*//*08检查argv[0]以查看程序是如何调用的*//*09处理命令行选项fromuser*//*10Dosomethinguseful*/}/*11Miscellaneoushelperfunctions*/Includeprojectheaderfilesinsecondsection/*01Projectincludefiles*/源代码如下:/*main.c-喵喵流codec*/.../*01projectincludefiles*/#include"main.h"#include"mmecode.h"#include"mmdecode.h"#include是C语言中的一个预处理命令,它会拷贝文件文件名内容到当前fi乐。如果程序员在头文件的名称周围使用双引号(""),编译器将在当前目录中查找该文件。如果文件被尖括号(<>)包围,编译器将在一组预定义的目录中查找该文件。main.h文件包含main.c文件中使用的定义和类型定义。我喜欢在头文件中放置尽可能多的声明,以便我可以在程序的其他地方使用这些定义。mmencode.h和mmdecode.h这两个头文件几乎一样,所以我以mmencode.h为例进行分析。/*mmencode.h-MeowMeow流编解码器*/#ifndef_MMENCODE_H#define_MMENCODE_H#includeintmm_encode(FILE*src,FILE*dst);#endif/*_MMENCODE_H*/#ifdef、#define、#endif指令统称为“guard”指令。它防止C编译器在一个文件中多次包含同一个文件。如果编译器在一个文件中发现多个定义/原型/声明,它将生成警告。因此这些保护措施是必要的。在这些守卫内部,只有两件事:#include指令和函数原型声明。我在这里包含了stdio.h头文件,这样我就可以在函数原型中使用FILE定义。函数原型也可以包含在其他C文件中,以在文件的命名空间中创建它。您可以将每个文件视为一个单独的命名空间,其中变量和函数不能被另一个文件中的函数或变量使用。编写头文件很复杂,在大型项目中很难管理。不要忘记使用守卫。喵喵编码的最终实现本程序的作用是根据字节对喵喵字符串进行编码和解码。事实上,这是项目中最简单的部分。到目前为止我所做的工作是让这个函数在适当的地方被调用:解析命令行,确定要使用的操作,打开要操作的文件。下面循环就是编码过程:/*mmencode.c-喵喵流编解码器*/...while(!feof(src)){if(!fgets(buf,sizeof(buf),src))break;for(i=0;i>4;fputs(tbl[hi],dst);fputs(tbl[lo],dst);}}简单来说,当文件(feof(3))中有数据块时,循环读取(feof(3))其中一个文件数据块。然后将读取的每个字节分成两个hi半字节和lo半字节。一个半字节是一个半字节,或4位。这里的技巧是可以用4位来编码16个值。我使用hi和lo作为包含半字节编码MeowMeow字符串的16字符串查找表tbl的索引。使用fputs(3)函数将这些字符串写入目标FILE流,然后我们继续处理缓冲区中的下一个字节。该表使用table.h中定义的宏进行初始化,我喜欢在没有特殊原因时使用它(例如,显示包含另一个项目的本地标头)。我将在以后的文章中进一步探讨原因。喵喵解码的实现我承认花了一些时间才开始工作。解码循环类似于编码:读取喵喵字符串到缓冲区,将编码从字符串转换为字节/*mmdecode.c-meowmeowstreamingcodec*/...intmm_decode(FILE*src,FILE*dst){if(!src||!dst){errno=EINVAL;返回-1;}returnstupid_decode(src,dst);}这不符合您的预期吗?这里,我通过对外暴露的mm_decode()函数暴露了stupid_decode()函数的细节。“外部”是指在这个文件之外。因为stupid_decode()函数不在那个头文件中,所以不能在其他文件中调用。当我们想要发布一个可靠的公共接口时,我们有时会这样做,但我们还没有完全解决函数的问题。在此示例中,我编写了一个I/O密集型函数,一次从源读取8个字节,解码1个字节并将其写入目标流。更好的实现一次处理大于8字节的缓冲区。更好的实现还可以通过缓冲区输出字节,从而减少将单个字节写入目标流的次数。/*mmdecode.c-喵喵流编解码器*/...intstupid_decode(FILE*src,FILE*dst){charbuf[9];decoded_byte_t字节;诠释我;while(!feof(src)){if(!fgets(buf,sizeof(buf),src))中断;byte.field.f0=isupper(buf[0]);byte.field.f1=isupper(buf[1]);字节。field.f2=isupper(buf[2]);byte.field.f3=isupper(buf[3]);byte.field.f4=isupper(buf[4]);byte.field.f5=isupper(buf[5]);byte.field.f6=isupper(buf[6]);byte.field.f7=isupper(buf[7]);fputc(byte.value,dst);}return0;}我没有使用编码器中使用的移位方法,而是创建了一个名为decoded_byte_t的自定义数据结构。/*mmdecode.c-喵喵流编解码器*/...typedefstruct{unsignedcharf7:1;无符号字符f6:1;无符号字符f5:1;无符号字符f4:1;无符号字符f3:1;无符号字符f2:1;无符号字符f1:1;unsignedcharf0:1;}fields_t;typedefunion{fields_t字段;无符号字符值;}decoded_byte_t;不放弃。decoded_byte_t定义为fields_t和unsignedchar的联合。您可以将联合中的命名成员视为同一内存区域的别名。在这种情况下,value和field指向相同的8位内存区域。将field.f0设置为1也会设置值中的最低有效位。虽然unsignedchar并不神秘,但fields_t的typedef可能看起来很陌生。现代C编译器允许程序员指定结构中各个位域的值。该字段所在的类型是无符号整数类型,成员标识符后跟一个冒号和一个指定位字段长度的整数。这种数据结构使得通过字段名访问字节中的每一位变得简单,并通过联合中的值字段访问组合值。我们依靠编译器生成正确的移位指令来访问字段,这可以在调试时为您节省大量时间。最后,因为stupid_decode()函数一次只能从源文件流中读取8个字节,所以效率不是很高。通常我们尽量减少读写次数来提高性能,减少调用系统调用的开销。请记住:大块的少量读/写比小块的大量读/写要好得多。总结用C语言编写一个多文件程序需要程序员进行更多的计划,而不仅仅是一个main.c。但是当您添加功能或重构时,只需一点点额外的努力就可以为您节省大量时间和麻烦。回想起来,我更喜欢这样做:多个文件,每个文件只有简单的功能;通过头文件公开这些文件中的一小部分功能;在头文件中保留数字常量和字符串常量;使用Makefiles而不是Bash脚本来自动化交易;使用main()函数处理命令行参数解析并作为程序主要功能的框架。