【问题标题】:Function prototype in header file doesn't match definition, how to catch this?头文件中的函数原型与定义不匹配,如何捕捉?
【发布时间】:2013-02-14 18:31:28
【问题描述】:

(我发现这个问题相似但不重复: How to check validity of header file in C programming language)

我有一个函数实现和一个头文件中的不匹配原型(同名,不同类型)。头文件包含在使用函数的 C 文件中,但不包含在定义函数的文件中。

这是一个最小的测试用例:

header.h:

void foo(int bar);

文件 1.c:

#include "header.h"
int main (int argc, char * argv[])
{
    int x = 1;
    foo(x);
    return 0;
}

文件 2.c:

#include <stdio.h>

typedef struct {
    int x;
    int y;
} t_struct;

void foo (t_struct *p_bar)
{
    printf("%x %x\n", p_bar->x, p_bar->y);
}

我可以用 VS 2010 编译它,没有错误或警告,但不出所料,当我运行它时它会出现段错误。

  • 编译器没问题(这个我明白)
  • 链接器没有捕捉到它(这让我有点惊讶)
  • 静态分析工具 (Coverity) 没有捕捉到它(对此我感到非常惊讶)。

我怎样才能发现这些类型的错误?

[编辑:我意识到如果我在file2.c 中也使用#include "header.h",编译器会抱怨。但是我有一个庞大的代码库,并不总是可能或不适合保证函数原型的所有头文件都包含在实现文件中。]

【问题讨论】:

  • #include "header.h" 也在实现文件中。
  • @DanielFischer:对不起,我意识到这可以在微不足道的情况下解决它,但这个最小的例子只是为了演示目的。实际代码库很大,很可能在多个地方都有原型化的函数,这些函数不能全部#include 到实现中。
  • 嗯。通常的方法是将每个函数原型放在 one 头文件中[当然每个头文件通常没有一个原型],并将该头文件包含在使用该函数的每个文件和它所在的文件中定义。你能举一个不合适的例子吗?
  • 也许有一个脚本会遍历所有源代码并确保 file.c 包含 file.h。
  • 或者你是说你的前任把代码库彻底搞砸了,对你来说太大而无法修复?

标签: c linker static-analysis function-prototypes


【解决方案1】:

file1.cfile2.c 中包含相同的头文件。这几乎可以防止原型冲突。

否则编译器无法检测到此类错误,因为编译器在编译file1.c时看不到该函数的源代码。相反,它只能信任已经给出的签名。

至少理论上,如果目标文件中存储了额外的元数据,链接器可以检测到这种不匹配,但我不知道这是否可行。

【讨论】:

  • 我应该在问题中说清楚 - 我意识到这会解决它,但我有一个庞大的代码库,不能保证所有对函数进行原型设计的头文件都可以或应该包含在实现文件中.
  • 那么我会说你有一个巨大的重构问题需要解决,然后才能信任你的链接器
  • @Elemental,我担心你可能是对的。这是我希望避免的!
  • 这在过去很长时间内通过运行lint(1) 解决了,我相信splint(遗憾地停产)做了很多它的前任所做的事情。但是,最好的办法是按照答案重新组织代码。
【解决方案2】:

-Werror-implicit-function-declaration-Wmissing-prototypes 或您支持的编译器之一上的等效项。如果声明没有在全局定义之前,它会出错或报错。

以某种形式的严格 C99 模式编译程序也应该生成这些消息。 GCC、ICC 和 Clang 都支持此功能(不确定 MS 的 C 编译器及其当前状态,因为 VS 2005 或 2008 是我用于 C 的最新版本)。

【讨论】:

  • 但是如果我在不同的地方有相同的函数原型,并且我#include其中一个并且恰好是正确的原型,那么这将无济于事:(
  • @Vicky 一种解决我使用过的 that 问题的方法是创建一个包含包/库中所有头文件的头文件——每个库都为所有人提供一个头文件它的头文件,然后您可以轻松地将它们组合起来并轻松保持最新状态,最后将所有这些依赖库的元头文件添加到一个翻译中,该翻译代表最终链接的可执行文件中使用的所有依赖项/库。这将补充我上面指定的选项。对于某些项目来说,这可能不是一个非常合理的更改计数,(续)
  • (续)显然它的扩展高度有一个上限。但这些大量翻译也可以存在于单独的目标中,因此它们不需要影响您的主要目标的构建时间。
  • 在正常情况下,这两个警告都会默认启用。
【解决方案3】:

您可以使用 http://frama-c.com 提供的 Frama-C 静态分析平台。

在你的例子中你会得到:

$ frama-c 1.c 2.c
[kernel] preprocessing with "gcc -C -E -I.  1.c"
[kernel] preprocessing with "gcc -C -E -I.  2.c"
[kernel] user error: Incompatible declaration for foo:
                     different type constructors: int vs. t_struct *
                     First declaration was at  header.h:1
                     Current declaration is at 2.c:8
[kernel] Frama-C aborted: invalid user input.

希望这会有所帮助!

【讨论】:

    【解决方案4】:

    看起来这对于 C 编译器来说是不可能的,因为它是如何将函数名称映射到符号对象名称的方式(直接,而不考虑实际签名)。

    但这在 C++ 中是可能的,因为它使用依赖于函数签名的 name mangling。所以在 C++ 中,void foo(int)void foo(t_struct*) 在链接阶段会有不同的名称,链接器会引发错误。

    当然,将庞大的 C 代码库依次切换到 C++ 并不容易。但是您可以使用一些相对简单的解决方法 - 例如将单个 .cpp 文件添加到您的项目中,并将所有 C 文件包含到其中(实际上是使用一些脚本生成它)。

    以您的示例和 VS2010 为例,我将 TestCpp.cpp 添加到项目中:

    #include "stdafx.h"
    
    namespace xxx
    {
    #include "File1.c"
    #include "File2.c"
    }
    

    结果是链接器错误 LNK2019:

    TestCpp.obj : error LNK2019: unresolved external symbol "void __cdecl xxx::foo(int)" (?foo@xxx@@YAXH@Z) referenced in function "int __cdecl xxx::main(int,char * * const)" (?main@xxx@@YAHHQAPAD@Z)
    W:\TestProjects\GenericTest\Debug\GenericTest.exe : fatal error LNK1120: 1 unresolved externals
    

    当然,对于庞大的代码库来说,这并不容易,可能还有其他问题导致编译错误,如果不更改代码库就无法修复。您可以通过使用条件 #ifdef 保护 .cpp 文件内容来部分缓解它,并且仅用于定期检查而不是常规构建。

    【讨论】:

      【解决方案5】:

      每个foo.c 文件中定义的每个(非静态)函数都应该在相应的foo.h 文件中有一个原型,foo.c 应该有#include "foo.h"。 (main 是唯一的例外。)foo.h 不应包含任何未在foo.c 中定义的函数的原型。

      每个函数都应该精确地原型化一次

      如果.h 文件不包含任何原型,则您可以拥有没有相应.c 文件的文件。唯一没有对应.h 文件的.c 文件应该是包含main 的文件。

      您已经知道这一点,而您的问题是您有一个庞大的代码库,但尚未遵循此规则。

      那么你如何从这里到那里?这就是我可能会这样做的方式。

      第 1 步(需要一次通过您的代码库):

      • 对于每个文件foo.c,创建一个文件foo.h(如果它尚不存在)。在foo.c 的顶部附近添加"#include "foo.h"。如果您对 .h.c 文件应该存在的位置有约定(在同一目录中或并行的 includesrc 目录中,请遵循它;如果没有,请尝试引入这样的约定)。
      • 对于foo.c 中的每个函数,将其原型复制到foo.h,如果它不存在的话。使用复制和粘贴来确保一切保持一致。 (参数名称在原型中是可选的,在定义中是强制性的;我建议在这两个地方都保留名称。)
      • 进行完整构建并修复出现的任何问题。

      这不会解决您的所有问题。对于某些功能,您仍然可以有多个原型。但是您会发现两个标头对于同一函数的原型不一致并且两个标头都包含在同一个翻译单元中的任何情况。

      一旦一切都构建好了,您应该拥有一个至少与您开始时一样正确的系统。

      第 2 步:

      • 对于每个文件foo.h,删除所有未在foo.c 中定义的函数原型。
      • 进行完整构建并修复出现的任何问题。如果bar.c 调用在foo.c 中定义的函数,则bar.c 需要#include "foo.h".

      对于这两个步骤,“修复出现的任何问题”阶段可能会很漫长且乏味。

      如果您不能一次性完成所有这些,您可以逐步完成很多。从一个或几个 .c 文件开始,清理它们的 .h 文件,并删除在其他地方声明的任何额外原型。

      每当您发现调用使用了不正确的原型的情况时,请尝试找出执行该调用的情况,以及它是如何导致您的应用程序行为异常的。创建一个错误报告并向你的回归测试套件添加一个测试(你有一个,对吧?)。您可以向管理层证明,由于您所做的所有工作,测试现在通过了;你真的不只是在胡闹。

      可以解析 C 的自动化工具可能很有用。 Ira Baxter 有一些建议。 ctags 也可能有用。根据您的代码格式,您可能可以将一些不需要完整 C 解析器的工具放在一起。例如,您可以使用grepsedperlfoo.c 文件中提取函数定义列表,然后手动编辑该列表以消除误报。

      【讨论】:

        【解决方案6】:

        很明显(“我有一个庞大的代码库”)你不能手动完成。

        您需要的是一个自动化工具,它可以在编译器看到它们时读取您的源文件,收集所有函数原型和定义,并验证所有定义/原型是否匹配。我怀疑你会发现这样的工具。

        当然,这种匹配多检查签名,这需要编译器前端之类的东西来比较签名。

        考虑

             typedef int T;
             void foo(T x);
        

        在一个编译单元中,并且

              typedef float T;
              void foo(T x);
        

        在另一个。您不能只比较签名“行”是否相等;你需要一些可以在检查时解析类型的东西。

        如果您使用的是 C 的 GCC 方言,GCCXML 可能会有所帮助;它从源文件中提取顶级声明作为 XML 块。不过,我不知道它是否会解析 typedef。您显然必须建立(相当多的)支持以在中心位置(数据库)收集定义并进行比较。比较 XML 文档的等价物至少相当简单,如果它们以常规方式格式化,则相当容易。这可能是您最容易的选择。

        如果这不起作用,您需要具有可自定义的完整 C 前端的东西。众所周知,GCC 是可用的,并且是难以定制的。 Clang 可用,并且可能会为此服务,但 AFAIK 仅适用于 GCC 方言。

        我们的 DMS 软件再造工具包具有适用于 C 的许多方言(GCC、MS、GreenHills 等)的 C 前端(具有完整的预处理能力),并构建具有完整类型信息的符号表。使用 DMS,您可能能够(取决于应用程序的实际规模)简单地处理所有编译单元,并仅为每个编译单元构建符号表。检查符号表条目是否“匹配”(根据编译器规则兼容,包括使用等效的 typedef)是内置在 C 前端中的;需要做的就是编排读取,并在各个编译单元的全局范围内为所有符号表条目调用匹配逻辑。

        无论您使用 GCC/Clang/DMS 执行此操作,都需要拼凑一个自定义工具。因此,与构建此类定制工具所需的精力相比,您已经决定了减少惊喜的重要性。

        【讨论】:

          猜你喜欢
          • 2018-08-22
          • 1970-01-01
          • 1970-01-01
          • 2019-07-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多