在C中使用.h文件的异常情况

时间:2014-11-10 11:59:20

标签: c++ c

在阅读有关过滤的文章时,我发现了.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" };是否正常?

13 个答案:

答案 0 :(得分:129)

#include这样的

Preprocessor指令只是进行了一些文本替换(请参阅cpp中的GNU GCC文档)。它可以出现在任何地方(在评论和字符串文字之外)。

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

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

原始问题在其自己的行上没有#include,因此代码错误。

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

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

我实际上更喜欢在自己的源文件中包含此类数据。所以我建议使用例如生成一个自包含的h-data.c文件。某些工具,例如GCC(因此文件const float h[345] = {将以};开头,以const float h[]结尾...) 如果它是一个常量数据,最好将其声明为.rodata(因此它可以位于Linux上的只读段,如h-data.c)。此外,如果嵌入数据很大,编译器可能需要花时间(无用地)优化它(然后您可以快速编译{{1}}而无需优化)。

答案 1 :(得分:10)

  

那么,使用float h [N] = {#include“f1.h”}是否正常?这样吗?

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

使用此功能的优点:它可以为您提供考虑更好解决方案所需的少量工作。

缺点:

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

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

答案 2 :(得分: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))

...

答案 3 :(得分:9)

不,这不是正常做法。

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


然而,有一种&#34;模式&#34;其中包括在随机位置包含文件:X-Macros,例如as those

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

答案 4 :(得分:7)

很久以前人们过度使用了预处理器。例如,参见设计的XPM file format以便人们可以:

#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进行开发,另一个返回静态数据用于发布版本。你可以编译正确的源文件conditionnaly。

答案 5 :(得分:5)

有时需要使用外部工具根据包含源代码的其他文件生成.C文件,让外部工具生成C文件,并将大量代码硬连接到生成工具中,或者代码以各种“不寻常”的方式使用#include指令。在这些方法中,我认为后者 - 虽然icky - 可能往往是最不邪恶的。

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

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

BTW,我曾经使用过的一个技巧就是当我想让一个程序只使用一个批处理文件来构建时,没有任何第三方工具,但想要计算它的构建次数。在我的批处理文件中,我包含了echo +1 >> vercount.i;然后在文件vercount.c中,如果我没记错的话:

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

实际效果是,我获得的值会在每个构建时递增,而不必依赖任何第三方工具来生成它。

答案 6 :(得分:3)

当预处理器找到#include指令时,它只是打开指定的文件并插入它的内容,就好像文件的内容已写在指令的位置一样。

答案 7 :(得分: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];
// ...

这对我来说似乎更正常 - 虽然上面的代码可以进一步改进(例如消除全局变量)。

答案 8 :(得分:3)

添加其他人说的内容 - f1.h的内容必须如下:

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

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

是的,它可能有评论,其他功能或宏用法,表达式等。

答案 9 :(得分:3)

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

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

如果您不希望使用冗长/不可读的部分(例如数据初始化)来混淆代码,那么这很有意义。事实证明,我的记录“数组初始化”文件长度为11000行。

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

对于某些具有多种替代实现的函数,我有一些这样的包含,具体取决于处理器,其中一些使用内联汇编。包含使代码更易于管理。

按照惯例,#include指令已用于包含头文件,即公开API的声明集。但没有任何要求。

答案 10 :(得分:2)

我读过人们想要重构并说这是邪恶的。我仍然在某些情况下使用过。有些人说这是一个预处理程序指令,所以包含文件内容。 这是我使用的案例:构建随机数。 我建立随机数,我不想每次在运行时编译时都这样做。因此,另一个程序(通常是脚本)只是用生成的数字填充文件。这避免了手动复制,这允许容易地改变数字,生成它们的算法和其他细节。你不能轻易责怪这种做法,在这种情况下,它只是正确的方式。

答案 11 :(得分:2)

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

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

#include "gensrc/myfile.h"

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

我向Eclipse邮件列表报告了样本,但似乎没有太多兴趣“修复”语法检查。

我更改了我的代码生成器以获取其他参数,因此它可以生成整个变量声明,而不仅仅是数据。现在它生成语法正确的包含文件。

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

答案 12 :(得分: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

cgroup_subsys.h中声明的每个SUBSYS(_x)都会成为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,而预处理器将自动在初始化文件中添加相关的枚举值/声明。