【问题标题】:What Is The Point of Symbolic Constants?符号常数的意义是什么?
【发布时间】:2011-02-21 03:36:59
【问题描述】:

我无法理解 C 中符号常量的意义,我确信它们是有原因的,但我似乎不明白为什么你不只使用变量。

#include <stdio.h>

main()
{
    float fahr, celsius;
    float lower, upper, step;

    lower = 0;
    upper = 300;
    step = 20;

    printf("%s\t %s\n", "Fahrenheit", "Celsius");
    fahr = lower;   
    while (fahr <= upper) {
        celsius = (5.0 / 9.0) * (fahr - 32.0);
        printf("%3.0f\t\t %3.2f\n", fahr, celsius);
        fahr = fahr + step;
    }

}

对比

#include <stdio.h>

#define LOWER   0
#define UPPER   300
#define STEP    20

main()
{
    float fahr, celsius;

    printf("%s\t %s\n", "Fahrenheit", "Celsius");
    fahr = LOWER;   
    while (fahr <= UPPER) {
        celsius = (5.0 / 9.0) * (fahr - 32.0);
        printf("%3.0f\t\t %3.2f\n", fahr, celsius);
        fahr = fahr + STEP;
    }

}

【问题讨论】:

    标签: c


    【解决方案1】:

    (预)编译器知道符号常量不会改变。它在编译时用该值替换常量。如果“常数”在变量中,通常无法确定该变量永远不会改变值。因此,编译后的代码必须从分配给变量的内存中读取值,这会使程序稍微变慢和变大。

    在 C++ 中,您可以将变量声明为 const,这告诉编译器几乎相同的事情。这就是为什么 C++ 中不赞成使用符号常量的原因。

    但是请注意,在 C(相对于 C++)中,const int 变量不是常量表达式。因此,尝试做这样的事情:

    const int a = 5;
    int b[a] = {1, 2, 3, 4, 5};
    

    在 C++ 中可以工作,但在 C 中会出现编译错误(假设 b 应该是静态绑定数组)。

    【讨论】:

    • 所以 C 中的 #define 很像 C++ 中的 const 吗?我是否认为它也很像 Java 中的 final?
    • #define foo 3 会在你的代码中使用foo 的任何实例,然后用3 替换它。然后它将该预处理代码传递给编译器。因此,它比 java 中的final 更有力一点,因为编译器从来没有真正看到一个符号,它只是看到值。
    • const 应该在 C 中与大多数编译器一起工作。我认为它最初是在 C89 中添加的,并且还包含在大多数主要 C 编译器实现的 C99 和 C11 中。 en.wikipedia.org/wiki/ANSI_C#Compilers_supporting_ANSI_C
    • C 中的 const 根本不是同一件事。在 C 语言中,您不能使用 const int 指定静态数组维度 - 它将创建一个可变长度数组......等等。
    • @AnttiHaapala - const 修饰符意味着值不会改变。但是您是正确的,const int 变量不是 C 中的常量表达式。为什么会这样,我无法理解。 (相比之下,它是 C++ 中的常量表达式。)我将更新我的答案以反映这一点。
    【解决方案2】:

    为什么命名常量有益的一个很好的例子来自 Kernighan 和 Pike 的优秀著作 The Practice of Programming(1999 年)。

    §1.5 幻数

    [...] 这个程序摘录在一个 24 x 80 光标寻址的终端上打印字母频率的直方图,由于大量的幻数,这是不必要的不​​透明:

    ...
    fac = lim / 20;
    if (fac < 1)
        fac = 1;
    for (i = 0, col = 0; i < 27; i++, j++) {
        col += 3;
        k = 21 - (let[i] / fac);
        star = (let[i] == 0) ? ' ' : '*';
        for (j = k; j < 22; j++)
            draw(j, col, star);
    }
    draw(23, 2, ' ');
    for (i = 'A'; i <= 'Z'; i++)
        printf("%c  ", i);
    

    代码包括数字 20、21、22、23 和 27 等。它们显然是相关的......或者它们是相关的吗?事实上,这个程序只有三个关键数字:24,屏幕上的行数; 80、列数;和 26,字母表中的字母数。但是这些都没有出现在代码中,这使得这些数字更加神奇。

    通过在计算中给主数命名,我们可以使代码更容易理解。例如,我们发现数字 3 来自 (80 - 1)/26,并且 let 应该有 26 个条目,而不是 27(可能是由 1 索引的屏幕坐标引起的非一错误)。进行其他一些简化,结果如下:

    enum {
        MINROW   = 1,                 /* top row */
        MINCOL   = 1,                 /* left edge */
        MAXROW   = 24,                /* bottom edge (<=) */
        MAXCOL   = 80,                /* right edge (<=) */
        LABELROW = 1,                 /* position of labels */
        NLET     = 26,                /* size of alphabet */
        HEIGHT   = (MAXROW - 4),      /* height of bars */
        WIDTH    = (MAXCOL - 1)/NLET  /* width of bars */
    };
    
        ...     
        fac = (lim + HEIGHT - 1) / HEIGHT;
        if (fac < 1)
            fac = 1;
        for (i = 0; i < NLET; i++) {
            if (let[i] == 0)
                continue;
            for (j = HEIGHT - let[i]/fac; j < HEIGHT; j++)
                draw(j+1 + LABELROW, (i+1)*WIDTH, '*');
        }
        draw(MAXROW-1, MINCOL+1, ' ');
        for (i = 'A'; i <= 'Z'; i++)
            printf("%c  ", i);
    

    现在更清楚了主循环的作用;这是一个从 0 到 NLET 的惯用循环,表明循环在数据的元素之上。对draw 的调用也更容易理解,因为像 MAXROW 和 MINCOL 这样的词提醒我们参数的顺序。最重要的是,现在可以使程序适应另一种显示尺寸或不同的数据。数字是神秘的,代码也是如此。

    修改后的代码实际上并没有使用MINROW,这很有趣;有人想知道剩余的 1 中哪一个应该是 MINROW。

    【讨论】:

    • +1 是一个很好的例子,说明为什么应该命名常量而不是将它们分散在代码中。但是,通过使用这些值声明变量可以获得相同的好处。符号常量(或枚举常量)相对于命名变量的好处并不是很明显。 (实际上,我不喜欢在这种情况下使用enum,在这种情况下没有明确的项目类别被枚举[除了“有用的常量”]。)
    • @Ted:这取决于上下文。该示例适用于固定的 24x80 屏幕;常数是合适的。如果考虑到当前窗口大小,适当初始化的命名变量显然更好。 OP 的代码将通过使用for (fahr = LOWER; fahr &lt;= UPPER; fahr += STEP) 循环得到最清晰的说明,我对命名常量与纯数字的相对优点持中立态度。在示例中,lower、upper 和 step 的值更接近于任意值。如果值为FREEZING_POINTBOILING_POINT(BOILING_POINT - FREEZING_POINT) / 30,则名称更好。
    • 我完全同意使用名称而不是原始数字。我只是不喜欢在这个特定示例中使用enum 而不是#define。使用常量(#define 和/或enum)而不是变量的好处在编译器可以执行诸如消除内部循环中的一系列计算之类的示例中变得更加清晰,因为它将表达式识别为编译时常量.这个例子并没有真正证明这种好处。 (我很惊讶还没有人提到符号常量对 switch 语句有多么有用。现在我知道了!:))
    • @Ted:我喜欢使用enum 而不是#define,因为调试器会了解enum 的值,因此您可以要求它打印 HEIGHT(例如) ,但如果你使用#define,则根本不知道HEIGHT这个名字。 OTOH,你不能在enum 上做#ifdef。您关于开关和案例标签的观点是有效的。
    • @cacoder — 是的:枚举值是编译时常量,编译器将像优化任何其他编译时常量一样优化它们。
    【解决方案3】:

    变量的范围仅限于声明它们的结构。当然,您可以使用变量而不是符号常量,但这可能需要大量工作。考虑一个经常使用弧度的应用程序。符号常量#define TWO_PI 6.28 对程序员来说具有很高的价值。

    【讨论】:

      【解决方案4】:

      Jonathan 在为什么中提出了一个很好的观点,您希望在 C(以及任何其他编程语言,顺便说一句)中使用符号常量。

      在语法上,在 C 中,这与 C++ 和许多其他语言不同,因为它对 如何 声明此类符号常量有很大的限制。所谓的const 限定变量不像在 C++ 中那样考虑这一点。

      • 您可以使用为任何常量表达式定义的宏:整数或浮点常量、静态变量的地址表达式,以及您从中形成的一些 形式的表达式。这些仅由编译器的预处理阶段处理,在其中使用复杂的表达式时必须小心。
      • 你可以以整数枚举常量的形式声明整数常量表达式,例如enum color { red = 0xFF00, green = 0x00FF00, blue = 0x0000FF };。它们只是有一些限制用途,因为它们的类型固定为int。因此,您不会涵盖您可能想要的所有值范围。
      • 如果您愿意,您可能还会看到 整数字符常量,如 'a'L'\x4567' 作为预定义的符号常量。它们将抽象概念(字符值“a”)翻译成执行平台的编码(ASCII、EBDIC 等)。

      【讨论】:

        【解决方案5】:

        Jonathan 提供了一个很好的符号常量用户示例。

        问题中使用的程序可能不是回答这个问题的最佳程序。但是,给定程序,符号常量在以下情况下可能更有意义:

        #include <stdio.h>
        
        #define FAHRENHEIT_TO_CELSIUS_CONVERSION_RATIO     5.0 / 9.0
        #define FAHRENHEIT_TO_CELSIUS_ZERO_OFFSET           32.0
        #define FAHRENHEIT_CELSIUS_COMMON_VALUE             -40.0   
        #define UPPER                                       300.0
        #define STEP                                        20.0
        
        int main()
        {
           float fahr, celsius;
        
            printf("%s\t %s\n", "Fahrenheit", "Celsius");
            fahr = FAHRENHEIT_CELSIUS_COMMON_VALUE;
            while (fahr <= UPPER) {
                celsius = (fahr - FAHRENHEIT_TO_CELSIUS_ZERO_OFFSET) * (FAHRENHEIT_TO_CELSIUS_CONVERSION_RATIO);
                printf("%3.0f\t\t %3.2f\n", fahr, celsius);
                fahr = fahr + STEP;
            }
        }
        

        这可能更容易理解为什么符号常量可能有用。

        该程序包含stdio.h,一个相当常见的包含文件。让我们看一下stdlib.h 中定义的一些符号常量。这个版本的stdio.h 来自 Xcode。

        #define BUFSIZ  1024            /* size of buffer used by setbuf */
        #define EOF     (-1)
        #define stdin   __stdinp
        #define stdout  __stdoutp
        #define stderr  __stderrp
        

        让我们也看看stdlib.h中定义的两个符号常量。

        #define EXIT_FAILURE    1
        #define EXIT_SUCCESS    0
        

        这些值可能因系统而异,但使用它们可以使 C 语言编程更加容易和可移植。众所周知,stdinstdoutstderr 的符号常量会在各种操作系统实现中发生变化。

        使用 BUFSIZ 为 C 输入缓冲区定义字符数组通常很有意义。 使用 EXIT_FAILURE 和 EXIT_SUCCESS 使代码更具可读性,我不必记住 0 是失败还是成功。 有人会更喜欢 (-1) 而不是 EOF?

        使用符号常量来定义数组的大小使得在一个地方更改代码变得更加容易,而不必到处搜索嵌入在代码中的特定数字。

        【讨论】:

          猜你喜欢
          • 2011-04-07
          • 2014-05-11
          • 1970-01-01
          • 1970-01-01
          • 2022-12-22
          • 2010-09-29
          • 2011-04-18
          • 2012-04-19
          • 2011-09-18
          相关资源
          最近更新 更多