Was ist der Sinn symbolischer Konstanten?

Lesezeit: 9 Minuten

Ich habe Probleme zu verstehen, was der Sinn symbolischer Konstanten in C ist. Ich bin sicher, dass es einen Grund dafür gibt, aber ich kann nicht erkennen, warum Sie nicht einfach eine Variable verwenden würden.

#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;
    }

}

Vs.

#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;
    }

}

Benutzeravatar von Ted Hopp
Ted Hopp

Der (Pre-)Compiler weiß, dass sich symbolische Konstanten nicht ändern. Es ersetzt den Wert für die Konstante zur Kompilierzeit. Wenn sich die “Konstante” in einer Variablen befindet, kann sie normalerweise nicht herausfinden, dass die Variable ihren Wert niemals ändern wird. Folglich muss der kompilierte Code den Wert aus dem der Variablen zugewiesenen Speicher lesen, was das Programm etwas langsamer und größer machen kann.

In C++ können Sie eine Variable deklarieren const, was dem Compiler ziemlich dasselbe mitteilt. Aus diesem Grund sind symbolische Konstanten in C++ verpönt.

Beachten Sie jedoch, dass in C (im Gegensatz zu C++) a const int variabel ist nicht ein konstanter Ausdruck. Versuchen Sie daher, Folgendes zu tun:

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

funktioniert in C++, gibt Ihnen aber einen Kompilierungsfehler in C (vorausgesetzt b sollte ein statisch gebundenes Array sein).

  • #define in C ist also ähnlich wie const in C++? Gehe ich recht in der Annahme, dass es auch Final in Java sehr ähnlich ist?

    – Petrus

    21. Februar 2011 um 3:53 Uhr

  • #define foo 3 würde buchstäblich jede Instanz nehmen foo in Ihrem Code, und ersetzen Sie es dann durch 3. Dieser vorverarbeitete Code wird dann an den Compiler übergeben. Daher ist es etwas kraftvoller als final Da der Compiler in Java nie wirklich ein Symbol sieht, sieht er nur den Wert.

    – Bill Lynch

    21. Februar 2011 um 4:20 Uhr


  • const sollte in C mit den meisten Compilern funktionieren. Ich denke, es wurde zuerst in C89 hinzugefügt und auch in C99 und C11 enthalten, die die meisten großen C-Compiler implementieren. en.wikipedia.org/wiki/ANSI_C#Compilers_supporting_ANSI_C

    – David Winicki

    28. Oktober 2014 um 4:06 Uhr

  • const in C bedeutet überhaupt nicht dasselbe. In C können Sie nicht verwenden const int um statische Array-Dimensionen anzugeben – es wird stattdessen ein Array variabler Länge erstellt … und so weiter.

    – Antti Haapala – Слава Україні

    22. April 2019 um 19:53 Uhr


  • @AnttiHaapala – Die const Modifikator bedeutet, dass sich der Wert nicht ändert. Aber Sie haben Recht, dass a const int variable ist kein konstanter Ausdruck in C. Warum das so ist, kann ich mir nicht erklären. (Im Gegensatz dazu ist es ein konstanter Ausdruck in C++.) Ich werde meine Antwort aktualisieren, um dies widerzuspiegeln.

    –Ted Hopp

    22. April 2019 um 21:47 Uhr

Benutzeravatar von Jonathan Leffler
Jonathan Leffler

Ein gutes Beispiel dafür, warum benannte Konstanten nützlich sind, stammt aus dem ausgezeichneten Buch Die Praxis des Programmierens von Kernighan und Pike (1999).

§1.5 Magische Zahlen

[…] Dieser Auszug aus einem Programm zum Drucken eines Histogramms von Buchstabenhäufigkeiten auf einem 24 x 80-Cursor-adressierten Terminal ist wegen einer Vielzahl magischer Zahlen unnötig undurchsichtig:

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

Der Code enthält unter anderem die Zahlen 20, 21, 22, 23 und 27. Sie sind eindeutig verwandt … oder nicht? Tatsächlich sind für dieses Programm nur drei Zahlen entscheidend: 24, die Anzahl der Zeilen auf dem Bildschirm; 80 die Anzahl der Spalten; und 26, die Anzahl der Buchstaben im Alphabet. Aber nichts davon erscheint im Code, was die Zahlen noch magischer macht.

