嵌入式系统中编写常量参数的最佳实践

9
这是一个有关嵌入式系统中 "C语言中的static const和#define的比较" 的案例。
在大型/中型项目中,对于已有的代码和模块,编写包括文件、模块等的常量参数的最佳实践是什么?
在一份"传承下来"的代码中,你不知道你选择的名称是否已在其他包含文件中定义,或者是否会在其他文件中作为extern或宏被调用。
有以下三个选项:
  1. static const int char_height = 12;
  2. #define CHAR_HEIGHT 12
  3. enum { char_height = 12 };
在未知内存限制的嵌入式系统上,哪种方法更好?
原始代码主要使用了#define,但这些常量以多种方式杂乱地实现(甚至在同一文件中的不同位置),因为似乎有几个人为某个设备开发了这个演示软件。
具体而言,这是一个演示代码,展示了某个设备的每个硬件和SDK功能。
大部分我考虑的数据是用于配置环境的:屏幕尺寸、字符集特征、提高代码可读性的内容。不是编译器和预处理器可以自动配置的内容。但由于代码量很大,我担心全局名称冲突,所以不愿使用 #define。
目前,我正在考虑重新从头开始重写项目,并重新实现大多数已编写的函数,以从一个 c 文件获取它们的常量,或者重新组织常量的实现方式,使其符合一种风格。
但是:
1. 这是一个单人项目(因此重新实现所有内容需要很长时间)。 2. 已经实现的代码可以正常工作,并且已经进行了多次修订。(如果它没有出现问题……)

你能把它们都改成 static const 吗?还是有一些被用作常量(例如数组大小或 case 标签)? - mafso
我还没有开始编码,也没有完成阅读所有的代码。但在这些情况下,我不会使用 static const - Sdlion
5个回答

6

始终考虑可读性和内存约束。此外,宏只是在编译之前发生的复制/粘贴操作。说到这里,我喜欢做以下事情:

  • 如果一个变量在一个C文件中使用(例如不能全局访问多个文件),则将所有常量定义为static const。任何定义为const的内容应放置在文件范围内的ROM中。显然,初始化后不能更改这些变量。
  • 我使用#define来定义所有常量值。
  • 如果enum有助于阅读性,我则使用它。任何需要固定值范围的地方,我都更喜欢枚举来明确说明意图。

尝试从面向对象的角度(即使C不是面向对象的语言)来处理项目。隐藏私有函数(不要在头文件中创建原型),避免使用全局变量,将应仅驻留在一个C模块(文件)中的变量标记为static等。


1
这里需要注意一点:相当数量的嵌入式C调试器会在你不在编译单元代码内部时,对于定义为静态的变量的可见性存在问题。 - Speed8ump
@bblincoe 你认为将常量值的 #define 放在一个单独但与上下文相关的文件中会更好,因为它们本来就是全局的吗? 例如 global_conf.c lcd_conf.c uart_conf.c避免在代码文件中使用 #define 来定义常量值可以防止“我不知道/记得那个 define”的名称冲突情况。 虽然对于这种情况,我必须先搜索每个文件以查找名称,然后才能在定义中使用它。 - Sdlion
面向对象是一种通用于任何编程语言的程序设计方法。有些语言支持某些面向对象的特性,而有些则不支持。C语言支持数据和函数的私有封装以及自主对象的创建。它唯一不支持的基本面向对象特性是继承。(类型安全、构造函数/析构函数、运算符重载、模板编程、异常处理等等都是方便的特性,但并非编写面向对象程序所必需的。) - Lundin
1
@Sdlion 我更喜欢将#define与相关内容分组。例如,如果我有一个UART驱动程序,我会将所有与UART相关的#define语句放在uart.h中,并包括任何被视为公共的函数原型。我会将私有的static const变量放在uart.c中,以限制外部模块(包括uart.h文件)的可见性。此外,在uart.c中,我会放置我的实现,比如函数void uartWrite(char byte); - bblincoe
2
顺便说一句,“这使它们可以在ROM中而不会占用宝贵的RAM”这个陈述很奇怪。一个const最终是存储在RAM还是ROM取决于它声明在哪个作用域以及编译器是否优化该常量。在文件作用域分配的常量,也就是这个问题所涉及的,总是最终存储在ROM中。 - Lundin

4

它们是三个不同的东西,应该在三种不同的情况下使用。

  • #define should be used for constants that need to be evaluated at compile time. One typical example is the size of a statically allocated array, i.e.

    #define N 10 
    
    int x[N];
    

    It is also fine to use #define all constants where it doesn't matter how or where the constant is allocated. People who claim that it is bad practice to do so only voice their own, personal, subjective opinions.

    But of course, for such cases you can also use const variables. There is no important difference between #define and const, except for the following cases:

  • const should be used where it matters at what memory address a constant is allocated. It should also be used for variables that the programmer will likely change often. Because if you used const, you an easily move the variable to a memory segment in EEPROM or data flash (but if you do so, you need to declare it as volatile).

    Another slight advantage of const is that you get stronger type safety than a #define. For the #define to get equal type safety, you have to add explicit type casts in the macro, which might get a bit harder to read.

    And then of course, since consts (and enums) are variables, you can reduce their scope with the static keyword. This is good practice since such variables do not clutter down the global namespace. Although the true source of name conflicts in the global namespaces are in 99% of all cases caused by poor naming policies, or no naming policies at all. If you follow no coding standard, then that is the true source of the problem.

    So generally it is fine to make constants global when needed, it is rather harmless practice as long as you have a sane naming policy (preferably all items belonging to one code module should share the same naming prefix). This shouldn't be confused with the practice of making regular variables global, which is always a very bad idea.

  • Enums should only be used when you have several constant values that are related to each other and you want to create a special type, such as:

    typedef enum
    {
      OK,
      ERROR_SOMETHING,
      ERROR_SOMETHING_ELSE
    } error_t;
    

    One advantage of the enum is that you can use a classic trick to get the number of enumerated items as another compile-time constant "free of charge":

    typedef enum
    {
      OK,
      ERROR_SOMETHING,
      ERROR_SOMETHING_ELSE,
    
      ERRORS_N  // the number of constants in this enum
    } error_t;
    

    But there are various pitfalls with enums, so they should always be used with caution.

    The major disadvantage of enum is that it isn't type safe, nor is it "type sane". First of all, enumeration constants (like OK in the above example) are always of the type int, which is signed.

    The enumerated type itself (error_t in my example) can however be of any type compatible with char or int, signed or unsigned. Take a guess, it is implementation-defined and non-portable. Therefore you should avoid enums, particularly as part of various data byte mappings or as part of arithmetic operations.


