在 C 语言中异常使用 .h 文件

Unusual usage of .h file in C

本文关键字:文件 异常 语言      更新时间:2023-10-16

在阅读有关过滤的文章时,我发现.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这样的预处理器指令只是在做一些文本替换(参见GCC内部的GNU cpp文档(。它可以出现在任何地方(注释和字符串文本之外(。

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

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

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

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

请编译器显示预处理的表单。使用 GCC 使用gcc -C -E -Wall yoursource.c > yoursource.i编译,并使用编辑器或分页器查看生成的yoursource.i

我实际上更喜欢将此类数据放在其自己的源文件中。所以我建议使用例如 GNU awk 等工具生成一个独立的h-data.c文件(因此文件h-data.c将以 const float h[345] = { 开头并以 }; 结尾......如果它是一个常量数据,最好将其声明为const float h[](这样它就可以像 Linux 上的 .rodata 一样位于只读段中(。此外,如果嵌入的数据很大,编译器可能需要一些时间来(无用地(优化它(然后你可以在没有优化的情况下快速编译你的h-data.c(。

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

这是一个替代解决方案:

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

那么,使用float h[N] = { #include "f1.h" };这样是正常的做法吗?

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

使用它的优点:它为您节省了思考更好解决方案所需的少量精力。

弊:

  • 它增加了代码的 WTF/SLOC 比率。
  • 它在客户端代码和包含的代码中引入了不寻常的语法。
  • 为了理解 F1.H 的作用,您必须查看它的使用方式(这意味着您需要在项目中添加额外的文档来解释这个野兽,或者人们必须阅读代码才能看到它的含义 - 这两种解决方案都是不可接受的(
在这种情况下,在

编写代码之前多花 20 分钟思考,可以在项目的生命周期中节省几十个小时的诅咒代码和开发人员。

不,这不是正常的做法。

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


然而,有一种"模式"涉及在这样的随机位置包含一个文件:X宏,例如那些。

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 的情况下,他无法在不重新编译应用程序的情况下更改数据。

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

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

使用标准 I/O 函数可以轻松解决加载数据的问题。如果您确实需要包含一些默认图像,则可以在源代码中提供图像的默认路径。这至少允许用户更改此值(在编译时通过#define-D选项(,或者无需重新编译即可更新图像文件。

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

什么时候可以接受?

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

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

但即使在这种情况下,我也更愿意创建一个真正的C源文件,而不是包含来自半格式化文件的浮点值。

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

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

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

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

顺便说一句,我使用它的一个技巧是,当我想允许仅使用批处理文件构建程序时,没有任何第三方工具,但想计算它被构建了多少次。 在我的批处理文件中,我包含了echo +1 >> vercount.i;那么在文件 vercount.c 中,如果我没记错的话:

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

净效果是,我得到一个值,该值在每次构建时都会递增,而不必依赖任何第三方工具来生成它。

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

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

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

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

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

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

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

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

对我来说是正常的做法。

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

当您不想将代码与冗长/不可阅读的部分(例如数据初始化(混杂在一起时,这很有意义。事实证明,我的记录"数组初始化"文件有 11000 行长。

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

对于某些函数,我有一些这样的包含,这些函数根据处理器具有多种替代实现,其中一些使用内联程序集。包含项使代码更易于管理。

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

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

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

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

#include "gensrc/myfile.h"

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

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

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

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

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

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

在宏SUBSYS(_x)的不同定义之后,你可以找到该指令#include <linux/cgroup_subsys.h>使用了两次;这个宏在 cgroup_subsys.h 中使用,以声明 Linux cgroups 的几个名称(如果你不熟悉 cgroups,它们是 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 即可添加 cgroups,而预处理器将自动在初始化文件中添加相关的枚举值/声明。