Indem wir den Hauptzahlen in der Berechnung Namen geben, können wir den Code leichter verständlich machen. Wir entdecken zum Beispiel, dass die Zahl 3 von (80 – 1)/26 kommt und dass let 26 Einträge haben sollte, nicht 27 (ein Off-by-One-Fehler, der möglicherweise durch 1-indizierte Bildschirmkoordinaten verursacht wird). Mit ein paar anderen Vereinfachungen ist dies das Ergebnis:

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

Jetzt ist klarer, was die Hauptschleife tut; Es ist eine idiomatische Schleife von 0 bis NLET, die anzeigt, dass die Schleife über den Elementen der Daten liegt. Auch die Anrufe an draw sind leichter zu verstehen, weil Wörter wie MAXROW und MINCOL uns an die Reihenfolge der Argumente erinnern. Am wichtigsten ist, dass es jetzt möglich ist, das Programm an eine andere Anzeigegröße oder andere Daten anzupassen. Die Zahlen sind entmystifiziert, ebenso der Code.

Der überarbeitete Code verwendet nicht wirklich MINROW, was interessant ist; man fragt sich, welche der restlichen Einsen MINROW sein sollten.

  • +1 für ein hervorragendes Beispiel dafür, warum man Konstanten benennen und nicht durch den Code streuen sollte. Der gleiche Vorteil kann jedoch durch die Deklaration von Variablen mit diesen Werten erzielt werden. Der Vorteil von symbolischen Konstanten (oder Aufzählungskonstanten) gegenüber benannten Variablen wird hieraus nicht so deutlich. (Eigentlich benutze ich nicht gerne enum in Situationen wie dieser, in denen keine eindeutige Klasse von Elementen aufgezählt wird [aside from “useful constants”].)

    –Ted Hopp

    21. Februar 2011 um 4:38 Uhr


  • @Ted: Es hängt vom Kontext ab. Das Beispiel gilt für einen festen 24×80-Bildschirm; Konstanten sind geeignet. Wenn Sie die aktuelle Fenstergröße berücksichtigen, sind entsprechend initialisierte benannte Variablen eindeutig besser. Der Code des OP würde am deutlichsten durch die Verwendung von a for (fahr = LOWER; fahr <= UPPER; fahr += STEP) loop, wo ich bezüglich der relativen Vorzüge benannter Konstanten gegenüber reinen Zahlen neutral bin. Im Beispiel sind die Werte für Lower, Upper und Step eher willkürlich. Wenn die Werte waren FREEZING_POINT, BOILING_POINT und (BOILING_POINT - FREEZING_POINT) / 30dann sind Namen besser.

    – Jonathan Leffler

    21. Februar 2011 um 5:51 Uhr

  • Ich stimme der Verwendung von Namen anstelle der rohen Zahlen vollkommen zu. Ich mag es einfach nicht zu benutzen enum Anstatt von #define in diesem speziellen Beispiel. Die Vorteile der Verwendung von Konstanten (#define und/oder enum) anstelle von Variablen werden viel klarer in Beispielen, in denen der Compiler Dinge wie das Eliminieren einer Reihe von Berechnungen in einer inneren Schleife tun kann, weil er einen Ausdruck als Kompilierzeitkonstante erkennt. Dieses Beispiel demonstriert diese Art von Vorteil nicht wirklich. (Ich bin überrascht, dass noch niemand erwähnt hat, wie nützlich symbolische Konstanten für Schalteranweisungen sind. Jetzt habe ich es! :))

    –Ted Hopp

    21. Februar 2011 um 7:51 Uhr

  • @Ted: Ich benutze gerne enum statt #define wenn ich kann, da bekommt der Debugger Bescheid enum Werte, also können Sie es bitten, HEIGHT (zum Beispiel) zu drucken, aber wenn Sie verwenden #define, der Name HEIGHT ist einfach nicht bekannt. OTOH, das geht nicht #ifdef auf ein enum. Ihr Punkt zu Schaltern und Gehäuseetiketten ist gültig.

    – Jonathan Leffler

    21. Februar 2011 um 14:00 Uhr


  • @cacoder — Ja: Aufzählungswerte sind Kompilierzeitkonstanten und der Compiler optimiert sie genauso wie jede andere Kompilierzeitkonstante.

    – Jonathan Leffler

    22. April 2019 um 18:03 Uhr

