September 2014

Rezension zum Buch 'C von A bis Z'  3. Aufl., Autor Jürgen Wolf, Galileo-Verlag

Sehr umfangreich und informativ, aber auch widersprüchlich, lückenhaft, mißverständlich und holprig.

Der Titel des Buches C von A bis Z ist durchaus
zutreffend, denn das Buch hat knapp 1200 Seiten, womit es
in die Nähe der Grenze zu einem zweibändigen Werk kommt.
Der Inhalt des Buches zeigt und erklärt C fast vollständig.
Neben der Abhandlung von 'C an sich' enthält das Buch
sehr viele weitere mit C verknüpfte Themen, die generell
nicht in einem C-Buch beschrieben werden müssen, damit es
ein C-Buch ist.
Das Buch soll auch für Programmieranfänger geeignet sein.
Die Themen werden weit überwiegend jeweils umfangreich
und aufwendig mit Beispielcode und oft auch mit grafischen
Darstellungen und Tabellen beschrieben.
Die Standard-Bibliothek wird umfangreich beschrieben.
Beispielcode und Beschreibungen orientieren sich am
Standard C99, insoweit sich C99 von C89 unterscheidet, was
allerdings nicht oft vorkommt.
Viele Fehler in Beispielcode, die in vielen anderen C-Büchern
vorhanden sind, sind in diesem Buch _nicht_ enthalten.
Der Beispielcode ist syntaktisch korrekt und weit
überwiegend auch mit erwarteter Funktion.
Es ist sehr wahrscheinlich, mit Hilfe dieses Buches
erfolgreich C lernen, C-Programme schreiben, kompilieren
und starten zu können.
Das Buch ist nützlich, allein wegen der schieren Größe, die
sehr viele Informationen bereithält, die überwiegend
korrekt sind.
Es werden durchaus (etwas überraschend) Feinheiten von C
erklärt und beispielsweise 'Definition' und 'Deklaration'
recht zutreffend beschrieben (siehe ganz unten).
Andererseits sind auch unübersehbar Fehler und heftige
Schwächen vielerorts im Buch enthalten.

Das Buch ist im vorderen Teil, der C an sich erklärt, schlechter
(fehlerhafter) als im hinteren Teil.
Das Buch erweckt den Eindruck, als ob zwei oder mehr Buchteile
(textlich) ein unterschiedliches Kreationsalter haben;
das Buch ist nicht aus einem Guß.
Es ist (indirekt) erkennbar, daß der Autor Sachverhalte korrekt
kennt und versteht, er beschreibt sie jedoch oft nicht klar
und korrekt, sondern seltsam, verwirrend, mißverständlich,
umständlich, unvollständig oder schlicht unzutreffend.
Das Buch ist u.a. deshalb vielerorts holprig zu lesen.
Daß der Autor einen angenehmen Schreibstil haben soll, können
wahrscheinlich nur Anfänger so empfinden.
Der Autor hat Formulierungsprobleme, wenn eine trockene,
schnörkellose, sehr exakte Datenblattsprache angebracht ist.
C besonders schnell zu lernen, ist mit dem Buch nicht möglich.
Dazu ist es zu groß, in Verbindung mit starker Verteilung
und Wiederholung der Themen.
Themen sind oft mehrmals beschrieben, dabei unterschiedlich
detailliert und/oder einmal korrekt, das andere Mal falsch
oder unklar.
Die Didaktik ist sicherlich verbesserungswürdig.
Diese Rezension führt über hundert Fehler und Kritikpunkte auf.
Das sind trotz Berücksichtigung der fast 1200 Seiten recht viel.
Dennoch ist das Buch deswegen nicht unbrauchbar, da es sich
überwiegend nicht um Fehlleistungen mit katastrophaler
Schadenwirkung handelt.



Zitat:
Text des Zitats
Leerzeile
Antwort zum Zitat


Zitat:
Der Präprozessor ist ein Teil des Compilers, der nicht das
Programm übersetzt, sondern kontrollierend nicht bleibende
Änderungen im Programmtext vornimmt. Diese Änderungen sind
jedoch nur temporär.

"Der Präprozessor liest den Programmtext und übergibt dem
(eigentlichen) Compiler eine veränderte Variante davon."
wäre eine bessere Formulierung.


Zitat:
Eine main-Funktion wird immer benötigt, damit der Compiler
weiß, wo er beginnen muss, das Programm zu übersetzen.