在这种特定情况下,没有适当的命名策略。 因此,我想只要遵循合理的命名规则,我的代码就不会与其他任何东西发生冲突(使用前缀应该足以与其他所有内容区分开)。 那么我从中得到的问题是 enum 并不是类型安全的(即实现定义),它无法提高代码的可读性。 - Sdlion
@Sdlion 这可能有助于可读性,但同时也会带来许多微妙的问题,正如我在答案中所提到的。 - Lundin

2

我同意bblincoe的观点...+1

我想知道您是否理解这种语法的差异以及它如何影响实现。有些人可能不关心实现,但如果您要进入嵌入式开发,也许应该了解一下。

当bblincoe提到ROM而不是RAM时。

static const int char_height = 12;

理想情况下,它应该占用.text实际空间并使用您指定的值预初始化该实际空间。由于是const,您不会更改它,但它确实有一个占位符?为什么常量需要占位符呢?请考虑一下,当然,您可以在将来修改二进制文件以打开或关闭某些功能或更改特定于板子的调整参数...
没有volatile,编译器不必始终使用该.text位置,它可以优化并将该值直接放入指令中,甚至更糟的是,优化数学运算并删除一些数学运算。
define和enum不占用存储空间,它们是编译器选择如何实现的常量,如果它们没有被优化掉,那么这些位有时会落在.text中,有时会落在.text的任何地方,这取决于指令集如何工作其立即数,特定常量等。
因此,define与enum基本上是您想要选择所有值还是希望编译器为您选择某些值,如果您想控制它,则定义,如果您希望编译器选择值,则为枚举。
因此,这真的不是最佳实践的事情,而是确定您的程序需要做什么并为该情况选择适当的编程解决方案的情况。
根据编译器和目标处理器,选择volatile static const int与不这样做可能会影响rom消耗。但这是一个非常特定的优化,并不是一般性的答案(与嵌入式无关,而与编译有关)。

感谢您对编译器部分的见解。我正在考虑的大多数数据是用于配置环境的数据:屏幕尺寸、字符集特性以及提高代码可读性的一些内容。并不是自动配置编译器和预处理器可以完成的工作。但由于其中有很多代码,我不太愿意使用 #define。 - Sdlion
不知道什么是定义,记住虽然硬件可以旋转,但通常非常静态。在硬件寄存器中执行某些魔法的数字并不是动态的,该芯片使用该数字集,您可以在代码中进行硬编码,无需添加针对维护、未来更改等方面的许多功能。那个硬件是静态的。如果这些定义是将来可能是其他动态事物,那么定义或硬编码值仍然可以使用。 - old_timer

1
Dan Saks解释了为什么他更喜欢枚举常量,这些文章是符号常量枚举常量 vs 常量对象。总的来说,避免使用宏,因为它们不遵守通常的作用域规则,并且符号名称通常无法保留给符号调试器。而且,最好使用枚举常量,因为它们不容易受到可能影响常量对象的性能惩罚。链接的文章中有更多细节。

虽然这些文章提到了C和C++,但我认为它更偏向于C++嵌入式代码。在关于这个主题的问题集合中,我读到在C++中高度不鼓励使用#define,但在C中则是常见做法,因为有时无法避免。尽管如此,如果常量是整数,枚举是一个不错的选择。 - Sdlion
我不确定静态常量是存储在代码中还是存储在RAM中,因为Dan Saks在C编程方面的评论和博客文章与其他人不同。 - Sdlion
1
枚举类型还有很多其他问题和意外行为。 - Lundin
1
@Sdlion 静态常量在嵌入式系统中始终存储在ROM中。如果您阅读由PC程序员编写的文章,它们可能会有些令人困惑,因为PC程序的地址空间中不存在真正的ROM。整个PC程序都是从RAM中执行的,PC程序中的常量存储在该RAM的只读部分中。 - Lundin

1
另一个需要考虑的问题是性能。对于整数,#define常量通常比const变量访问速度更快,因为const需要从ROM(或RAM)中获取,而#define值通常是立即指令参数,因此它会随着指令一起获取(没有额外的周期)。
至于命名冲突,我喜欢使用前缀,如MOD_OPT_,其中MOD是模块名称,OPT表示定义是编译时选项等。如果它们是公共API的一部分,请将#defines仅包含在头文件中;否则,如果它们在多个源文件中需要,则使用.inc文件;如果它们只特定于该文件,则在源文件本身中定义它们。

如果const没有声明为volatile,那么#define只有在编译器优化后才能更快地被包含在程序的机器代码中。 - Lundin
虽然为#define定义一个命名约定是有帮助的,但这正是我在这个项目中最担心的事情。 - Sdlion

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