Funktionen und Programmstruktur in C

Lerne, C-Programme so zu organisieren, wie echte Projekte aufgebaut sind. Schreibe Funktionen mit Parametern, Rückgabewerten und Prototypen, erfahre, warum C Argumente als Werte übergibt, und verfolge Aufrufe und Rekursion schrittweise. Steuere mit Gültigkeitsbereichen und Speicherklassen, darunter static und extern, wo Variablen sichtbar sind und wie lange sie existieren. Verwende den Präprozessor für Headerdateien, Makros und bedingte Kompilierung, teile ein Programm auf Quell- und Headerdateien auf und erstelle es mit make, sodass nur geänderte Dateien erneut kompiliert werden.

AnfängerEinheit 2 von 8 in Kurs: C-Programmierung – Zeiger, Speicherverwaltung und sicheres C

Was du lernst

  1. Teile ein Programm in Funktionen auf, die Parameter entgegennehmen und Ergebnisse zurückgeben, deklariere sie mit Prototypen, verstehe, dass C Argumente als Werte übergibt, und verfolge, wie Aufrufe einschließlich rekursiver Aufrufe ausgeführt und beendet werden.
    • Funktionen definieren, die Eingaben entgegennehmen und ein Ergebnis zurückgeben
      Schreibe Funktionsdefinitionen mit Rückgabetyp, Parametern und einem Rumpf, verwende return und void richtig und rufe Funktionen innerhalb von Ausdrücken auf.
    • Funktionen mit Prototypen deklarieren
      Deklariere eine Funktion vor ihrem ersten Aufruf mit einem Prototyp, erfahre, was der Compiler prüft und umwandelt, und vermeide leere Klammern und implizite Deklarationen.
    • Argumente als Werte übergeben und das Ergebnis zurückgeben
      Erkläre, dass jedes C-Argument in seinen Parameter kopiert wird, sage voraus, was eine Funktion ändern kann und was nicht, und verwende Rückgabewerte, um Ergebnisse zurückzugeben.
    • Funktionsaufrufe und Rekursion schrittweise nachvollziehen
      Verfolge, wie jeder Aufruf eigene Parameter und lokale Variablen erhält und zu seinem Aufrufer zurückkehrt, und schreibe und verfolge rekursive Funktionen mit einem Basisfall.
  2. Unterscheide den Gültigkeitsbereich eines Namens von der Lebensdauer einer Variablen, verwende statische lokale Variablen, um einen Zustand zwischen Aufrufen zu erhalten, und wäge die Nachteile globaler Variablen ab.
    • Festlegen, wo ein Name verwendet werden kann (Gültigkeitsbereich)
      Wende Block-, Datei- und Funktionsprototyp-Gültigkeitsbereich an, sage voraus, auf welche Variable sich ein Name bei einer Verdeckung bezieht, und vermeide Fehler beim Gültigkeitsbereich.
    • Die Lebensdauer einer Variablen mit Speicherklassen festlegen
      Unterscheide automatische und statische Speicherdauer, verwende statische lokale Variablen, um Werte zwischen Aufrufen zu speichern, und lerne die Bedeutung von auto, register, static und extern kennen.
    • Daten mit globalen Variablen umsichtig gemeinsam nutzen
      Definiere und verwende Variablen mit Dateigültigkeitsbereich (globale Variablen), erkläre ihre Sichtbarkeit und Initialisierung und bevorzuge Parameter und Rückgabewerte, wo eine globale Variable eine versteckte Kopplung schaffen würde.
  3. Verwende #include, #define und Makros sicher und wähle Code zur Übersetzungszeit mit bedingten Direktiven, vordefinierten Makros und Compileroptionen aus.
    • Dateien einbinden und Makros definieren
      Erkläre, was der Präprozessor vor der Kompilierung tut, binde Headerdateien ein, definiere Konstanten und funktionsähnliche Makros und vermeide die klassischen Fallstricke von Makros.
    • Code bedingt kompilieren
      Verwende #if, #ifdef, #ifndef, #elif, #else und #endif mit -D-Optionen und vordefinierten Makros, um Code zur Übersetzungszeit ein- oder auszuschließen, auch mit Include-Guards.
  4. Kompiliere mehrere Quelldateien getrennt und linke sie, schreibe Headerdateien zur gemeinsamen Nutzung von Deklarationen und steuere mit static und extern, welche Namen andere Dateien sehen können.
    • Mehrere Quelldateien kompilieren und linken
      Beschreibe die Schritte von der Quelldatei bis zur ausführbaren Datei, kompiliere Dateien getrennt in Objektdateien, linke sie und unterscheide Übersetzungsfehler von Linkerfehlern.
    • Headerdateien schreiben
      Schreibe gemeinsam genutzte Deklarationen in eine geschützte Headerdatei, binde sie dort ein, wo sie benötigt wird, auch in ihrer eigenen Quelldatei, und halte Definitionen aus ihr heraus.
    • Die Bindung mit static und extern steuern
      Verwende extern, um eine Variable zwischen Dateien gemeinsam zu nutzen, und static, um Funktionen und Variablen auf ihre Datei zu beschränken, und behebe Linkerfehler, die durch eine falsche Verwendung entstehen.
  5. Schreibe Makefiles, deren Regeln und Abhängigkeiten nur Geändertes erneut erstellen, und halte sie mit Variablen, automatischen Variablen, Musterregeln und Pseudozielen kurz.
    • Regeln und Abhängigkeiten in Makefiles festlegen
      Schreibe Regeln mit Zielen, Abhängigkeiten und mit Tabulator eingerückten Rezepten, sage voraus, welche Befehle make nach einer Änderung ausführt, und führe Header-Abhängigkeiten korrekt auf.
    • Makefiles mit Variablen und Musterregeln vereinfachen
      Verwende Variablen wie CC und CFLAGS, die automatischen Variablen $@, $< und $^, Musterregeln, eingebaute Regeln und Pseudoziele, um ein Makefile kurz und korrekt zu halten.

Funktionen und Programmstruktur in C einfach erklärt

5 Kapitel

1

Funktionen schreiben, deklarieren und aufrufen

Eine Funktion gibt einer Aufgabe einen Namen, eine Reihe von Eingaben und eine klare Möglichkeit, ein Ergebnis zu erzeugen. Du erstellst Definitionen und Prototypen und verfolgst dann Werte in Aufrufe hinein und zurück zu ihren Aufrufern. Kleine Ablaufprotokolle machen die Regeln sichtbar: Ein Parameter erhält eine Kopie, ein Rückgabewert braucht einen Ort, an dem er verwendet wird, und ein rekursiver Aufruf muss irgendwann enden. Die Beispiele verwenden C17; explizite Parameterlisten machen den Code auch beim Kompilieren als C23 verständlich.

1.1 Funktionen definieren, die Eingaben entgegennehmen und ein Ergebnis zurückgeben

Eine Funktion fasst eine Aufgabe so zusammen, dass ein anderer Teil des Programms sie aufrufen kann. Gib ihr einen Namen, der diese Aufgabe beschreibt, und lege ihre Eingaben und ihr Ergebnis fest, bevor du ihren Rumpf schreibst. In einem gewöhnlichen C-Programm mit einer Host-Umgebung beginnt die Ausführung in main.

Eine Definition erstellen
int square(int n)
{
    return n * n;
}

Die Definition hat einen Rückgabetyp (int), einen Namen (square), eine Parameterliste (int n) und einen Rumpf in geschweiften Klammern. Der Parameter ist die lokale Variable, die eine Eingabe erhält. Ein Argument ist der Wert, den ein Aufruf übergibt: In square(6) ist dieser Wert 6. Der Aufruf ergibt 36. Definiere jede Funktion auf Dateiebene außerhalb anderer Funktionen; Standard-C erlaubt keine verschachtelten Funktionsdefinitionen.

Jeder Parameter braucht einen eigenen Typ. Schreibe int max(int a, int b) und trenne die Deklarationen durch ein Komma. Gib bei einem Aufruf Werte ohne ihre Typen an: max(8, 3).

Einen Wert an den Aufrufer zurückgeben
int max(int a, int b)
{
    if (a > b)
        return a;
    return b;
}

Ein return beendet den aktuellen Aufruf sofort. Bei max(8, 3) wird das erste return ausgeführt, sodass das zweite nie erreicht wird. Ein zurückgegebener Ausdruck wird in den Rückgabetyp der Funktion umgewandelt. Wähle diesen Typ so, dass die benötigte Art von Ergebnis erhalten bleibt.

Verwende für ein Ergebnis mit Nachkommateil durchgehend Gleitkommaarithmetik:

double c_to_f(double c)
{
    return c * 9.0 / 5.0 + 32.0;
}

Der Aufruf c_to_f(37.0) gibt 98.6 zurück. Eine Ganzzahlversion mit c * 9 / 5 + 32 gibt 98 zurück, weil die Division den Nachkommateil abschneidet. Stellst du den Ausdruck zu 9 / 5 * c + 32 um, wird zuerst 9 / 5 zu 1 ausgewertet, was für 37 den Wert 69 ergibt. Ein Gleitkomma-Rückgabetyp allein kann eine bereits im Rumpf ausgeführte Ganzzahlberechnung nicht rückgängig machen.

Das Ergebnis verwenden

Ein Aufruf, der einen Wert zurückgibt, ist ein Ausdruck. Mit der obigen Definition ergibt square(3) + square(4) den Wert 25. In square(square(2)) gibt der innere Aufruf 4 zurück. Dieser Wert wird zum Argument des äußeren Aufrufs, der 16 ergibt. Eine Anweisung wie square(5); ist zulässig, verwirft aber den zurückgegebenen Wert.

Ausgeben und Zurückgeben erfüllen unterschiedliche Aufgaben. printf zeigt Text an; return liefert ein Ergebnis, das der Aufrufer speichern oder in einem anderen Ausdruck verwenden kann. Erreicht eine gewöhnliche Funktion mit einem anderen Rückgabetyp als void ihre schließende Klammer, ohne zurückzukehren, und verwendet der Aufrufer ihr Ergebnis, ist das Verhalten undefiniert. Kompiliere mit -Wall -Wextra, um fehlende Rückgaben leichter zu finden.

Funktionen, die keinen Wert liefern

Eine void-Funktion erledigt eine Aufgabe, ohne einen Wert zurückzugeben. Sie kann mit return; vorzeitig beendet werden. Zum Beispiel kann void print_line(int width) eine Zeile mit Bindestrichen ausgeben, aber int x = print_line(5); ist ungültig, weil der Aufruf keinen Wert liefert. Soll die Funktion keine Eingaben entgegennehmen, schreibe void in die Parameterliste, wie in int read_sensor(void).

int main(void) gibt einen Status an die Host-Umgebung zurück. Verwende return 0; für Erfolg; <stdlib.h> stellt außerdem EXIT_SUCCESS und EXIT_FAILURE bereit. Das Erreichen des Endes von main gibt automatisch null zurück. Diese Ausnahme gilt nicht für andere Funktionen, die einen Wert zurückgeben.

Wichtige Erkenntnisse

  • Eine Definition enthält einen Rückgabetyp, einen Namen, Parameter mit Typangaben und einen Rumpf.
  • Ein Parameter erhält ein Argument, das ein Aufruf übergibt.
  • return beendet den Aufruf; eine Ausgabe liefert nicht dessen Ergebnis.
  • Verwende void, wenn kein Wert zurückgegeben wird, und (void), wenn es keine Parameter gibt.
  • Wähle Arithmetik und Rückgabetypen so, dass das benötigte Ergebnis erhalten bleibt.

1.2 Funktionen mit Prototypen deklarieren

Ein Funktionsaufruf benötigt eine Deklaration, die der Compiler bereits gesehen hat. Steht die Definition später, setze einen Prototyp vor den ersten Aufruf. Der Prototyp teilt dem Compiler mit, wie er den Aufruf prüfen soll, ohne den Funktionsrumpf bereitzustellen.

Die Zusage vom Rumpf trennen
#include <stdio.h>

double c_to_f(double c);

int main(void)
{
    printf("%.1f\n", c_to_f(25.0));
    return 0;
}

double c_to_f(double c)
{
    return c * 9.0 / 5.0 + 32.0;
}

Der Prototyp endet mit einem Semikolon. Die Definition hat einen Rumpf und kein Semikolon zwischen ihrer Parameterliste und der öffnenden geschweiften Klammer. Eine nützliche Anordnung ist: Headerdateien, Prototypen, main, dann weitere Definitionen. Die Ausgabe lautet hier 77.0.

Parameternamen sind in Prototypen optional: double c_to_f(double); deklariert dieselbe Funktion. Namen wie c können eine Eingabe dokumentieren, müssen aber nicht mit den Namen in der Definition übereinstimmen. Alle Deklarationen müssen beim Rückgabetyp und den Parametertypen übereinstimmen. Eine verwendete Funktion benötigt eine Definition; wiederholte kompatible Prototypen erzeugen keine weiteren Funktionen.

Was der Compiler prüft

Mit einem Prototyp prüft der Compiler die Anzahl der Argumente und wandelt jeden übergebenen Wert in den deklarierten Parametertyp um. Bei double c_to_f(double c); haben sowohl c_to_f() als auch c_to_f(20.0, 5.0) die falsche Anzahl und erfordern eine Diagnose.

Ein Unterschied zwischen numerischen Typen kann dagegen eine zulässige Umwandlung sein:

int half(int n)
{
    return n / 2;
}

Bei half(7.9) erhält der Parameter durch die Umwandlung zunächst den Wert 7. Die Ganzzahldivision ergibt dann 3. Bei half(-7.9) ergibt die Umwandlung -7 und die Division -3. In beiden Fällen werden Nachkommateile in Richtung null abgeschnitten. Der Aufruf erhält den ursprünglichen double-Wert nicht allein deshalb, weil das Argument mit einem Dezimalpunkt geschrieben wurde. Ergänze -Wconversion, wenn du Warnungen zu solchen Umwandlungen erhalten möchtest.

Ausdrücklich angeben, wenn es keine Parameter gibt
DeklarationBedeutung in C17Bedeutung in C23
int read_sensor(void);Keine ParameterKeine Parameter
int read_sensor();Parameterinformation nicht festgelegtKeine Parameter

In C17 verhindert eine leere Parameterliste in einer Deklaration eine vollständige Argumentprüfung; sie erlaubt keine beliebigen Argumente für die spätere Funktion. Verwende durchgehend (void). GCC 15 hat seinen standardmäßigen C-Dialekt auf gnu23 geändert, sodass -std=c17 den beabsichtigten Standard ausdrücklich festlegt, wenn du diesen Beispielen folgst.

Ein Deklarationsproblem beheben

Der Aufruf einer nicht deklarierten Funktion verstößt gegen modernes C. Binde die Standardheaderdatei ein, die sie deklariert, etwa <stdio.h> für printf, oder ergänze einen eigenen passenden Prototyp. Aktuelle Versionen von GCC und Clang lehnen implizite Deklarationen normalerweise ab.

Ein Prototyp wie double area(double r); kann nicht zu einer Definition passen, die float zurückgibt und float entgegennimmt. Binde die Headerdatei eines Moduls in die Datei ein, die seine Funktionen definiert, damit der Compiler sie vergleichen kann. Versuche nicht, eine Typabweichung durch eine explizite Typumwandlung beim Aufruf zu beheben: Korrigiere Deklaration und Definition so, dass sie dieselbe Funktion beschreiben.

Wichtige Erkenntnisse

  • Setze eine Deklaration vor den ersten Aufruf.
  • Ein Prototyp endet mit einem Semikolon; eine Definition liefert den Rumpf.
  • Prototypen ermöglichen die Prüfung der Argumentanzahl und legen Argumentkonvertierungen fest.
  • Verwende (void) sowohl in C17 als auch in C23 für keine Parameter.
  • Binde die deklarierende Headerdatei ein und halte alle Deklarationen miteinander vereinbar.
  • Auch bei einer zulässigen numerischen Umwandlung kann ein Nachkommateil verloren gehen.

1.3 Argumente als Werte übergeben und das Ergebnis zurückgeben

C übergibt jedes Argument als Wert: Jeder Parameter beginnt mit einer Kopie des Werts seines Arguments. Eine Zuweisung an einen gewöhnlichen Ganzzahlparameter ändert diese Kopie, auch wenn der Parameter denselben Namen wie eine Variable des Aufrufers hat.

Die Kopie verfolgen

Mit eingebundenem <stdio.h> und dieser Definition vor ihrem Aufruf:

void double_it(int n)
{
    n *= 2;
    printf("inside: %d\n", n);
}