Das ist falsch und hochgradig seltsam.
Der Compiler übersetzt alle Übersetzungseinheiten (Dateien.c)
pauschal jeweils vom Beginn bis zum Ende, ohne irgendwelche
Vorschriften zur Reihenfolge der Übersetzungseinheiten
oder besondere Reihenfolgen innerhalb der Übersetzungseinheiten.


Zitat:
int main(void)
void könnten Sie hier auch ganz weglassen

Kann man, sollte man jedoch nicht. Warum auch?


Zitat:
Dabei ist void eigentlich auch nicht ganz leer. Wenn Sie
mit void den sizeof-Operator verwenden, erfahren Sie, dass
void ein Byte an Speicher benötigt.

sizeof(void) ist eine ungültige Anwendung von sizeof.
Durch cc -pedantic ... erfährt man das.


Zitat:
Mit einer Deklaration machen Sie den Compiler mit einem
Namen (Bezeichner) bekannt und verknüpfen diesen Namen
mit einem Typ. Der Typ wiederum beinhaltet die
Informationen über die Art der Bezeichner und

"Art der Bezeichner" ist falsch -> Speicherobjekt.


Zitat:
return innerhalb von main():
Es wird also mit return hier der Funktion main der Wert 0
zurückgegeben. Genaueres dazu erfahren Sie in einem

Es ist eher umgekehrt.


Zitat:
Die Funktion printf() wird bei der Ausführung von rechts
nach links abgearbeitet. Dabei sucht die Funktion nach
einem Ausgabetext (Stringkonstante) und Formatanweisungen.

Diese Formulierung ist hochgradig seltsam und konfus.


Zitat:
Sicherlich ist Ihnen %d im Formatstring aufgefallen.
Das Prozentzeichen ist ein Formatzeichen. Der Compiler
sieht jetzt nach, um welches Format es sich dabei
handelt. Im Beispiel ist es das d (für dezimal).

Das Nachschauen ist ein freiwilliger Service mancher
Compiler. Der Standard verlangt hier keine Diagnose.


Zitat:
printf("Wert von int a=%d ,b=%d, c=%d\n", a, b, c);
Hinter dem Ende der Hochkommata befindet sich jetzt
der Variablenname a. Damit wird der Wert von int a
an der Position ausgegeben, an der sich das
Formatzeichen %d befindet. Gleiches geschieht mit den

Das ist eine seltsame und umständliche Formulierung.
"Nach dem Format-String folgen die Wert-Argumente,
die hinsichtlich Anzahl und Reihenfolge mit den
Formatspezifizierern übereinstimmen müssen."
ist wohl eine bessere Formulierung.


Zitat:
BCD steht für Binary Coded Decimals und bedeutet, dass die
Zahlen nicht binär, sondern als Zeichen gespeichert werden.

"nicht binär"? "Binary Coded" heißt doch binär kodiert.
BCD speichert 0000-1001 (0-9) in 4 Bit (ein Nibble).


Zitat:
signed ist bei allen Datentypen voreingestellt.

Ein char ist nicht signed char per Voreinstellung!
Eine Seite später in einer Tabelle ist das korrekt
dargestellt. Im nachfolgenden Text erneut falsch.
Etwa 30 Seiten später ist es wieder richtig erklärt.
Das sind vier Beschreibungsorte.


Zitat:
Wenn das Schlüsselwort unsigned vor einen Datentyp
gestellt wird, sind keine negativen Zahlen mehr möglich.
Somit verdoppelt sich aber der positive Wert der Zahl.

Das stimmt nicht ganz: +127 -> +255


Zitat:
Sollten Sie z.B. zwei Zahlen subtrahieren, und es kommt
ein negatives Ergebnis heraus, und es wurde vor dem Datentyp
des Ergebnisses das Schlüsselwort unsigned gesetzt, so wird
das Minus der negativen Zahl ignoriert, und es wird eine
positive Zahl daraus.

Das ist eine der Formulierungen, die richtig weh tun.


Zitat:
Die dezimale Konstante kann durch folgende Datentypen
dargestellt werden:
int, unsigned long, long, long long

Das ist falsch. Alle Ganzzahlkonstanten können sechs
verschiedene Typen haben. Konstanten können nicht
durch Typen _dargestellt_ werden, sie _haben_ einen Typ.
Die Darstellung bezieht sich auf 123, 0123, 0x123.
Oktale und hexadezimale Konstanten sollen fünf Typen
haben können. Hier fehlt ein Typ.
Die dem falschen Text folgende Tabelle 5.12 auf der
gleichen Seite zeigt den Sachverhalt korrekt.


