在我在网上看到的所有代码中,程序总是被分解成许多较小的文件。不过,对于我所有的学校项目,我只需要一个包含我使用的所有结构和函数的巨大 C 源文件就可以了。
我想学习如何将我的程序分成更小的文件,这似乎是专业的标准。(顺便说一句,为什么会这样——仅仅是为了便于阅读吗?)
我四处搜索,我能找到的所有信息都是建立图书馆,我不认为这不是我想做的事。我希望我能提供更多帮助,但我不完全确定如何实现这一点——我只确定我想要的最终产品。
在我在网上看到的所有代码中,程序总是被分解成许多较小的文件。不过,对于我所有的学校项目,我只需要一个包含我使用的所有结构和函数的巨大 C 源文件就可以了。
我想学习如何将我的程序分成更小的文件,这似乎是专业的标准。(顺便说一句,为什么会这样——仅仅是为了便于阅读吗?)
我四处搜索,我能找到的所有信息都是建立图书馆,我不认为这不是我想做的事。我希望我能提供更多帮助,但我不完全确定如何实现这一点——我只确定我想要的最终产品。
好吧,这正是您想要的:将您的代码拆分为多个库!
举个例子,在一个文件中你有:
#include <stdio.h>
int something() {
return 42;
}
int bar() {
return something();
}
void foo(int i) {
printf("do something with %d\n", i);
}
int main() {
foo(bar());
return 0;
}
您可以将其拆分为:
mylib.h:
#ifndef __MYLIB_H__
#define __MYLIB_H__
#include <stdio.h>
int bar();
void foo();
#endif
注意:上面的预处理器代码称为“守卫”,用于不运行两次此头文件,因此您可以在多个地方调用相同的包含,并且没有编译错误
mylib.c:
#include <mylib.h>
int something() {
return 42;
}
int bar() {
return something();
}
void foo(int i) {
printf("do something with %d\n", i);
}
myprog.c:
#include <mylib.h>
int main() {
foo(bar());
return 0;
}
编译它:
gcc -c mylib.c -I./
gcc -o myprog myprog.c -I./ mylib.o
现在的优势?
仅仅是为了阅读方便吗?
主要原因是_
可维护性:在您所描述的大型单体程序中,更改文件一部分中的代码可能会在其他地方产生意想不到的影响。回到我的第一份工作,我们的任务是加速驱动 3D 图形显示的代码。它是一个单一的、整体的、超过 5000main
行的函数(在宏伟的计划中并没有那么大,但大到令人头疼),我们所做的每一个改变都打破了其他地方的执行路径。这是一路写得很糟糕的代码(goto
s galore,实际上是数百个单独的变量,具有令人难以置信的信息名称,例如nv001x
,读起来像老式 BASIC 的程序结构,没有做任何事情的微优化,只是使代码更难阅读,脆弱得要命)但是将它们全部放在一个文件中会使糟糕的情况变得更糟。我们最终放弃了,并告诉客户我们要么必须从头开始重写整个东西,要么他们必须购买更快的硬件。他们最终购买了更快的硬件。
可重用性:一遍又一遍地编写相同的代码是没有意义的。如果您想出了一些通常有用的代码(例如,XML 解析库或通用容器),请将其保存在单独编译的源文件中,并在必要时将其链接。
可测试性:将函数分解为各自独立的模块,使您可以将这些函数与其余代码隔离开来进行测试;您可以更轻松地验证每个单独的功能。
可构建性:好的,所以“可构建性”不是一个真正的词,但是每次更改一两行代码时从头开始重新构建整个系统可能会很耗时。我曾在非常大的系统上工作过,其中完整的构建可能需要几个小时。通过分解代码,您可以限制必须重新构建的代码量。更不用说任何编译器都会对它可以处理的文件大小有一些限制。我上面提到的那个图形驱动程序?我们试图加速它的第一件事是在打开优化的情况下编译它(从 O1 开始)。编译器吃掉了所有可用内存,然后吃掉了所有可用的交换空间,直到内核崩溃并关闭了整个系统。我们真的无法建造启用了任何优化的代码(这可以追溯到 128 MB 是很多非常昂贵的内存的时代)。如果该代码被分成多个文件(见鬼,同一个文件中只有多个函数),我们就不会遇到这个问题。
并行开发:没有“能力”这个词,但是通过将源代码分解为多个文件和模块,您可以并行化开发。我在一个文件上工作,你在另一个文件上工作,其他人在第三个文件上工作,等等。我们不会冒险那样踩对方的代码。
仅仅是为了阅读方便吗?
不,它还可以为您节省大量编译时间;当您更改一个源文件时,您只需重新编译该文件,然后重新链接,而不是重新编译所有内容。但重点是将程序划分为一组分离良好的模块,这些模块比单个整体“blob”更容易理解和维护。
对于初学者,请尝试遵守 Rob Pike 的“数据占主导地位”的规则:围绕一堆数据结构(struct
通常是 's)设计您的程序,并对它们进行操作。将属于单个数据结构的所有操作放入单独的模块中。使所有static
不需要被模块外函数调用的函数。
易于阅读的一点是分解文件,但另一点是当您构建一个包含多个文件(头文件和源文件)的项目时,一个好的构建系统只会重新构建已修改的文件,从而缩短构建时间。
至于如何将一个单体文件分解为多个文件,有很多方法可以走。对我来说,我会尝试对功能进行分组,例如,所有输入处理都放在一个源文件中,输出放在另一个源文件中,而许多不同函数使用的函数则放在第三个源文件中。我会对结构/常量/宏、组相关结构/等做同样的事情。在单独的头文件中。我还将仅在单个源文件中使用的函数标记为static
,因此它们不能错误地从其他源文件中使用。
只是给你一个想法。
创建一个名为 print.c 的文件,将其放入:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void print_on_stdout(const char *msg) {
if (msg) fprintf(stdout, "%s\n", msg);
}
void print_on_stderr(const char *msg) {
if (msg) fprintf(stderr, "%s\n", msg);
}
创建一个名为 print.h 的文件,将其放入:
void print_on_stdout(const char *msg);
void print_on_stderr(const char *msg);
创建一个名为 main.c 的文件,将其放入:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "print.h"
int main() {
print_on_stdout("test on stdout");
print_on_stderr("test on stderr");
return 0;
}
现在,对于每个 C 文件,编译:
gcc -Wall -O2 -o print.o -c print.c
gcc -Wall -O2 -o main.o -c main.c
然后链接编译的文件以生成可执行文件:
gcc -Wall -O2 -o test print.o main.o
运行 ./test 并享受。
好吧,我不是专家,但我总是试图在比函数更大的实体中思考。如果我有一组逻辑上属于一起的函数,我将它放入一个单独的文件中。通常,如果功能相似,并且有人想要其中一个功能,他可能还需要该组中的一些其他功能。
拆分单个文件的需要与您为文件使用不同文件夹的原因相同:人们希望对众多功能进行一些逻辑组织,这样他们就不需要 grep 巨大的单个源文件找到需要的。这样,当您考虑/开发程序的某些固定部分时,您可以忘记程序的不相关部分。
拆分的另一个原因可能是您可以通过在标题中不提及它来从其余代码中隐藏一些内部函数。这种方式明确地将内部函数(仅在.c
文件内部需要)与程序的外部“宇宙”感兴趣的函数分开。
一些更高级的语言甚至将“函数属于一起”的概念扩展为“在同一事物上工作的函数,作为一个实体呈现”——并将其称为类。
拆分的另一个历史原因是单独的编译功能。如果您的编译器速度很慢(例如,C++ 经常出现这种情况),将代码拆分为多个文件意味着如果您只修改一个位置,则很有可能只需要重新编译一个文件以获取更改. 由于现代 C 编译器与典型处理器速度相比并没有那么慢,因此这对您来说可能不是问题。