Der Aufrufer führt aus:

int score = 21;
double_it(score);
printf("after: %d\n", score);

Die Ausgabe lautet inside: 42, danach after: 21. Der Parameter ändert sich von 21 auf 42; keine Anweisung weist dem score des Aufrufers einen Wert zu.

Ein zurückgegebenes Ergebnis behalten
int doubled(int n)
{
    return n * 2;
}
Anweisung des AufrufersAuswirkung auf score mit dem Anfangswert 21
doubled(score);Verwirft 42; score bleibt 21
score = doubled(score);Speichert 42 in score

Die Funktion berechnet einen Wert; die Zuweisung des Aufrufers aktualisiert dessen Variable. Die Rückgabe des Werts einer lokalen Variablen ist sicher: int total = a + b; return total; kopiert den Wert heraus, bevor die automatische lokale Variable aufhört zu existieren. Berechnungen, die nur Parameter und zurückgegebene Ergebnisse verwenden, lassen sich testen, ohne versteckten gemeinsam genutzten Zustand einzurichten.

Verstehen, warum ein Tausch nur mit Werten scheitert
void swap(int a, int b)
{
    int t = a;
    a = b;
    b = t;
}

Nach int p = 3, q = 7; swap(p, q); gelten beim Aufrufer weiterhin p == 3 und q == 7. Der Rumpf hat seine Kopien vertauscht. Um Objekte des Aufrufers über Parameter zu ändern, sind ihre Adressen nötig, die als Zeigerwerte übergeben werden.

Argumente in eine Reihenfolge bringen, wenn sie wichtig ist

Argumente können Ausdrücke wie a + 1 oder square(b) sein. Ihre Auswertung ist abgeschlossen, bevor der Rumpf der aufgerufenen Funktion beginnt, aber ihre Auswertungsreihenfolge ist nicht festgelegt. Zwei Aufrufe einer zustandsändernden Funktion next_ticket() in einer Ausgabe können die Ticketnummern in beliebiger Reihenfolge liefern. Verwende getrennte Anweisungen:

int first = next_ticket();
int second = next_ticket();
printf("%d %d\n", first, second);

Vermeide f(i, i++): Das nicht sequenzierte Lesen und Ändern von i führt zu undefiniertem Verhalten.

Wichtige Erkenntnisse

  • Parameter erhalten Kopien der Argumentwerte.
  • Speichere ein zurückgegebenes Ergebnis mit einer Zuweisung, wenn beim Aufrufer eine Aktualisierung nötig ist.
  • Ein Tausch nur mit Werten ändert seine Parameter, nicht die Variablen des Aufrufers.
  • Verwende getrennte Anweisungen, wenn die Auswertungsreihenfolge der Argumente wichtig ist.

1.4 Funktionsaufrufe und Rekursion schrittweise nachvollziehen

Ein Aufruf hält seinen Aufrufer an, bis ein Ergebnis bereitsteht. Verfolgst du die aktiven Aufrufe, verstehst du, wie ein Programm an die richtige Stelle zurückkehrt und warum Rekursion eine Abbruchregel braucht.

Einen Datensatz pro aktivem Aufruf führen

Die meisten Implementierungen verwenden einen Aufrufstapel. Jeder aktive Aufruf hat einen Stackframe, der seine Parameterwerte, automatischen lokalen Variablen und die Fortsetzungsstelle im Aufrufer festhält. Der Standard legt das Verhalten fest, ohne diese bestimmte Speicheranordnung vorzuschreiben.

Wenn main die Funktion print_table aufruft und print_table die Funktion c_to_f aufruft, kehrt die Umrechnungsfunktion zuerst zurück. Die Ausführung wird in print_table fortgesetzt, das später zu main zurückkehrt. Gewöhnliche automatische lokale Variablen gehören zu einem Aufruf. Bei einer statischen lokalen Variablen ist es anders: Alle Aufrufe teilen sich ihren einen gespeicherten Wert.

Eine rekursive Berechnung abwärts verfolgen

Eine rekursive Funktion ruft sich selbst für ein kleineres Problem auf. Ein Basisfall kehrt direkt zurück; ein Rekursionsfall nähert sich ihm an.

int factorial(int n)
{
    if (n <= 1)
        return 1;
    return n * factorial(n - 1);
}

Bei factorial(4) kann der erste Aufruf seine Multiplikation erst abschließen, wenn factorial(3) zurückkehrt. Dieser Aufruf wartet auf factorial(2), das auf factorial(1) wartet.

Aktiver AufrufBerechnung, die auf ein Ergebnis wartet
factorial(4)4 * factorial(3)
factorial(3)3 * factorial(2)
factorial(2)2 * factorial(1)
factorial(1)Basisfall: 1 zurückgeben

Am tiefsten Punkt sind vier Fakultätsaufrufe aktiv. Jeder hat sein eigenes n; das Erreichen von n == 1 ändert die Werte der früheren Aufrufe nicht.

Die Rückgaben aufwärts verfolgen

Der Aufruf im Basisfall gibt 1 zurück. Der Aufruf mit n == 2 berechnet nun 2 * 1 und gibt 2 zurück. Der nächste gibt 3 * 2, also 6, zurück; der erste gibt 4 * 6, also 24, zurück. Trage die Rückgabewerte beim Aufstieg in ein Ablaufprotokoll ein, nachdem das Ergebnis der aufgerufenen Funktion bekannt ist.

Die Abbruchbedingung muss für die unterstützten Eingaben erreichbar sein. Lautete die Prüfung n == 1, würde der Start bei null zu weiteren Aufrufen mit -1, -2 und kleineren Werten führen, statt eins zu erreichen. Fortgesetzte Rekursion erschöpft Ressourcen und kann zum Absturz führen. Die umfassendere Prüfung n <= 1 stoppt diese Eingaben, obwohl die mathematische Fakultät für nichtnegative Ganzzahlen verwendet wird.

Sowohl die Tiefe als auch den Ergebnistyp prüfen

Ein korrekter Abbruch garantiert nicht, dass das Ergebnis in den Typ passt. Auf einer Plattform mit einem 32-Bit-int ist der größte Wert 2147483647. factorial(12) ergibt 479001600, was hineinpasst; factorial(13) bräuchte 6227020800, was nicht hineinpasst. Ein Überlauf bei vorzeichenbehafteten Ganzzahlen führt zu undefiniertem Verhalten.

Eine Schleife kann die Fakultät berechnen und dabei ein laufendes Produkt in einem Funktionsaufruf führen. Rekursion ist nützlich, wenn das nächste Problem dieselbe Struktur in kleinerem Maßstab hat. Bei positiven Ganzzahlen ist die Summe der Ziffern ein weiteres Beispiel: sum_digits(472) benötigt 2 + sum_digits(47), dann 2 + 7 + sum_digits(4), was 13 ergibt. Ein Ablaufprotokoll sollte sowohl die kleiner werdende Eingabe als auch die spätere Verknüpfung der zurückgegebenen Werte zeigen.

Wichtige Erkenntnisse

  • Aufrufe enden in umgekehrter Reihenfolge ihrer Verschachtelung.
  • Jeder Aufruf hat eigene Parameter und automatische lokale Variablen.
  • Rekursion braucht einen erreichbaren Basisfall und muss sich ihm annähern.
  • Verfolge Aufrufe abwärts und Rückgabewerte aufwärts.
  • Prüfe, ob rekursive Ergebnisse in ihren numerischen Typ passen.
2

Die Sichtbarkeit und Lebensdauer von Variablen steuern

Ein Name kann in einem kleinen Teil des Quelltexts sichtbar sein, während seine Variable während der gesamten Programmausführung existiert. Wenn du diese beiden Eigenschaften auseinanderhältst, verstehst du lokale, statische lokale und globale Variablen viel leichter. Du verfolgst Namen durch verschachtelte Blöcke, vergleichst neue mit gespeicherten Werten und entscheidest, wie eine Funktion die benötigten Daten erhält. Diese Entscheidung beeinflusst, wie leicht du ein falsches Ergebnis zurückverfolgen und eine Funktion für sich testen kannst.

2.1 Festlegen, wo ein Name verwendet werden kann (Gültigkeitsbereich)

Der Gültigkeitsbereich ist der Teil des Quelltexts, in dem ein Name verwendet werden kann. Lies die geschweiften Klammern und Deklarationen, um zu ermitteln, welche Variable ein Ausdruck bezeichnet; die Reihenfolge der Aufrufe ändert diese Regeln nicht.

Die Begrenzung des Gültigkeitsbereichs finden

Eine innerhalb eines Blocks deklarierte Variable hat Blockgültigkeitsbereich. Er beginnt unmittelbar nach ihrem Deklarator und endet an der schließenden Klammer dieses Blocks. Parameter sind innerhalb ihrer Funktionsrümpfe sichtbar. Eine Deklaration in einem for-Kopf wie for (int i = 0; i < 10; i++) macht i während dieser Schleife verfügbar, aber nicht danach.

Ein außerhalb aller Funktionen deklarierter Name hat Dateigültigkeitsbereich, der von seiner Deklaration bis zum Ende der Übersetzungseinheit reicht. Funktionen oberhalb dieser Deklaration benötigen eine frühere Deklaration, um ihn zu verwenden. Ein Parametername in einem Prototyp hat Funktionsprototyp-Gültigkeitsbereich, der mit dem Prototyp endet. Daher kann auf double c_to_f(double c); eine Definition folgen, die den Parameter degrees nennt.

Verdeckung von innen nach außen verfolgen
#include <stdio.h>

int x = 10;

void show(void)
{
    printf("%d\n", x);
}

int main(void)
{
    int x = 5;
    {
        int x = 1;
        printf("%d\n", x);
    }
    printf("%d\n", x);
    show();
    return 0;
}

Die innere Ausgabe verwendet 1, die nächste verwendet 5, und show gibt 10 aus. Es gilt die nächstgelegene sichtbare Deklaration von x. Beim Verlassen des inneren Blocks wird die äußere lokale Variable wieder sichtbar. Der Aufruf von show macht die lokale Variable von main dort nicht sichtbar: show wurde dort definiert, wo das x mit Dateigültigkeitsbereich sichtbar ist.

Verschiedene Funktionen dürfen jeweils ein lokales i, count oder result deklarieren. Dies sind unterschiedliche Variablen, weil ihre Deklarationen zu verschiedenen Blöcken gehören.

Einen Wert so deklarieren, dass er später verwendet werden kann

Dieser Code lässt sich nicht kompilieren:

if (temp > 30.0) {
    int warning = 1;
}
printf("%d", warning);

Die Ausgabe liegt außerhalb des Gültigkeitsbereichs von warning. Muss der Wert danach verwendet werden, deklariere und initialisiere ihn vor der bedingten Anweisung:

int warning = 0;
if (temp > 30.0) {
    warning = 1;
}
printf("%d", warning);

Die Initialisierung liefert auch den Wert für den Pfad, bei dem die Bedingung falsch ist.

Eine versehentlich neue Variable vermeiden

Die Deklaration int total = 0; innerhalb einer Schleife erzeugt bei jedem Eintritt in ihren Block eine neue lokale Variable. Wenn du Werte in einem äußeren total aufsummieren wolltest, verwende eine Zuweisung wie total += value;. Aktiviere -Wshadow, um verdeckte Namen leichter zu finden.

Der Gültigkeitsbereich beginnt, bevor der Initialisierer ausgewertet wird. Innerhalb eines Blocks liest int x = x + 1; das neue, nicht initialisierte x, auch wenn ein äußeres x existiert. Verwende einen anderen Namen, wenn der Initialisierer den äußeren Wert benötigt.

Der Gültigkeitsbereich beschreibt, wo ein Name im Quellcode sichtbar ist. Die Speicherdauer beschreibt, wie lange das zugehörige Objekt während der Programmausführung existiert. Eine statische lokale Variable hat Blockgültigkeitsbereich, während ihr gespeicherter Wert während der gesamten Ausführung existiert.

Wichtige Erkenntnisse

  • Der Blockgültigkeitsbereich endet an der zugehörigen schließenden Klammer.
  • Namen mit Dateigültigkeitsbereich sind unterhalb ihrer Deklarationen sichtbar.
  • Eine innere Deklaration verdeckt eine sichtbare äußere Deklaration.
  • Eine aufgerufene Funktion kann die automatischen lokalen Variablen ihres Aufrufers nicht sehen.
  • Der Gültigkeitsbereich beginnt, bevor ein Initialisierer ausgewertet wird.
  • Gültigkeitsbereich und Speicherdauer beantworten unterschiedliche Fragen.

2.2 Die Lebensdauer einer Variablen mit Speicherklassen festlegen

Die Speicherdauer beschreibt, wie lange der Speicher einer Variablen existiert. Verwende automatische Variablen für Aufgaben, die zu einem Aufruf gehören, und statischen Speicher, wenn ein Wert zwischen Aufrufen erhalten bleiben muss.

Einen neuen Wert mit einem gespeicherten Wert vergleichen

Parameter und gewöhnliche Variablen mit Blockgültigkeitsbereich haben automatische Speicherdauer. Jeder Funktionsaufruf erhält seine eigenen Instanzen. Eine Deklaration ohne Initialisierer wie int sum; liefert keinen brauchbaren Anfangswert. Initialisiere einen Akkumulator, bevor du ihn liest: int sum = 0;.

Variablen mit Dateigültigkeitsbereich und als static deklarierte Variablen mit Blockgültigkeitsbereich haben statische Speicherdauer. Sie existieren während der gesamten Programmausführung. Die Initialisierung erfolgt einmal vor dem Programmstart; ohne ausdrücklichen Initialisierer beginnen numerische Objekte bei null.

int next_ticket(void)
{
    static int n = 0;
    n++;
    return n;
}

Drei Aufrufe in getrennten Anweisungen geben 1, 2 und 3 zurück. Der Initialisierer wird beim Eintritt in die Funktion nicht erneut ausgeführt. Entfernst du static, entsteht bei jedem Aufruf ein neues automatisches n = 0, sodass alle drei Aufrufe 1 zurückgeben.

Der Name n hat weiterhin Blockgültigkeitsbereich. Andere Funktionen können ihn nicht direkt benennen. Auch rekursive Aufrufe teilen sich dieselbe statische lokale Variable; sie erhalten keine getrennten statischen Kopien wie bei automatischen lokalen Variablen.

Einmal initialisieren, bei Bedarf zuweisen

Ein statischer numerischer Initialisierer muss in C ein konstanter Ausdruck sein. static int limit = 12; ist zulässig; static int limit = read_sensor(); ist unzulässig. Nimm eine Zuweisung zur Laufzeit im ausführbaren Code vor, wenn ein Wert aus einem Aufruf stammen muss. Eine Anweisung wie n = 0; wird immer ausgeführt, wenn die Ausführung sie erreicht. Stünde sie am Anfang von next_ticket, würde sie den Zähler bei jedem Aufruf zurücksetzen.

Die Speicherklassenspezifizierer verstehen
SpezifiziererBedeutung in diesem C17-Code
autoAusdrückliche automatische Speicherdauer für eine Blockvariable; wird meist weggelassen
registerEin Hinweis auf schnellen Zugriff; die Adresse darf nicht ermittelt werden
static innerhalb einer FunktionErhaltener Speicher mit einem lokalen Namen
static im DateigültigkeitsbereichInterne Bindung, die den Namen innerhalb seiner Übersetzungseinheit hält
externEine Deklaration, die sich auf ein vorhandenes Objekt oder eine vorhandene Funktion bezieht

Moderne Compiler wählen die Registerbelegung selbst, sodass das Schreiben von register den Code selten verbessert. auto int x; bedeutet in C17 dasselbe wie ein gewöhnliches lokales int x;. C23 erlaubt außerdem die Typinferenz mit auto x = 3.5;, was ein double deklariert.

C17 hat außerdem _Thread_local, in C23 thread_local geschrieben, für Thread-Speicher. Die Grammatik zählt typedef zu den Speicherklassenspezifizierern, obwohl es einen Typ benennt, statt Speicher auszuwählen. In C17 steht normalerweise nur ein Speicherklassenspezifizierer in einer Deklaration; _Thread_local kann zusätzlich zu static oder extern stehen.

Ein extern int count; im Blockgültigkeitsbereich erzeugt keine automatische lokale Kopie. Es deklariert ein Objekt mit Bindung und statischer Speicherdauer. Berücksichtige immer, wo die Deklaration steht, bevor du entscheidest, was ein Speicherklassen-Schlüsselwort bewirkt.