Zitat:
scanf("%[0-9]", str);
In diesem Beispiel werden Sie aufgefordert, nur Zahlen
einzugeben. scanf() liest so lange Zahlen ein, bis das
erste Zeichen nicht zwischen 0 bis 9 ist.

Das ist eine schwer verdauliche Formulierung.
"scanf() liest so lange Ziffern [0123456789] ein, bis
ein Zeichen nicht eine Ziffer 0 bis 9 ist."
ist wohl besser.


Zitat:
Bitte beachten Sie, dass bei einer Division und bei der
Verwendung des Modulo-Operators keine 0 vorkommen darf.

Was darf nicht 0 sein: Dividend, Divisor, Quotient?
Der rechte Operand (Divisor) darf nicht Null sein.


Zitat:
Erweiterte Darstellung arithmetischer Operatoren:
Die arithmetischen Operatoren, die im vorangegangenen
Abschnitt verwendet wurden, lassen sich auch noch
in anderer Form darstellen, und zwar in einer
kürzeren Schreibweise: a+=b ist gleichwertig zu a=a+b
Es wird dabei auch von einem Zuweisungsoperator gesprochen.

Vorstehend ist erneut eine befremdliche Formulierung zu lesen.
"Mit Hilfe von kombinierten Zuweisungsoperatoren
können arithmetische Ausdrücke verkürzt dargestellt werden."
ist eine kürzere und klarere Formulierung.


Zitat:
printf("A : %d Bytes\n", sizeof((char)'A'));
Zum Beispiel: 'A' benötigt ein Byte Speicherplatz, da
'A' vom Typ char ist.

Nein, 'A' ist vom Typ int und benötigt mindestens
zwei Byte Speicherplatz.


Zitat:
Dies geschieht, wenn Sie z.B. einem int-Wert
einen float-Wert zuweisen.

Werte können nicht einander zugewiesen werden.


Zitat:
implizite Umwandlungen von float nach double.
Auch hierbei wird bei Berechnungen ein float automatisch
in ein double konvertiert. Daher erfolgen alle Berechnungen
immer mit derselben Genauigkeit.

Das stimmt nicht. Bei Funktionsargumenten (...) und bei
Nachbarschaft eines double gibt es eine Erweiterung
von float nach double, sonst nicht.
Die intel-FPU kann intern _nur_ mit 80 Bit rechnen.


Zitat:
Die automatische Typumwandlung (implizit) funktioniert
nicht bei den Zuweisungsoperatoren und den logischen
Operatoren && und ||.

Das ist ein bemerkenswertes Zitat. Direkt davor wurden die
impliziten Typumwandlungen bei Zuweisungen beschrieben.


Zitat:
Die Zeile while(1) verkörpert die Endlosschleife.
Der Inhalt dieser Schleife ist immer wahr, da Sie hier
den Wert 1 haben.

Die Bedingungsprüfung ergibt stets 1, da die Bedingung
aus einer Konstante ungleich 0 besteht.


Zitat:
for(x = 0; y <= 75; y = (++x*5) + 50)

Anfänger könnten denken, daß ++x*5 (und ähnlich)
stets geklammert werden muß.


Zitat:
for(y=2; x<20;) {
Sie sehen hier, dass nicht unbedingt alle Variablen
einer for-Schleife deklariert werden müssen.
Lediglich die beiden Semikolons müssen immer
in der for-Schleife stehen.

Die vorstehende Formulierung nimmt einem die Luft weg.
Das ist katastrophaler Unsinn!


Zitat:
8.11.1 continue
Die continue-Anweisung beendet nur die aktuelle
Schleifenausführung. Das bedeutet, dass ab dem Aufruf
von continue im Anweisungsblock der Schleife alle
anderen Anweisungen übersprungen werden und die
Programmausführung zur Schleife mit der nächsten
Ausführung zurückspringt:

Wieder eine Formulierung mit mehreren Stolpersteinen.
"Durch continue wird an das Ende des Schleifenkörpers
gesprungen, quasi in die Endklammer } hinein."
ist viel kürzer und viel klarer.


Zitat:
Globale Variablen können Sie sich als Vorwärtsdeklarationen
von Funktionen vorstellen. Und wie der Name schon sagt, sind
globale Variablen für alle Funktionen gültig.

Die vorstehende Formulierung greift mehrfach daneben.
"Globale Objekte (externe Bindung oder datei-lokal) sind in
einer Datei ab ihrer Deklaration sichtbar und zugreifbar."


