.h文件在C中的异常用法


77

在阅读有关过滤的文章时,我发现.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" };这种方式是正常的做法吗?


25
这很罕见。也就是说,它很少使用:f1.h可能是由外部工具生成的,并用作您(已编译)程序的输入。外部工具通常会生成完整的头文件,例如float h[N] = { ... }在inside中f1.h这里的例子
ta.speot。是

8
它不应编译,请参见C11 6.10.2中的语法。# include "q-char-sequence" new-line。新线在哪里?
伦丁2014年

2
那么...所有这些说这是有效代码的人如何设法将其编译?我无法使用gcc进行管理,error: "stray #"无论通过哪种标准或选项,我都能得到。
伦丁2014年

2
@Lundin:一些编译器允许在任何地方使用预处理器位,而在其他情况下,您只需插入换行符即可。不会破坏这个特定的例子。
Mooing Duck

2
@ user657267:很少见的情况惯例。我的职业生涯大约有三到四个场合,这挽救了我的生活。正如Jason R所写的那样,令人欣慰的是,这种方式包含了系数(没有示例中的语法错误)。或通常来说,您想要在应用程序中包含的导入数据。对于代码而言,重要的是可读性,并且每次数据更改的可读性差时,具有奇异的解析器或语法只是为了生成整洁的C或C ++文件。
塞巴斯蒂安·马赫

Answers:


132

预处理指令喜欢#include只是在做一些文字替换(见GNU的文档CPPGCC)。它可以发生在任何地方(注释和字符串文字之外)。

但是,a#include应该#作为其行的第一个非空白字符。所以你要编码

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

原始问题没有#include单独的代码,因此代码有误。

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

要求编译器向您显示预处理的表单。使用GCC进行编译,gcc -C -E -Wall yoursource.c > yoursource.i并与编辑器或寻呼机一起查看生成的内容yoursource.i

实际上,我更喜欢在自己的源文件中包含此类数据。因此,我建议h-data.c使用诸如GNU awk之类的工具生成一个自包含文件(这样文件h-data.c将以...开头const float h[345] = {和结尾};)。如果它是一个常量数据,则最好对其进行声明const float h[](以便它可以处于读状态) -段,例如.rodata在Linux上)。另外,如果嵌入的数据很大,则编译器可能会花一些时间(无用地)对其进行优化(然后您可以在h-data.c不进行优化的情况下快速进行编译)。


3
为什么要重定向输出,而不仅仅是使用gcc -C -E -o yoursource.i -Wall yoursource.c?有选择是有原因的!
戴夫

26
因为>-o我短一个字符,并且因为我am脚。
Basile Starynkevitch 2014年

10
实际上,我的第一个Unix是在Sun3,SunOS3.2 ...上(所以是1987年)
Basile Starynkevitch 2014年

10

因此,使用float h [N] = {#include“ f1.h”}是正常做法吗?这条路?

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

使用此方法的优点:它省去了考虑更好的解决方案所需的少量工作。

缺点:

  • 它会增加代码的WTF / SLOC比。
  • 它在客户端代码和所包含的代码中引入了不寻常的语法。
  • 为了了解f1.h的功能,您必须查看其用法(这意味着您需要向项目中添加额外的文档来解释这种野兽,否则人们将不得不阅读代码以查看其含义)表示-两种解决方案都不可接受)

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


4
缺少换行符,#include则应在单独的行上。
Basile Starynkevitch 2014年

正如Starynkevitch指出的那样,此C代码将使用换行符括起来的include指令进行编译。请参阅ISO / IEC 9899(C11)§6.102“预处理指令由一系列预处理令牌组成,这些预处理令牌以#预处理令牌开头”……或紧随包含至少一个换行符的空白,并且以下一个换行符结束。” “ ...”。f1.h提供的内容从上下文可以明显看出。这将是一个用逗号分隔的表达式列表,与N尺寸的float兼容。我预计这些系数无论如何都来自单独的标准或文档。
user1155120 2014年

1
什么是“正常”?我认为这是一种很好的做法,因为它可以为导入的数据提供更好的可读性,而不是具有奇特而复杂的触发器,从而又可以创建整洁的源文件。语法不常见,但是对于知道预处理器功能的有经验的C或C ++程序员来说,这应该是一个惊喜,但不要太大。
塞巴斯蒂安·马赫

@BasileStarynkevitch,我知道它应该在单独的一行上。我对回答OP提出的问题比对代码更感兴趣。
utnapistim 2014年

@phresnel,以我的专业经验(超过8年的C ++),我只在生产代码中见过一次,那是Stephan Lavavej在解释它们如何生成具有可变数量参数的std :: function专门化时,参数放入样板声明中(我认为它早于C ++ 11)。这就是为什么我认为它是非典型的(“不正常”)。整洁的源文件和良好的导入数据可读性不是相互排斥的(在这方面,这是错误的选择)。
utnapistim 2014年

10

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

这是一个替代解决方案:

文件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))

...