Wichtige Erkenntnisse

  • Initialisiere automatische Variablen, bevor du sie liest.
  • Statischer Speicher besteht während der gesamten Programmausführung und ist standardmäßig null.
  • Eine statische lokale Variable behält ihren Wert, während ihr Name lokal bleibt.
  • Ein statischer Initialisierer wird einmal ausgeführt; eine Zuweisung wird ausgeführt, wenn sie erreicht wird.
  • Die Position von static bestimmt, ob seine Hauptwirkung die Lebensdauer oder die Bindung betrifft.

2.3 Daten mit globalen Variablen umsichtig gemeinsam nutzen

Eine globale Variable wird im Dateigültigkeitsbereich außerhalb aller Funktionen deklariert. Sie behält während der gesamten Ausführung einen Wert und kann mehreren Funktionen Zugriff auf gemeinsam genutzten Zustand geben.

Das gemeinsam genutzte Objekt verfolgen
#include <stdio.h>

double total_km = 0.0;

void add_trip(double km)
{
    total_km += km;
}

int main(void)
{
    add_trip(12.5);
    add_trip(7.25);
    printf("%.2f\n", total_km);
    return 0;
}

Beide Aufrufe aktualisieren dasselbe Objekt und ergeben 19.75. Der Parameter km ist weiterhin eine Kopie; die Funktion weist unabhängig davon einer globalen Variablen, die sie benennen kann, einen Wert zu. Die Wertübergabe verhindert diese Zuweisung nicht.

Globale Variablen haben statische Speicherdauer. Numerische globale Variablen beginnen standardmäßig bei null und benötigen konstante Ausdrücke als Initialisierer: int error_count; beginnt bei 0, und double limit = 2.5 * 4; beginnt bei 10.0.

Sichtbarkeit und Verdeckung prüfen

Ein globaler Name ist nur unterhalb seiner Deklaration sichtbar. Eine Funktion oberhalb der Definition benötigt eine frühere Deklaration, etwa extern double total_km;.

Enthält add_trip stattdessen:

double total_km = 0.0;
total_km += km;

so erzeugt und aktualisiert die Funktion ein lokales Objekt. Die globale Variable bleibt unverändert. Entferne die zusätzliche Deklaration, um die gemeinsam genutzte Summe zu aktualisieren; -Wshadow hilft, versehentliche Verdeckung zu erkennen.

Eingaben ausdrücklich angeben, wenn gemeinsamer Zustand unnötig ist

Ein gemeinsam genutzter Fehlerzähler kann nützlich sein, wenn mehrere Funktionen Fehler melden. Eine Berechnung, die eine globale Variable liest, hat jedoch eine Eingabe, die in ihrer Parameterliste fehlt. Tests müssen diesen Zustand herstellen, und um einen falschen Wert zurückzuverfolgen, musst du die Stellen finden, die ihn möglicherweise schreiben.

Eine Alternative für die Streckenberechnung ist:

double with_trip(double total, double km)
{
    return total + km;
}

Der Aufrufer verwendet total = with_trip(total, 12.5);. Ein Test übergibt beide Eingaben direkt, ohne globale Variablen vorbereiten zu müssen.

Gemeinsam genutzte Namen beschränken

Gewöhnliche Variablen mit Dateigültigkeitsbereich haben externe Bindung. Nutze eine solche Variable über Deklarationen und eine Definition gemeinsam; wiederholte Definitionen in verschiedenen Dateien zerstören diese Anordnung. static im Dateigültigkeitsbereich bewirkt dagegen interne Bindung, sodass eine andere Datei ein eigenes unabhängiges Objekt mit demselben Namen haben kann. Beide Objekte bestehen weiterhin während der gesamten Ausführung: Das Schlüsselwort beschränkt hier die Bindung, nicht die Lebensdauer.

Wichtige Erkenntnisse

  • Eine globale Variable behält einen Wert über Aufrufe hinweg und hat statische Speicherdauer.
  • Wertübergabe hindert eine Funktion nicht daran, eine sichtbare globale Variable zu ändern.
  • Gemeinsam genutzter Zustand bringt Eingaben und Auswirkungen mit sich, die eine Parameterliste nicht zeigt.
  • Bevorzuge ausdrückliche Parameter und Rückgabewerte, wenn gemeinsam genutzter Zustand unnötig ist.
3

Den C-Präprozessor verwenden

Bevor C-Code kompiliert wird, bindet der Präprozessor Headerdateien ein, expandiert Makros und wählt bedingte Zweige aus. Diese Vorgänge verändern den Code, den der Compiler erhält, sodass ein kurzes Makro einen Ausdruck auf überraschende Weise ändern kann. Du untersuchst Expansionen, gruppierst Ausdrücke sicher und steuerst mit Optionen und Include-Guards, welcher Text erhalten bleibt. Entscheidend ist der Unterschied zwischen der Auswahl von Code beim Erstellen und der Auswahl eines Pfads während der Programmausführung.

3.1 Dateien einbinden und Makros definieren

Der Präprozessor wandelt Quelltext um, bevor der Compiler C-Ausdrücke und Typen prüft. Seine Direktiven beginnen mit #. Untersuche seine Ausgabe mit gcc -E file.c, wenn sich eine Einbindung oder Makroexpansion schwer erklären lässt.

Deklarationen einbinden und Konstanten benennen

#include <stdio.h> fügt eine Standardheaderdatei ein. Verwende #include "convert.h" für eine Headerdatei des Projekts: GCC durchsucht zuerst das Verzeichnis der einbindenden Datei und dann seine konfigurierten Header-Suchpfade. Die Option -Iinclude fügt ein Suchverzeichnis hinzu. Das Einbinden kopiert Headertext; es linkt keine kompilierten Funktionen.

Ein objektähnliches Makro ersetzt Tokens:

#define MAX_STUDENTS 30

Spätere Verwendungen des Tokens MAX_STUDENTS werden zu 30. Der Präprozessor lässt die Zeichenkette "MAX_STUDENTS" und den anderen Namen MAX_STUDENTS_2 unverändert. Schreibe weder = noch ein abschließendes Semikolon. Mit #define PI 3.14159; wird return PI * r * r; zum fehlerhaften Code return 3.14159; * r * r; expandiert. Eine Direktive endet normalerweise am Zeilenumbruch; ein abschließender umgekehrter Schrägstrich setzt sie fort.

Vor dem Rechnen expandieren

Bei einem funktionsähnlichen Makro steht kein Leerzeichen zwischen seinem Namen und der öffnenden Klammer:

#define SQUARE_BAD(x) x * x
#define SQUARE(x) ((x) * (x))

SQUARE_BAD(2 + 3) wird zu 2 + 3 * 2 + 3, was 11 ergibt. 100 / SQUARE_BAD(5) wird zu 100 / 5 * 5, was 100 ergibt. Das geklammerte Makro ergibt entsprechend 25 und 4. Setze jeden Parameter und den gesamten Ersetzungsausdruck in Klammern, damit umgebende Operatoren seine Gruppierung nicht ändern können.

Schreibst du stattdessen #define SQUARE (x) ((x) * (x)), entsteht ein objektähnliches Makro, dessen Ersetzungstext mit (x) beginnt. Das Leerzeichen ändert die Bedeutung der Definition.

Klammern verhindern keine wiederholte Auswertung

SQUARE(i++) wird zu ((i++) * (i++)) expandiert. Es ändert i zweimal ohne Sequenzierung, sodass das Verhalten undefiniert ist. Eine echte Funktion wie square(i++) wertet das Argument einmal aus, bevor sie in den Rumpf eintritt. Bevorzuge für Berechnungen Funktionen, insbesondere wenn ein Eingabeausdruck den Zustand ändert.

Ein funktionsähnliches Makro hat keine deklarierten Parameter- oder Rückgabetypen. Sein expandierter C-Code wird weiterhin auf Typen geprüft, aber es hat keinen Prototyp, der Argumentkonvertierungen festlegt. Makros bleiben für Include-Guards, bedingte Kompilierung und ganzzahlige konstante Ausdrücke nützlich. In C17 ist eine const int-Variable kein ganzzahliger konstanter Ausdruck; ein Makro oder eine enum-Konstante kann einen case-Wert liefern. C23 führt außerdem constexpr ein.

Argumente in Tokens oder Text umwandeln
#define SHOW(expr) printf(#expr " = %d\n", expr)
#define CAT(a, b) a##b

SHOW(3 + 4); verwendet #, um das Argument in Text umzuwandeln, und gibt 3 + 4 = 7 aus. CAT(temp, 1) verwendet ##, um Tokens zum Namen temp1 zu verbinden. Die Stringifizierung verwendet die übergebenen Argument-Tokens, ohne Makros innerhalb dieses Vorkommens zu expandieren; ein gewöhnliches Vorkommen des Arguments wird expandiert.

Makros folgen nicht dem Blockgültigkeitsbereich. Eine Definition bleibt bis zu #undef NAME oder bis zum Ende der Übersetzungseinheit aktiv, auch wenn die Direktive zwischen geschweiften Klammern geschrieben wurde. Entferne eine alte Definition mit #undef, bevor du ihr einen anderen Ersetzungstext gibst.

Wichtige Erkenntnisse

  • Die Präprozessierung bindet Text ein und expandiert Tokens vor der C-Kompilierung.
  • Eine Makrodefinition enthält weder = noch ein abschließendes Semikolon.
  • Setze Makroparameter und den vollständigen Ersetzungsausdruck in Klammern.
  • Wiederholte Makroargumente können Seiteneffekte wiederholen; verwende für Berechnungen eine Funktion.
  • # stringifiziert ein Argument und ## verbindet Tokens.
  • Ein Makro bleibt über Blockgrenzen hinweg definiert, bis seine Definition aufgehoben wird.

3.2 Code bedingt kompilieren

Die bedingte Kompilierung wählt aus, welche Quelltextzeilen den Compiler erreichen. Sie kann Debug-Ausgaben aktivieren, plattformspezifischen Code auswählen oder eine Headerdatei vor wiederholtem Einbinden schützen.

Existenz oder Wert prüfen
#define DEBUG 0

#ifdef DEBUG
/* Kept: DEBUG exists. */
#endif

#if DEBUG
/* Removed: DEBUG expands to zero. */
#endif

#ifdef DEBUG fragt, ob das Makro definiert ist. #if DEBUG wertet seinen Ersetzungswert als ganzzahligen konstanten Ausdruck aus. Innerhalb von #if zählt ein nicht definierter Bezeichner als null; ein falsch geschriebener Makroname kann einen Zweig stillschweigend entfernen. Die Ausdrücke defined DEBUG und defined(DEBUG) prüfen beide die Existenz.

Verwende #ifndef NAME für die umgekehrte Existenzprüfung. Schließe jede bedingte Anweisung mit #endif. #elif fügt eine weitere Bedingung hinzu und #else stellt die Alternative bereit. C23 ergänzt #elifdef und #elifndef; C17 kann diese Prüfungen mit #elif defined(NAME) oder #elif !defined(NAME) ausdrücken.

Die Einstellung beim Erstellen wählen
#ifdef DEBUG
printf("debug: c = %.1f\n", c);
#endif

gcc -std=c17 -DDEBUG main.c definiert DEBUG als 1 und behält die Ausgabe bei. Ohne diese Definition wird die Zeile vor der Kompilierung entfernt. -DLEVEL=2 liefert einen Wert, als stünde #define LEVEL 2 vor dem Text der Datei.

Das unterscheidet sich von einem gewöhnlichen if, dessen Bedingung zur Laufzeit ausgewertet wird. Präprozessorbedingungen können keine gewöhnliche Laufzeitvariable lesen. #if 0 entfernt einen Block, auch wenn sein Code bereits /* */-Kommentare enthält, die ein sicheres Einschließen in einen weiteren solchen Kommentar verhindern würden.

Eine Headerdatei schützen
#ifndef CONVERT_H
#define CONVERT_H

double c_to_f(double c);

#endif

Das erste Einbinden definiert CONVERT_H und behält den Prototyp bei. Späteres Einbinden in derselben Übersetzungseinheit überspringt den Inhalt. Verwende einen unverwechselbaren Namen für den Include-Guard, der auf dem Namen der Headerdatei beruht; vermeide führende Unterstriche, die für die Implementierung reserviert sind. Eine andere Übersetzungseinheit beginnt ihren eigenen Präprozessierungslauf und bindet die Deklarationen für sich ein. Ein Include-Guard verhindert nicht, dass verschiedene Objektdateien dasselbe externe Symbol definieren.

Viele Compiler unterstützen #pragma once, aber es ist keine ISO-C-Direktive. Include-Guards funktionieren mit standardisierten bedingten Direktiven.

Die vordefinierten Informationen lesen
NameInformation
__FILE__Aktueller Quelldateiname als Zeichenkette
__LINE__Aktuelle Quelltextzeilennummer
__DATE__, __TIME__Datum und Uhrzeit der Übersetzung
__STDC_VERSION__Gewählte Standardversion

Zu den Versionswerten gehören 199901L für C99, 201112L für C11, 201710L für C17 und 202311L für C23. __func__ enthält den Namen der aktuellen Funktion, ist aber ein vordefinierter Bezeichner und kein Makro.

Ein plattformabhängiger Zweig kann defined(_WIN32) oder defined(__linux__) prüfen und anschließend #else für eine Alternative verwenden. Um eine nicht unterstützte Erstellung ausdrücklich abzulehnen, schreibe eine Bedingung, gefolgt von #error "This program needs C11 or later" und #endif. Ein fehlendes #endif erzeugt dagegen einen Direktivenfehler wie „unterminated conditional directive“.

Wichtige Erkenntnisse

  • #ifdef prüft die Existenz; #if prüft einen Wert.
  • Nicht definierte Bezeichner in #if werden als null ausgewertet.
  • -DNAME=value liefert eine Makrodefinition für die Erstellung.
  • Include-Guards verhindern wiederholte Verarbeitung innerhalb einer Übersetzungseinheit.
  • Vordefinierte Makros stellen Informationen über Datei, Zeile, Erstellungszeitpunkt und Standard bereit.
4

Ein Programm auf Quell- und Headerdateien aufteilen

Ein größeres Programm kann zusammengehörige Funktionen in getrennten Quelldateien halten und ihre Deklarationen über Headerdateien gemeinsam nutzen. Jede Quelldatei wird unabhängig kompiliert, und der Linker verbindet die entstehenden Objektdateien. Du erstellst ein kleines Modul, entscheidest, welche Namen andere Dateien benötigen, und untersuchst fehlende oder doppelte Definitionen. Wenn Aufrufer und definierende Datei dieselbe Headerdatei einbinden, kann der Compiler Abweichungen vor der Programmausführung erkennen.

4.1 Mehrere Quelldateien kompilieren und linken

Bei der getrennten Kompilierung wird jede Quelldatei vor dem Linken des Programms in eine Objektdatei umgewandelt. So können unveränderte Objektdateien beim Erstellen wiederverwendet werden, und zusammengehörige Funktionen erhalten einen klaren Platz.

Die Erstellungsschritte verfolgen

Eine Übersetzungseinheit ist eine .c-Datei nach der Präprozessierung einschließlich der Inhalte ihrer eingebundenen Headerdateien. Jede Einheit wird unabhängig kompiliert.

SchrittErgebnisGCC-Option, die dort stoppt
PräprozessierungExpandierter C-Text-E
KompilierungAssemblertext-S
AssemblierungObjektdatei-c
LinkenAusführbare DateiNormale vollständige Erstellung

Eine Objektdatei enthält Maschinencode und Symbolinformationen über Namen, die sie definiert oder benötigt. Auf Unix-ähnlichen Systemen hat sie gewöhnlich die Endung .o. Der Linker verbindet Verwendungen externer Namen mit Definitionen aus Objektdateien und Bibliotheken.

Zwei Dateien kompilieren und linken

Angenommen, convert.h deklariert double c_to_f(double c);, convert.c bindet diese Headerdatei ein und definiert die Funktion, und main.c bindet die Headerdatei ein und ruft sie auf.

gcc -Wall -Wextra -std=c17 -c main.c
gcc -Wall -Wextra -std=c17 -c convert.c
gcc main.o convert.o -o temp

Der erste Befehl erzeugt main.o, ohne zu linken. Der zweite erzeugt convert.o. Der letzte erstellt die ausführbare Datei temp aus beiden Objektdateien. Du kannst auch mit einem Befehl kompilieren und linken:

gcc -Wall -Wextra -std=c17 main.c convert.c -o temp

