在嵌入式C代码中,为了可维护性,是否可以#include .c源文件?

29

我不是专业的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源文件的有效情况吗?还有其他更好的处理我的结构的方法吗?


21
即使文件中包含变量定义,你仍然可以使用.h作为文件的后缀名。 - Some programmer dude
8
把要包含的文件命名为"file.inc"或"file.c.inc"会更好吗? - pmg
12
你是否意识到,如果这个文件被包含在其他需要#define的C文件中,那么每个文件都会得到自己的数组副本? - Gerhardh
7
我该注意什么?为什么不把这个结构体放在它自己的.C文件中,然后在需要访问时使用“extern”引用它呢? - Mawg says reinstate Monica
2
在编写过程中,将数据一起声明在某种格式的一个文件中,然后编写一个小工具(作为构建过程的一部分在构建系统上运行),从该文件中读取并生成一个.c文件和一个.h文件,这两个文件都标有AUTOGENERATED - DO NOT TOUCH,EDIT source file name INSTEAD,这样做是否可行? - Daniel Schepler
显示剩余3条评论
5个回答

40

你可以使用指定初始化器来初始化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]),那么数组的长度将会比最大的元素索引多一个。

这样你就可以在.h文件中保留索引值和外部声明elems数组,并在一个单独的.c文件中定义elems数组的内容。


这是现代C语言的正确答案。我基于此发布了一个具有附加建议的答案。 - Lundin
2
这仍然无法阻止 OP 编写 [TOTO_IND] = {"TITI", 27, "English"} - vgru
1
@Groo 我认为原始代码比这个更容易出错。在每个元素初始化器中添加注释以指示它与哪个索引符号相关联会有所帮助,但指定的初始化器更好。 - Ian Abbott
5
需要注意的是指定初始化器是C99的特性,在C95中无法编译,并且不能在C++11中使用。 - cat
2
针对@Groo的观点,OP可以使用宏(例如 #define decl(idx, n, lang) [idx##_IND] = {#idx, n, lang})。 - Kevin
显示剩余5条评论

33

你应该使用指定的初始化器,就像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__)

另一个可能的完整性错误是,elem 的某些元素可能没有被显式地初始化,因此具有默认初始化程序。运行时代码应该检查那些没有被显式初始化的元素,因为我认为这不能通过 _Static_assert 来完成。 - Ian Abbott
7
@GiacomoAlzetta说的是一个不好笑的玩笑 - 在非洲下雨肯定是一种祝福。我已经更新了问题,并加入了一个断言来指出冒犯的那句话。 - Lundin
2
@cat 是的,我20年前就会这样做了。现在大多数嵌入式系统编译器都支持C11,但还有一些可能仅支持C99。此外,C(和C++)标签使用政策是默认使用当前标准(C17),除非OP明确告知我们使用其他版本。 - Lundin
指定初始化程序的一个限制是没有编译时验证来确保所有枚举成员都具有关联的初始值。Groo描述的“x-macros”方法将确保枚举和数组具有相同的项目,并以相同的顺序,有些情况下还可以添加额外的一致性保证(例如,通过将提供的名称与“_IND”连接以生成索引,并对它们进行字符串化以用作初始化值)。 - supercat
@Kevin:说得对。就我个人而言,我觉得C预处理器非常接近有用的边缘,一般让一些事情成为可能,但并不太干净。添加一些扩展使其更加强大将会相当容易(例如,一个内置函数可以接受一个整数表达式和某种格式说明符,并产生具有该值和该格式的标记),但标准已经基本扼杀了创新。 - supercat
显示剩余8条评论

18
其他答案已经更加清晰地介绍了这个问题,但为了完整起见,这里提供一种 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" },
};

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

1
或者只需使用TOTO等枚举,因为您可能希望它们与0到n相对应。我认为在x宏内部具有索引本身很方便。 - Lundin
非常好的解决方案。对我来说有点困惑...下次需要重构代码时我会尝试一下;) - ncenerar
1
@ncenerar:是的,这些宏通常不被推荐使用,因为很难可视化它们的扩展方式,也无法进行调试,即无法逐步执行代码。然而,这基本上是一种代码生成形式,使用C预处理器而不是外部工具。当您有类似的重复数据时,您还可以配置一个外部构建脚本,每当您更改列表时,它将为您生成代码,这样做会更好,因为您最终得到的是实际的“可调试”代码。 - vgru
好的解决方案。您可能甚至不需要指定初始化器,因为一切都通过 #include "elements.inc" 放入正确的顺序中了。如果程序员想要在宏的第三个参数中使用其他内容而不是字符串字面量,则我可能会直接将字符串包含在其中(replacing French with "French" and #description with description)。 - Ian Abbott
3
使用预处理器的优点是避免了对除构建C程序所需工具之外的任何工具的依赖,从而可以方便地移植到其他构建系统。 - supercat

5

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

以下是建议的解决方案:

在 file.h 中

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

extern const MyElements elems[MAX_ELEMS];

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

修改后,在所需的 .c 文件中放置 #include "file.h"


1
“矛盾”不是正确的词来描述在多个文件中使用的static const。这样做有一些好处。将数组设置为外部链接可以防止一些优化。 - Eric Postpischil
3
如果这个数组需要在多个文件中使用,将其静态化将导致多个数组副本存在。OP提到这个数组的大小很大,因此这个选项被忽略了。 - Rishikesh Raje
你说得对,static 这里不合适,但是你构建这行代码的方式听起来好像变量的 static 和函数声明的 static 意思一样(显然它们的含义并不相同)。 - cat
我已经修改了文本以进一步解释。 - Rishikesh Raje

1
为了回答关于在 .c 文件中使用 #include 的具体问题(其他答案提供了更好的选项,尤其是 Groo 提出的选项),通常不需要这么做。在 .c 文件中的所有内容都可以被外部访问和调用,因此您可以通过函数原型和 #extern 引用它。例如,您可以在主要的 .c 文件中使用 #extern const MyElements elems []; 引用您的表格。或者,您可以将定义放在一个 .h 文件中并进行包含。这样可以按照您想要的方式隔离代码。请记住,#include 所做的一切只是将所包含文件的内容插入到 #include 语句的位置,因此它不必具有任何特定的文件扩展名。 .h 是约定俗成的名称,大多数 IDE 将自动将 .c 文件添加到编译文件列表中,但就编译器而言,命名是任意的。

编译时常量(如enum)通常无法通过extern导入。在与其他语言链接时,这种导入有时可以通过类似于“extern uint32_t addr_is_link_date; #define unix_format_link_date ((uint32_t)&addr_is_link_date)”的东西来解决,这在某些情况下可能可用于需要整数常量的上下文中[例如在静态const结构中包含],但这种情况很少见。 - supercat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接