Vorwort
In meinen
Kommentaren bezog
ich mich mehrmals auf Andrew Tanenbaums Buch Operating Systems Design and Implementation, seine
erste Ausgabe, und wie C darin dargestellt wird. Und diese Kommentare waren schon immer von Interesse. Ich entschied, dass es Zeit war, eine Übersetzung dieser Einführung in C zu veröffentlichen. Es ist immer noch relevant. Zwar gibt es sicherlich diejenigen, die noch nichts über die Programmiersprache
PL / 1 und vielleicht sogar über das Betriebssystem
Minix gehört haben .
Diese Beschreibung ist auch aus historischer Sicht interessant und um zu verstehen, wie weit die C-Sprache seit ihrer Geburt und die IT-Branche insgesamt gegangen ist.
Ich möchte sofort reservieren, dass meine zweite Sprache Französisch ist:

Dies wird jedoch durch 46 Jahre
Programmiererfahrung ausgeglichen.
Also, fangen wir an, Andrew Tanenbaum ist an der Reihe.
Einführung in die C-Sprache (S. 350 - 362)
Die Programmiersprache C wurde von Dennis Ritchie von AT & T Bell Laboratories als übergeordnete Programmiersprache für die Entwicklung des UNIX-Betriebssystems erstellt. Derzeit ist die Sprache in verschiedenen Bereichen weit verbreitet. C ist besonders bei Systemprogrammierern beliebt, da Sie damit Programme einfach und präzise schreiben können.
Das Hauptbuch, das die C-Sprache beschreibt, ist Brian Kernigan und Dennis Ritchies Buch The C Programming Language (1978). Bücher über die C-Sprache wurden von Bolon (1986), Gehani (1984), Hancock und Krieger (1986), Harbison und Steele (1984) und vielen anderen geschrieben.
In diesem Anhang werden wir versuchen, eine ziemlich vollständige Einführung in C zu geben, damit diejenigen, die mit Hochsprachen wie Pascal, PL / 1 oder Modula 2 vertraut sind, den größten Teil des in diesem Buch enthaltenen MINIX-Codes verstehen können. C-Funktionen, die in MINIX nicht verwendet werden, werden hier nicht behandelt. Zahlreiche subtile Punkte wurden weggelassen. Der Schwerpunkt liegt auf dem Lesen von C-Programmen und nicht auf dem Schreiben von Code.
A.1. C Sprachgrundlagen
Ein C-Programm besteht aus einer Reihe von Prozeduren (oft als Funktionen bezeichnet, auch wenn sie keine Werte zurückgeben). Diese Verfahren enthalten Deklarationen, Operatoren und andere Elemente, die dem Computer gemeinsam mitteilen, was zu tun ist. Abbildung A-1 zeigt eine kleine Prozedur, bei der drei ganzzahlige Variablen deklariert und Werte zugewiesen werden. Der Name der Prozedur ist main. Die Prozedur hat keine formalen Parameter, was durch das Fehlen von Bezeichnern zwischen den Klammern hinter dem Prozedurnamen angezeigt wird. Der Hauptteil der Prozedur ist in geschweiften Klammern ({}) eingeschlossen. Dieses Beispiel zeigt, dass C Variablen hat und dass diese Variablen vor der Verwendung deklariert werden müssen. C hat auch Operatoren, in diesem Beispiel sind dies Zuweisungsoperatoren. Alle Anweisungen müssen mit einem Semikolon enden (im Gegensatz zu Pascal, bei dem Doppelpunkte zwischen Anweisungen verwendet werden, nicht danach).
Kommentare beginnen mit den Zeichen "/ *" und enden mit den Zeichen "* /" und können mehrere Zeilen umfassen.
main () { int i, j, k; i = 10; j = i + 015; k = j * j + 0xFF; } . Al. .
Die Prozedur enthält drei Konstanten. Konstante 10 in der ersten Zuordnung
Es ist eine gewöhnliche Dezimalkonstante. Die 015-Konstante ist eine Oktalkonstante
(gleich 13 in Dezimalzahl). Oktalkonstanten beginnen immer bei Null. Die Konstante 0xFF ist eine hexadezimale Konstante (entspricht 255 Dezimalstellen). Hexadezimale Konstanten beginnen immer mit 0x. Alle drei Typen werden in C verwendet.
A.2. Grundlegende Datentypen
C hat zwei Haupttypen von Daten (Variablen): eine Ganzzahl und ein Zeichen, die als int bzw. char deklariert sind. Es gibt keine separate boolesche Variable. Die int-Variable wird als boolesche Variable verwendet. Wenn diese Variable 0 enthält, bedeutet dies falsch / falsch, und jeder andere Wert bedeutet wahr / wahr. C hat auch Gleitkommatypen, aber MINIX verwendet sie nicht.
Sie können kurze, lange oder vorzeichenlose „Adjektive“ auf einen int-Typ anwenden, der einen Wertebereich (vom Compiler abhängig) definiert. Die meisten 8088-Prozessoren verwenden 16-Bit-Ganzzahlen für int und short int und 32-Bit-Ganzzahlen für long int. Ganzzahlen ohne Vorzeichen (int ohne Vorzeichen) auf dem 8088-Prozessor haben einen Bereich von 0 bis 65535 und nicht von -32768 bis +32767, wie dies bei normalen Ganzzahlen (int) der Fall ist. Ein Zeichen benötigt 8 Bits.
Der Registerspezifizierer ist auch für int und char zulässig und ist ein Hinweis für den Compiler, dass die deklarierte Variable im Register abgelegt werden sollte, damit das Programm schneller arbeitet.
Einige Anzeigen sind in Abb. 1 dargestellt. A - 2.
int i; short int z1, z2; / * */ char c; unsigned short int k; long flag_poll; register int r; . -2. .
Die Konvertierung zwischen Typen ist zulässig. Zum Beispiel der Bediener
flag_pole = i;
erlaubt, auch wenn i vom Typ int ist und flag_pole lang ist. In vielen Fällen
Es ist notwendig oder nützlich, Konvertierungen zwischen Datentypen zu erzwingen. Für die erzwungene Konvertierung reicht es aus, den Zieltyp in Klammern vor dem Ausdruck für die Konvertierung zu setzen. Zum Beispiel:
( (long) i);
weist an, die Ganzzahl i in long zu konvertieren, bevor sie als Parameter an die Prozedur p übergeben wird, die den long-Parameter erwartet.
Achten Sie beim Konvertieren zwischen Typen auf das Zeichen.
Bei der Konvertierung eines Zeichens in eine Ganzzahl behandeln einige Compiler Zeichen als signiert, dh von - 128 bis +127, während andere sie als behandeln
vorzeichenlos, dh von 0 bis 255. In MINIX sind Ausdrücke wie
i = c & 0377;
Dies konvertiert von (Zeichen) in eine Ganzzahl und führt dann ein logisches UND aus
(kaufmännisches Und) mit der Oktalkonstante 0377. Das Ergebnis ist, dass die hohen 8 Bits
werden auf Null gesetzt, wodurch c gezwungen wird, als vorzeichenlose 8-Bit-Zahl im Bereich von 0 bis 255 betrachtet zu werden.
A.3. Verbindungstypen und Zeiger
In diesem Abschnitt werden vier Möglichkeiten zum Erstellen komplexerer Datentypen beschrieben: Arrays, Strukturen, Vereinigungen und Zeiger. Ein Array ist eine Sammlung / Menge von Elementen desselben Typs. Alle Arrays in C beginnen mit Element 0.
Ankündigung
int a [10];
deklariert ein Array a mit 10 Ganzzahlen, die in den Elementen des Arrays von [0] bis a [9] gespeichert werden sollen. Zweitens können Arrays drei oder mehr Dimensionen haben, werden jedoch in MINIX nicht verwendet.
Eine Struktur ist eine Sammlung von Variablen, normalerweise verschiedener Typen. Die Struktur in C ähnelt der Aufzeichnung in Pascal. Betreiber
struct {int i; char c;} s;
deklariert s als eine Struktur, die zwei Elemente enthält, die Ganzzahl i und das Zeichen c.
Um das Mitglied i der Struktur s 6 zuzuweisen, schreiben Sie den folgenden Ausdruck:
si = 6;
wobei der Punktoperator angibt, dass das Element i zur Struktur s gehört.
Eine Gewerkschaft ist auch eine Gruppe von Mitgliedern, ähnlich einer Struktur, mit der Ausnahme, dass zu jedem Zeitpunkt nur einer von ihnen einer Gewerkschaft angehören kann. Ankündigung
union {int i; char c;} u;
bedeutet, dass Sie eine Ganzzahl oder ein Zeichen haben können, aber nicht beide. Der Compiler sollte genügend Platz zum Kombinieren reservieren, damit er das größte (aus Sicht des belegten Speichers) Kombinationselement aufnehmen kann. Vereinigungen werden in MINIX nur an zwei Stellen verwendet (um eine Nachricht als Vereinigung mehrerer verschiedener Strukturen zu definieren und um einen Plattenblock als Vereinigung eines Datenblocks, eines i-Node-Blocks, eines Katalogblocks usw. zu definieren).
Zeiger werden verwendet, um Maschinenadressen in C zu speichern. Sie werden sehr, sehr oft verwendet. Ein Sternchen (*) kennzeichnet einen Zeiger in Anzeigen. Ankündigung
int i, *pi, a [10], *b[10], **ppi;
deklariert eine Ganzzahl i, einen Zeiger auf eine Ganzzahl pi, ein Array a mit 10 Elementen, ein Array b mit 10 Zeigern auf Ganzzahlen und einen Zeiger auf einen Zeiger ppi auf eine Ganzzahl.
Die genauen Syntaxregeln für komplexe Deklarationen, die Arrays, Zeiger und andere Typen kombinieren, sind etwas komplex. Glücklicherweise verwendet MINIX nur einfache Deklarationen.
Abbildung A-3 zeigt die Deklaration eines Arrays z von Strukturtabellenstrukturen, von denen jede hat
drei Mitglieder, Ganzzahl i, Zeiger cp auf Zeichen und Zeichen c.
struct table { int i; / * */ char *cp, c; } z [20]; . - 3. .
Arrays von Strukturen sind in MINIX üblich. Ferner kann die Namenstabelle als Strukturtabellenstruktur deklariert werden, die in nachfolgenden Deklarationen verwendet werden kann. Zum Beispiel
register struct table *p;
deklariert p als Zeiger auf eine Strukturtabellenstruktur und schlägt vor, diese zu speichern
im Register. Während der Programmausführung kann p beispielsweise z [4] oder anzeigen
zu jedem anderen Element in z, von dem alle 20 Elemente Strukturen vom Typ Strukturtabelle sind.
Um p zu einem Zeiger auf z [4] zu machen, schreiben Sie einfach
p = &z[4];
wobei das kaufmännische Und als unärer (monadischer) Operator bedeutet "nimm die Adresse dessen, was darauf folgt". Kopieren Sie den Wert von Element i in die Ganzzahlvariable n
Die Struktur, auf die p zeigt, kann wie folgt ausgeführt werden:
n = p->i;
Beachten Sie, dass der Pfeil verwendet wird, um über einen Zeiger auf ein Mitglied der Struktur zuzugreifen. Wenn wir die Variable z verwenden, müssen wir den Punktoperator verwenden:
n = z [4] .i;
Der Unterschied besteht darin, dass z [4] eine Struktur ist und der Punktoperator die Elemente auswählt
von zusammengesetzten Typen (Strukturen, Arrays) direkt. Mithilfe von Zeigern wählen wir keinen Teilnehmer direkt aus. Der Zeiger weist an, zuerst eine Struktur und erst dann ein Mitglied dieser Struktur auszuwählen.
Manchmal ist es praktisch, einem zusammengesetzten Typ einen Namen zu geben. Zum Beispiel:
typedef unsigned short int unshort;
definiert unshort als unsigned short (unsigned short integer). Jetzt kann unshort im Programm als Haupttyp verwendet werden. Zum Beispiel
unshort ul, *u2, u3[5];
deklariert eine kurze vorzeichenlose Ganzzahl, einen Zeiger auf eine kurze vorzeichenlose Ganzzahl und
ein Array von kurzen Ganzzahlen ohne Vorzeichen.
A.4. Betreiber
Prozeduren in C enthalten Erklärungen und Anweisungen. Wir haben die Erklärungen bereits gesehen, daher werden wir jetzt die Operatoren betrachten. Der Zweck der Bedingungs- und Schleifenoperatoren ist im Wesentlichen der gleiche wie in anderen Sprachen. Abbildung A - 4 zeigt einige Beispiele dafür. Das einzige, worauf Sie achten sollten, ist, dass geschweifte Klammern verwendet werden, um Operatoren zu gruppieren, und die while-Anweisung hat zwei Formen, von denen die zweite der Wiederholungsanweisung von Pascal ähnlich ist.
C hat auch eine for-Anweisung, sieht aber in keiner anderen Sprache wie eine for-Anweisung aus. Die for-Anweisung hat die folgende Form:
for (<>; <>; <>) ;
Dasselbe kann durch die while-Anweisung ausgedrückt werden:
<> while(<>) { <>; <> }
Betrachten Sie als Beispiel die folgende Aussage:
for (i=0; i <n; i = i+l) a[i]=0;
Dieser Operator setzt die ersten n Elemente des Arrays a auf Null. Die Ausführung des Operators beginnt mit dem Setzen von i auf Null (dies erfolgt außerhalb der Schleife). Dann wird der Operator wiederholt, bis i <n ist, während die Zuweisung und Erhöhung von i durchgeführt wird. Anstelle des Operators, dem aktuellen Element eines Null-Arrays einen Wert zuzuweisen, kann natürlich ein zusammengesetzter Operator (Block) in geschweiften Klammern stehen.
if (x < 0) k = 3; if (x > y) { i = 2; k = j + l, } if (x + 2 <y) { j = 2; k = j - 1; } else { m = 0; } while (n > 0) { k = k + k; n = n - l; } do { / * while */ k = k + k; n = n - 1; } while (n > 0); . A-4. if while C.
C hat auch einen Operator ähnlich dem Falloperator in Pascal. Dies ist eine switch-Anweisung. Ein Beispiel ist in Abbildung A-5 dargestellt. Abhängig vom Wert des in switch angegebenen Ausdrucks wird die eine oder andere case-Anweisung ausgewählt.
Wenn der Ausdruck keiner der case-Anweisungen entspricht, wird die Standardanweisung ausgewählt.
Wenn der Ausdruck keiner case-Anweisung zugeordnet ist und die Standardanweisung fehlt, wird die Ausführung ab der nächsten Anweisung nach der switch-Anweisung fortgesetzt.
Es ist zu beachten, dass Sie die break-Anweisung verwenden, um den case-Block zu verlassen. Wenn keine break-Anweisung vorhanden ist, wird der nächste case-Block ausgeführt.
switch (k) { case 10: i = 6; break; case 20: i = 2; k = 4; break; / * default* / default: j = 5; } . A-5. switch
Die break-Anweisung wirkt auch innerhalb der for- und while-Schleifen. Es ist zu beachten, dass die Ausgabe nur eine Ebene höher ist, wenn sich die break-Anweisung in einer Reihe verschachtelter Schleifen befindet.
Eine verwandte Anweisung ist die continue-Anweisung, die die Schleife nicht verlässt.
bewirkt jedoch den Abschluss der aktuellen Iteration und den Beginn der nächsten Iteration
sofort. Dies ist im Wesentlichen eine Rückkehr zum Anfang der Schleife.
C verfügt über Prozeduren, die mit oder ohne Parameter aufgerufen werden können.
Laut Kernigan und Ritchie (S. 121) ist es nicht gestattet, Arrays zu übertragen.
Strukturen oder Prozeduren als Parameter, obwohl Zeiger auf all dies übergeben werden
erlaubt. Gibt es ein Buch oder nicht (es wird in meinem Gedächtnis auftauchen: - „Wenn es Leben auf dem Mars gibt, wenn es kein Leben auf dem Mars gibt“), erlauben viele C-Compiler Strukturen als Parameter.
Der Name des Arrays bedeutet, wenn er ohne Index geschrieben ist, einen Zeiger auf ein Array, was die Übertragung eines Array-Zeigers vereinfacht. Wenn also a der Name eines Arrays eines beliebigen Typs ist, kann es durch Schreiben an g übergeben werden
g();
Diese Regel gilt nur für Arrays, diese Regel gilt nicht für Strukturen.
Prozeduren können Werte zurückgeben, indem sie eine return-Anweisung ausführen. Diese Anweisung kann einen Ausdruck enthalten, dessen Ergebnis als Wert der Prozedur zurückgegeben wird, aber der Aufrufer kann den Rückgabewert sicher ignorieren. Wenn die Prozedur einen Wert zurückgibt, wird der Typwert vor den Prozedurnamen geschrieben, wie in Abb. A-6. Prozeduren können wie Parameter keine Arrays, Strukturen oder Prozeduren zurückgeben, sondern Zeiger darauf zurückgeben. Diese Regel ist für eine effizientere Implementierung ausgelegt - alle Parameter und Ergebnisse entsprechen immer einem Maschinenwort (in dem die Adresse gespeichert ist). Compiler, die die Verwendung von Strukturen als Parameter ermöglichen, ermöglichen normalerweise auch deren Verwendung als Rückgabewerte.
int sum (i, j) int i, j ; { return (i + j); } . -6. , .
C hat keine eingebauten E / A. Die Eingabe / Ausgabe wird durch Aufrufen von Bibliotheksfunktionen implementiert, von denen die häufigsten unten dargestellt sind:
printf («x=% dy = %oz = %x \n», x, y, z);
Der erste Parameter ist die Zeichenfolge zwischen Anführungszeichen (tatsächlich ist dies ein Array von Zeichen).
Jedes Zeichen, das kein Prozentsatz ist, wird einfach so gedruckt, wie es ist.
Wenn ein Prozentsatz auftritt, wird der folgende Parameter in der Form gedruckt, die durch den Buchstaben nach dem Prozentsatz definiert ist:
d - Drucken als Dezimalzahl
o - als oktale Ganzzahl drucken
u - Drucken als vorzeichenlose Dezimalzahl
x - als hexadezimale Ganzzahl drucken
s - als Zeichenfolge drucken
c - als ein Zeichen drucken
Die Buchstaben D, 0 und X sind auch für den Dezimal-, Oktal- und Hexadezimaldruck langer Zahlen zulässig.
A.5. Ausdrücke
Ausdrücke werden durch Kombinieren von Operanden und Operatoren erstellt.
Arithmetische Operatoren wie + und - und Vergleichsoperatoren wie <
und> ähnlich wie ihre Gegenstücke in anderen Sprachen. % Operator
Modulo verwendet. Es ist erwähnenswert, dass der Gleichheitsoperator == ist und der Ungleichheitsoperator! =. Um zu überprüfen, ob a und b gleich sind, können Sie wie folgt schreiben:
if (a == b) <>;
Mit C können Sie den Zuweisungsoperator daher auch mit anderen Operatoren kombinieren
a += 4;
entspricht der Aufnahme
= + 4;
Auf diese Weise können auch andere Operatoren kombiniert werden.
C hat Operatoren zum Bearbeiten von Wortbits. Sowohl Verschiebungen als auch bitweise logische Operationen sind zulässig. Die linken und rechten Schichtoperatoren sind <<
bzw. >>. Bitweise logische Operatoren &, | und ^, die logisches UND (UND) sind, einschließlich ODER (ODER) bzw. exklusives ODER (XOP). Wenn i den Wert 035 (oktal) hat, hat der Ausdruck i & 06 den Wert 04 (oktal). Ein anderes Beispiel, wenn i = 7, dann
j = (i << 3) | 014;
und bekomme 074 für j.
Eine weitere wichtige Gruppe von Operatoren sind unäre Operatoren, von denen jeder nur einen Operanden akzeptiert. Als unärer Operator erhält kaufmännisches Und & die Adresse einer Variablen.
Wenn p ein Zeiger auf eine ganze Zahl ist und i eine ganze Zahl ist, der Operator
p = &i;
berechnet die Adresse i und speichert sie in der Variablen p.
Das Gegenteil von einer Adresse ist ein Operator, der einen Zeiger als Eingabe verwendet und den Wert an dieser Adresse berechnet. Wenn wir dem Zeiger p gerade die Adresse i zugewiesen haben, hat * p die gleiche Bedeutung wie i.
Mit anderen Worten, als unärer Operator folgt auf ein Sternchen ein Zeiger (oder
Ausdruck mit einem Zeiger) und gibt den Wert des Elements zurück, auf das es zeigt. Wenn i einen Wert von 6 hat, dann der Operator
j = *;
wird j die Nummer 6 zuweisen.
Der Betreiber! (das Ausrufezeichen ist der Negationsoperator) gibt 0 zurück, wenn sein Operand ungleich Null ist, und 1, wenn sein Operator 0 ist.
Es wird zum Beispiel hauptsächlich in if-Anweisungen verwendet
if (!x) k=0;
prüft den Wert von x. Wenn x Null (falsch) ist, wird k der Wert 0 zugewiesen. Eigentlich ist der Operator! bricht die darauf folgende Bedingung ab, genau wie der Operator not in Pascal.
Der Operator ~ ist ein bitweiser Komplementoperator. Jede 0 in ihrem Operanden
wird 1 und jede 1 wird 0.
Der Operator sizeof gibt die Größe seines Operanden in Bytes an. In Bezug auf
Ein Array von 20 Ganzzahlen a auf einem Computer mit 2-Byte-Ganzzahlen, z. B. Größe von a, hat einen Wert von 40.
Die letzte Gruppe von Operatoren sind die Operatoren für Zunahme und Abnahme.
Betreiber
++;
bedeutet eine Erhöhung von p. Wie viel p zunimmt, hängt von seinem Typ ab.
Ganzzahlen oder Zeichen werden um 1 erhöht, Zeiger jedoch um 1
die Größe des Objekts, auf das auf diese Weise gezeigt wird, wenn a ein Array von Strukturen ist und p ein Zeiger auf eine dieser Strukturen ist, und wir schreiben
p = &a[3];
um p auf eine der Strukturen im Array zu zeigen, dann nach dem Erhöhen von p
zeigt auf a [4], egal wie groß die Strukturen sind. Betreiber
p--;
ähnlich dem p ++ - Operator, außer dass der Wert des Operanden eher verringert als erhöht wird.
In Aussage
n = k++;
Wenn beide Variablen ganze Zahlen sind, wird der ursprüngliche Wert von k n und zugewiesen
erst dann steigt k an. In Aussage
n = ++ k;
k steigt zuerst an, dann wird sein neuer Wert in n gespeichert.
Somit kann ein ++ (oder -) Operator vor oder nach seinem Operanden geschrieben werden, was zu verschiedenen Werten führt.
Die letzte Aussage ist das? (Fragezeichen), das eine von zwei Alternativen auswählt
durch einen Doppelpunkt getrennt. Zum Beispiel ein Operator,
i = (x < y ? 6 : k + 1);
vergleicht x mit y. Wenn x kleiner als y ist, bekomme ich den Wert 6; Andernfalls erhält die Variable i den Wert k + 1. Die Klammern sind optional.
A.6. Programmstruktur
Ein C-Programm besteht aus einer oder mehreren Dateien, die Prozeduren und Deklarationen enthalten.
Diese Dateien können einzeln zu Objektdateien kompiliert werden, die dann (mithilfe des Linkers) miteinander verknüpft werden, um ein ausführbares Programm zu bilden.
Im Gegensatz zu Pascal können Prozedurdeklarationen nicht verschachtelt werden, daher werden alle auf der „obersten Ebene“ in der Programmdatei geschrieben.
Es ist zulässig, Variablen außerhalb der Prozeduren zu deklarieren, z. B. am Anfang der Datei vor der ersten Deklaration der Prozedur. Diese Variablen sind global und können in jeder Prozedur im gesamten Programm verwendet werden, es sei denn, das statische Schlüsselwort steht vor der Deklaration. In diesem Fall können diese Variablen nicht in einer anderen Datei verwendet werden. Die gleichen Regeln gelten für Verfahren. Innerhalb einer Prozedur deklarierte Variablen sind lokal für die Prozedur.
Die Prozedur kann auf die in einer anderen Datei deklarierte Ganzzahlvariable v zugreifen (vorausgesetzt, die Variable ist nicht statisch) und sie als extern deklarieren:
extern int v;
Jede globale Variable muss genau einmal ohne das Attribut extern deklariert werden, um Speicher dafür zuzuweisen.
Variablen können initialisiert werden, wenn sie deklariert werden:
int size = 100;
Arrays und Strukturen können ebenfalls initialisiert werden. Globale Variablen, die nicht explizit initialisiert werden, erhalten den Standardwert Null.
A.7. C Präprozessor
Bevor die Quelldatei an den C-Compiler übertragen wird, wird sie automatisch verarbeitet
ein Programm namens Präprozessor. Es ist die Ausgabe des Präprozessors, nicht
Das ursprüngliche Programm wird dem Eingang des Compilers zugeführt. Präprozessor führt aus
Drei grundlegende Konvertierungen in einer Datei, bevor sie an den Compiler übergeben werden:
1. Aufnahme von Dateien.
2. Definition und Ersetzung von Makros.
3. Bedingte Kompilierung.
Alle Präprozessoranweisungen beginnen mit einem Nummernzeichen (#) in der 1. Spalte.
Bei einer Ansichtsrichtlinie
#include "prog.h"
Vom Präprozessor erfüllt, enthält es die Datei prog.h Zeile für Zeile in
das Programm, das an den Compiler übergeben werden soll. Wenn die Direktive #include als geschrieben ist
#include <prog.h>
Anschließend wird die enthaltene Datei im Verzeichnis / usr / include anstelle des Arbeitsverzeichnisses durchsucht. In C ist es üblich, die von mehreren Dateien verwendeten Deklarationen in einer Header-Datei (normalerweise mit dem Suffix .h) zu gruppieren und gegebenenfalls einzuschließen.
Der Präprozessor erlaubt auch Makrodefinitionen. Zum Beispiel
#define BLOCK_SIZE 1024
definiert das BLOCK_SIZE-Makro und weist ihm den Wert 1024 zu. Von nun an wirdjedes Vorkommen einer Zeichenfolge mit 10 Zeichen "BLOCK_SIZE" in der Dateidurch eine 4- stellige Zeichenfolge "1024" ersetzt, bevor der Compiler die Datei mit dem Programm sieht. Konventionell werden Makronamen in Großbuchstaben geschrieben. Makros können Parameter haben, in der Praxis jedoch nur wenige.Das dritte Merkmal des Präprozessors ist die bedingte Kompilierung. MINIX verfügt über mehrereStellen, an denen Code speziell für den 8088-Prozessor geschrieben wurde. Dieser Code sollte beim Kompilieren für einen anderen Prozessor nicht berücksichtigt werden. Diese Abschnitte sehen folgendermaßen aus: #ifdef i8088 < 8088> #endif
Wenn das Zeichen i8088 definiert ist, sind die Anweisungen zwischen den beiden Präprozessoranweisungen #ifdef i8088 und #endif in der Präprozessorausgabe enthalten. Andernfalls werden sie übersprungen. Aufruf des Compilers mit dem Befehl cc -c -Di8088 prog.c
oder durch Aufnahme einer Anweisung in das Programm #define i8088
Wir definieren das Symbol i8088, damit alle abhängigen Codes für 8088 enthalten sind. Während der Entwicklung von MINIX wird möglicherweise spezieller Code für 68000er und andere Prozessoren erworben, die ebenfalls verarbeitet werden.Betrachten Sie als Beispiel für die Funktionsweise des Präprozessors das Programm Abb. A-7 (a). Es enthält eine prog.h-Datei, deren Inhalt wie folgt lautet: int x; #define MAXAELEMENTS 100
Stellen Sie sich vor, der Compiler wurde von einem Befehl aufgerufen cc -E -Di8088 prog.c
Nachdem die Datei den Präprozessor durchlaufen hat, erfolgt die Ausgabe wie in Abb. A-7 (b).Es ist diese Ausgabe und nicht die Quelldatei, die als Eingabe an den C-Compiler übergeben wird.
Beachten Sie, dass der Präprozessor seine Arbeit erledigt und alle Zeilen gelöscht hat, die mit dem # -Zeichen beginnen. Wenn der Compiler so aufgerufen würde cc -c -Dm68000 prog.c
dann wäre ein weiterer Druck enthalten. Wenn es so heißen würde: cc -c prog.c
dann wäre kein Druck enthalten. (Der Leser kann darüber nachdenken, was passieren würde, wenn der Compiler mit beiden Flags -D-Flags aufgerufen würde.)A.8. Redewendungen
In diesem Abschnitt werden einige Konstrukte betrachtet, die typisch für C sind, aber in anderen Programmiersprachen nicht üblich sind. Betrachten Sie zunächst die Schleife: while (n--) *p++ = *q++;
Die Variablen p und q sind normalerweise Zeichenzeiger, und n ist ein Zähler. Die Schleife kopiert die n-Zeichenfolge von wo q zeigt auf wo p zeigt. Bei jeder Iteration der Schleife nimmt der Zähler ab, bis er 0 erreicht, und jeder der Zeiger nimmt zu, so dass sie nacheinander auf Speicherzellen mit einer höheren Zahl zeigen.Ein weiteres gängiges Design: for (i = 0; i < N; i++) a[i] = 0;
Dies setzt die ersten N Elemente von a auf 0. Eine alternative Möglichkeit, diese Schleife zu schreiben, ist wie folgt: for (p = &a[0]; p < &a[N]; p++) *p = 0;
In dieser Anweisung wird der ganzzahlige Zeiger p so initialisiert, dass er auf das Nullelement des Arrays zeigt. Die Schleife wird fortgesetzt, bis p die Adresse des N-ten Elements des Arrays erreicht. Ein Zeigerkonstrukt ist viel effizienter als ein Arraykonstrukt und wird daher normalerweise verwendet.Zuweisungsoperatoren können an unerwarteten Stellen angezeigt werden. Zum Beispiel
if (a = f (x)) < >;
ruft zuerst die Funktion f auf, weist dann das Ergebnis des Aufrufs der Funktion a zu undprüft schließlich, ob es wahr (nicht Null) oder falsch (Null) ist. Wenn a ungleich Null ist, ist die Bedingung erfüllt. Betreiber
if (a = b) < >;
Außerdem wird zuerst der Wert der Variablen b der Variablen a überprüft und dann a geprüft, ob der Wert ungleich Null ist. Und dieser Operator ist völlig anders als if (a == b) < >;
Dadurch werden zwei Variablen verglichen und der Operator ausgeführt, wenn sie gleich sind.Nachwort
Das ist alles. Sie werden nicht glauben, wie sehr ich es genossen habe, diesen Text vorzubereiten. Wie viel ich mich aus derselben C-Sprache als nützlich erinnerte. Ich hoffe, auch Sie werden es genießen, in die wunderbare Welt der C-Sprache einzutauchen.