Übergib die Quelldateien zum Kompilieren und die Objektdateien zum Linken. Headerdateien gelangen über #include hinein; führe sie nicht als getrennt zu kompilierende Programmquellen auf. Ändert sich nur convert.c, verwende main.o wieder, erstelle convert.o erneut und linke erneut. Ändert sich eine gemeinsam genutzte Headerdatei, erstelle jede Objektdatei erneut, deren Quelldatei sie einbindet.

Den fehlgeschlagenen Schritt untersuchen

Ein Übersetzungsfehler betrifft eine Übersetzungseinheit: ungültige Syntax, eine falsche Argumentanzahl oder widersprüchliche sichtbare Typen. Der Linker kann diese Fehler nicht beheben.

Ein Linkerfehler betrifft das assemblierte Programm. Erstellst du nur main.c, kann die Kompilierung dank des Prototyps gelingen, während das Linken „undefined reference to c_to_f“ oder „Undefined symbols“ meldet. Die Deklaration kündigt eine Funktion an; sie liefert nicht deren Maschinencode. Ergänze convert.c bei einer kombinierten Erstellung oder convert.o beim Linken.

Zwei gelinkte externe Definitionen derselben Funktion verursachen einen Fehler wegen eines doppelten Symbols. Ein gewöhnliches Programm hat ein main; das Linken zweier Quellen, die beide diese Funktion definieren, schlägt fehl. Auf ihre Datei beschränkte static-Funktionen können in verschiedenen Übersetzungseinheiten gleiche Namen haben, weil sie interne Bindung besitzen.

Auch Bibliotheken liefern Definitionen. Auf vielen Linux-Systemen benötigt Code, der in <math.h> deklarierte Funktionen verwendet, eine Linkoption für die Mathematikbibliothek: gcc main.c -o app -lm. Setze diese Bibliotheksoption hinter den Code, der sie benötigt.

Die Schnittstelle einbinden, die Implementierung kompilieren

Vermeide #include "convert.c". Das kopiert Funktionsrümpfe in eine andere Übersetzungseinheit. Wird convert.c ebenfalls kompiliert und gelinkt, erscheinen seine externen Definitionen doppelt. Die .h-Datei eines Moduls stellt seine Deklarationen gemeinsam bereit; seine .c-Datei wird einmal als Quelldatei kompiliert.

Wichtige Erkenntnisse

  • Eine Übersetzungseinheit ist eine präprozessierte Quelldatei mit ihrem eingebundenen Text.
  • -c erzeugt eine Objektdatei, ohne zu linken.
  • Linke jede benötigte Objektdatei in die ausführbare Datei.
  • Fehlende und doppelte Definitionen sind Linkerprobleme.
  • Binde Headerdateien ein; kompiliere Quelldateien getrennt.

4.2 Headerdateien schreiben

Die Headerdatei eines Moduls stellt seine Schnittstelle gemeinsam bereit; seine Quelldatei liefert die Implementierung. Sowohl Aufrufer als auch Implementierung binden die Headerdatei ein, damit ihre Deklarationen übereinstimmen.

Die Schnittstelle einmal deklarieren

Eine geschützte convert.h enthält:

#ifndef CONVERT_H
#define CONVERT_H

double c_to_f(double c);
double f_to_c(double f);

#endif

Die Implementierung in convert.c bindet sie vor den Rümpfen ein:

#include "convert.h"

double c_to_f(double c)
{
    return c * 9.0 / 5.0 + 32.0;
}

double f_to_c(double f)
{
    return (f - 32.0) * 5.0 / 9.0;
}

Ein Aufrufer bindet dieselbe Headerdatei und <stdio.h> ein. Der Aufruf printf("%.1f %.1f\n", c_to_f(25.0), f_to_c(212.0)); gibt 77.0 100.0 aus.

Definiert die Implementierung float c_to_f(float c), macht das Einbinden ihrer Headerdatei die widersprüchlichen Typen für den Compiler sichtbar. Bei unabhängiger Kompilierung ohne diese Deklaration kann die Abweichung unentdeckt bleiben.

Den richtigen Inhalt wählen
In einer Headerdatei gemeinsam bereitstellenIn einer Quelldatei belassen
FunktionsprototypenFunktionsrümpfe mit externer Bindung
extern-VariablendeklarationenDefinitionen gemeinsam genutzter Objekte
Benötigte Makros und TypenInterne Hilfsfunktionen und interner Zustand

Eine Headerdatei mit int count = 0; definiert in jeder einbindenden Übersetzungseinheit ein Objekt und verursacht doppelte externe Definitionen. Selbst int count; ist eine vorläufige Definition. Die Standardeinstellung von GCC meldet seit Version 10 wiederholte Kopien beim Linken, während manche Toolchains gemeinsame Symbole zusammenführen und den Fehler verbergen. Verwende extern int count; in der Headerdatei und eine Definition in einer Quelldatei.

Das Einbinden zuverlässig gestalten

Ein Include-Guard verhindert wiederholte Verarbeitung innerhalb einer Übersetzungseinheit, auch beim indirekten Einbinden über andere Headerdateien. Kompatible Prototypen dürfen sich wiederholen, aber Typdefinitionen wie ein enum können Fehler wegen erneuter Definition verursachen.

Eine eigenständige Headerdatei bindet ein, was ihre Deklarationen benötigen. Verwendet ein Prototyp size_t, binde dort <stddef.h> ein. Aufrufer sollten keine besondere Reihenfolge beim Einbinden benötigen.

Bei Headerdateien des Projekts bewirken Anführungszeichen, dass GCC zuerst das Verzeichnis der einbindenden Datei durchsucht. Füge ein separates Headerverzeichnis mit -Iinclude hinzu, wie in gcc -Iinclude -std=c17 -c src/main.c. Die Objektdatei der Implementierung muss weiterhin gelinkt werden.

Wichtige Erkenntnisse

  • Headerdateien stellen Prototypen, extern-Deklarationen, Makros und Typen gemeinsam bereit.
  • Belasse Definitionen gemeinsam genutzter Objekte und Funktionsrümpfe mit externer Bindung in Quelldateien.
  • Binde die Headerdatei eines Moduls in dessen eigene Implementierung ein.
  • Verwende Include-Guards und binde die Headerdateien ein, die deine Deklarationen benötigen.

4.3 Die Bindung mit static und extern steuern

Die Bindung bestimmt, ob sich Deklarationen an verschiedenen Stellen auf dieselbe Entität beziehen. Sie unterscheidet sich vom Gültigkeitsbereich, der beschreibt, wo ein Name verwendet werden kann, und von der Speicherdauer, die beschreibt, wie lange ein Objekt existiert.

Einen Namen mit einem Objekt verbinden
BindungBedeutung
ExternDeklarationen können sich über Übersetzungseinheiten hinweg auf eine Entität beziehen
InternDeklarationen beziehen sich innerhalb einer Übersetzungseinheit auf eine Entität
KeineEin lokaler Name, etwa ein gewöhnlicher Parameter, bezeichnet seine eigene Entität

Funktionen und gewöhnliche Variablen mit Dateigültigkeitsbereich haben standardmäßig externe Bindung. static im Dateigültigkeitsbereich bewirkt interne Bindung. Gewöhnliche automatische lokale Variablen und Parameter haben keine Bindung.

Um einen Fehlerzähler gemeinsam zu nutzen, setze diese Deklarationen in eine geschützte Headerdatei:

extern int error_count;
void log_error(int code);

Eine Quelldatei bindet diese Headerdatei ein und stellt Folgendes bereit:

int error_count = 0;

void log_error(int code)
{
    printf("error: %d\n", code);
    error_count++;
}

Diese Quelldatei bindet für printf auch <stdio.h> ein. Andere Quelldateien binden die gemeinsam genutzte Headerdatei ein, keine zweite Definition. Nach drei Aufrufen bezieht sich jeder Aufrufer, der error_count liest, auf dasselbe Objekt und sieht 3.

Deklaration und Definition unterscheiden

extern int error_count; deklariert ein vorhandenes Objekt und reserviert keinen neuen Speicher. int error_count = 0; definiert es. Ein Initialisierer ändert den Fall: Im Dateigültigkeitsbereich ist extern int error_count = 0; trotz des Schlüsselworts eine Definition. Belasse den Initialisierer in der einen definierenden Datei.

Bei gewöhnlichen öffentlichen Funktionsdeklarationen ist extern optional: double c_to_f(double c); deklariert bereits eine Funktion mit externer Bindung. Binde die öffentliche Deklaration in ihrer definierenden Datei ein, damit der Compiler die Kompatibilität prüfen kann.

Zustand hinter Funktionen verbergen
static int count = 0;

void counter_increment(void)
{
    count++;
}

int counter_value(void)
{
    return count;
}

Der gespeicherte Wert existiert während der gesamten Ausführung, aber nur diese Übersetzungseinheit kann über den Namen count darauf zugreifen. Setze die öffentlichen Funktionsprototypen in eine Headerdatei. Eine andere Datei kann den Zähler über diese Funktionen erhöhen und lesen, ohne die interne Variable direkt zu ändern. Auch eine Hilfsfunktion kann static sein, wenn andere Dateien sie nicht benötigen.

Eine falsche Bindungswahl untersuchen

Eine Definition static int count; in einer Headerdatei erzeugt in jeder einbindenden Übersetzungseinheit einen unabhängigen internen Zähler. Die Aktualisierung eines Zählers aktualisiert die anderen nicht. Include-Guards führen diese Objekte nicht zusammen: Jede Übersetzungseinheit präprozessiert ihre eigene Kopie der Headerdatei.

Eine in einer Übersetzungseinheit als static definierte Funktion kann einen externen Aufruf aus einer anderen nicht erfüllen; das Linken schlägt für diesen externen Namen fehl. Gib nur Funktionen, die andere Dateien benötigen, externe Bindung und deklariere diese in der Headerdatei.

Innerhalb einer Funktion gibt static einer lokalen Variablen dauerhaften Speicher, ohne ihren Gültigkeitsbereich zu erweitern. Außerhalb von Funktionen gibt es einem Objekt oder einer Funktion interne Bindung. Variablen mit Dateigültigkeitsbereich haben mit oder ohne dieses Schlüsselwort statische Speicherdauer.

Wichtige Erkenntnisse

  • Externe Bindung verbindet Deklarationen über Übersetzungseinheiten hinweg.
  • Nutze eine Variable über extern-Deklarationen und eine Definition gemeinsam.
  • static im Dateigültigkeitsbereich beschränkt ein Objekt oder eine Funktion auf seine beziehungsweise ihre Übersetzungseinheit.
  • Eine Headerdatei mit einer statischen Variablen gibt den einbindenden Übersetzungseinheiten getrennte Objekte.
  • Ein Initialisierer macht eine extern-Objektdeklaration im Dateigültigkeitsbereich zu einer Definition.
  • Der Werterhalt durch lokales static und die Bindung durch static im Dateigültigkeitsbereich haben unterschiedliche Aufgaben.
5

Programme aus mehreren Dateien mit make erstellen

Für eine korrekte Programmerstellung reicht eine Liste von Compilerbefehlen nicht aus: Es muss bekannt sein, welche Dateien von welchen anderen Dateien abhängen. Ein Makefile hält diese Beziehungen fest, sodass eine Änderung am Quelltext die betroffenen Objektdateien erneut kompiliert und anschließend das Programm erneut linkt. Du verfolgst diese Entscheidungen über erneutes Erstellen, berücksichtigst Header-Abhängigkeiten und verkürzt wiederholte Befehle mit Variablen und Musterregeln. Probeläufe zeigen den Plan, bevor ein Rezept ausgeführt wird.

5.1 Regeln und Abhängigkeiten in Makefiles festlegen

make liest Makefile oder makefile im Projektverzeichnis und aktualisiert das angeforderte Ziel. Ohne Zielargument erstellt es normalerweise das erste gewöhnliche Ziel.

Ausgaben und Eingaben festhalten

Eine Regel beginnt mit target: prerequisites. Ihr Rezept steht entweder in den folgenden, mit Tabulator eingerückten Zeilen oder nach einem Semikolon in derselben Zeile:

CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g

temp: main.o convert.o; $(CC) $(CFLAGS) -o temp main.o convert.o

main.o: main.c convert.h; $(CC) $(CFLAGS) -c main.c

convert.o: convert.c convert.h; $(CC) $(CFLAGS) -c convert.c

.PHONY: clean
clean:; rm -f temp main.o convert.o

Hier ist temp das Standardziel. make clean fordert die Aufräumaktion an. Schreibst du ein Rezept in die nächste Zeile, verwende ein echtes Tabulatorzeichen: Leerzeichen können „missing separator“ verursachen. Eine Editoreinstellung, die Leerraumzeichen sichtbar macht, hilft, den Unterschied zu erkennen.

Entscheidungen über erneutes Erstellen verfolgen

Make aktualisiert zuerst die Abhängigkeiten eines Ziels. Anschließend erstellt es das Ziel erneut, wenn es fehlt oder eine Abhängigkeit einen neueren Änderungszeitpunkt hat.

  • Erste Erstellung: Beide Quelldateien kompilieren, dann temp linken, drei Befehle.
  • Keine Änderungen: Es müssen keine Programmrezepte ausgeführt werden.
  • Nur convert.c ändern: Erneut kompilieren und linken, zwei Befehle; main.o wiederverwenden.
  • convert.h ändern: Beide Quelldateien erneut kompilieren und linken, drei Befehle.

Die Headerdatei betrifft beide Übersetzungseinheiten, obwohl keine der beiden Quelldateien bearbeitet wurde. Stell dir vor, wie Eingabedateien in Objektdateien und diese anschließend in die ausführbare Datei einfließen. Änderungen wirken sich entlang dieser festgehaltenen Beziehungen aus.

Header-Abhängigkeiten berücksichtigen

Die unvollständige Regel main.o: main.c erfasst Änderungen an convert.h nicht. Make liest keinen C-Quelltext, um Einbindungen zu entdecken; es folgt den deklarierten Abhängigkeiten. Führe eingebundene Headerdateien auf oder lade vom Compiler generierte Abhängigkeitsdateien, um das Linken veralteter Objektdateien zu vermeiden.

Abhängige Shell-Vorgänge zusammenhalten

Getrennte Rezeptzeilen verwenden normalerweise getrennte Shells. Eine Zeile, die nur cd build enthält, ändert das Verzeichnis für die nächste Zeile nicht. Verwende einen Befehl wie cd build && gcc -c ../main.c, wenn die Kompilierung von diesem Verzeichniswechsel abhängt. Sieh dir geplante Rezepte mit make -n vorab an.

Wichtige Erkenntnisse

  • Regeln benennen Ziele, Abhängigkeiten und mit Tabulator eingerückte Rezepte.
  • Make aktualisiert Abhängigkeiten, bevor es entscheidet, ob ihr Ziel erneut erstellt werden muss.
  • Ein fehlendes Ziel oder eine neuere Abhängigkeit löst ein erneutes Erstellen aus.
  • Änderungen an Headerdateien müssen das erneute Erstellen jeder betroffenen Objektdatei auslösen.
  • Jede Rezeptzeile wird normalerweise in ihrer eigenen Shell ausgeführt.

5.2 Makefiles mit Variablen und Musterregeln vereinfachen

Variablen und Musterregeln vermeiden wiederholte Compilerbefehle und erhalten dabei den Abhängigkeitsgraphen. Die Beispiele setzen jedes Rezept nach einem Semikolon in seine Regelzeile. Kürze ein Makefile erst, nachdem seine Abhängigkeiten für die Erstellung korrekt sind.

Gemeinsam genutzte Einstellungen benennen
CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -g
OBJS = main.o convert.o

temp: $(OBJS); $(CC) $(CFLAGS) -o $@ $^

%.o: %.c convert.h; $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

.PHONY: clean
clean:; rm -f temp $(OBJS)

$(CC) expandiert die Compilervariable; $(CFLAGS) expandiert ihre Optionen. CPPFLAGS enthält üblicherweise Präprozessoroptionen wie -DDEBUG oder -Iinclude. Die Optionen -Wall -Wextra aktivieren Warnungen, -g fügt Debuginformationen hinzu und -O2 aktiviert die Optimierung. Nicht definierte make-Variablen werden zu leerem Text expandiert. Schreibe $(CC), nicht $CC: Letzteres bedeutet die einbuchstabige Variable C, gefolgt von einem wörtlichen C.

Automatische Variablen innerhalb einer Regel lesen
VariableWert in der aktuellen Regel
$@Zielname
$<Erste Abhängigkeit
$^Alle Abhängigkeiten, wobei doppelte Namen entfernt werden
$?Abhängigkeiten, die neuer als das Ziel sind

Für main.o ist der Stamm des Musters main, sodass %.c zu main.c wird. Das Kompilierrezept verwendet $<, um diese Quelldatei an den Compiler zu übergeben, und $@, um main.o zu benennen. Die Headerdatei ist eine Abhängigkeit, wird aber nicht als weitere Eingabe für die Kompilierung übergeben.

In der Linkregel sind alle Abhängigkeiten Objektdateien. $^ wird zu main.o convert.o expandiert, und $@ wird zu temp. Würdest du dort $< verwenden, würde nur main.o übergeben und die Definitionen in convert.o gingen verloren.

GNU make hat eingebaute Regeln, die x.c unter Verwendung von CC, CPPFLAGS und CFLAGS zu x.o kompilieren können. Eine ausdrückliche Abhängigkeitsregel ohne Rezept kann dieses eingebaute Rezept verwenden. GNU make kann sogar mit make hello ein einfaches Programm aus hello.c ohne Makefile erstellen.

Aktionsziele als Pseudoziele kennzeichnen

clean bezeichnet eine Aktion und keine zu erstellende Datei. .PHONY: clean stellt sicher, dass die Aktion auch ausgeführt wird, wenn eine Datei namens clean existiert. rm -f toleriert, dass Dateien bereits fehlen. Auch ein all-Ziel, das mehrere Programme zusammenfasst, ist gewöhnlich ein Pseudoziel.

Headerlisten generieren und erneutes Erstellen untersuchen

Füge -MMD -MP zu den Compileroptionen hinzu, um neben jeder Objektdatei eine .d-Datei zu schreiben. -MMD hält Abhängigkeiten von eigenen Headerdateien fest; -MP fügt leere Regeln hinzu, die helfen, wenn eine erfasste Headerdatei entfernt wird. Lade die Listen nach dem gewöhnlichen Standardziel mit:

-include $(OBJS:.o=.d)

Die Ersetzung wandelt Objektdateinamen in Namen von Abhängigkeitsdateien um. Das führende Minus toleriert fehlende .d-Dateien bei der ersten Erstellung. Entferne diese generierten Dateien in clean zusammen mit den Objektdateien.

make -n zeigt Befehle vorab, make -B erzwingt erneutes Erstellen und make -j4 erlaubt bis zu vier Rezepte gleichzeitig. Korrekte Abhängigkeiten sind auch für parallele Erstellungen wichtig. Eine Änderung des Texts von CFLAGS allein macht vorhandene Objektdateien nicht älter als eine Abhängigkeit. Verwende nach einer Änderung der Optionen make -B oder erstelle nach einer Bereinigung erneut, damit jede Objektdatei die neuen Einstellungen verwendet.

Wichtige Erkenntnisse

  • Verwende $(NAME), um eine benannte make-Variable zu expandieren.
  • $@, $< und $^ bezeichnen das Ziel, die erste Abhängigkeit und alle Abhängigkeiten.
  • Musterregeln verwenden ein Rezept für Dateien mit passenden Stämmen wieder.
  • Pseudoziele führen Aktionen auch dann aus, wenn passende Dateien existieren.
  • Generierte .d-Dateien halten Abhängigkeiten von eigenen Headerdateien fest.
  • Erzwinge nach einer Änderung der Compileroptionen ein erneutes Erstellen oder bereinige vorher.

Funktionen und Programmstruktur in C: Beispiele

5 Interaktive Lektionen

1

Einen Funktionsaufruf erstellen und verfolgen

19 Schritte
Schreibe und deklariere eine Funktion, behalte ihr Ergebnis und verfolge eine rekursive Berechnung.
  1. 1
    EinführungMit einer kleinen Aufgabe beginnen

    Verfolge einen Wert in eine Funktion hinein und finde dann heraus, wohin ihr Ergebnis gelangt. Wir erstellen eine Definition und einen Prototyp, unterscheiden einen kopierten Parameter von seinem Aufrufer und verfolgen rekursive Aufrufe abwärts bis zu einem Basisfall und über ihre Rückgabewerte wieder zurück.

  2. 2
    BeispielEiner Aufgabe eine typisierte Eingabe geben

    Wir möchten eine Funktion, die einen festen Korrekturwert zu einem ganzzahligen Messwert addiert.

    int corrected(int reading)
    {
        return reading + 4;
    }
    

    int vor dem Namen beschreibt das Ergebnis. int reading deklariert einen Parameter, und der Rumpf berechnet den Rückgabewert. Definiere diese Funktion außerhalb aller anderen Funktionsrümpfe; Standard-C unterstützt keine verschachtelten Definitionen.

  3. 3
    BeispielEinen Aufruf als Ausdruck verwenden

    Bei int result = corrected(9); initialisiert das Argument 9 den Parameter reading. Der Rückgabeausdruck ergibt 13, und die Zuweisung des Aufrufers speichert diesen Wert in result.

    Der Aufruf kann auch innerhalb eines anderen Ausdrucks stehen: 2 * corrected(9) ergibt 26. Schreibst du nur corrected(9);, wird der Aufruf ebenfalls ausgeführt, sein Rückgabewert aber verworfen.

  4. 4
    TippZwischen Rückgabe und Ausgabe wählen

    Eine Berechnung kann ein Ergebnis zurückgeben, ohne es auszugeben. So kann ein Aufrufer es speichern, vergleichen oder später ausgeben.

    Eine als void deklarierte Funktion kann eine Ausgabeaufgabe übernehmen, liefert aber keinen Wert für eine Zuweisung. Innerhalb der Funktion beendet return; sie ohne Wert. Achte bei einer Funktion mit Rückgabewert darauf, dass jeder Pfad, der ein Ergebnis liefern soll, ein passendes return erreicht; die Anzeige einer Zahl liefert sie nicht an den Aufrufer.

  5. 5
    HerausforderungDie Rechnung vor der Rückgabe prüfen

    Was gibt diese Funktion für average_pair(5, 8) zurück?

    double average_pair(int a, int b)
    {
        return (a + b) / 2;
    }
    

    ---reveal---

    Sie gibt 6.0 zurück: Die Ganzzahladdition ergibt 13, die Ganzzahldivision ergibt 6, und die Umwandlung bei der Rückgabe ergibt 6.0. Verwende 2.0, damit die Division mit Gleitkommazahlen erfolgt, wenn du 6.5 benötigst. Der Rückgabetyp kann einen bereits verworfenen Nachkommateil nicht wiederherstellen.

  6. 6
    ErklärungDen Compiler vor dem Aufruf informieren

    Steht der Rumpf später, setze int corrected(int reading); vor den Aufruf. Dieser Prototyp ermöglicht dem Compiler, Argumentanzahl und Umwandlungen zu prüfen. Sein Semikolon kennzeichnet eine Deklaration; die Definition liefert den Rumpf.

  7. 7
    KernaussageEine leere Eingabeliste angeben

    Verwende int read_sensor(void); sowohl in C17 als auch in C23 für keine Parameter. In C17 lässt int read_sensor(); die Parameterinformation offen; C23 ändert diese Schreibweise so, dass sie keine Parameter bedeutet. Wähle C17 mit -std=c17 ausdrücklich aus.

  8. 8
    HerausforderungEine zulässige Umwandlung verfolgen

    Der Compiler hat int corrected(int reading); gesehen. Was gibt corrected(6.8) zurück, und muss der Aufruf abgelehnt werden?

    ---reveal---

    Der Aufruf kann kompiliert werden. Der Prototyp bewirkt die Umwandlung von 6.8 in die Ganzzahl 6, dann addiert die Funktion 4 und gibt 10 zurück. Eine Warnung bei der Umwandlung kann helfen, verlorene Informationen aufzudecken; verwende -Wconversion. Ein Prototyp prüft einen Aufruf anhand seiner deklarierten Typen, aber nicht jeder abweichende numerische Argumenttyp ist ein Fehler.

  9. 9
    BeispielZwei getrennte Werte darstellen

    Betrachte eine andere Funktion:

    void change(int reading)
    {
        reading += 4;
    }
    

    Der Aufrufer führt int sample = 9; change(sample); aus. Beim Eintritt enthalten sample und der Parameter jeweils 9 in getrennten Objekten. Der Rumpf ändert seinen Parameter auf 13. Nach der Rückkehr des Aufrufs enthält sample weiterhin 9: Es gab keine Zuweisung an diese Variable.

  10. 10
    VerbindungDen Aufrufer ein Ergebnis behalten lassen

    Verwende sample = corrected(sample);, um das Ergebnis zu behalten. Die Funktion rechnet mit einer Kopie; der Aufrufer speichert dann ihren Rückgabewert. Gleiche Namen würden ihre Objekte nicht verbinden; die Zuweisung aktualisiert sample.

  11. 11
    HerausforderungDen verworfenen Aufruf finden

    Ausgehend von int sample = 7; führt der Aufrufer aus:

    corrected(sample);
    sample = corrected(sample);
    

    Welchen Wert hat sample danach?

    ---reveal---

    Der erste Aufruf berechnet 11 und verwirft den Wert, sodass sample bei 7 bleibt. Der zweite erhält ebenfalls 7, gibt 11 zurück und diesmal speichert der Aufrufer den Wert. Der Endwert ist 11, nicht 15; ein Aufruf aktualisiert seine Argumentvariable nicht allein dadurch, dass er einen geänderten Wert zurückgibt.

  12. 12
    ErklärungEine Abbruchregel für Rekursion ergänzen

    Eine rekursive Funktion ruft sich selbst für ein kleineres Problem auf:

    int total_to(int n)
    {
        if (n <= 0) return 0;
        return n + total_to(n - 1);
    }
    

    Bei einer positiven Eingabe führt das Abziehen von eins zum Basisfall hin. Der Basisfall antwortet direkt. Der Rekursionsfall kann seine Addition erst abschließen, wenn der kleinere Aufruf zurückkehrt.

  13. 13
    BeispielDie Aufrufe beim Abstieg notieren

    Verfolge total_to(3). Der erste Aufruf enthält n == 3 und wartet darauf, 3 zu total_to(2) zu addieren. Dieser Aufruf enthält sein eigenes n == 2 und wartet auf total_to(1).

    Der Aufruf mit n == 1 wartet auf total_to(0), das sofort 0 zurückgibt. An diesem tiefsten Punkt sind vier Aufrufe aktiv. Ersetze die früheren Werte von n nicht durch null; sie gehören zu verschiedenen Aufrufen.

  14. 14
    BeispielDie Ergebnisse auf dem Rückweg eintragen

    Sobald total_to(0) den Wert 0 zurückgibt, können die wartenden Berechnungen abgeschlossen werden:

    • Der Aufruf mit n == 1 gibt 1 + 0, also 1, zurück.
    • Der Aufruf mit n == 2 gibt 2 + 1, also 3, zurück.
    • Der Aufruf mit n == 3 gibt 3 + 3, also 6, zurück.

    Die meisten Implementierungen halten diese aktiven Aufrufe in Stackframes. Bei jeder Rückkehr wird der automatische Zustand des beendeten Aufrufs freigegeben und der Aufrufer nach dem Aufruf fortgesetzt.

  15. 15
    HerausforderungPrüfen, ob ein Basisfall erreichbar ist

    Angenommen, die Basisprüfung in total_to wird zu n == 0 geändert, und der rekursive Schritt verwendet weiterhin n - 1. Was geht schief, wenn das anfängliche Argument -2 ist?

    ---reveal---

    Die Eingaben werden zu -2, -3, -4 und entfernen sich weiter von null. Die Abbruchprüfung existiert, ist aber auf diesem Pfad unerreichbar. Fortgesetzte Rekursion kann den Stack-Speicher erschöpfen. Die Version mit n <= 0 stoppt bei dieser Eingabe sofort.

  16. 16
    TippAuch die Größe des Ergebnisses prüfen

    Eine endende rekursive Berechnung kann dennoch den Wertebereich ihres Rückgabetyps überschreiten. Bei einem 32-Bit-int passt factorial(12) hinein, während der mathematische Wert von factorial(13) das Maximum 2147483647 überschreitet.

    Ein Überlauf bei vorzeichenbehafteten Ganzzahlen ist undefiniertes Verhalten. Prüfe vor der Wahl rekursiver Eingaben sowohl, ob die Aufrufe enden, als auch, ob Zwischen- und Endergebnisse der Rechnung in die verwendeten Typen passen. Auch bei einer Schleife musst du die Rechnung prüfen.

  17. 17
    TippAufrufe trennen, wenn die Reihenfolge wichtig ist

    Alle Argumentausdrücke werden vollständig ausgewertet, bevor der Rumpf der aufgerufenen Funktion beginnt, aber C legt ihre Auswertungsreihenfolge nicht fest. Ändern zwei Aufrufe gemeinsam genutzten Zustand, berechne ihre Ergebnisse in getrennten Anweisungen, bevor du sie weitergibst.

    Vermeide f(i, i++): Der Ausdruck liest und ändert i ohne die erforderliche Sequenzierung. Die Wertübergabe macht einen unsicheren Argumentausdruck nicht sicher.

  18. 18
    HerausforderungDeklaration, Umwandlung und Aufrufe verbinden

    Nachdem total_to vor der Verwendung deklariert wurde, schreibt ein Aufrufer int x = 2; total_to(x); x = total_to(3.9);. Welchen Wert hat x am Ende?

    ---reveal---

    Das erste Ergebnis wird verworfen, sodass x bei 2 bleibt. Das zweite Argument wird in 3 umgewandelt; der rekursive Ablauf ergibt 6, was die Zuweisung in x speichert. Die Deklaration steuert die Umwandlung, die Rekursion berechnet den Wert und der Aufrufer entscheidet, ob er ihn behält.

  19. 19
    ZusammenfassungDrei Prüfpunkte für einen Aufruf verwenden

    Prüfe bei einem Aufruf seine Deklaration, verfolge seine kopierten Eingaben und ermittle, wo sein Rückgabewert verwendet wird. Verfolge bei Rekursion jeden aktiven Aufruf getrennt und kombiniere die Ergebnisse vom Basisfall aus nach außen.

2

Namen und gespeicherte Werte verfolgen

13 Schritte
Verfolge einen Block, einen statischen Zähler und eine gemeinsam genutzte Summe, ohne ihre Objekte zu verwechseln.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungMit einer kleinen Aufgabe beginnen
  2. 2
    BeispielDie nächstgelegene sichtbare Deklaration lesen
  3. 3
    ErklärungDen Gültigkeitsbereich am Ort der Funktionsdefinition beachten
  4. 4
    HerausforderungEinen nach einem Block benötigten Wert bereitstellen
  5. 5
    BeispielDen Neustart eines automatischen Zählers beobachten
  6. 6
    BeispielDen Zähler mit static erhalten
  7. 7
    HerausforderungInitialisierung und Zuweisung unterscheiden
  8. 8
    TippDas Schlüsselwort an seiner Position verstehen
  9. 9
    BeispielEine globale Aktualisierung verfolgen
  10. 10
    HerausforderungDie verdeckende lokale Variable finden
  11. 11
    VerbindungEine Berechnung leichter testbar machen
  12. 12
    HerausforderungGültigkeitsbereich und gespeicherten Zustand verbinden
  13. 13
    ZusammenfassungNamen und Lebensdauern getrennt vermerken
3

Code vor der Kompilierung untersuchen

13 Schritte
Expandiere Ausdrücke, wähle einen Debug-Zweig aus und verfolge einen Include-Guard.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungMit einer kleinen Aufgabe beginnen
  2. 2
    ErklärungSehen, was das Einbinden tatsächlich liefert
  3. 3
    BeispielTokens vor der Anwendung von Operatorregeln ersetzen
  4. 4
    BeispielInnere und äußere Gruppierung schützen
  5. 5
    HerausforderungErkennen, was Klammern nicht beheben können
  6. 6
    TippMakroregeln von Gültigkeitsregeln trennen
  7. 7
    BeispielEine Einstellung auf Existenz und Wert prüfen
  8. 8
    HerausforderungEinen Zweig über eine Compileroption auswählen
  9. 9
    BeispielDas erste Einbinden einer Headerdatei verfolgen
  10. 10
    KernaussageIn einer anderen Einheit mit einem neuen Include-Guard-Zustand beginnen
  11. 11
    VerbindungEine Diagnose im Quelltext verorten
  12. 12
    HerausforderungExpansion und Zweigauswahl verbinden
  13. 13
    ZusammenfassungDie Umwandlung der Reihe nach untersuchen
4

Ein Programm aus zwei Quelldateien erstellen

