#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 预处理器。目标是将重复减少到最低限度,尽管有一些缺点:
- 如果您不习惯,使用预处理器生成枚举和结构的源文件可能看起来很复杂。
- 与生成源文件的外部构建脚本相比,使用 x-macros 你永远无法看到编译期间生成的代码的样子,除非你使用编译器设置并手动检查预处理文件。
- 由于看不到预处理的输出,因此无法使用调试器单步执行生成的代码,就像处理外部脚本生成的代码一样。
您首先在单独的文件中创建一个宏列表调用,例如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
文件添加到要编译的文件列表中,但就编译器而言,命名是任意的。
我不是专业的 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 预处理器。目标是将重复减少到最低限度,尽管有一些缺点:
- 如果您不习惯,使用预处理器生成枚举和结构的源文件可能看起来很复杂。
- 与生成源文件的外部构建脚本相比,使用 x-macros 你永远无法看到编译期间生成的代码的样子,除非你使用编译器设置并手动检查预处理文件。
- 由于看不到预处理的输出,因此无法使用调试器单步执行生成的代码,就像处理外部脚本生成的代码一样。
您首先在单独的文件中创建一个宏列表调用,例如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
文件添加到要编译的文件列表中,但就编译器而言,命名是任意的。