7
这种方法会“烧毁”符号F1_ARRAYF1_H。生成文件后,避免使用此类预处理符号可能会很有用。对于人工创建的头文件,您的解决方案更好,但对于一堆生成的数据文件则不是。
哈珀2014年

4
我看不出为什么这比现有解决方案更好。现在,很难自动生成该数据文件,并且会污染符号空间。正如哈珀已经提到的那样,对于人工生成的数据,这可能会更好,但是比让人工在真实的源文件中生成数据还要糟糕。如果太复杂了,人类一代可能仍然是错误的。
塞巴斯蒂安·马赫

@phresnel:为什么更难自动生成该数据文件?它仅包含6行以上的常量文本(4个预处理器指令和2个花括号)。
barak manos 2014年

@harper:谢谢您的评论。对于一堆生成的数据文件,由于每个文件都有唯一的名称,因此您可以轻松地为每个文件创建几个唯一的预处理器符号。通过脚本生成那些恒定的文本行也不应该太困难。
barak manos 2014年

@barakmanos:确实;现在,您需要以某种方式打印这些多余的行。您必须适合您现有的Python或Haskell程序,以不仅输出逗号分隔的值,而且输出C代码。如果您的系数来自专有供应商,例如来自实验室或某些光反射率计算,则现在您必须调用其他函数cat,并确保您的cat和脚本确实在运行。
塞巴斯蒂安·马赫

9

不,这不是正常的做法。

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


但是,存在一个“模式”,其中涉及在这样的随机位置包含文件:X-Macros,例如那些

X宏的用法是一次定义一个集合,然后在各个地方使用它。单一定义可确保整体的一致性。作为一个简单的示例,请考虑:

// 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

7

很久以前人们就过度使用预处理器。例如,查看XPM文件格式,该文件格式旨在使人们可以:

#include "myimage.xpm"

在他们的C代码中。

它不再被认为是好的。

OP的代码如下所示,C所以我将讨论C

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

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

为什么认为它不好?

因为它很不灵活。您必须重新编译整个应用程序才能更改图像。您甚至不能包含两个具有相同名称的图像,因为它将产生不可编译的代码。对于OP,他无法在不重新编译应用程序的情况下更改数据。

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

紧密耦合还为数据强加了一种格式,例如,如果您要存储10x10矩阵值,则可以选择在源代码中使用一维数组或二维数组。从一种格式切换到另一种格式将对您的数据文件进行更改。

的此问题加载数据容易地解决通过使用标准的I / O功能。如果确实需要包括一些默认图像,则可以在源代码中提供图像的默认路径。这至少将允许用户更改该值(通过在编译时通过#define-D选项),或更新映像文件而无需重新编译。

在OP的情况下,如果FIR系数和x, y向量作为参数传递,则其代码将更可重用。您可以创建一个struct以容纳这些值。该代码不会是低效率的,并且即使与其他系数一样也可以重用。除非用户传递覆盖文件路径的命令行参数,否则可以在启动时从默认文件加载系数。这将消除对任何全局变量的需求,并使程序员的意图明确。您甚至可以在两个线程中使用相同的FIR函数,前提是每个线程都有自己的struct

什么时候可以接受?

无法进行数据动态加载时。在这种情况下,您必须静态加载数据,并且不得不使用此类技术。

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

但是即使在那种情况下,我还是希望创建一个真实的C源文件,而不是包括来自半格式文件的浮点值。

例如,提供一个C返回系数的实函数,而不是使用半格式的数据文件。C然后,可以在两个不同的文件中定义此功能,一个使用I / O进行开发,另一个使用返回静态数据的发行版本。您将有条件地编译正确的源文件。


@YvesDaoust一方面,这是一个巨大的安全漏洞。由于XPM是图像文件,因此您通常会从其他来源获得它们,而不是自己制作。恶意用户可以轻易地修改XPM文件以结束字符串,从而轻易地注入代码。
2014年

@trlkly:恶意用户如何获得对源代码和资产版本控制系统的访问,以及对生产构建机器的访问?一方面,您不太了解C或C ++。
塞巴斯蒂安·马赫2014年

“预处理器#include指令旨在包含源代码”:预处理器不在乎,它从块中构建完整的文件,并且编译器不知道。(这对于预编译的标头可能有所不同,但它们不属于标准。)在更干净的情况下,我会使用它们。
Yves Daoust 2014年

@YvesDaoust当然,您可以使用C预处理程序在所需的任何文件类型上执行所需的任何宏替换。但是我的回答是在这个问题的背景下。C预处理器之所以被命名为C预处理器,是因为以下原因:它旨在预处理C文件。最后,C文件旨在作为源代码。因此,我认为这是对的:旨在包括文件的指令(旨在供预处理器使用)本身旨在对源代码进行预处理的指令是旨在包括源代码的指令。:)
fjardon 2014年

@phresnel您在说什么?他们可以直接访问源代码,因为流氓XPM文件只是#includeda文件。这就是问题中正在讨论的场景。XPM可以访问构建环境,因为其中包含了文件。当然,如果您很聪明,那么在使用XPM之前,请先检查它是否有效#include,但是它仍然是一个主要的安全缺陷。
2014年