Zitat:
Aber Achtung: Statische Variablen müssen schon
bei ihrer Deklaration initialisiert werden!

Nein, sie werden implizit (0) oder explizit einmal
beim Programmstart initialisiert.


Zitat:
9.9.4 static
Das Schlüsselwort static wird vor immer währenden Variablen
mit einem beschränkten Geltungsbereich gesetzt.

Das ist eine seltsame und unbefriedigende Formulierung.


Zitat:
} while(reg & (STATUS_A|STATUS_B) == 0);
Manche Compiler erkennen jetzt an der while-Schleife, dass
hier immer die gleiche Adresse überprüft wird, und optimieren
die do while-Schleife einfach weg.

Adresse?! Klammerung fehlt ->
} while( (reg & (STATUS_A|STATUS_B)) == 0);


Zitat:
Mit volatile verhindern Sie (analog zu Variablen), dass
der Compiler den Quellcode optimiert und die Funktion
immer wieder neu aus dem Hauptspeicher gelesen werden muss.

Man muß es bezweifeln, daß volatile auf Code-Objekte wirkt.
Selbstmodifizierender Code ist nicht vorgesehen.


Zitat:
case 2 : halbieren(z);
Im gezeigten Beispiel sieht der Funktionsaufruf
mit Argument folgendermaßen aus: halbieren(zahl);


Zitat:
Neben call-by-value existiert auch call-by-reference, womit
statt einem Wert eine Adresse kopiert wird.

Eine Adresse ist auch ein Wert, der kopiert wird.
Deshalb call-by-value. call-by-reference gibt es in C nicht.


