符号常量的作用是什么?

17

我很难理解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;
    }

}
5个回答

20
(pre)编译器知道符号常量不会改变。它在编译时将常量的值替换为其值。如果“常量”在变量中,则通常无法确定变量永远不会更改值。因此,编译后的代码必须从分配给变量的内存中读取值,这可能会使程序略微变慢和变大。
在C++中,您可以声明一个变量为const,这基本上告诉编译器相同的事情。这就是为什么符号常量在C++中被反对的原因。
但请注意,在C(而非C ++)中,const int变量不是常量表达式。因此,尝试执行以下操作是行不通的:
const int a = 5;
int b[a] = {1, 2, 3, 4, 5};

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


4
在C语言中,#define与C++中的const非常相似,你的想法是对的。同时,它也很像Java中的final。 - Peter
3
#define foo 3 的意思是在你的代码中,任何出现 foo 的地方都会被替换成 3。然后编译器会处理这个预处理过的代码。因此,它比 Java 中的 final 更加强制,因为编译器实际上并没有看到一个符号,而只是看到了一个值。 - Bill Lynch
const 在 C 语言中应该能够在大多数编译器中正常工作。我认为它最早是在 C89 中添加的,并且也包含在 C99 和 C11 中,这些版本是大多数主要的 C 编译器所实现的。http://en.wikipedia.org/wiki/ANSI_C#Compilers_supporting_ANSI_C - David Winiecki
在C语言中,const并不意味着完全相同的含义。在C语言中,你不能使用const int来指定静态数组的维度 - 这将会创建一个可变长度的数组...等等。 - Antti Haapala -- Слава Україні
@AnttiHaapala - const修饰符意味着该值不会改变。但您正确指出,在C中,const int变量不是常量表达式。为什么会这样,我无法理解。(相比之下,在C++中它是一个常量表达式。)我将更新我的答案以反映这一点。 - Ted Hopp

9

命名常量有益的一个很好的例子来自 Kernighan 和 Pike (1999) 的优秀书籍《编程实践》

§1.5 Magic Numbers

[...] This excerpt from a program to print a histogram of letter frequencies on a 24 by 80 cursor-addressed terminal is needlessly opaque because of a host of magic numbers:

...
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);

The code includes, among others, the numbers 20, 21, 22, 23, and 27. They're clearly related...or are they? In fact, there are only three numbers critical to this program: 24, the number of rows on the screen; 80, the number of columns; and 26, the number of letters in the alphabet. But none of these appears in the code, which makes the numbers that do even more magical.

By giving names to the principal numbers in the calculation, we can make the code easier to follow. We discover, for instance, that the number 3 comes from (80 - 1)/26 and that let should have 26 entries, not 27 (an off-by-one error perhaps caused by 1-indexed screen coordinates). Making a couple of other simplifications, this is the result:

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);

Now it's clearer what the main loop does; it's an idiomatic loop from 0 to NLET, indicating that the loop is over the elements of the data. Also the calls to draw are easier to understand because words like MAXROW and MINCOL remind us of the order of arguments. Most important, it's now feasible to adapt the program to another size of display or different data. The numbers are demystified and so is the code.

修改后的代码实际上并没有使用MINROW,这很有意思;人们不禁想知道哪些剩余的1应该是MINROW。

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

3

变量的作用域仅限于它们声明的结构。当然,您可以使用变量代替符号常量,但这可能需要大量工作。考虑一个经常使用弧度的应用程序。符号常量#define TWO_PI 6.28对程序员非常有价值。


2

Jonathan在讨论中提到了一个很好的观点,即为什么你想在C(以及任何其他编程语言)中使用符号常量。

在C语言中,从句法上来看,这与C++和许多其他语言不同,因为它对您声明此类符号常量的方式非常限制。所谓的“const”限定变量在C++中不适用于此。

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

0

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吗?

使用符号常量来定义数组的大小,可以使更改代码变得更加容易,而不必在代码中搜索特定的数字。


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