- Lernen
- Informatik & Informationswissenschaften
- Programmierung
- C-Programmierung – Zeiger, Speicherverwaltung und sicheres C
- Funktionen und Programmstruktur in C
Funktionen und Programmstruktur in C
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
- 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ückgebenSchreibe Funktionsdefinitionen mit Rückgabetyp, Parametern und einem Rumpf, verwende
returnundvoidrichtig und rufe Funktionen innerhalb von Ausdrücken auf. - Funktionen mit Prototypen deklarierenDeklariere 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ückgebenErklä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 nachvollziehenVerfolge, 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.
- Funktionen definieren, die Eingaben entgegennehmen und ein Ergebnis zurückgeben
- 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 festlegenUnterscheide 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 nutzenDefiniere 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.
- Festlegen, wo ein Name verwendet werden kann (Gültigkeitsbereich)
- Verwende #include, #define und Makros sicher und wähle Code zur Übersetzungszeit mit bedingten Direktiven, vordefinierten Makros und Compileroptionen aus.
- Dateien einbinden und Makros definierenErklä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 kompilierenVerwende #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.
- Dateien einbinden und Makros definieren
- 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 linkenBeschreibe die Schritte von der Quelldatei bis zur ausführbaren Datei, kompiliere Dateien getrennt in Objektdateien, linke sie und unterscheide Übersetzungsfehler von Linkerfehlern.
- Headerdateien schreibenSchreibe 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 steuernVerwende 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.
- Mehrere Quelldateien kompilieren und linken
- 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 festlegenSchreibe 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 vereinfachenVerwende Variablen wie
CCundCFLAGS, die automatischen Variablen$@,$<und$^, Musterregeln, eingebaute Regeln und Pseudoziele, um ein Makefile kurz und korrekt zu halten.
- Regeln und Abhängigkeiten in Makefiles festlegen
Funktionen und Programmstruktur in C einfach erklärt
1Funktionen 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.
returnbeendet 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
| Deklaration | Bedeutung in C17 | Bedeutung in C23 |
|---|---|---|
int read_sensor(void); | Keine Parameter | Keine Parameter |
int read_sensor(); | Parameterinformation nicht festgelegt | Keine 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 Aufrufers | Auswirkung 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 Aufruf | Berechnung, 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.
2Die 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
| Spezifizierer | Bedeutung in diesem C17-Code |
|---|---|
auto | Ausdrückliche automatische Speicherdauer für eine Blockvariable; wird meist weggelassen |
register | Ein Hinweis auf schnellen Zugriff; die Adresse darf nicht ermittelt werden |
static innerhalb einer Funktion | Erhaltener Speicher mit einem lokalen Namen |
static im Dateigültigkeitsbereich | Interne Bindung, die den Namen innerhalb seiner Übersetzungseinheit hält |
extern | Eine 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
staticbestimmt, 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.
3Den 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
| Name | Information |
|---|---|
__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
#ifdefprüft die Existenz;#ifprüft einen Wert.- Nicht definierte Bezeichner in
#ifwerden als null ausgewertet. -DNAME=valueliefert 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.
4Ein 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.
| Schritt | Ergebnis | GCC-Option, die dort stoppt |
|---|---|---|
| Präprozessierung | Expandierter C-Text | -E |
| Kompilierung | Assemblertext | -S |
| Assemblierung | Objektdatei | -c |
| Linken | Ausführbare Datei | Normale 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.
-cerzeugt 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 bereitstellen | In einer Quelldatei belassen |
|---|---|
| Funktionsprototypen | Funktionsrümpfe mit externer Bindung |
extern-Variablendeklarationen | Definitionen gemeinsam genutzter Objekte |
| Benötigte Makros und Typen | Interne 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
| Bindung | Bedeutung |
|---|---|
| Extern | Deklarationen können sich über Übersetzungseinheiten hinweg auf eine Entität beziehen |
| Intern | Deklarationen beziehen sich innerhalb einer Übersetzungseinheit auf eine Entität |
| Keine | Ein 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. staticim 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
staticund die Bindung durchstaticim Dateigültigkeitsbereich haben unterschiedliche Aufgaben.
5Programme 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
templinken, drei Befehle. - Keine Änderungen: Es müssen keine Programmrezepte ausgeführt werden.
- Nur
convert.cändern: Erneut kompilieren und linken, zwei Befehle;main.owiederverwenden. 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
| Variable | Wert 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
1Einen Funktionsaufruf erstellen und verfolgen
19 Schritte
- 1Einfü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.
- 2BeispielEiner 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; }intvor dem Namen beschreibt das Ergebnis.int readingdeklariert 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. - 3BeispielEinen Aufruf als Ausdruck verwenden
Bei
int result = corrected(9);initialisiert das Argument9den Parameterreading. Der Rückgabeausdruck ergibt13, und die Zuweisung des Aufrufers speichert diesen Wert inresult.Der Aufruf kann auch innerhalb eines anderen Ausdrucks stehen:
2 * corrected(9)ergibt26. Schreibst du nurcorrected(9);, wird der Aufruf ebenfalls ausgeführt, sein Rückgabewert aber verworfen. - 4TippZwischen 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
voiddeklarierte Funktion kann eine Ausgabeaufgabe übernehmen, liefert aber keinen Wert für eine Zuweisung. Innerhalb der Funktion beendetreturn;sie ohne Wert. Achte bei einer Funktion mit Rückgabewert darauf, dass jeder Pfad, der ein Ergebnis liefern soll, ein passendesreturnerreicht; die Anzeige einer Zahl liefert sie nicht an den Aufrufer. - 5HerausforderungDie 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.0zurück: Die Ganzzahladdition ergibt13, die Ganzzahldivision ergibt6, und die Umwandlung bei der Rückgabe ergibt6.0. Verwende2.0, damit die Division mit Gleitkommazahlen erfolgt, wenn du6.5benötigst. Der Rückgabetyp kann einen bereits verworfenen Nachkommateil nicht wiederherstellen. - 6Erklä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. - 7KernaussageEine leere Eingabeliste angeben
Verwende
int read_sensor(void);sowohl in C17 als auch in C23 für keine Parameter. In C17 lässtint read_sensor();die Parameterinformation offen; C23 ändert diese Schreibweise so, dass sie keine Parameter bedeutet. Wähle C17 mit-std=c17ausdrücklich aus. - 8HerausforderungEine zulässige Umwandlung verfolgen
Der Compiler hat
int corrected(int reading);gesehen. Was gibtcorrected(6.8)zurück, und muss der Aufruf abgelehnt werden?---reveal---
Der Aufruf kann kompiliert werden. Der Prototyp bewirkt die Umwandlung von
6.8in die Ganzzahl6, dann addiert die Funktion4und gibt10zurü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. - 9BeispielZwei 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 enthaltensampleund der Parameter jeweils9in getrennten Objekten. Der Rumpf ändert seinen Parameter auf13. Nach der Rückkehr des Aufrufs enthältsampleweiterhin9: Es gab keine Zuweisung an diese Variable. - 10VerbindungDen 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 aktualisiertsample. - 11HerausforderungDen verworfenen Aufruf finden
Ausgehend von
int sample = 7;führt der Aufrufer aus:corrected(sample); sample = corrected(sample);Welchen Wert hat
sampledanach?---reveal---
Der erste Aufruf berechnet
11und verwirft den Wert, sodasssamplebei7bleibt. Der zweite erhält ebenfalls7, gibt11zurück und diesmal speichert der Aufrufer den Wert. Der Endwert ist11, nicht15; ein Aufruf aktualisiert seine Argumentvariable nicht allein dadurch, dass er einen geänderten Wert zurückgibt. - 12Erklä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.
- 13BeispielDie Aufrufe beim Abstieg notieren
Verfolge
total_to(3). Der erste Aufruf enthältn == 3und wartet darauf,3zutotal_to(2)zu addieren. Dieser Aufruf enthält sein eigenesn == 2und wartet auftotal_to(1).Der Aufruf mit
n == 1wartet auftotal_to(0), das sofort0zurückgibt. An diesem tiefsten Punkt sind vier Aufrufe aktiv. Ersetze die früheren Werte vonnnicht durch null; sie gehören zu verschiedenen Aufrufen. - 14BeispielDie Ergebnisse auf dem Rückweg eintragen
Sobald
total_to(0)den Wert0zurückgibt, können die wartenden Berechnungen abgeschlossen werden:- Der Aufruf mit
n == 1gibt1 + 0, also1, zurück. - Der Aufruf mit
n == 2gibt2 + 1, also3, zurück. - Der Aufruf mit
n == 3gibt3 + 3, also6, 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.
- Der Aufruf mit
- 15HerausforderungPrüfen, ob ein Basisfall erreichbar ist
Angenommen, die Basisprüfung in
total_towird zun == 0geändert, und der rekursive Schritt verwendet weiterhinn - 1. Was geht schief, wenn das anfängliche Argument-2ist?---reveal---
Die Eingaben werden zu
-2,-3,-4und 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 mitn <= 0stoppt bei dieser Eingabe sofort. - 16TippAuch die Größe des Ergebnisses prüfen
Eine endende rekursive Berechnung kann dennoch den Wertebereich ihres Rückgabetyps überschreiten. Bei einem 32-Bit-
intpasstfactorial(12)hinein, während der mathematische Wert vonfactorial(13)das Maximum2147483647ü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.
- 17TippAufrufe 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 ändertiohne die erforderliche Sequenzierung. Die Wertübergabe macht einen unsicheren Argumentausdruck nicht sicher. - 18HerausforderungDeklaration, Umwandlung und Aufrufe verbinden
Nachdem
total_tovor der Verwendung deklariert wurde, schreibt ein Aufruferint x = 2; total_to(x); x = total_to(3.9);. Welchen Wert hatxam Ende?---reveal---
Das erste Ergebnis wird verworfen, sodass
xbei2bleibt. Das zweite Argument wird in3umgewandelt; der rekursive Ablauf ergibt6, was die Zuweisung inxspeichert. Die Deklaration steuert die Umwandlung, die Rekursion berechnet den Wert und der Aufrufer entscheidet, ob er ihn behält. - 19ZusammenfassungDrei 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.
2Namen und gespeicherte Werte verfolgen
13 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungMit einer kleinen Aufgabe beginnen
- 2BeispielDie nächstgelegene sichtbare Deklaration lesen
- 3ErklärungDen Gültigkeitsbereich am Ort der Funktionsdefinition beachten
- 4HerausforderungEinen nach einem Block benötigten Wert bereitstellen
- 5BeispielDen Neustart eines automatischen Zählers beobachten
- 6BeispielDen Zähler mit static erhalten
- 7HerausforderungInitialisierung und Zuweisung unterscheiden
- 8TippDas Schlüsselwort an seiner Position verstehen
- 9BeispielEine globale Aktualisierung verfolgen
- 10HerausforderungDie verdeckende lokale Variable finden
- 11VerbindungEine Berechnung leichter testbar machen
- 12HerausforderungGültigkeitsbereich und gespeicherten Zustand verbinden
- 13ZusammenfassungNamen und Lebensdauern getrennt vermerken
3Code vor der Kompilierung untersuchen
13 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungMit einer kleinen Aufgabe beginnen
- 2ErklärungSehen, was das Einbinden tatsächlich liefert
- 3BeispielTokens vor der Anwendung von Operatorregeln ersetzen
- 4BeispielInnere und äußere Gruppierung schützen
- 5HerausforderungErkennen, was Klammern nicht beheben können
- 6TippMakroregeln von Gültigkeitsregeln trennen
- 7BeispielEine Einstellung auf Existenz und Wert prüfen
- 8HerausforderungEinen Zweig über eine Compileroption auswählen
- 9BeispielDas erste Einbinden einer Headerdatei verfolgen
- 10KernaussageIn einer anderen Einheit mit einem neuen Include-Guard-Zustand beginnen
- 11VerbindungEine Diagnose im Quelltext verorten
- 12HerausforderungExpansion und Zweigauswahl verbinden
- 13ZusammenfassungDie Umwandlung der Reihe nach untersuchen
4Ein Programm aus zwei Quelldateien erstellen
14 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungMit einer kleinen Aufgabe beginnen
- 2ErklärungEine Übersetzungseinheit bis zur Objektdatei verfolgen
- 3BeispielDie gemeinsam genutzte Deklaration schreiben
- 4BeispielDie Implementierung in eine eigene Quelldatei setzen
- 5HerausforderungEine gemeinsam genutzte Variable richtig platzieren
- 6BeispielGetrennt kompilieren, dann linken
- 7HerausforderungEinen nicht aufgelösten Verweis beheben
- 8TippDie Implementierung nicht in den Aufrufer kopieren
- 9BeispielEinen Zähler innerhalb des Moduls halten
- 10ErklärungDrei Eigenschaften des Zählers unterscheiden
- 11HerausforderungZwei unabhängige Zähler erklären
- 12TippEine Headerdatei eigenständig gestalten
- 13HerausforderungInternen Zugriff und Schnittstelle gemeinsam korrigieren
- 14ZusammenfassungJeden Bedarf dem passenden Erstellungsschritt zuordnen
5Nur Geändertes erneut erstellen
15 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungMit einer kleinen Aufgabe beginnen
- 2BeispielEingaben für ausführbare Datei und Objektdateien festhalten
- 3BeispielEiner Objektdatei ein Rezept geben
- 4BeispielErste Erstellung und erneutes Erstellen nach einer Quelltextänderung ausführen
- 5HerausforderungEine Änderung an einer gemeinsam genutzten Headerdatei verfolgen
- 6TippDen Plan vor der Ausführung untersuchen
- 7ErklärungWiederholte Einstellungen durch Variablen ersetzen
- 8BeispielDas Linkrezept expandieren
- 9BeispielEin Kompiliermuster für beide Objektdateien verwenden
- 10HerausforderungEine Quelldatei wählen, ohne die Headerdatei zu kompilieren
- 11TippAufräumen als Aktion festlegen
- 12VerbindungDen Compiler Header-Abhängigkeiten erfassen lassen
- 13HerausforderungParalleles erneutes Erstellen korrekt halten
- 14HerausforderungGeänderte Optionen auf das ganze Programm anwenden
- 15ZusammenfassungDen Graphen prüfen, bevor du dem Ergebnis vertraust
Funktionen und Programmstruktur in C: Übungsaufgaben
Set 1: Werte durch Funktionsaufrufe verfolgen10 Übungsfragen
Frage 1 von 10
int triple(int value) { return value * 3; }?Antwort zeigenAntwort ausblenden
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
void report(int code) einem Aufrufer?Antwort zeigenAntwort ausblenden
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
double portion(int total) { return total / 4; } für portion(11) zurück?Antwort zeigenAntwort ausblenden
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
int capacity(void) steht unterhalb von main. Welche Deklaration oberhalb von main beschreibt in C17 einen Aufruf ohne Eingaben vollständig?Antwort zeigenAntwort ausblenden
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
int fifth(int n) { return n / 5; } vor der Verwendung deklariert ist, was ergibt fifth(-14.8)?Antwort zeigenAntwort ausblenden
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
double stretch(int n);. Welche Definition ist mit dieser Deklaration vereinbar?Antwort zeigenAntwort ausblenden
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);
}
Antwort zeigenAntwort ausblenden
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);
Antwort zeigenAntwort ausblenden
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);
}
Antwort zeigenAntwort ausblenden
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);
}
Antwort zeigenAntwort ausblenden
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
i aus for (int i = 0; i < 6; i++) { } verwendet werden?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);
}
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);
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
int amount = amount + 2; innerhalb eines Blocks nicht mit einem äußeren amount?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
static int calls = 0; während einer Programmausführung initialisiert?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;
}
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;
}
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
int permitted(int n) liest ein globales limit. Warum könnte dasselbe Argument bei einem späteren Aufruf ein anderes Ergebnis liefern?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);
}
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
read_sensor() initialisieren. Warum ist static int first = read_sensor(); in C17 ungültig?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
#define LIMIT 18 im späteren Quellcode?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
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
#define LIMIT 12; die Anweisung int count = LIMIT * 2; ungültig?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);
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))
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
#ifdef TRACE bei #define TRACE 0?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
#if VERBOSE einen Zweig stillschweigend überspringen, wenn bei der Erstellung nur VERB0SE definiert wird?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
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));
}
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
int mode = 2;. Zwei getrennte Quelldateien binden sie ein, werden kompiliert und dann zusammengelinkt. Warum verhindert der Include-Guard keinen Fehler wegen einer Mehrfachdefinition?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
gcc -std=c17 -c driver.c normalerweise auf einem Unix-ähnlichen System?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
reading ein, aber beim Linken nur seiner Objektdatei wird ein nicht aufgelöster Verweis auf reading gemeldet. Was sollte ergänzt werden?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
int classify(double x);, aber ihre Quelldatei definiert double classify(double x). Was macht die Abweichung beim Kompilieren der definierenden Quelldatei sichtbar?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
static int cached = 3;, wenn es außerhalb aller Funktionen deklariert wird?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
int failures verwenden können, das in einer einzigen Quelldatei definiert ist?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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
size_t packet_count(void);. Was macht die Headerdatei eigenständig?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
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?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
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
$@ und $^ für die Linkregel tool: main.o engine.o expandiert?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 $@
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
clean verhindern, dass eine nicht besonders gekennzeichnete Regel clean: ohne Abhängigkeiten Objektdateien entfernt?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
.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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
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?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?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe
Schlüsselbegriffe
A
Abhängigkeit
Argument
cube(5 + 1) wird das Argument zu 6 ausgewertet, bevor der Rumpf von cube beginnt.Argumentkonvertierung
5.8 an einen int-Parameter wandelt den Wert in 5 um; er wird nicht zur nächsten Ganzzahl gerundet.Aufrufer
main die Funktion read_sensor aufruft, ist main der Aufrufer, der auf den zurückgegebenen Messwert wartet.Aufrufstapel
main zurückkehrt.auto
auto kennzeichnet in C17 ausdrücklich automatische Speicherdauer; C23 verwendet es außerdem, um einen Typ aus einem Initialisierer abzuleiten.auto int level; dasselbe Speicherverhalten wie ein gewöhnliches lokales int level;.automatische Speicherdauer
automatische Variable
$@ bezeichnet sensor.o, während das Rezept für diese Objektdatei ausgeführt wird.B
Basisfall
1 zurück, sodass die wartenden Multiplikationen in früheren Aufrufen abgeschlossen werden können.bedingte Kompilierung
Bindung
static ihre Bindung auf eine Übersetzungseinheit beschränkt.Blockgültigkeitsbereich
if-Blocks deklarierte Variable kann nach dem Schließen dieses Blocks in einer Ausgabe nicht mehr benannt werden.D
Dateigültigkeitsbereich
defined-Operator
defined ergibt eins, wenn sein Makronamenoperand definiert ist, und null, wenn er nicht definiert ist.#if defined(DEBUG), um die Existenz auch dann zu prüfen, wenn DEBUG den Ersetzungstext 0 hat.Deklaration
main ermöglicht die Prüfung eines Aufrufs, selbst wenn der Funktionsrumpf später folgt.E
eigenständige Headerdatei
size_t deklariert, bindet <stddef.h> vor dieser Deklaration ein.extern
extern kann eine Entität mit Bindung deklarieren, ohne ein Objekt zu definieren, sofern kein Initialisierer die Deklaration zu einer Definition macht.extern int errors;, während eine Quelldatei die initialisierte Objektdefinition liefert.externe Bindung
F
Funktion
funktionsähnliches Makro
Funktionsdefinition
double scale(double factor) muss mit dem Prototyp übereinstimmen, der sowohl im Aufrufer als auch in der Implementierung eingebunden wird.Funktionsprototyp
double scale(double factor); weist den Compiler an, das Argument in double umzuwandeln.Funktionsprototyp-Gültigkeitsbereich
value nennen, während die Definition denselben Parameter n nennt.G
gemeinsam genutzter Zustand
generierte Abhängigkeitsdatei
-MMD kann sensor.d schreiben, wodurch eine spätere Änderung an einer Headerdatei das erneute Erstellen von sensor.o auslöst.globale Variable
main verfügbar bleibt.Gültigkeitsbereich
H
Headerdatei
I
Include-Guard
interne Bindung
check definieren, ohne dass beim Linken eine Kollision entsteht.K
keine Bindung
L
Linker
M
make-Variable
$(CFLAGS) expandiert wird.CC-Zuweisung ändert den Compilertext, den Rezepte mit einem Verweis auf $(CC) verwenden.Mehrfachdefinition
main definieren, verursachen einen Fehler wegen Mehrfachdefinition oder eines doppelten Symbols.Modul
Musterregel
%, um einen gemeinsamen Dateinamenstamm zu erkennen, sodass ein Abhängigkeitsmuster und ein Rezept viele Ziele erstellen können.%.o: %.c ordnet sensor.o der Datei sensor.c zu, indem sie sensor als gemeinsamen Stamm verwendet.N
nicht aufgelöster Verweis
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
O
objektähnliches Makro
#define LIMIT 16 ersetzt das Token LIMIT, lässt aber "LIMIT" unverändert.Objektdatei
sensor.c kann die Programmerstellung sensor.o ersetzen und ein unverändertes main.o wiederverwenden.P
Parameter
int cube(int n) erhält der Parameter n für jeden Aufruf einen eigenen Eingabewert.Präprozessor
gcc -E sample.c aus, um den expandierten Code zu untersuchen, bevor der Compiler C-Ausdrücke prüft.Präprozessordirektive
# und steuert die Umwandlung von Quelltext.#include "sensor.h" fügt Headertext in den aktuellen Präprozessierungslauf ein.Probelauf
make -n gibt gewöhnliche Erstellungsrezepte aus, ohne sie auszuführen, und zeigt so die geplanten Compiler- und Linkerbefehle.Pseudoziel
.PHONY deklarierte Aktion, sodass eine vorhandene Datei mit diesem Namen ihr Rezept nicht unterdrücken kann.clean als Pseudoziel stellt sicher, dass das Aufräumen auch ausgeführt wird, wenn im Verzeichnis eine Datei namens clean erscheint.R
Regel
main.o führt sowohl main.c als auch die Headerdateien des Projekts auf, die während seiner Kompilierung eingebunden werden.register
register kennzeichnet eine lokale Variable mit einem Optimierungshinweis, den Compiler ignorieren dürfen, und verbietet das Ermitteln ihrer Adresse.register zu einem Schleifenzähler zwingt einen modernen Compiler nicht, ein Prozessorregister zu verwenden.Rekursion
Rekursionsfall
Rezept
Rückgabewert
double result zugewiesen werden, ohne ihn auszugeben.S
Seiteneffekt
Speicherdauer
Stackframe
factorial(4) haben vier Fakultätsaufrufe getrennte aktive Stackframes.Standardziel
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
static deklariertes Objekt mit Blockgültigkeitsbereich, das einen gespeicherten Wert über wiederholte Aufrufe hinweg behält.statische Speicherdauer
Stringifizierung
# vor einem Makroparameter im Ersetzungstext, um die übergebenen Argument-Tokens in ein Zeichenkettenliteral umzuwandeln.3 + 4 stringifizieren, um den Ausdruck neben seinem berechneten Wert anzuzeigen.T
Token-Verkettung
## in einer Makro-Ersetzungsliste, um benachbarte Tokens zu einem einzelnen Präprozessortoken zu verbinden.temp und 2 verbindet, kann im expandierten Quelltext den Bezeichner temp2 bilden.Ü
Übersetzungseinheit
V
Verdeckung
total in einer Schleife kann den eigentlich gemeinten äußeren Akkumulator unverändert lassen.versteckte Abhängigkeit
void
void gibt als Rückgabetyp an, dass kein Wert zurückgegeben wird, oder allein in einer Parameterliste, dass es keine Parameter gibt.void-Funktion zur Ausgabe erledigt ihre Ausgabeaufgabe, kann aber keinen Wert für die rechte Seite einer Zuweisung liefern.vordefiniertes Makro
__FILE__ und __LINE__ kann angeben, woher ihre Meldung stammt.vorläufige Definition
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
n innerhalb einer Funktion lässt die gewöhnliche Ganzzahlvariable des Aufrufers unverändert.Z
Ziel
#
#error
#error verlangt eine Diagnose, wenn sie während der Präprozessierung erreicht wird, und ermöglicht so die Ablehnung einer nicht unterstützten Programmerstellung.#error auswählen, wenn der gewählte C-Standard älter ist, als das Programm benötigt.Über diese Einheit
Erstelle deine eigenen Lernmaterialien
Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.