Variablen sind lokal auf die Struktur beschränkt, in der sie deklariert sind. Natürlich könnten Sie Variablen anstelle von symbolischen Konstanten verwenden, aber das könnte viel Arbeit erfordern. Stellen Sie sich eine Anwendung vor, die häufig Radianten verwendet. Die symbolische Konstante #define TWO_PI 6.28 wäre für den Programmierer von hohem Wert.

Jonathan hat einen guten Punkt gemacht warum Sie möchten symbolische Konstanten in C (und in jeder anderen Programmiersprache, BTW) verwenden.

Syntaktisch unterscheidet sich dies in C von C++ und vielen anderen Sprachen, da es sehr restriktiv ist wie Sie können eine solche symbolische Konstante deklarieren. Sogenannt const Qualifizierte Variablen berücksichtigen dies nicht wie in C++.

  • Sie können ein Makro verwenden, das für einen beliebigen konstanten Ausdruck definiert ist: Ganzzahl- oder Gleitkommakonstanten, Adressausdrücke von statischen Variablen und etwas Ausdrucksformen, die du daraus bildest. Diese werden nur von der Vorverarbeitungsphase des Compilers behandelt, und Sie müssen vorsichtig sein, wenn Sie komplizierte Ausdrücke darin verwenden.
  • Sie können ganzzahlige konstante Ausdrücke in Form von deklarieren Integer-Aufzählungskonstanten wie im enum color { red = 0xFF00, green = 0x00FF00, blue = 0x0000FF };. Sie sind nur von eingeschränktem Nutzen, da sie auf Typ festgelegt sind int. Sie würden also nicht alle Wertebereiche abdecken, die Sie sich wünschen könnten.
  • Sie könnten auch sehen ganzzahlige Zeichenkonstanten wie 'a' oder L'\x4567' als vordefinierte symbolische Konstanten, wenn Sie möchten. Sie übersetzen ein abstraktes Konzept (den Zeichenwert “a”) in die Kodierung der ausführenden Plattform (ASCII, EBDIC, was auch immer).

Benutzeravatar von pacmaninbw
pacmaninbw

Jonathan liefert ein hervorragendes Beispiel für die Verwendung symbolischer Konstanten.

Es ist möglich, dass das in der Frage verwendete Programm nicht das beste ist, um diese Frage zu beantworten. Angesichts des Programms könnten symbolische Konstanten jedoch in folgendem Fall sinnvoller sein:

#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;
    }
}

Möglicherweise ist dadurch verständlicher, warum symbolische Konstanten nützlich sein könnten.

Das Programm beinhaltet stdio.h, eine ziemlich verbreitete Include-Datei. Schauen wir uns einige der symbolischen Konstanten an, die in definiert sind stdlib.h. Diese Version von stdio.h ist von Xcode.

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

Sehen wir uns auch zwei symbolische Konstanten an, die in definiert sind stdlib.h.

#define EXIT_FAILURE    1
#define EXIT_SUCCESS    0

Diese Werte können von System zu System variieren, aber ihre Verwendung macht das Programmieren in C viel einfacher und portierbarer. Die symbolischen Konstanten für stdin, stdout und stderr Es ist bekannt, dass sie sich in verschiedenen Betriebssystemimplementierungen ändern.

Die Verwendung von BUFSIZ zum Definieren von Zeichenarrays für C-Eingabepuffer ist im Allgemeinen sehr sinnvoll. Die Verwendung von EXIT_FAILURE und EXIT_SUCCESS macht den Code viel lesbarer, und ich muss mich nicht daran erinnern, ob 0 ein Fehler oder ein Erfolg ist. Würde jemand (-1) EOF vorziehen?

Die Verwendung einer symbolischen Konstante zum Definieren der Größe von Arrays macht es viel einfacher, Code an einer Stelle zu ändern, anstatt überall nach einer bestimmten Zahl suchen zu müssen, die in den Code eingebettet ist.

1432680cookie-checkWas ist der Sinn symbolischer Konstanten?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy