#include .c 源文件是否可以用于嵌入式 C 代码的可维护性?

Is it OK to #include .c source file for maintainability of embedded C code?

我不是专业的 C 程序员,我知道包含 .c 来自其他人的源文件被认为是不好的做法,但我认为这有助于提高可维护性。

我有一个包含很多元素的大结构,我使用 #define 来保留索引。

#define TOTO_IND 0 
#define TITI_IND 1 
…
#define TATA_IND 50

static const MyElements elems [] = {
    {"TOTO", 18, "French"},
    {"TITI", 27, "English"},
    ...,
    {"TATA", 45, "Spanish"}
}

因为我需要从索引访问结构,所以我需要保持 #define 和结构声明同步。这意味着我必须在正确的位置插入新元素并相应地更新 #define

容易出错,我不是很喜欢(但是出于性能考虑,我没有找到更好的解决方案)。

不管怎样,这个文件也包含了很多处理这个结构的函数。我也想保持代码分离并避免全局变量。

为了让事情“更简单”,我正在考虑将这个“容易出错的定义”移动到一个只包含这个结构的 .c 源文件中。该文件将是“危险的小心文件”并将其包含在我实际的“正常功能”文件中。

你怎么看?包含 .c 源文件是否有效?还有其他更好的方法来处理我的结构吗?

在多个文件中将 const 定义为 static 不是一个好主意,因为它会创建大变量 MyElements 的多个实例。这将增加嵌入式系统中的内存。需要删除 static 限定符。

这是一个建议的解决方案:

in file.h

#define TOTO_IND 0 
#define TITI_IND 1 
…
#define TATA_IND 50
#define MAX_ELEMS 51

extern const MyElements elems[MAX_ELEMS];

in file.c

#include "file.h"
const MyElements elems [MAX_ELEMS] = {
    {"TOTO", 18, "French"},
    {"TITI", 27, "English"},
    ...,
    {"TATA", 45, "Spanish"}
}

修改后,将#include "file.h"放入需要的.c文件中

您可以使用指定的初始值设定项来初始化 elems[] 的元素,而不必知道每个索引标识符(或宏)的显式值。

const MyElements elems[] = {
    [TOTO_IND] = {"TOTO", 18, "French"},
    [TITI_IND] = {"TITI", 27, "English"},
    [TATA_IND] = {"TATA", 45, "Spanish"},
};

数组元素将以相同的方式初始化,即使您更改它们在源代码中出现的顺序也是如此:

const MyElements elems[] = {
    [TITI_IND] = {"TITI", 27, "English"},
    [TATA_IND] = {"TATA", 45, "Spanish"},
    [TOTO_IND] = {"TOTO", 18, "French"},
};

如果数组长度是从上面的初始化程序自动设置的(即使用 [] 而不是 [NUM_ELEMS]),则长度将比最大元素索引多一。

这允许您将 elems 数组的索引值和外部声明保存在一个 .h 文件中,并在单独的 .c 文件中定义 elems 数组内容。

您应该使用指定的初始值设定项,如 Ian Abbot 的回答所示。

此外,如果数组索引是相邻的,就像这里的情况一样,您可以改用枚举:

toto.h

typedef enum
{
  TOTO_IND,
  TITI_IND,
  ...
  TATA_IND,
  TOTO_N    // this is not a data item but the number of items in the enum
} toto_t;

toto.c

const MyElements elems [] = {
  [TITI_IND] = {"TITI", 27, "English"},
  [TATA_IND] = {"TATA", 45, "Spanish"},
  [TOTO_IND] = {"TOTO", 18, "French"},
};

现在您可以使用静态断言验证整个数组的数据完整性:

_Static_assert(sizeof elems/sizeof *elems == TOTO_N, 
               "Mismatch between toto_t and elems is causing rain in Africa");

_Static_assert(sizeof elems/sizeof *elems == TOTO_N, ERR_MSG);

其中 ERR_MSG 定义为

#define STR(x) STR2(x)
#define STR2(x) #x
#define ERR_MSG "Mismatching toto_t. Holding on line " STR(__LINE__)

其他答案已经以更清晰的方式涵盖了它,但为了完整起见,如果您愿意走这条路并冒着同事的愤怒的风险,这里是一个 x-macros 方法。

X-macros 是一种代码生成形式,使用内置的 C 预处理器。目标是将重复减少到最低限度,尽管有一些缺点:

  1. 如果您不习惯,使用预处理器生成枚举和结构的源文件可能看起来很复杂。
  2. 与生成源文件的外部构建脚本相比,使用 x-macros 你永远无法看到编译期间生成的代码的样子,除非你使用编译器设置并手动检查预处理文件。
  3. 由于看不到预处理的输出,因此无法使用调试器单步执行生成的代码,就像处理外部脚本生成的代码一样。

您首先在单独的文件中创建一个宏列表调用,例如elements.inc,此时没有定义宏实际做什么:

// elements.inc

// each row passes a set of parameters to the macro,
// although at this point we haven't defined what the
// macro will output

XMACRO(TOTO, 18, French)
XMACRO(TITI, 27, English)
XMACRO(TATA, 45, Spanish)

然后每次需要包含此列表时定义宏,以便每次调用都呈现到您要创建的构造的一行中——您通常会连续多次重复此操作,即

// concatenate id with "_IND" to create enums, ignore code and description
// (notice how you don't need to use all parameters each time)
// e.g. XMACRO(TOTO, 18, French) => TOTO_IND,
#define XMACRO(id, code, description) id ## _IND,
typedef enum
{
#    include "elements.inc"
     ELEMENTS_COUNT
}
Elements;
#undef XMACRO

// create struct entries
// e.g. XMACRO(TOTO, 18, French) => [TOTO_IND] = { "TOTO", 18, "French" },
#define XMACRO(id, code, description) [id ## _IND] = { #id, code, #description },
const MyElements elems[] = {
{
#    include "elements.inc"
};
#undef XMACRO

这将被预处理成类似的东西:

typedef enum
{
    TOTO_IND,
    TITI_IND,
    TATA_IND,
    ELEMENTS_COUNT
}
Elements;

const MyElements elems[] = {
{
    [TOTO_IND] = { "TOTO", 18, "French" },
    [TITI_IND] = { "TITI", 27, "English" },
    [TATA_IND] = { "TATA", 45, "Spanish" },
};

显然,以生成代码变得更加复杂为代价,频繁维护列表变得更加容易。

要解决关于将 #include.c 文件一起使用的具体问题(其他答案建议更好的选择,尤其是 Groo 的那个),通常没有必要。

.c 文件中的所有内容都可以对外显示和访问,因此您可以通过函数原型和 #extern 引用它。因此,例如,您可以在主 .c 文件中使用 #extern const MyElements elems []; 引用 table。

或者,您可以将定义放在 .h 文件中并包含它。这使您可以根据需要隔离代码。请记住,#include 所做的只是将包含文件的内容插入到 #include 语句所在的位置,因此它不必具有任何特定的文件扩展名。 .h 按照惯例使用,大多数 IDE 会自动将 .c 文件添加到要编译的文件列表中,但就编译器而言,命名是任意的。