C语言中.h文件的不常用

在阅读有关过滤的文章时,我发现了一些奇怪的使用.h文件 – 用它来填充系数的数组:

 #define N 100 // filter order float h[N] = { #include "f1.h" }; //insert coefficients of filter float x[N]; float y[N]; short my_FIR(short sample_data) { float result = 0; for ( int i = N - 2 ; i >= 0 ; i-- ) { x[i + 1] = x[i]; y[i + 1] = y[i]; } x[0] = (float)sample_data; for (int k = 0; k < N; k++) { result = result + x[k]*h[k]; } y[0] = result; return ((short)result); } 

那么,通常使用float h[N] = { #include "f1.h" }; 这条路?

#include这样的预处理指令只是做一些文本replace(见GCC里的GNU cpp的文档)。 它可以发生在任何地方(评论和string文字之外)。

但是, #include应该有#作为它的第一个非空白字符。 所以你会编码

 float h[N] = { #include "f1.h" }; 

原来的问题没有#include自己的行,所以有错误的代码。

这是不正常的做法,但这是允许的做法。 在这种情况下,我会build议使用其他扩展名比.h例如#include "f1.def"#include "f1.data"

请求编译器向您显示预处理的表单。 用GCC编译时使用gcc -C -E -Wall yoursource.c > yoursource.i然后用编辑器或寻呼机查看生成的yoursource.i

我其实更喜欢在自己的源文件中有这样的数据。 所以我会build议生成一个自包含的h-data.c文件,例如使用一些像GNU awk这样的工具(所以文件h-data.c将以const float h[345] = {并且以};结束}; 。)如果它是一个常量数据,最好声明它是const float h[] (所以它可以坐在像Linux上的.rodata这样的只读段中)。 另外,如果embedded数据很大,编译器可能需要一些时间(无用地)对其进行优化(然后可以在不进行优化的情况下快速编译h-data.c )。

那么,通常使用float h [N] = {#include“f1.h”}; 这条路?

这是不正常的,但它是有效的(将被编译器接受)。

使用这个function的好处是:它为您提供了一个思考更好解决scheme所需的less量工作。

缺点:

  • 它会增加您的代码的WTF / SLOC比率。
  • 它会在客户端代码和包含的代码中引入不寻常的语法。
  • 为了理解f1.h的作用,你必须看看它是如何使用的(这意味着要么你需要添加额外的文档到你的项目来解释这个野兽,否则人们将不得不阅读代码,看看它是什么意味着 – 两种解决scheme都不可以

这是在编写代码之前额外花费20分钟时间考虑的情况之一,在项目的整个生命周期内,您可以节省数十小时的诅咒代码和开发人员。

正如在前面的答案中已经解释的那样,这不是正常的做法,而是一个有效的做法。

这是一个替代解决scheme:

文件f1.h:

 #ifndef F1_H #define F1_H #define F1_ARRAY \ { \ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, \ 10,11,12,13,14,15,16,17,18,19, \ 20,21,22,23,24,25,26,27,28,29, \ 30,31,32,33,34,35,36,37,38,39, \ 40,41,42,43,44,45,46,47,48,49, \ 50,51,52,53,54,55,56,57,58,59, \ 60,61,62,63,64,65,66,67,68,69, \ 70,71,72,73,74,75,76,77,78,79, \ 80,81,82,83,84,85,86,87,88,89, \ 90,91,92,93,94,95,96,97,98,99 \ } // Values above used as an example #endif 

文件f1.c:

 #include "f1.h" float h[] = F1_ARRAY; #define N (sizeof(h)/sizeof(*h)) ... 

不,这不是正常的做法。

直接使用这种格式几乎没有什么好处,而是可以在单独的源文件中生成数据,或者在这种情况下至less可以形成完整的定义。


然而,有一个“模式”,其中包括一个文件在这样的随机地方: Xmacros ,如那些 。

X-macro的用法是定义一个集合并在不同的地方使用它。 单一的定义确保了整体的一致性。 作为一个微不足道的例子,考虑一下:

 // def.inc MYPROJECT_DEF_MACRO(Error, Red, 0xff0000) MYPROJECT_DEF_MACRO(Warning, Orange, 0xffa500) MYPROJECT_DEF_MACRO(Correct, Green, 0x7fff00) 

现在可以使用多种方式:

 // MessageCategory.hpp #ifndef MYPROJECT_MESSAGE_CATEGORY_HPP_INCLUDED #define MYPROJECT_MESSAGE_CATEGORY_HPP_INCLUDED namespace myproject { enum class MessageCategory { # define MYPROJECT_DEF_MACRO(Name_, dummy0_, dummy1_) Name_, # include "def.inc" # undef MYPROJECT_DEF_MACRO NumberOfMessageCategories }; // enum class MessageCategory enum class MessageColor { # define MYPROJECT_DEF_MACRO(dumm0_, Color_, dummy1_) Color_, # include "def.inc" # undef MYPROJECT_DEF_MACRO NumberOfMessageColors }; // enum class MessageColor MessageColor getAssociatedColorName(MessageCategory category); RGBColor getAssociatedColorCode(MessageCategory category); } // namespace myproject #endif // MYPROJECT_MESSAGE_CATEGORY_HPP_INCLUDED 

很久以前,人们滥用预处理器。 请参阅XPM文件格式 ,以便人们可以:

 #include "myimage.xpm" 

在他们的C代码。

这不再被认为是好的。

OP的代码看起来像C所以我会谈论C

为什么过度使用预处理器?

预处理器#include指令旨在包含源代码。 在这种情况下,在OP的情况下,它不是真正的源代码,而是数据

为什么它被认为是坏的?

因为它非常不灵活 。 如果不重新编译整个应用程序,则无法更改图像。 甚至不能包含两个具有相同名称的图像,因为它会产生不可编译的代码。 在OP的情况下,他不能在没有重新编译应用程序的情况下更改数据。

另一个问题是它在数据和源代码之间创build了紧密耦合 ,例如数据文件必须至less包含由源代码文件中定义的Nmacros指定的值的数量。

紧耦合还会为数据添加格式,例如,如果要存储10×10个matrix值,则可以select在源代码中使用单维数组或二维数组。 从一种格式切换到另一种格式会改变数据文件。

加载数据的这个问题很容易通过使用标准的I / O函数来解决。 如果你真的需要包含一些默认的图像,你可以给你的源代码中的图像默认path 。 这将至less允许用户更改此值(在编译时通过#define-D选项),或者更新映像文件而无需重新编译。

在OP的情况下,如果将FIR系数和x, y向量作为parameter passing,则其代码将更具可重用性。 你可以创build一个struct来保存这些值。 代码效率不高,即使使用其他系数也可以重用 。 除非用户通过命令行参数覆盖文件path,否则系统可以从默认文件启动加载 。 这将消除对任何全局variables的需求,并明确程序员的意图。 您甚至可以在两个线程中使用相同的FIR函数,前提是每个线程都有自己的struct

什么时候可以接受?

当你不能做dynamic加载的数据。 在这种情况下, 你必须静态加载你的数据 ,你不得不使用这种技术。

我们应该注意到,无法访问文件意味着您正在为一个非常有限的平台进行编程,因此您必须进行权衡。 例如,如果您的代码在微控制器上运行,就会出现这种情况。

但即使在这种情况下,我宁愿创build一个真正的C源文件,而不是从半格式文件中包含浮点值。

例如,提供一个实际的C函数来返回系数,而不是具有半格式的数据文件。 这个C函数可以在两个不同的文件中定义,一个使用I / O进行开发,另一个返回静态数据。 你会编译正确的源文件conditionnaly。

在某些情况下,需要使用外部工具来生成基于其他文件的源代码的C文件,使用外部工具生成具有过多的硬编码到生成工具的代码量的C文件,或者使用代码#include指令以各种“不寻常”的方式。 在这些方法中,我会build议后者 – 尽pipe是恶心的 – 往往可能是最less的罪恶。

我build议避免使用.h后缀来处理不遵守与头文件相关的常规约定的文件(例如,通过包含方法定义,分配空间,需要一个不寻常的包含上下文(例如在方法的中间),需要多个包含不同的macros定义等。我通常也避免使用.c.cpp文件通过#include包含到其他文件,除非这些文件主要是单独使用[我可能在某些情况下,例如有一个文件fooDebug.c包含#define SPECIAL_FOO_DEBUG_VERSION [新行]`#include“foo.c”“`如果我希望从同一个源生成具有不同名称的两个目标文件,其中一个是肯定的“普通”版本。

我通常的做法是使用.i作为人工生成的或机器生成的文件的后缀,这些文件被devise为包含在其他C或C ++源文件中,但通常情况下, 如果文件是机器生成的,我通常会将生成工具包含在第一行中,并在其中标注用于创build该工具的注释。

顺便说一句,我曾经使用过的一个技巧就是当我想让一个程序只用一个batch filebuild立,而没有任何第三方工具,但是要计算它被build立​​了多less次。 在我的batch file中,我包括echo +1 >> vercount.i ; 然后在文件vercount.c,如果我没记错的话:

 const int build_count = 0 #include "vercount.i" ; 

最终的结果是,我得到了一个在每个版本上都增加的值,而不必依靠任何第三方工具来生成它。

正如在评论中已经说过的,这不是正常的做法。 如果我看到这样的代码,我试图重构它。

例如f1.h可能看起来像这样

 #ifndef _f1_h_ #define _f1_h_ #ifdef N float h[N] = { // content ... } #endif // N #endif // _f1_h_ 

和.c文件:

 #define N 100 // filter order #include “f1.h” float x[N]; float y[N]; // ... 

这对我来说似乎更加正常 – 尽pipe上面的代码还可以进一步改进(例如,消除全局variables)。

加上其他人所说的 – f1.h的内容必须是这样的:

 20.0f, 40.2f, 100f, 12.40f -122, 0 

因为f1.h的文本正在初始化有问题的数组!

是的,它可能有评论,其他function或macros使用,expression等

对我来说这是正常的做法。

预处理器允许您将源文件拆分成任意数量的块,这些块由#include指令组装。

当你不想用冗长的/不可读的部分(比如数据初始化)来混淆代码时,这是很有意义的。 事实certificate,我的logging“数组初始化”文件是11000行长。

当代码的某些部分是由一些外部工具自动生成的时候,我也会使用它们:使工具生成他的块非常方便,并将其包含在手工编写的其余代码中。

我有一些这样的函数包含一些function,有几个替代实现取决于处理器,其中一些使用内联汇编。 包含使代码更易于pipe理。

按照传统,#include指令已被用于包含头文件,即暴露API的声明集。 但没有任何要求。

当预处理程序find#include指令时,它只是打开指定的文件并插入它的内容,就好像文件的内容将被写入指令的位置一样。

我看到人们想要重构,并说这是邪恶的。 我仍然在某些情况下使用。 正如有些人说这是一个预处理指令,所以包括文件的内容。 以下是我使用的一个例子:build立随机数字。 我build立随机数字,我不希望这样做,每次我都没有在运行时编译。 所以另外一个程序(通常是一个脚本)只是用生成的数字填充文件。 这可以避免手工复制,这可以很容易地改变数字,生成它们的algorithm和其他细节。 你不能轻易责备这种做法,在这种情况下,这只是正确的做法。

我使用了OP的技术,为variables声明的数据初始化部分放置一个包含文件很长一段时间。 就像OP一样,生成了包含的文件。

我将生成的.h文件分离到一个单独的文件夹中,以便可以轻松识别它们:

 #include "gensrc/myfile.h" 

当我开始使用Eclipse时,这个scheme崩溃了。 Eclipse语法检查不够复杂,无法处理这个问题。 它会通过报告没有的语法错误来做出反应。

我向Eclipse邮件列表报告了示例,但似乎没有太多的兴趣来修复语法检查。

我改变了我的代码生成器以获取额外的参数,所以它可以生成整个variables声明,而不仅仅是数据。 现在它生成语法正确的包含文件。

即使我不使用Eclipse,我认为这是一个更好的解决scheme。

在Linux内核中,我发现了一个例子,IMO,漂亮。 如果你看看cgroup.h头文件

http://lxr.free-electrons.com/source/include/linux/cgroup.h

你可以find指令#include <linux/cgroup_subsys.h>两次,在macrosSUBSYS(_x)不同定义之后; 这个macros在cgroup_subsys.h中用来声明几个Linux cgroup的名字(如果你不熟悉cgroup,它们是Linux提供的用户友好界面,必须在系统启动时进行初始化)。

在代码片段中

 #define SUBSYS(_x) _x ## _cgrp_id, enum cgroup_subsys_id { #include &ltlinux/cgroup_subsys.h&gt CGROUP_SUBSYS_COUNT, }; #undef SUBSYS 

在cgroup_subsys.h中声明的每个SUBSYS(_x)成为enum cgroup_subsys_idtypes的元素,而在代码片段

 #define SUBSYS(_x) extern struct cgroup_subsys _x ## _cgrp_subsys; #include &ltlinux/cgroup_subsys.h&gt #undef SUBSYS 

每个SUBSYS(_x)变成struct cgroup_subsystypesvariables的声明。

这样,内核程序员可以通过修改cgroup_subsys.h来添加cgroups,而预处理器会自动在初始化文件中添加相关的枚举值/声明。