14 Schritte
Erstelle eine Headerdatei und eine Implementierung, linke ihre Objektdateien und schütze internen Zustand.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungMit einer kleinen Aufgabe beginnen
  2. 2
    ErklärungEine Übersetzungseinheit bis zur Objektdatei verfolgen
  3. 3
    BeispielDie gemeinsam genutzte Deklaration schreiben
  4. 4
    BeispielDie Implementierung in eine eigene Quelldatei setzen
  5. 5
    HerausforderungEine gemeinsam genutzte Variable richtig platzieren
  6. 6
    BeispielGetrennt kompilieren, dann linken
  7. 7
    HerausforderungEinen nicht aufgelösten Verweis beheben
  8. 8
    TippDie Implementierung nicht in den Aufrufer kopieren
  9. 9
    BeispielEinen Zähler innerhalb des Moduls halten
  10. 10
    ErklärungDrei Eigenschaften des Zählers unterscheiden
  11. 11
    HerausforderungZwei unabhängige Zähler erklären
  12. 12
    TippEine Headerdatei eigenständig gestalten
  13. 13
    HerausforderungInternen Zugriff und Schnittstelle gemeinsam korrigieren
  14. 14
    ZusammenfassungJeden Bedarf dem passenden Erstellungsschritt zuordnen
5

Nur Geändertes erneut erstellen

15 Schritte
Halte Abhängigkeiten fest, verfolge erneutes Erstellen und kürze ein Makefile, ohne seine Bedeutung zu verändern.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungMit einer kleinen Aufgabe beginnen
  2. 2
    BeispielEingaben für ausführbare Datei und Objektdateien festhalten
  3. 3
    BeispielEiner Objektdatei ein Rezept geben
  4. 4
    BeispielErste Erstellung und erneutes Erstellen nach einer Quelltextänderung ausführen
  5. 5
    HerausforderungEine Änderung an einer gemeinsam genutzten Headerdatei verfolgen
  6. 6
    TippDen Plan vor der Ausführung untersuchen
  7. 7
    ErklärungWiederholte Einstellungen durch Variablen ersetzen
  8. 8
    BeispielDas Linkrezept expandieren
  9. 9
    BeispielEin Kompiliermuster für beide Objektdateien verwenden
  10. 10
    HerausforderungEine Quelldatei wählen, ohne die Headerdatei zu kompilieren
  11. 11
    TippAufräumen als Aktion festlegen
  12. 12
    VerbindungDen Compiler Header-Abhängigkeiten erfassen lassen
  13. 13
    HerausforderungParalleles erneutes Erstellen korrekt halten
  14. 14
    HerausforderungGeänderte Optionen auf das ganze Programm anwenden
  15. 15
    ZusammenfassungDen Graphen prüfen, bevor du dem Ergebnis vertraust

Funktionen und Programmstruktur in C: Übungsaufgaben

50 Übungsfragen in 5 Sets

Set 1: Werte durch Funktionsaufrufe verfolgen10 Übungsfragen

Frage 1 von 10

Was ist der Parameter in int triple(int value) { return value * 3; }?
  1. triple
  2. int
  3. value
  4. value * 3
Antwort zeigenAntwort ausblenden
Antwort: C
Der Parameter ist value, die Variable, die mit dem übergebenen Argument initialisiert wird. triple benennt die Funktion; value * 3 ist der zurückgegebene Ausdruck, kein Parameter.

Frage 2 von 10

Was erlaubt void report(int code) einem Aufrufer?
  1. report(4) in einem int speichern.
  2. report(4); als Anweisung aufrufen.
  3. report(4) zu einer Ganzzahl addieren.
  4. report(4) aus einer int-Funktion zurückgeben.
Antwort zeigenAntwort ausblenden
Antwort: B
Ein void-Aufruf kann seine Aufgabe als Anweisung ausführen, liefert aber kein Ergebnis. Der Parameter int code beschreibt eine Eingabe und macht den Aufruf daher nicht in einer Ganzzahlzuweisung verwendbar.

Frage 3 von 10

Welchen Wert gibt double portion(int total) { return total / 4; } für portion(11) zurück?
  1. 2.0
  2. 2.75
  3. 3.0
  4. 2.5
Antwort zeigenAntwort ausblenden
Antwort: A
Beide Operanden von total / 4 sind Ganzzahlen, daher berechnet der Rumpf 11 / 4 als 2. Dieses Ergebnis wird anschließend in double umgewandelt und ergibt 2.0. Ein Rückgabetyp double macht die vorherige Division nicht zur Gleitkommadivision; 2.75 würde einen Gleitkommaoperanden erfordern.

Frage 4 von 10

Eine Definition von int capacity(void) steht unterhalb von main. Welche Deklaration oberhalb von main beschreibt in C17 einen Aufruf ohne Eingaben vollständig?
  1. int capacity();
  2. void capacity(int);
  3. int capacity(void) { }
  4. int capacity(void);
Antwort zeigenAntwort ausblenden
Antwort: D
int capacity(void); deklariert ausdrücklich ein Ganzzahlergebnis und keine Parameter. In C17 lässt int capacity(); die Parameterinformation offen, statt den Aufruf ohne Eingaben vollständig zu beschreiben.

Frage 5 von 10

Wenn int fifth(int n) { return n / 5; } vor der Verwendung deklariert ist, was ergibt fifth(-14.8)?
  1. -3
  2. -2
  3. -2.96
  4. -14
Antwort zeigenAntwort ausblenden
Antwort: B
Die Umwandlung in den Parametertyp schneidet zuerst den Nachkommateil in Richtung null ab und ergibt n == -14. Die Ganzzahldivision durch 5 ergibt dann -2. Die Wahl von -3 würde abrunden, statt die C-Regel des Abschneidens in Richtung null für diese Umwandlungen und die Ganzzahldivision anzuwenden.

Frage 6 von 10

Eine Headerdatei deklariert double stretch(int n);. Welche Definition ist mit dieser Deklaration vereinbar?
  1. int stretch(int count) { return count * 1.5; }
  2. double stretch(double n) { return n * 1.5; }
  3. double stretch(void) { return 1.5; }
  4. double stretch(int count) { return count * 1.5; }
Antwort zeigenAntwort ausblenden
Antwort: D
Der Rückgabetyp muss double und der Parametertyp int bleiben; der Parametername darf sich ändern. Wenn der Name n beibehalten, sein Typ aber in double geändert wird, widerspricht dies weiterhin dem Prototyp. Die Kompatibilität hängt von den deklarierten Typen und der Parameterliste ab, nicht von übereinstimmenden Parameternamen.

Frage 7 von 10

Was gibt das folgende vollständige Programm aus?

#include <stdio.h>
void lower(int n) { n -= 4; }
int main(void) {
    int level = 17;
    lower(level);
    printf("%d\n", level);
}
  1. 17
  2. 13
  3. 4
  4. 0
Antwort zeigenAntwort ausblenden
Antwort: A
Der Aufruf kopiert 17 in den Parameter n, und die Subtraktion ändert nur diesen Parameter. level in main enthält bei der Ausgabe weiterhin 17. Das naheliegende Ergebnis 13 würde eine Zuweisung eines Rückgabewerts an level oder einen anderen Vorgang erfordern, der das Objekt des Aufrufers tatsächlich ändert.

Frage 8 von 10

Welchen Wert hat x am Ende?

int adjust(int x) { x += 3; return x * 2; }
/* In main: */
int x = 4;
adjust(x);
x = adjust(x + 1);
  1. 22
  2. 28
  3. 16
  4. 14
Antwort zeigenAntwort ausblenden
Antwort: C
Der erste Aufruf berechnet 14, verwirft den Wert aber, sodass das x des Aufrufers bei 4 bleibt. Der nächste Aufruf erhält 5, erhöht seine lokale Variable auf 8 und gibt 16 zurück, was dem Aufrufer zugewiesen wird. Ein Ergebnis von 22 übernimmt fälschlich die lokale Änderung des ersten Aufrufs, als hätte sie den Aufrufer vor dem zweiten Aufruf verändert.

Frage 9 von 10

Was gibt depth(3) zurück?

int depth(int n) {
    if (n == 0) return 2;
    return 1 + depth(n - 1);
}
  1. 3
  2. 4
  3. 5
  4. 6
Antwort zeigenAntwort ausblenden
Antwort: C
Die Aufrufe steigen von 3 bis 0 ab, wo der Basisfall 2 zurückgibt. Jeder der drei wartenden Aufrufe addiert eins, sodass 3, 4 und dann 5 entstehen. Ein Ergebnis von 3 zählt die rekursiven Schritte, verliert aber den vom Basisfall gelieferten Wert.

Frage 10 von 10

Welchen Wert speichert int answer = collect(4.9);?

int collect(int n) {
    if (n <= 1) return 1;
    return n + collect(n - 2);
}
  1. 7
  2. 8
  3. 6
  4. 5
Antwort zeigenAntwort ausblenden
Antwort: A
Der int-Parameter erhält 4, sodass die aktiven Aufrufe 4, 2 und 0 verwenden. Der letzte gibt 1 zurück; die früheren Aufrufe geben 2 + 1 == 3 und 4 + 3 == 7 zurück. Das Ergebnis 6 lässt den Aufruf mit der Eingabe null fälschlich null zurückgeben, obwohl der tatsächliche Basisfall eins zurückgibt.
Set 2: Lokale Namen und gespeicherten Zustand verfolgen10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Wo kann der Name i aus for (int i = 0; i < 6; i++) { } verwendet werden?
  1. In der gesamten umgebenden Funktion.
  2. In der gesamten restlichen Quelldatei.
  3. In jeder von der Schleife aufgerufenen Funktion.
  4. Innerhalb dieser for-Anweisung.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Welche Werte gibt dieses Programm aus?

#include <stdio.h>
int size = 9;
void show(void) { printf("%d ", size); }
int main(void) {
    int size = 4;
    show();
    printf("%d", size);
}
  1. 9 4
  2. 4 4
  3. 9 9
  4. 4 9
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Was wird ausgegeben?

int total = 0;
for (int i = 1; i <= 3; i++) {
    int total = i;
    total += 2;
}
printf("%d", total);
  1. 6
  2. 0
  3. 12
  4. 5
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Warum initialisiert int amount = amount + 2; innerhalb eines Blocks nicht mit einem äußeren amount?
  1. Der äußere Name verliert seinen gespeicherten Wert.
  2. Alle Initialisierer laufen vor Beginn des Gültigkeitsbereichs.
  3. Der neue Name ist in seinem Initialisierer im Gültigkeitsbereich.
  4. Beide Deklarationen bezeichnen dasselbe lokale Objekt.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Wie oft wird static int calls = 0; während einer Programmausführung initialisiert?
  1. Einmal, vor dem Programmstart.
  2. Einmal pro Aufruf der Funktion.
  3. Einmal auf jeder Rekursionsebene.
  4. Einmal bei jedem Lesen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Welche Werte geben drei getrennte Aufrufe von tick zurück?

int tick(void) {
    static int n = 5;
    n += 2;
    return n;
}
  1. 7, 7, 7
  2. 5, 7, 9
  3. 7, 9, 11
  4. 5, 5, 5
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Welchen Wert hat result nach int result = visit(2);?

int visit(int n) {
    static int calls = 0;
    calls++;
    if (n == 0) return calls;
    int inner = visit(n - 1);
    return inner + calls;
}
  1. 6
  2. 9
  3. 7
  4. 3
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Eine Berechnung int permitted(int n) liest ein globales limit. Warum könnte dasselbe Argument bei einem späteren Aufruf ein anderes Ergebnis liefern?
  1. Die Übergabe eines int-Arguments ändert seine Quellvariable.
  2. Jeder Aufruf setzt jedes Objekt im Dateigültigkeitsbereich auf null.
  3. Ein lokaler Name im Aufrufer ersetzt die globale Variable.
  4. Ein anderer Vorgang könnte limit geändert haben.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Welche zwei Ganzzahlen werden ausgegeben?

#include <stdio.h>
int count = 6;
int advance(int n) { count += n; return count; }
int main(void) {
    int count = 2;
    count = advance(count);
    int next = advance(1);
    printf("%d %d", count, next);
}
  1. 3 4
  2. 9 9
  3. 2 7
  4. 8 9
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Eine Funktion muss eine gespeicherte numerische Variable zur Laufzeit mit read_sensor() initialisieren. Warum ist static int first = read_sensor(); in C17 ungültig?
  1. Eine statische lokale Variable kann nach der Initialisierung nicht gelesen werden.
  2. Ein statischer numerischer Initialisierer muss konstant sein.
  3. Eine Funktion kann keinen Wert an eine lokale Variable zurückgeben.
  4. Eine statische lokale Variable wird bei jedem Aufruf initialisiert.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 3: Makros expandieren und Zweige beim Erstellen auswählen10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Welchen Text ersetzt #define LIMIT 18 im späteren Quellcode?
  1. Die Zeichen LIMIT innerhalb von "LIMIT".
  2. Das eigenständige Token LIMIT.
  3. Das Suffix des Namens LOCAL_LIMIT.
  4. Die gesamte Anweisung mit LIMIT.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Welchen Wert hat 5 * INC(3)?

#define INC(x) x + 1
  1. 16
  2. 20
  3. 19
  4. 15
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Warum macht #define LIMIT 12; die Anweisung int count = LIMIT * 2; ungültig?
  1. Das Makro kann nach = kein Token ersetzen.
  2. Ein numerisches Makro braucht einen deklarierten Rückgabetyp.
  3. Das Semikolon wird Teil des Ausdrucks.
  4. Das Makro macht count zu einem Konstantennamen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Welche Werte haben a und b nach diesem Codeausschnitt?

#define TWICE(x) x + x
#define GROUP(x) ((x) + (x))
int a = 30 / TWICE(2 + 1);
int b = 30 / GROUP(2 + 1);
  1. a = 5, b = 5
  2. a = 10, b = 5
  3. a = 19, b = 10
  4. a = 19, b = 5
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Wie verhält sich CUBE(i++) bei dieser Definition?

#define CUBE(x) ((x) * (x) * (x))
  1. Das Argument wird einmal ausgewertet und sein Wert wird kubiert.
  2. Es wird dreimal in festgelegter Reihenfolge von links nach rechts erhöht.
  3. Es wird abgelehnt, weil ein Makro keine Ausdrücke annehmen kann.
  4. Es entsteht undefiniertes Verhalten durch wiederholte nicht sequenzierte Änderungen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Was bewirkt #ifdef TRACE bei #define TRACE 0?
  1. Es überspringt den Zweig, weil TRACE zu null expandiert.
  2. Es behält den Zweig, weil TRACE definiert ist.
  3. Es lehnt die Definition ab, weil null kein Name ist.
  4. Es liest eine Laufzeitvariable namens TRACE.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Warum könnte #if VERBOSE einen Zweig stillschweigend überspringen, wenn bei der Erstellung nur VERB0SE definiert wird?
  1. Der nicht definierte Bezeichner VERBOSE zählt als null.
  2. Die beiden Namen teilen sich eine Definition, weil sie ähnlich aussehen.
  3. Jeder nicht definierte Präprozessorname ist ein schwerwiegender Übersetzungsfehler.
  4. Die Direktive wartet auf einen Laufzeitwert von VERBOSE.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Welcher Wert für result erreicht bei -DLEVEL=2 den Compiler?

#define STEP(x) ((x) + 1)
#if defined(LEVEL) && LEVEL > 2
int result = STEP(5);
#elif LEVEL == 2
int result = 2 * STEP(3);
#else
int result = STEP(1);
#endif
  1. 6
  2. 7
  3. 8
  4. 2
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Welchen Text gibt dieses vollständige Programm aus?

