何时不在 C 中创建单独的接口 (.h) 和实现 (.c)?

When to not create a separate interface (.h) and implementation (.c) in C?

我目前正在过渡到使用 C 语言工作,主要专注于开发大型库。我来自大量基于 C++ 的应用程序编程,尽管我不能声称精通这两种语言。

我很好奇的是,许多流行的开源库何时以及为什么选择不将它们的代码与 .h 文件和相应的 .c 文件以 1-1 的关系分开 - 即使在 .h 文件的情况下也是如此。 c 没有生成可执行文件。

过去,我一直认为以这种方式构建代码不仅在组织方面是最佳的,而且对于链接目的也是如此——而且我不明白 C 语言缺少 OOD 功能会有什么影响影响这一点(更不用说不分离实现和接口也发生在 C++ 库中)。

仅当某些内容包含在多个编译单元中时,您才需要一个单独的 .h 文件。

"keep things local unless you have to share" 智慧的一种形式。

In the past I'd been lead to believe that structuring code in this manner is optimal not only in organization, but also for linking purposes -- and I don't see how the lack of OOD features of C would effect this (not to mention not separating the implementation and interface also occurs in C++ libraries).

在传统的 C 代码中,您总是将声明放在 .h 文件中,将定义放在 .c 文件中。这确实是为了优化编译——原因是,它使每个编译单元占用最少的内存,因为它只有输出代码所需的定义,如果你管理得当,它只有它也需要声明。它还可以很容易地看出您没有违反单一定义规则。

在现代机器中,从没有可怕的构建时间的角度来看,这样做不太重要——机器现在有很多内存。

  • 在 C++ 中,模板文件通常只在 header.
  • 近年来,您也有人尝试使用 so-called "Unity Builds",其中您有一个包含所有其他源文件的编译单元,并且您可以一次构建它。看这里:The benefits / disadvantages of unity builds?

所以今天,1-1 通信主要是风格/组织方面的事情。

在 C 中没有内在的技术原因来提供成对的 .c.h 文件。当然没有与链接相关的原因,因为在传统的 C 用法中,.c .h 文件都与链接没有任何直接关系。

将与多个 .c 文件相关的声明收集到较少数量的 .h 文件中是完全可能的,并且具有潜在的优势。仅提供一个或少量 header 文件可以更轻松地使用关联库:您无需记住或查找每个函数、类型的声明需要哪些 header,或变量。

然而,这样做至少会产生三个后果:

  • 你很难确定在哪里可以找到集体 header 中声明的函数的实现。
  • 你使你的项目更容易受到大规模重建级联的影响,因为大多数 object 文件依赖于少数 header 中的一个或多个,并且对你的函数签名的更改或添加都影响少数 header 中的一个。
  • 编译器必须花更多的精力来消化一个大的 header 和所有库的声明,而不是消化一个或少量的 header 狭隘地关注给定的特定声明.c 文件,正如@ChrisBeck 观察到的那样。然而,与 C++ 代码相比,这对 C 代码来说往往不是一个问题。

一个非常非常基本但完全现实的场景,其中不需要 .h 和 .c 文件之间的 1-1 关系,甚至是不可取的:

main.h
//A lib's/extension/applications' main header file
//for user API -> obfuscated types
typedef struct _internal_str my_type;
//API functions
my_type * init_resource( void );//some arguments will probably be required
//get helper resource -> not part of the API, but the lib uses it internally in all translation units
const struct helper_str *get_help( void );

现在这个 get_help 函数,正如评论所说,不是库的一部分 API。但是,构成该库的所有 .c 文件都在使用它,并且 get_help 函数在 helper.c 翻译单元中定义。该文件可能如下所示:

#include "main.h"
#include <third/party.h>
//static functions
static
third_party_type *init_external_resource( void )
{
    //implement this
}
static
void cleanup_stuff(third_party_type *p)
{
    third_party_free(p);
}
const struct helper_str *get_help( void )
{
    //implementation of external function
}

好的,这很方便:不添加另一个 .h 文件,因为您只调用了 1 个外部函数。但这并不是不使用单独的头文件的 好的 理由,对吧?同意。这不是一个很好的理由。

但是:假设您的代码很大程度上依赖于这个第三方库,并且您正在构建的任何组件的每个组件都使用该库的不同部分。 "help" 您 need/want 来自此 helper.c 文件的内容可能有所不同。那时您可以决定创建多个头文件,以控制 helper.c 文件在项目内部的使用方式。例如:你在翻译单元 X 和 Y 中有一些日志记录,这些文件可能包括这样的文件:

//specific_help.h
char * err_to_log_msg(int error_nr);//relevant arguments, of course...

虽然一个文件不接近输出,但是,例如,管理线程安全或信号,可能想要调用 helper.c 中的一个函数,以在检测到某些事件时释放一些资源(信号、击键、鼠标事件……等等)。此文件可能包含一个头文件,如:

//system_help.h
void free_helper_resources(int level);

所有这些头文件 link 返回到 helper.c 中定义的函数,但您最终可能会为单个 c 文件得到 10 个头文件。

一旦你有了这些不同的头文件来公开一系列功能,你可能最终会向这些头文件中的每一个添加特定的 typedef,这取决于这两个组件如何交互......嗯,无论如何,这是一个品味问题。

许多人只会选择一个头文件与 helper.c 文件一起使用,并将其包含在内。他们可能不会使用他们有权访问的一半功能,但他们需要担心的文件会更少。
另一方面,如果其他人开始修改他们的代码,他们可能会想在某个不属于的文件中添加函数:他们可能会在 signal/event 处理文件中添加日志记录函数,反之亦然

最后:运用你的常识,不要暴露太多。删除 static 关键字很容易,如果确实需要,只需将原型添加到头文件即可。