(我发现这个问题类似但不重复: How to check validity of header file in C programming language)
我有一个函数实现,以及一个头文件中的不匹配原型(同名,不同类型)。头文件包含在使用该函数的C文件中,但不包含在定义该函数的文件中。
这是一个最小的测试用例:
header.h:
void foo(int bar);
FILE1.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编译它,没有任何错误或警告,但毫不奇怪,当我运行它时会出现段错误。
我怎样才能发现这些错误?
[编辑:我意识到如果我在file2.c中#include“header.h”,编译器会抱怨。但是我有一个巨大的代码库,并不总是可能或适当的保证函数原型的所有头都包含在实现文件中。]
答案 0 :(得分:5)
file1.c
和file2.c
都包含相同的头文件。这几乎可以防止相互冲突的原型。
否则,编译器无法检测到这样的错误,因为编译器编译file1.c
时编译器看不到该函数的源代码。相反,它只能信任已经给出的签名。
至少从理论上讲,如果在目标文件中存储了额外的元数据,链接器就能够检测到这种不匹配,但我不知道这是否真的可行。
答案 1 :(得分:5)
-Werror-implicit-function-declaration
,-Wmissing-prototypes
或您支持的某个编译器上的等效文件。如果声明不在全局定义之前,它将会出错或抱怨。
以某种形式的严格C99模式编译程序也应该生成这些消息。 GCC,ICC和Clang都支持这一功能(不确定MS的C编译器及其当前状态,因为VS 2005或2008是我用于C的最新版本。)
答案 2 :(得分:2)
您可以使用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.
希望这有帮助!
答案 3 :(得分:1)
每个foo.c
文件中定义的每个(非静态)函数都应在相应的foo.h
文件中包含原型,而foo.c
应该具有#include "foo.h"
。 (main
是唯一的例外。)foo.h
不应包含foo.c
中未定义的任何函数的原型。
每个函数都应该一次原型。
如果文件不包含任何原型,您可以拥有.h
个没有相应.c
个文件的文件。唯一没有相应.c
文件的.h
文件应该是包含main
的文件。
您已经知道这一点,而您的问题是您有一个庞大的代码库,其中没有遵循此规则。
那你怎么从这里到那里?这是我可能会这样做的。
第1步(需要对代码库进行一次传递):
foo.c
,如果文件foo.h
尚不存在,请创建该文件。在"#include "foo.h"
顶部附近添加foo.c
。如果你有.h
和.c
文件存在的约定(在同一目录或并行include
和src
目录中,请遵循它;如果没有,请尝试引入这样的惯例)。foo.c
中的每个功能,如果原型不存在,请将其原型复制到foo.h
。使用复制粘贴确保一切都保持一致。 (参数名称在原型中是可选的,在定义中是强制性的;我建议在两个地方保留名称。)这不会解决你所有的问题。对于某些功能,您仍然可以拥有多个原型。但是你会发现任何两个标题具有相同函数的不一致原型的情况和两个标题都包含在同一个翻译单元中。
一旦所有东西都干净利落,你应该拥有一个至少和你开始时一样正确的系统。
第2步:
foo.h
,删除foo.c
中未定义的函数的所有原型。bar.c
调用foo.c
中定义的函数,则bar.c
需要#include "foo.h".
对于这两个步骤,“修复任何出现的问题”阶段可能是漫长而乏味的。
如果你不能一次完成所有这些,你可以逐步完成很多工作。从一个或几个.c
文件开始,清理他们的.h
文件,并删除在其他地方声明的任何额外原型。
每当您发现调用使用不正确的原型的情况时,请尝试找出执行该调用的情况,以及它如何导致您的应用程序行为异常。创建一个错误报告并将测试添加到回归测试套件中(你有一个,对吧?)。您可以向管理层证明,由于您已完成的所有工作,测试现已通过;你真的不只是搞乱了。
可以解析C的自动化工具可能很有用。艾拉巴克斯特有一些建议。 ctags
也可能有用。根据代码的格式化,您可以将一些不需要完整C解析器的工具放在一起。例如,您可以使用grep
,sed
或perl
从foo.c
文件中提取函数定义列表,然后手动编辑列表以消除误报。
答案 4 :(得分:1)
使用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
当然,对于庞大的代码库来说,这不会那么容易,可能会有其他问题导致编译错误,如果不更改代码库就无法修复。您可以通过使用条件.cpp
保护#ifdef
文件内容来部分缓解它,并且仅用于定期检查而不是常规构建。
答案 5 :(得分:0)
它很明显(“我有一个巨大的代码库”)你不能手工完成这个。
您需要的是一个自动化工具,可以在编译器看到源文件时读取源文件,收集所有函数原型和定义,并验证所有定义/原型是否匹配。我怀疑你会找到这样的工具。
当然,这个匹配很多检查签名,这需要像编译器的前端来比较签名。
考虑
typedef int T;
void foo(T x);
在一个编译单元中,
typedef float T;
void foo(T x);
另一个。你不能只是比较签名“线”的平等;你需要能够在检查时解决类型的东西。
如果你使用GCC的C语言,GCCXML可能会有所帮助;它从源文件中提取顶级声明为XML块。不过,我不知道它是否会解析typedef。您显然必须构建(相当多)支持以在中心位置(数据库)收集定义并进行比较。比较XML文档的等价物至少相当简单,如果它们以常规方式格式化,则非常容易。这可能是你最容易的赌注。
如果这不起作用,您需要具有可以自定义的完整C前端的东西。 GCC是着名的,并且很难定制。 Clang可用,可能会为此服务,但AFAIK仅适用于GCC方言。
我们的DMS软件再造工具包具有C前端(具有完全预处理功能),适用于C(GCC,MS,GreenHills等)的多种方言,并构建具有完整类型信息的符号表。使用DMS,您可以(根据应用程序的实际规模)简单地处理所有编译单元,并为每个编译单元构建符号表。检查符号表条目是否“匹配”(根据编译器规则兼容,包括使用等效的typedef)内置于C前端;所有人需要做的是编排读数,并在各个编译单元的全局范围内调用所有符号表条目的匹配逻辑。
无论您是使用GCC / Clang / DMS进行此操作,拼凑自定义工具还是相当多的工作。因此,与构建此类自定义工具的能量相比,您已经决定了减少吸引力所需的重要性。