#include <stdio.h>
#define JOIN(a, b) a##b
#define PRINT(x) printf(#x "=%d", x)
int main(void) {
    int item2 = 7;
    PRINT(JOIN(item, 2));
}
  1. item2=7
  2. JOIN(item, 2)=7
  3. JOIN(item, 2)=2
  4. item2=2
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Eine geschützte Headerdatei enthält int mode = 2;. Zwei getrennte Quelldateien binden sie ein, werden kompiliert und dann zusammengelinkt. Warum verhindert der Include-Guard keinen Fehler wegen einer Mehrfachdefinition?
  1. Die zweite Quelldatei verwendet den Include-Guard-Zustand der ersten erneut.
  2. Der Include-Guard wandelt beide Definitionen in gemeinsame Deklarationen um.
  3. Jede Übersetzungseinheit bindet ihre eigene externe Definition ein.
  4. Der Linker entfernt Include-Guard-Namen vor der Prüfung auf Duplikate.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 4: Headerdateien, Objektdateien und Bindung untersuchen10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Was erzeugt gcc -std=c17 -c driver.c normalerweise auf einem Unix-ähnlichen System?
  1. Eine Objektdatei namens driver.o.
  2. Eine ausführbare Datei namens driver.
  3. Einen Assemblertext namens driver.s.
  4. Eine Headerdatei namens driver.h.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Ein Aufrufer bindet einen korrekten Prototyp für reading ein, aber beim Linken nur seiner Objektdatei wird ein nicht aufgelöster Verweis auf reading gemeldet. Was sollte ergänzt werden?
  1. Ein weiterer kompatibler Prototyp im Aufrufer.
  2. Ein Include-Guard um die vorhandene Funktionsdeklaration.
  3. Die Objektdatei mit der Definition von reading unter den Linkeingaben.
  4. Dieselbe Headerdatei als separate kompilierte Eingabe.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Eine Headerdatei deklariert int classify(double x);, aber ihre Quelldatei definiert double classify(double x). Was macht die Abweichung beim Kompilieren der definierenden Quelldatei sichtbar?
  1. Die definierende Quelldatei zu einem späteren Linkbefehl hinzufügen.
  2. Einen zweiten Prototyp nur in die aufrufende Quelldatei setzen.
  3. Einen Include-Guard nur um den Code des Aufrufers setzen.
  4. Dieselbe Headerdatei in die definierende Quelldatei einbinden.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Welche Bindung hat static int cached = 3;, wenn es außerhalb aller Funktionen deklariert wird?
  1. Externe Bindung.
  2. Interne Bindung.
  3. Keine Bindung.
  4. Automatische Bindung.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Eine geschützte Headerdatei enthält static int errors; und void record(void);. Eine Quelldatei bindet sie ein und definiert record so, dass es errors erhöht; eine andere bindet sie ein, ruft record zweimal auf und gibt dann ihr eigenes errors aus. Was wird ausgegeben und warum?
  1. 2, weil beide Einbindungen denselben gemeinsamen Zähler bezeichnen.
  2. 1, weil der Include-Guard nur einen erfolgreichen Aufruf erlaubt.
  3. 0, weil die beiden Quelldateien unabhängige Zähler haben.
  4. Ein Linkerfehler, weil beide statischen Zähler extern sind.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Was gehört in eine Headerdatei, damit mehrere Quelldateien ein int failures verwenden können, das in einer einzigen Quelldatei definiert ist?
  1. int failures = 0;
  2. extern int failures;
  3. extern int failures = 0;
  4. static int failures = 0;
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

main.c bindet "calc.c" ein; calc.c bindet eine geschützte calc.h ein und definiert die öffentliche Funktion int calc(int n). Die Erstellung mit gcc main.c calc.c -o run meldet zwei Definitionen von calc. Welche Änderung erhält die getrennte Kompilierung und prüft Aufrufe korrekt?
  1. calc.h in main.c einbinden und beide Quelldateien bei der Erstellung behalten.
  2. calc.c aus der Erstellung entfernen und die Einbindung in main.c behalten.
  3. Beide Quelldateien behalten und einen weiteren Include-Guard zu calc.h hinzufügen.
  4. main.c in calc.c einbinden, damit beide Definitionen übereinstimmen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Eine Quelldatei definiert static int quota = 9; und int quota_value(void) { return quota; }. Eine andere bindet eine Headerdatei ein, die sowohl extern int quota; als auch int quota_value(void); deklariert, und verwendet dann beide Namen. Wie wird die Schnittstelle korrekt repariert, während der gespeicherte Kontingentwert intern bleibt?
  1. static von der Variablen entfernen und alle Verwendungen von quota behalten.
  2. static int quota; zur Headerdatei hinzufügen und direkte Lesezugriffe behalten.
  3. extern int quota; im Aufrufer vor jeder Verwendung wiederholen.
  4. Die externe Variablendeklaration entfernen und zum Lesen quota_value() verwenden.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Eine Headerdatei deklariert size_t packet_count(void);. Was macht die Headerdatei eigenständig?
  1. Jeden Aufrufer bitten, <stddef.h> nach der Headerdatei einzubinden.
  2. Die Deklaration mit kopiertem Typnamen in jede Quelldatei verschieben.
  3. Die Implementierungsdatei bei Aufrufern vor der Headerdatei einbinden.
  4. <stddef.h> vor dieser Deklaration in der Headerdatei einbinden.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Zwei Quelldateien binden jeweils eine Headerdatei mit extern int jobs = 1; ein. Eine erhöht jobs, und die andere gibt den Wert aus. Welche Korrektur erzeugt ein gemeinsames Objekt und erhält die Typprüfung in beiden Quelldateien?
  1. extern int jobs; in der Headerdatei und eine Definition in einer Quelldatei verwenden.
  2. static int jobs = 1; in der Headerdatei und keine Definition in einer Quelldatei verwenden.
  3. Die Headerdatei unverändert lassen und mit einem neuen Makronamen schützen.
  4. Die Headerdatei entfernen und jobs in beiden Quelldateien initialisieren.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Set 5: Zuverlässige inkrementelle Programmerstellungen planen10 Übungsfragen

Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.

Frage 1 von 10

Was macht es nötig, ein vorhandenes gewöhnliches make-Ziel erneut zu erstellen, nachdem seine Abhängigkeiten aktualisiert wurden?
  1. Seine Quelldatei hat mehr Zeilen als zuvor.
  2. Eine Abhängigkeit hat einen neueren Änderungszeitpunkt.
  3. Sein Rezept enthält mehr als einen Befehl.
  4. Derselbe Befehl wurde an einem früheren Tag ausgeführt.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Die ausführbare Datei viewer hängt von main.o und screen.o ab; jede Objektdatei hängt von ihrer zugehörigen Quelldatei ab. Nur screen.c wird neuer als seine Objektdatei, und alle anderen Eingaben bleiben unverändert. Welche Rezepte sind nötig?
  1. main.c kompilieren, dann viewer linken.
  2. Beide Quelldateien kompilieren, dann viewer linken.
  3. viewer linken, ohne eine Quelldatei zu kompilieren.
  4. screen.c kompilieren, dann viewer linken.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Eine Quelldatei bindet settings.h ein, aber ihre Objektdateiregel führt nur die Quelldatei auf. Warum kann eine Änderung an der Headerdatei das Programm nach make unverändert lassen?
  1. Make hat keinen deklarierten Pfad von der Headerdatei zur Objektdatei.
  2. Headerdateien werden immer nur zur Programmlaufzeit verarbeitet.
  3. Der Linker stellt den früheren Inhalt der Headerdatei automatisch wieder her.
  4. Eine Einbindung im Quelltext verhindert, dass die Objektdatei von Headerdateien abhängt.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Zu welchen Werten werden $@ und $^ für die Linkregel tool: main.o engine.o expandiert?
  1. main.o und engine.o
  2. tool und main.o
  3. tool und main.o engine.o
  4. main.o engine.o und tool
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Welche Eingaben und Ausgabe des Compilers werden für die folgende Regel verwendet, wenn make sensor.o erstellt? Nimm CPPFLAGS = -DTRACE=1 und CFLAGS = -std=c17 an.

%.o: %.c shared.h; $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
  1. Quelle sensor.c, Ausgabe sensor.o, mit -DTRACE=1 -std=c17.
  2. Quellen sensor.c shared.h, Ausgabe sensor.o, mit -DTRACE=1 -std=c17.
  3. Quelle sensor.o, Ausgabe sensor.c, mit -DTRACE=1 -std=c17.
  4. Quelle sensor.c, Ausgabe shared.h, mit -DTRACE=1 -std=c17.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Nach einer neuen Erstellung haben die Zeitstempel die Reihenfolge shared.h < main.o < helper.o < app. Die Regeln lauten app: main.o helper.o, main.o: main.c shared.h und helper.o: helper.c. Danach wird nur shared.h bearbeitet. Beide Quelldateien binden die Headerdatei ein. Was geschieht und was ist falsch?
  1. Beide Objektdateien werden vor dem Linken erneut erstellt; der Abhängigkeitsgraph ist vollständig.
  2. Nur helper.o wird vor dem Linken erneut erstellt; main.o fehlt eine Abhängigkeit.
  3. Keine Objektdatei wird erneut erstellt; Headerdateien können keine Objektdateirezepte auslösen.
  4. Nur main.o wird vor dem Linken erneut erstellt; helper.o fehlt eine Abhängigkeit.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Warum kann eine Datei namens clean verhindern, dass eine nicht besonders gekennzeichnete Regel clean: ohne Abhängigkeiten Objektdateien entfernt?
  1. Make reserviert jedes Ziel namens clean für eingebaute Kompilierung.
  2. Die Shell verweigert Befehle von Zielen mit gleichnamigen Dateien.
  3. Make kann das vorhandene Ziel als bereits aktuell behandeln.
  4. Der Compiler entfernt Aufräumrezepte bei aktivierter Optimierung.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Ein Projekt lädt generierte .d-Dateien nach seiner Regel für die ausführbare Datei. main.d führt main.o: main.c a.h auf; worker.d führt worker.o: worker.c b.h a.h auf. Alle Ausgaben sind aktuell, dann wird nur a.h bearbeitet. Was sollte make -n anzeigen?
  1. Nur main.c kompilieren, dann linken; keines dieser Rezepte ausführen.
  2. Beide Quelldateien kompilieren, dann linken; keines dieser Rezepte ausführen.
  3. Beide Quelldateien kompilieren, dann linken; alle drei Rezepte ausführen.
  4. Nur linken; keine Rezepte ausführen, weil Abhängigkeitsdateien bereits existieren.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Ein Projekt enthält app: main.o work.o, main.o: main.c shared.h und work.o: work.c. Alle Ausgaben sind aktuell. CFLAGS ändert sich von -O0 zu -O2, dann wird nur shared.h bearbeitet. Beide Rezepte verwenden CFLAGS. Was tut gewöhnliches make und was sorgt für einheitliche neue Optionen?
  1. Beide Objektdateien erneut erstellen und linken; für die neuen Optionen ist keine weitere Neuerstellung nötig.
  2. main.o erneut erstellen und linken; eine vollständige Neuerstellung erzwingen, um auch work.o zu aktualisieren.
  3. Nur erneut linken und beide Objektdateien behalten; die Headerdatei erneut ändern, um die Optionen anzuwenden.
  4. work.o erneut erstellen und linken; eine vollständige Neuerstellung erzwingen, um auch main.o zu aktualisieren.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

app hängt von main.o und codec.o ab. Jede Objektdatei hat eine vollständige Abhängigkeitsregel für Quell- und Headerdateien; nur codec.h wird von beiden Quelldateien gemeinsam genutzt. Warum kann make -j4 app nach einer Änderung dieser Headerdatei gleichzeitig kompilieren und trotzdem korrekt linken?
  1. Das Linken darf nach Fertigstellung einer beliebigen Objektdatei beginnen und die andere alte wiederverwenden.
  2. Jede Objektdatei wartet auf die andere, dann beginnt das Linken mit beiden alten Dateien.
  3. Beide Objektdateirezepte dürfen gleichzeitig laufen; das Linken wartet auf die Fertigstellung beider.
  4. Das Linken läuft zuerst, um app zu reservieren, dann ersetzen beide Objektdateirezepte die Datei.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe

Schlüsselbegriffe

68 Schlüsselbegriffe, jeweils mit Definition

A

Abhängigkeit
Eine Abhängigkeit ist eine deklarierte Eingabe oder Voraussetzung, die make prüft und aktualisiert, bevor es entscheidet, ob ein Ziel erneut erstellt werden muss.
Eine gemeinsam genutzte Headerdatei ist eine Abhängigkeit jeder Objektdatei, deren Übersetzungseinheit diese Headerdatei einbindet.
Argument
Ein Argument ist ein bei einem Aufruf übergebener Ausdruck, der den Anfangswert eines Parameters liefert.
In cube(5 + 1) wird das Argument zu 6 ausgewertet, bevor der Rumpf von cube beginnt.
Argumentkonvertierung
Eine Argumentkonvertierung wandelt einen übergebenen Argumentwert in den Parametertyp um, den ein sichtbarer Funktionsprototyp angibt.
Die Übergabe von 5.8 an einen int-Parameter wandelt den Wert in 5 um; er wird nicht zur nächsten Ganzzahl gerundet.
Aufrufer
Ein Aufrufer ist die Funktion, die eine andere Funktion aufruft und nach deren Rückkehr fortgesetzt wird.
Wenn main die Funktion read_sensor aufruft, ist main der Aufrufer, der auf den zurückgegebenen Messwert wartet.
Aufrufstapel
Ein Aufrufstapel ist die übliche Implementierungsstruktur, die aktive Funktionsaufrufe verfolgt und ihnen die Rückkehr in umgekehrter Reihenfolge ihrer Verschachtelung ermöglicht.
Eine von einer Tabellenausgabe aufgerufene Umrechnungsfunktion kehrt zurück, bevor die Tabellenausgabe zu main zurückkehrt.
auto
Das Schlüsselwort auto kennzeichnet in C17 ausdrücklich automatische Speicherdauer; C23 verwendet es außerdem, um einen Typ aus einem Initialisierer abzuleiten.
In C17 hat auto int level; dasselbe Speicherverhalten wie ein gewöhnliches lokales int level;.
automatische Speicherdauer
Die automatische Speicherdauer bindet den Speicher gewöhnlicher lokaler Variablen und Parameter an ihre Blockausführung oder ihren Funktionsaufruf.
Zwei aktive rekursive Aufrufe haben getrennte automatische Parameter, obwohl ihre Namen übereinstimmen.
automatische Variable
Eine automatische Variable wird von make für die aktuelle Regel gesetzt und liefert Werte wie deren Ziel- oder Abhängigkeitsnamen.
Die automatische Variable $@ bezeichnet sensor.o, während das Rezept für diese Objektdatei ausgeführt wird.

B

Basisfall
Ein Basisfall beantwortet eine Eingabe direkt, ohne einen weiteren rekursiven Aufruf auszuführen.
Der Basisfall der Fakultätsfunktion gibt 1 zurück, sodass die wartenden Multiplikationen in früheren Aufrufen abgeschlossen werden können.
bedingte Kompilierung
Die bedingte Kompilierung behält Quelltextzweige anhand von Präprozessorprüfungen bei oder überspringt sie, bevor der entstehende Code kompiliert wird.
Eine durch bedingte Kompilierung entfernte Debug-Ausgabe fehlt in der entstehenden ausführbaren Datei.
Bindung
Die Bindung bestimmt, ob sich Deklarationen eines Namens an verschiedenen Stellen auf dasselbe Objekt oder dieselbe Funktion beziehen.
Eine globale Variable kann statische Speicherdauer haben, während static ihre Bindung auf eine Übersetzungseinheit beschränkt.
Blockgültigkeitsbereich
Der Blockgültigkeitsbereich beschränkt einen lokalen Namen auf den Bereich unmittelbar nach seinem Deklarator bis zum Ende des umgebenden Blocks.
Eine innerhalb eines if-Blocks deklarierte Variable kann nach dem Schließen dieses Blocks in einer Ausgabe nicht mehr benannt werden.

D

Dateigültigkeitsbereich
Der Dateigültigkeitsbereich reicht von einer Deklaration außerhalb einer Funktion bis zum Ende der Übersetzungseinheit.
Eine oberhalb einer globalen Variablen definierte Hilfsfunktion benötigt eine frühere Deklaration, bevor sie diese Variable verwenden kann.
defined-Operator
Der Präprozessoroperator defined ergibt eins, wenn sein Makronamenoperand definiert ist, und null, wenn er nicht definiert ist.
Verwende #if defined(DEBUG), um die Existenz auch dann zu prüfen, wenn DEBUG den Ersetzungstext 0 hat.
Deklaration
Eine Deklaration führt einen Namen und die Typinformationen ein, die zur Prüfung seiner Verwendung benötigt werden.
Eine Deklaration oberhalb von main ermöglicht die Prüfung eines Aufrufs, selbst wenn der Funktionsrumpf später folgt.

E

eigenständige Headerdatei
Eine eigenständige Headerdatei bindet die Headerdateien ein, die ihre eigenen Deklarationen benötigen, sodass keine besondere Einbindereihenfolge nötig ist.
Eine Headerdatei, die eine Funktion mit size_t deklariert, bindet <stddef.h> vor dieser Deklaration ein.
extern
Das Schlüsselwort extern kann eine Entität mit Bindung deklarieren, ohne ein Objekt zu definieren, sofern kein Initialisierer die Deklaration zu einer Definition macht.
Eine gemeinsam genutzte Headerdatei deklariert extern int errors;, während eine Quelldatei die initialisierte Objektdefinition liefert.
externe Bindung
Externe Bindung ermöglicht es Deklarationen in verschiedenen Übersetzungseinheiten, dasselbe Objekt oder dieselbe Funktion zu bezeichnen.
Eine öffentliche Zählerfunktion hat externe Bindung, sodass Aufrufer in einer anderen Quelldatei zu ihrer Definition linken können.