Zitat:
int bignum(int a, int b) {
big = bignum(wert1, wert2);
Damit wird der Variablen big, die zwingend
vom Datentyp int sein muss, der

Nein, big darf beispw. auch long oder long long sein.


Zitat:
Gemäß dem ANSI-C-Standard muss mindestens eine Funktion
in einem Programm den Namen main() besitzen.

Mehr als eine main() darf und kann es nicht geben.
Der Standard nennt hier keine Anzahl.


Zitat:
Shellskript
if [ ret -eq 0 ]

if [ $ret -eq 0 ]
Wertzugriff per $Dollar wird oft notwendig sein.


Zitat:
Wichtig ist in diesem Zusammenhang die Bedeutung des
Begriffs »Startup-Code«. Der Startup-Code wird zu Beginn
des Prozesses erzeugt (meist in Assembler) und dient
zum Beenden eines Prozesses.

Das ist eine falsche und total konfuse Formulierung.
Beim Start eines Prozesses wird doch nicht
der Startup-Code in Assembler erzeugt!
Der Startup-Code dient zum Starten(!) und Beenden des Prozesses.
Der Startup-Code wird fertig kompiliert mit dem Compiler
zusammen geliefert und wird nach kompilieren des (später)
ausführbaren Programmes durch den Linker mit eingebunden.


Zitat:
Kurz gesagt ist eine Rekursion eine Funktion, die sich
selbst aufruft und sich selbst immer wieder neu definiert.
Damit sich aber eine Rekursion nicht unendlich oft selbst
aufruft, sondern irgendwann

Eine Rekursion liegt vor, wenn eine Funktion sich selbst
direkt oder indirekt (über andere Funktionen) aufruft.
Rekursive Funktionen definieren sich nicht selbst
immer wieder neu. Sie werden vom Programmierer definiert.
Eine Rekursion kann sich nicht selbst aufrufen.
In C sind solche Formulierungen nicht angebracht.


Zitat:
Am Anfang des Stacks befindet sich der Startup-Code, der
die main()-Funktion aufruft, die eine Position unter dem
Startup-Code liegt.

Auf dem Stack befindet sich kein Code, sondern Daten.


Zitat:
Die define-Direktive ist im Übrigen eine rein für die
Programmiersprache C gedachte Direktive. Ein reiner
C++-Compiler wird define deshalb nicht erkennen und
kompilieren.

Im C++Buch von Bjarne Stroustrup ist define allerdings
ohne Einschränkung definiert.
Auch in C wird define nicht kompiliert.


Zitat:
void function( int (*ptr)[SPALTE] ) {
void function( int feld[][SPALTE] ) {
Da eine aufgerufene Funktion keinen Speicher für ein Array
bereitstellt, muss die gesamte Größe des Arrays
(erste Dimension) nicht angegeben werden - weshalb
hier die (Dimension) Zeile weggefallen ist.

Das ist nicht der Grund dafür.
Der Compiler muß bei char arr[10][4][5] nur wissen, daß pro
Schritt in der ersten Dimension er den Zeiger um 4x5=20 Bytes
verändern muß, also bei arr[z][s][e] -> arr[z+1][s][e].
Vor der ersten Dimension gibt es keinen Schritt, für den er
die erste Dimension kennen müßte.
((char*)arr)[3*4*5] entspricht arr[3][4][5].


Zitat:
fgets(str, 100, stdin);
Sollten Sie in diesem Beispiel 120 Zeichen eingegeben haben,
liest fgets() davon 98 sichtbare Zeichen plus Newline-Zeichen
(\n) plus Stringende-Zeichen (\0) ein. fgets() hängt am Ende
des Strings immer ein \n-Zeichen an.

Das stimmt nicht. fgets() liest maximal 100-1 Zeichen vom
Stream und hängt eine \0 an. Ein \n vom Stream oder EOF
oder ERROR oder aber ein _anderes_ 99-stes Zeichen vom
Stream beendet fgets().
Außerdem kann fgets() durch Eingabe von ^D/^Z beendet werden.
Dadurch ist ebenfalls kein \n am Ende vor \0.


Zitat:
Zeiger sind im Prinzip nichts anderes als ganz normale
Variablen, die statt Datenobjekten wie Zahlen, Zeichen
oder Strukturen eben Adressen eines bestimmten
Speicherbereichs beinhalten.
Was können Sie mit Zeigern auf Adressen so alles machen?

Adressen sind ganz normale Ganzzahlen. Erst wenn mit dieser
Zahl eine Speicherstelle adressiert (dereferenziert) wird,
schält sich der Unterschied heraus.
Was sind Zeiger auf Adressen? Zeiger _sind_ Adressen.
Zeiger-Objekte enthalten Adressen.


Zitat:
Mit der Funktion strtok() können Sie einen String anhand
von Tokens in einzelne Teilstrings zerlegen.
char *strtok(char *s1, const char *s2);
Damit wird der String s1 durch das Token getrennt, das
sich in s2 befindet. Ein Token ist ein String, der keine
Zeichen aus s2 enthält.

Ein String wird _nicht_ anhand von Tokens zerlegt, sondern
mit Hilfe von Trennzeichen per s2 (Delimiter/Separator).
s2 zeigt _nicht_ auf Token.
Der letzte Zitatsatz ist korrekt - und damit ein Widerspruch.
Der Autor verdreht hier allerlei.


Zitat:
Der String wird jetzt von der Funktion strtok()
zwischengespeichert.

Nein, strtok() merkt sich nur Adressen.
char *s1 beinhaltet kein const.
Bei Zwischenspeicherung würde strtok() falsche Adressen liefern.
s2 darf bei jedem Aufruf auf andere Trennzeichen zeigen.


Zitat:
12.6.1 Zeiger als Rückgabewert
char input[MAX];
fgets(input, MAX, stdin);
return strtok(input, "\n");

Es wird ein Zeiger auf lokalen nichtstatischen Speicher nach
außen gegeben. strtok() liefert diesen Zeiger aus input[].
Auf den nächsten zwei Seiten erklärt der Autor korrekt, daß
man keine Zeiger auf lokalen nichtstatischen Speicher
retournieren darf, da solcher Speicher dann nicht mehr
existiert. Er beschreibt auch, daß return input; deshalb
nicht funktioniert, übersieht jedoch, daß
return strtok(input, "\n"); ebenfalls nicht funktioniert.


Zitat:
/* Möglichkeit3: Einen Zeiger als Argument übergeben */
char *test4(char *ptr){
char buffer[10];
ptr = buffer;
strcpy(buffer, "testwert");
return ptr;
}

Der retournierte ptr erhält zuvor die Adresse eines
nichtstatischen lokalen Puffers, der nicht mehr existiert.


Zitat:
Ein Zeiger ist die Adresse einer Adresse, während
ein Array-Name nur eine Adresse darstellt.
Zeigerdeklarationen als formale Parameter einer Funktion
austauschbar sind, weil hierbei (und nur hierbei) ein Array
in einen Zeiger zerfällt.

Ein Zeiger ist ein Adressenwert, der auf eine Speicherstelle
zeigt. Ein Zeigerobjekt, das eine Adresse enthält, wird meist
vereinfachend ebenfalls als Zeiger bezeichnet.
Ein Array-Name repräsentiert in mehreren Kontexten die
Adresse auf sein erstes Element, nicht nur als Parameter.


Zitat:
Ein Array belegt zum Programmstart automatisch einen
Speicherbereich, der nicht mehr verschoben oder in der Größe
verändert werden kann.

Das gilt nur für statische Arrays.


Zitat:
Zeiger, die als Zusatz das Schlüsselwort const enthalten,
sind sogenannte Read-only-Zeiger. Das bedeutet, dass auf
diese Zeiger nur lesend zugegriffen werden kann.

Ein Zeiger kann kein Schlüsselwort enthalten.
Bei einer Deklaration kann ein Objekt oder eine Adresse
mit const qualifiziert werden.


Zitat:
Äquivalenz zwischen Zeigern und Arrays:
int array[5]={ 1,2,3,4,5 }; /* eindim. Array */
int *ptr = array; /* int-Zeiger verweist auf array[0] */

Zitat:
value = malloc(size*sizeof(int));
scanf("%d", &value[i]);
Zum besseren Verständnis zeige ich hier dasselbe Programm
nochmals, aber statt mit Arrays nun mit Zeigern:
scanf("%d",(value+i));

Der Autor beschreibt den Sachverhalt korrekt, übersichtlich
und aufwendig in einem Unterkapitel.
Etwa 30 Seiten später im _Text_ erklärt er den Zeiger value
jedoch zu einem Array. Beispielsweise die Wortwahl
'mit Array-Zugriffsschreibweise' wäre korrekt gewesen.


Zitat:
*(int *)void_ptr = 100;
Da der gecastete void-Zeiger allein noch nicht dereferenziert
werden kann, wird hier einfach ein weiterer Zeiger verwendet.

Nein, der Typ des Zeigers wird per Typ-cast verändert.
Ein weiterer Zeiger entsteht nicht.


Zitat:
zahlen = realloc(zahlen,max*sizeof(int));

Dies ist generell unsicherer Code. Falls realloc() NULL
retourniert, ist der ehemalige Wert von zahlen verloren.
Es kann dann keine Freigabe des Speichers mehr erfolgen.
Es wird nicht ausdrücklich mitgeteilt, daß realloc() auch
einen anderen Zeigerwert zurückgeben kann als den alten.
Manchmal müssen in solchen Fällen Zeiger neu justiert werden.


Zitat:
Zweidimensionale dynamische Arrays
//int matrix[zeile][spalte];
int ** matrix;
matrix = malloc(zeile * sizeof(int *));
for(i = 0; i < zeile; i++) {
matrix[i] = malloc(spalte * sizeof(int));
}
matrix[i][j] = i + j; /* matrix[zeile][spalte] */

Große Nachteile dieses Konzepts sind malloc-Aufrufe für jede
Zeile (zeile), was ja z.B. 50000 sein können, und ebenso
viele Freigaben per free(), in der richtigen Reihenfolge.
Vorteil ist, das die Spalten unterschiedlich groß sein
können, auch nachträglich.
------------------------------------------------
Ab C99 ist folgendes möglich:
int (*matrix)[spalte];
matrix = malloc(zeile*spalte*sizeof(int));
matrix[i][j] = i + j; /* matrix[zeile][spalte] */
Dies wird nicht gezeigt, wie auch VLAs nicht.
Nur einmal malloc() und free() nötig.
------------------------------------------------
Folgendes benötigt nicht C99:
int **matrix;
matrix= malloc(zeile*sizeof(int*) + zeile*spalte*sizeof(int));
for (z=0; z < zeile; ++z) {
matrix[z]= (int*)(matrix+zeile) + z*spalte;
}
matrix[i][j] = i + j; /* matrix[zeile][spalte] */
Nur einmal malloc() und free() nötig. Portabel.
int **matrix; ist kein C-Array, sondern ein Zeiger auf ein
int*-Array mit dahintergeschalteten int-Arrays.
------------------------------------------------


Zitat:
struct adres { ... } adressen;
Alle Daten wurden in einer Struktur (struct)
namens adres zusammengefasst.

Nein, es wurde ein Strukturtyp struct adres definiert.
Der Name (Bezeichner) der Struktur lautet adressen.


Zitat:
Wenn Sie den Typnamen dieser Struktur nicht benötigen,
kann sie auch ohne deklariert werden:
struct { ... } lib;
Es spricht auch nichts dagegen, mehrere Typen
auf einmal zu deklarieren:
struct index { ... } lib1, lib2, lib3;
Hiermit wurden drei Variablen vom Typ index deklariert.

Man kann nicht mehrere Typen auf einmal deklarieren.
Es wurden drei Strukturen vom Typ struct index angelegt.
Es wurden auch nicht drei Variablen vom Typ index
deklariert, sondern vom Typ struct index.


Zitat:
} adressen = {"Ernest", "Hemming" ,3434, "Havanna" ,1913};

Korrekt: 'Ernest Hemingway'


Zitat:
struct { ... } werte1, werte2;
werte2 = werte1; // Bitte vermeiden Sie solche Zuweisungen.
Das ist in C zwar erlaubt, kann aber zu Fehlern führen, wenn
ein Compiler dies nicht unterstützt.
Sicherer wäre die folgende Möglichkeit:
memcpy(&werte2, &wert1, sizeof(werte1));

Das ist falsch und unlogisch. Wenn Strukturen als Ganzes
an Funktionen übergeben und von diesen retourniert werden
können, muß ein direktes Kopieren erst recht möglich sein.
Das ist gemäß Standard auch auch der Fall.
werte2 = struct_copy(werte1); ist nicht nötig.


Zitat:
Daher wurde der sogenannte Elementkennzeichnungsoperator (->)
eingeführt.

Der Standard nennt -> und . 'member-access-operators'.
Es sind schlicht Zugriffsoperatoren.


Zitat:
struct index lib[3];
strcpy(lib[1].titel, "Hallo"); //richtig
Der Variablenname der Struktur lautet schließlich
lib und nicht titel.

Nein, lib ist der Name des Struktur-Arrays.
Die Struktur ist hier lib[1].
Einen Strukturnamen gibt es hier garnicht.


Zitat:
typedef unsigned double QWORD; // 1 QUAD WORD = 64 BIT

Vorzeichenlose Gleitkommatypen gibt es nicht.


Zitat:
Bitfelder sind Strukturelemente, die mit weniger als 1 Byte
in eine Struktur gepackt werden können.

Das ist eine ungenaue, mißverständliche Formulierung.
"Bitfelder sind Strukturelemente, die für einen Wertbereich
definiert werden können, der 1 Bit bis n Bits entspricht."
ist eine exaktere Beschreibung.


Zitat:
Laut ANSI C müssen die einzelnen Elemente von Bitfeldern
vom Datentyp int oder unsigned int sein.

Seit C99 ist auch der Typ _Bool erlaubt.
Eine Seite später wird dieser Typ auch genannt, in einer
doppelten Beschreibung.


Zitat:
beispielsweise ein Bitfeld mit 40 Bits (fünf Bytes) erstellen.
Hier muss der Compiler ein weiteres Rechnerwort (sizeof(int))
reservieren, sodass das fünfte Byte am Anfang des nächsten
Rechnerworts liegt und somit insgesamt 8 Bytes benötigt werden.

Die Breite eines Bitfelds darf nicht die Breite seines Typs
übertreffen. Dies ist ein error und der Compiler
wird dies nicht kompilieren!


Zitat:
In C ist es auch möglich, einzelne Bits einer Struktur
mit sogenannten Bitfeldern anzusprechen.

Das ist eine ungenaue, mißverständliche Formulierung.
Außerdem ist das eine doppelte Erklärung im Kapitel.


Zitat:
Solche namenlosen Bitfelder dienen zum Auffüllen
eines Bitfeldes, um die Bitzahl auf ein bestimmtes
Rechnerwort einzustellen.

Nein, sie dienen dazu, um das interne Datenobjekt
aufzufüllen, welches die Bitfelder aufnimmt.
Es gibt kein Bitfeld innerhalb eines Bitfeldes!


Zitat:
Ein Bitfeld mit einer Breite von n kann außerdem 2n
verschiedene Werte speichern.

Nein, es sind 2n verschiedene Werte.


Zitat:
Im Prinzip können Sie sich eine Datei als ein riesengroßes
char-Array vorstellen. Das char-Array besteht dabei aus einer
Folge von Bits und Bytes - unabhängig davon, ob es sich
um eine Textdatei oder eine ausführbare Datei handelt.

Die Formulierung ist insgesamt unglücklich und unscharf.
Ein Dateiinhalt besteht aus 0 bis n Bytes. Von einer Folge
von Bits sollte besser nicht gesprochen werden.


Zitat:
int fflush(FILE *datei);
War eine Datei zum Lesen geöffnet, werden die noch nicht
gelesenen Zeichen im Eingabepuffer gelöscht.

fflush() ist im Standard nur für Ausgaben definiert.
Auf der darauffolgenden Buchseite wird darüber informiert.


Zitat:
Die Puffergröße ist abhängig vom Compiler, liegt aber
meistens bei 512 und 4096 Bytes.
Der Wert für BUFSIZ dürfte in der Regel 256 KB, 512 KB
oder auch 4096 KB betragen. Dies ist abhängig vom System
und vom Compiler.

Erneut eine Doublette auf zwei Seiten hintereinander.
Einmal fast korrekt, das andere Mal falsch.


Zitat:
char buf[1024];
int puffer[100];
fread(&puffer, sizeof(int), 10, quelle);
fwrite(buf, 1, i, z);
if((write(fh, &puffer, sizeof(puffer))) == -1) {
read(0, &puffer, sizeof(puffer));

Der Adressenoperator & wird im Buch oft überflüssig
verwendet. Die Adressenwerte von puffer und &puffer
sind allerdings gleich, der Adressentyp nicht.


Zitat:
WORD Word = 0x22CCDDEE;
byte[0]byte[1]byte[2]byte[3]
22_____CC_____DD_____EE Speicherinhalt Little Endian
EE_____DD_____CC_____22 Speicherinhalt Big Endian
/* Ist Byte[0] == 11 */
/* oder ist Byte[0] == CC */
mithilfe einer Maske (FF == 256 == 1 Byte) gezielt getestet.
Werden bei dem Ausdruck ((Word >> 0) & 0xFF)) praktisch keine
Bits auf 0 gesetzt und stimmt danach der ausgewertete Ausdruck
mit Byte[0] überein, haben Sie ein Little-Endian-System.

Der Code ist korrekt, aber mehrere Beschreibungen sind
wieder mal fehlerhaft:
22 ist das hochwertigste Byte in 0x22CCDDEE.
Little und Big Endian sind folglich vertauscht.
Byte[0] == 11 und Byte[0] == CC sind beide falsch.
FF ist 255, nicht 256.
"praktisch keine Bits auf 0 gesetzt" ist seltsam und falsch.


Zitat:
/* kompletten Inhalt mit \0 überschreiben */
fwrite((char *)'\0', 1, size, rem);

Als Pufferadresse wird ein Null-Pointer (NULL)
übergeben. Das ist UB.
Der Pufferinhalt muß 0 sein, nicht das Argument.


Zitat:
int cmp_str(const void *s1, const void *s2) {
return (strcmp(*(char **)s1, *(char **)s2));

Weniger umständlich:
A) return strcmp((char *)s1, (char *)s2);
B) return strcmp(s1, s2);


Zitat:
int cmp_integer(const void *wert1, const void *wert2) {
return (*(int*)wert1 - *(int*)wert2);

Die Compare-Funktion für qsort() ist meistens korrekt,
aber auch fehlerhaft, wie vorstehend.
-10000-10000 = -20000 jedoch
-30000-30000 = +5536 in 16 Bit.


Zitat:
JavaScript ist eine Untermenge von Java, die direkt im
Webbrowser implementiert ist.
Java hat übrigens mit JavaScript bis auf die
Namensähnlichkeit nichts gemeinsam.

Das ist ein Widerspruch.


Zitat:
mit UNSIGNED von 0 bis 4294967296
mit UNSIGNED von 0 bis 18446744073709551616

Die korrekten Werte sind um 1 geringer.


Zitat aus dem Standard:
The declaration
char s[] = "abc", t[3] = "abc";
defines plain char array objects s and t whose elements
are initialized with character string literals.

Der Standard nennt Objektvereinbarungen wie vorstehend
fast überall definierende Deklarationen.
Das mag für den einen oder anderen überraschend sein.
Der Begriff Definition ist _nicht_ normativ!
Die Begriffe Declarator und Declaration hingegen sind normativ!


FAZIT:
Der Autor hat in seinem sehr umfangreichen Buch eine
große Menge an Wissen und Information aufgefahren.
Dennoch fehlen nicht unwichtige Informationen.
Und es sind erhebliche Formulierungsschwächen,
Widersprüchliches und fehlerhafte Aussagen erkennbar.

Es sollte (idealerweise) nach Ablauf eines geeigneten
Zeitraums ergänzend ein weiteres (kleineres) C-Buch
beschafft werden, das fehlerfreier, aktueller (C11) ist
und noch mehr Aspekte von C beleuchtet (Vollständigkeit).