5

在某些情况下,要么需要使用外部工具根据包含源代码的其他文件来生成.C文件,要么需要外部工具来生成带有大量代码的C文件,这些代码被硬连接到生成工具中,或者需要代码使用#include各种“异常”方式的指令。在这些方法中,我建议后者(尽管很棘手)通常是最不邪恶的。

我建议避免对.h不遵守与头文件相关联的常规约定的文件使用后缀(例如,通过包括方法定义,分配空间,需要不寻常的包含上下文(例如,在方法中间),需要多个除非定义了这些宏,.c否则我通常也避免使用或.cpp#include这些文件合并到其他文件中,除非这些文件主要是独立使用的。[在某些情况下,我可能拥有fooDebug.c包含#define SPECIAL_FOO_DEBUG_VERSION[newline]`#include“ foo的文件。 c“``如果我希望从同一个源生成两个名称不同的目标文件,并且其中一个肯定是“普通”版本。]

我的常规做法是使用.i人为生成或机器生成的文件作为后缀,这些文件旨在(但以通常的方式)包含在其他C或C ++源文件中;如果文件是机器生成的,我通常会在生成工具的第一行中包含一个注释,以标识用于创建该文件的工具。

顺便说一句,我使用此技巧的一个窍门是当我想允许仅使用批处理文件来构建程序而没有任何第三方工具,但想计算其生成次数。在我的批处理文件中,我包括了echo +1 >> vercount.i;然后在vercount.c文件中,如果我没记错的话:

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

最终结果是,我得到的值在每次构建时都会递增,而不必依赖任何第三方工具来生成它。



3

正如评论中已经说过的,这不是正常的做法。如果看到这样的代码,则尝试对其进行重构。

例如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];
// ...

这对我来说似乎更正常-尽管上述代码可以进一步改进(例如,消除全局变量)。


3

除了其他人所说的之外,-的内容f1.h必须像这样:

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

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

是的,它可能具有注释,其他功能或宏用法,表达式等。


3

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

预处理程序允许您将源文件分成任意多个块,这些块由#include指令组装。

当您不想用冗长/不可读的部分(例如数据初始化)使代码混乱时,这很有道理。事实证明,我的记录“数组初始化”文件长11000行。

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

对于某些功能,我有一些这样的包含,根据处理器的不同,它们有几种替代实现,其中一些使用内联汇编。包含内容使代码更易于管理。

传统上,#include指令已用于包含头文件,即公开API的声明集。但是没有什么要求。


2

我读到人们想重构,并说这是邪恶的。我仍然在某些情况下使用。正如某些人所说,这是一个前处理器指令,因此包括文件的内容。这是我使用的一种情况:建立随机数。我建立随机数,并且每次运行时都不编译时,我不想这样做。因此,另一个程序(通常是脚本)仅将生成的数字包括在文件中。这样可以避免手工复制,从而可以轻松更改数字,生成数字的算法以及其他优点。您不能轻易责怪这种做法,在这种情况下,这只是正确的方法。


1
您为什么不在编译时建立随机数?这个用例对我来说似乎不太有效。
塞巴斯蒂安·马赫

2

我使用OP的技术在相当长的一段时间内将include文件放在变量声明的数据初始化部分中。与OP一样,生成了包含的文件。

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

#include "gensrc/myfile.h"

当我开始使用Eclipse时,这种方案就瓦解了。Eclipse语法检查不够完善,无法处理此问题。如果没有语法错误,它将通过报告语法错误做出反应。

我向Eclipse邮件列表报告了示例,但是似乎对“修复”语法检查没有太大兴趣。

我将代码生成器更改为采用其他参数,以便可以生成整个变量声明,而不仅仅是数据。现在,它会生成语法正确的包含文件。

即使我没有使用Eclipse,我也认为这是一个更好的解决方案。


1

在Linux内核中,我找到了一个漂亮的IMO示例。如果您查看cgroup.h头文件

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

您可以#include <linux/cgroup_subsys.h>在宏的不同定义之后找到使用两次的指令SUBSYS(_x);在cgroup_subsys.h内部使用此宏来声明Linux cgroup的几个名称(如果您不熟悉cgroup,它们是Linux提供的用户友好界面,必须在系统启动时进行初始化)。

在代码段中

#define SUBSYS(_x) _x ## _cgrp_id,
enum cgroup_subsys_id {
#include <linux/cgroup_subsys.h>
   CGROUP_SUBSYS_COUNT,
};
#undef SUBSYS

在代码段中,每个SUBSYS(_x)在cgroup_subsys.h中声明的元素都成为type的元素。enum cgroup_subsys_id

#define SUBSYS(_x) extern struct cgroup_subsys _x ## _cgrp_subsys;
#include <linux/cgroup_subsys.h>
#undef SUBSYS

每个都SUBSYS(_x)成为类型变量的声明struct cgroup_subsys

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


我同意+1。这与X宏有关。我将其视为对预处理器进行编程以编写我的代码的一部分的方法,从而使代码可维护。有些人不同意。
Mike Dunlavey
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.