F

Funktion
Eine Funktion ist ein benannter Block ausführbaren Codes, der bei einem Aufruf eine Aufgabe erledigt.
Eine Funktion zur Temperaturumrechnung ermöglicht mehreren Teilen eines Programms, eine getestete Berechnung wiederzuverwenden.
funktionsähnliches Makro
Ein funktionsähnliches Makro nimmt Token-Argumente entgegen und setzt sie vor der gewöhnlichen C-Kompilierung in einen Ersetzungstext ein.
Klammern in einem funktionsähnlichen Makro erhalten die Gruppierung, verhindern aber nicht die wiederholte Auswertung von Argumenten.
Funktionsdefinition
Eine Funktionsdefinition liefert den ausführbaren Rumpf einer Funktion zusammen mit ihrem Namen, Rückgabetyp und ihrer Parameterliste.
Die Definition von double scale(double factor) muss mit dem Prototyp übereinstimmen, der sowohl im Aufrufer als auch in der Implementierung eingebunden wird.
Funktionsprototyp
Ein Funktionsprototyp ist eine Deklaration, die den Rückgabetyp und die Parametertypen einer Funktion zur Prüfung von Aufrufen angibt.
Der Prototyp double scale(double factor); weist den Compiler an, das Argument in double umzuwandeln.
Funktionsprototyp-Gültigkeitsbereich
Der Funktionsprototyp-Gültigkeitsbereich beschränkt einen Parameternamen auf den Funktionsprototyp, der ihn deklariert.
Ein Prototyp darf seinen Parameter value nennen, während die Definition denselben Parameter n nennt.

G

gemeinsam genutzter Zustand
Gemeinsam genutzter Zustand sind dauerhaft gespeicherte Daten, die mehrere Vorgänge lesen oder ändern können.
Eine gemeinsam genutzte Fehleranzahl macht einen späteren Bericht von den früheren Funktionen abhängig, die sie aktualisiert haben.
generierte Abhängigkeitsdatei
Eine generierte Abhängigkeitsdatei erfasst vom Compiler ermittelte Quell- und Headerdateien als Abhängigkeiten, die make bei späteren Erstellungen lädt.
Die Kompilierung mit -MMD kann sensor.d schreiben, wodurch eine spätere Änderung an einer Headerdatei das erneute Erstellen von sensor.o auslöst.
globale Variable
Eine globale Variable ist ein im Dateigültigkeitsbereich deklariertes Objekt mit statischer Speicherdauer, das unterhalb seiner Deklaration sichtbar ist.
Zwei Funktionen zum Hinzufügen von Fahrten können eine globale Streckensumme aktualisieren, die für main verfügbar bleibt.
Gültigkeitsbereich
Der Gültigkeitsbereich eines Namens ist der Bereich des Quellcodes, in dem seine Deklaration sichtbar ist.
Eine Variable kann weiterhin existieren, während ihr Name durch eine andere Deklaration in einem verschachtelten Block verdeckt wird.

H

Headerdatei
Eine Headerdatei stellt Deklarationen, Makros oder Typen durch Einbinden ihres Texts gemeinsam bereit.
Eine Headerdatei zur Umrechnung deklariert beide Umrechnungsrichtungen, ohne deren kompilierte Rümpfe zu liefern.

I

Include-Guard
Ein Include-Guard verhindert die wiederholte Verarbeitung einer Headerdatei innerhalb einer Übersetzungseinheit.
Zwei Headerdateien können eine gemeinsame geschützte Headerdatei einbinden, ohne deren Inhalt in derselben Einheit zu wiederholen.
interne Bindung
Interne Bindung beschränkt die gemeinsame Identität eines Namens auf eine Übersetzungseinheit und hält seine Entität gegenüber anderen Einheiten intern.
Zwei Dateien können jeweils eine statische Hilfsfunktion namens check definieren, ohne dass beim Linken eine Kollision entsteht.

K

keine Bindung
Keine Bindung bedeutet, dass ein Name seine eigene Entität bezeichnet, statt Deklarationen durch interne oder externe Bindung zu verbinden.
Gewöhnliche automatische lokale Variablen und Parameter haben keine Bindung, auch wenn eine andere Funktion dieselben Namen verwendet.

L

Linker
Ein Linker verbindet Objektdateien und Bibliotheken und löst externe Namen zu Definitionen auf.
Der Linker benötigt die Objektdatei mit der Definition einer Umrechnungsfunktion, auch wenn der Aufrufer ihren Prototyp eingebunden hat.

M

make-Variable
Eine make-Variable benennt wiederverwendbaren Text wie Compilereinstellungen oder Objektdateilisten, der mit einem Verweis wie $(CFLAGS) expandiert wird.
Eine Änderung an einer CC-Zuweisung ändert den Compilertext, den Rezepte mit einem Verweis auf $(CC) verwenden.
Mehrfachdefinition
Ein Fehler wegen Mehrfachdefinition tritt auf, wenn beim Linken widersprüchliche Definitionen desselben Namens mit externer Bindung übergeben werden.
Zwei gelinkte Quelldateien, die beide main definieren, verursachen einen Fehler wegen Mehrfachdefinition oder eines doppelten Symbols.
Modul
Ein Modul fasst zusammengehörigen C-Implementierungscode in einer Quelldatei mit einer passenden Headerdatei zusammen.
Ein Zählermodul kann zwei öffentliche Funktionen bereitstellen und seinen gespeicherten Zählerstand intern halten.
Musterregel
Eine Musterregel verwendet %, um einen gemeinsamen Dateinamenstamm zu erkennen, sodass ein Abhängigkeitsmuster und ein Rezept viele Ziele erstellen können.
Die Regel %.o: %.c ordnet sensor.o der Datei sensor.c zu, indem sie sensor als gemeinsamen Stamm verwendet.

N

nicht aufgelöster Verweis
Ein nicht aufgelöster Verweis ist ein Linkfehler, bei dem für einen benötigten externen Namen keine Definition in den gelinkten Eingaben verfügbar ist.
Wenn sensor.o beim Linken fehlt, kann ein nicht aufgelöster Verweis auf eine öffentliche Lesefunktion entstehen, die nur sensor.o definiert.
nicht festgelegte Auswertungsreihenfolge der Argumente
Eine nicht festgelegte Auswertungsreihenfolge der Argumente bedeutet, dass C keine bestimmte Auswertungsreihenfolge für die Argumentausdrücke eines Aufrufs vorschreibt.
Zwei Aufrufe zur Erzeugung von Ticketnummern in einer Ausgabe können ihre Nummern in beliebiger Reihenfolge liefern.

O

objektähnliches Makro
Ein objektähnliches Makro verknüpft einen Namen mit Ersetzungstokens, die eingesetzt werden, wenn dieser Name später erscheint.
Die Definition #define LIMIT 16 ersetzt das Token LIMIT, lässt aber "LIMIT" unverändert.
Objektdatei
Eine Objektdatei enthält kompilierten Maschinencode und Symbolinformationen, die der Linker mit anderen Objektdateien verbinden kann.
Nach einer Änderung nur an sensor.c kann die Programmerstellung sensor.o ersetzen und ein unverändertes main.o wiederverwenden.

P

Parameter
Ein Parameter ist eine Funktionsvariable, die beim Eintritt mit einer Kopie des Argumentwerts initialisiert wird.
In int cube(int n) erhält der Parameter n für jeden Aufruf einen eigenen Eingabewert.
Präprozessor
Der Präprozessor wandelt Quelltext durch Direktiven um, etwa durch Einbindung, Makroersetzung und bedingte Auswahl.
Führe gcc -E sample.c aus, um den expandierten Code zu untersuchen, bevor der Compiler C-Ausdrücke prüft.
Präprozessordirektive
Eine Präprozessordirektive beginnt mit # und steuert die Umwandlung von Quelltext.
Die Direktive #include "sensor.h" fügt Headertext in den aktuellen Präprozessierungslauf ein.
Probelauf
Ein Probelauf mit make -n gibt gewöhnliche Erstellungsrezepte aus, ohne sie auszuführen, und zeigt so die geplanten Compiler- und Linkerbefehle.
Untersuche nach einer Änderung an einer Headerdatei einen Probelauf, um zu sehen, welche Objektdateien make erneut erstellen will.
Pseudoziel
Ein Pseudoziel bezeichnet eine mit .PHONY deklarierte Aktion, sodass eine vorhandene Datei mit diesem Namen ihr Rezept nicht unterdrücken kann.
Die Deklaration von clean als Pseudoziel stellt sicher, dass das Aufräumen auch ausgeführt wird, wenn im Verzeichnis eine Datei namens clean erscheint.

R

Regel
Eine make-Regel benennt ein Ziel, seine Abhängigkeiten und mögliche Rezeptbefehle, die benötigt werden, um dieses Ziel aus diesen Eingaben zu aktualisieren.
Die Regel für main.o führt sowohl main.c als auch die Headerdateien des Projekts auf, die während seiner Kompilierung eingebunden werden.
register
Das Schlüsselwort register kennzeichnet eine lokale Variable mit einem Optimierungshinweis, den Compiler ignorieren dürfen, und verbietet das Ermitteln ihrer Adresse.
Das Hinzufügen von register zu einem Schleifenzähler zwingt einen modernen Compiler nicht, ein Prozessorregister zu verwenden.
Rekursion
Rekursion liegt vor, wenn eine Funktion sich selbst aufruft, um eine weitere Instanz eines Problems zu lösen.
Eine Funktion zur Ziffernsumme kann die letzte Ziffer verarbeiten und die verbleibenden Ziffern rekursiv addieren.
Rekursionsfall
Ein Rekursionsfall ist ein Zweig, der einen Teil eines Problems löst, indem er dieselbe Funktion mit einer Eingabe aufruft, die näher an einem Basisfall liegt.
Das Abziehen von eins von einer positiven Fakultätseingabe führt den Rekursionsfall zu seiner Abbruchprüfung hin.
Rezept
Ein Rezept ist die Folge von Shell-Befehlen, die einer make-Regel zugeordnet ist; gewöhnliche Rezeptzeilen beginnen mit echten Tabulatorzeichen.
Das Rezept für eine Objektdatei kompiliert ihre Quelldatei, während das Rezept für die ausführbare Datei die benötigten Objektdateien linkt.
Rückgabewert
Ein Rückgabewert ist das Ergebnis, das eine Funktion ihrem Aufrufer zur Verwendung in einem Ausdruck liefert.
Der Rückgabewert einer Umrechnung kann double result zugewiesen werden, ohne ihn auszugeben.

S

Seiteneffekt
Ein Seiteneffekt ist eine Änderung des Programmzustands oder eine Interaktion wie eine Ausgabe, die über die reine Berechnung des Werts eines Ausdrucks hinausgeht.
Das Erhöhen eines gemeinsam genutzten Zählers ist ein Seiteneffekt, sodass die Reihenfolge getrennter Zähleraufrufe wichtig sein kann.
Speicherdauer
Die Speicherdauer beschreibt unabhängig von der Sichtbarkeit des Namens, wie lange der Speicher eines Objekts während der Ausführung existiert.
Eine statische lokale Variable verbindet Blockgültigkeitsbereich mit einer Speicherdauer während der gesamten Programmausführung.
Stackframe
Ein Stackframe ist ein Datensatz für einen aktiven Aufruf, der typischerweise dessen Parameter, automatische lokale Variablen und Rückkehrstelle enthält.
Am tiefsten Punkt von factorial(4) haben vier Fakultätsaufrufe getrennte aktive Stackframes.
Standardziel
Das Standardziel ist das Ziel, das make ohne ausdrückliches Zielargument auswählt, normalerweise das erste gewöhnliche Ziel in der Datei.
Steht all oder die endgültige ausführbare Datei an erster Stelle, endet ein gewöhnlicher make-Lauf nicht bei einer einzelnen Objektdatei.
statische lokale Variable
Eine statische lokale Variable ist ein mit static deklariertes Objekt mit Blockgültigkeitsbereich, das einen gespeicherten Wert über wiederholte Aufrufe hinweg behält.
Ein statischer lokaler Ticketzähler steigt bei jedem Aufruf, statt erneut bei seinem Initialisierer zu beginnen.
statische Speicherdauer
Statische Speicherdauer bedeutet, dass ein Objekt während der gesamten Programmausführung existiert und einmal vor dem Programmstart initialisiert wird.
Ein numerisches Objekt mit Dateigültigkeitsbereich ohne ausdrücklichen Initialisierer beginnt bei null und behält spätere Zuweisungen.
Stringifizierung
Die Stringifizierung verwendet # vor einem Makroparameter im Ersetzungstext, um die übergebenen Argument-Tokens in ein Zeichenkettenliteral umzuwandeln.
Ein Diagnosemakro kann 3 + 4 stringifizieren, um den Ausdruck neben seinem berechneten Wert anzuzeigen.

T

Token-Verkettung
Die Token-Verkettung verwendet ## in einer Makro-Ersetzungsliste, um benachbarte Tokens zu einem einzelnen Präprozessortoken zu verbinden.
Ein Makroaufruf, der temp und 2 verbindet, kann im expandierten Quelltext den Bezeichner temp2 bilden.

Ü

Übersetzungseinheit
Eine Übersetzungseinheit ist eine Quelldatei nach der Präprozessierung zusammen mit ihrem eingebundenen Text, die unabhängig übersetzt wird.
Das Einbinden derselben Headerdatei in zwei Quelldateien liefert Deklarationen für zwei getrennte Übersetzungseinheiten.

V

Verdeckung
Verdeckung liegt vor, wenn eine innere Deklaration einen sichtbaren äußeren Namen verbirgt, sodass Verwendungen im inneren Gültigkeitsbereich die neue Entität auswählen.
Die Deklaration eines zweiten total in einer Schleife kann den eigentlich gemeinten äußeren Akkumulator unverändert lassen.
versteckte Abhängigkeit
Eine versteckte Abhängigkeit ist eine Eingabe oder Zustandsanforderung, die eine Funktion beeinflusst, ohne in ihrer Parameterliste zu erscheinen.
Für eine Berechnung, die einen globalen Grenzwert liest, muss dieser Grenzwert vorbereitet werden, bevor die Funktion getestet werden kann.
void
Das Schlüsselwort void gibt als Rückgabetyp an, dass kein Wert zurückgegeben wird, oder allein in einer Parameterliste, dass es keine Parameter gibt.
Eine void-Funktion zur Ausgabe erledigt ihre Ausgabeaufgabe, kann aber keinen Wert für die rechte Seite einer Zuweisung liefern.
vordefiniertes Makro
Ein vordefiniertes Makro wird von der Implementierung bereitgestellt und liefert Informationen wie die aktuelle Quelltextzeile oder die Sprachversion.
Eine Diagnose mit __FILE__ und __LINE__ kann angeben, woher ihre Meldung stammt.
vorläufige Definition
Eine vorläufige Definition ist eine Objektdeklaration im Dateigültigkeitsbereich ohne Initialisierer oder extern, die eine Definition liefern kann.
int count; in einer gemeinsam genutzten Headerdatei ist kein portabler Ersatz für eine extern-Deklaration.

W

Wertübergabe
Die Wertübergabe ist die Regel für die Argumentübergabe in C: Jeder Parameter erhält eine Kopie des Werts, den sein Argumentausdruck berechnet.
Eine Zuweisung an den Parameter n innerhalb einer Funktion lässt die gewöhnliche Ganzzahlvariable des Aufrufers unverändert.

Z

Ziel
Ein Ziel ist die Ausgabedatei oder Aktion, die make aktualisieren soll.
Das Ziel für die ausführbare Datei steht gewöhnlich an erster Stelle, sodass make ohne Argumente das gesamte Programm erstellt.

#

#error
Die Direktive #error verlangt eine Diagnose, wenn sie während der Präprozessierung erreicht wird, und ermöglicht so die Ablehnung einer nicht unterstützten Programmerstellung.
Eine Versionsprüfung kann #error auswählen, wenn der gewählte C-Standard älter ist, als das Programm benötigt.

Über diese Einheit

Von StudeliFehler melden

Erstelle deine eigenen Lernmaterialien

Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.

Jetzt starten