Georg-August-Universität Göttingen
Institut für Informatik


Kompaktkurs
Grundlagen der C-Programmierung


Wintersemester 2009/2010

FAQ (Frequently Asked Questions)
 

Hinweise zu Softwarepaketen und sämtliche Links befinden sich auf dem Stand von März 2010.

  Inhalt

Wie kann ich Unix-Befehle unter Windows nutzen?
printf erzeugt seltsamerweise keine Ausgabe. Warum?
Wenn ich meinem Programm ein Sternchen (*) als Argument übergebe, funktioniert es nicht. Warum?
In welcher Beziehung stehen Zeiger und Arrays wirklich zueinander?
Wenn ich ein zweidimensionales Array an einen Zeiger auf einen Zeiger übergebe, gibt es Probleme. Wieso?
Wieso beschwert sich der Compiler, wenn ich einen int** zu const int** machen will?

  FAQ

Wie kann ich Unix-Befehle unter Windows nutzen?

Die Nutzung von Unix-Befehlen unter Windows ist möglich, und zwar mittels MSYS. Das ist ein Windows-Port der Unix-Shell. Dazu musst du die Installationsdatei MSYS-1.0.11.exe runterladen und anschließend starten. Das Standardinstallationsverzeichnis ist C:\msys\. Im Verzeichnis C:\msys\1.0\bin stehen anschließend die wichtigsten Unix-Befehle zur Verfügung. Ein Compiler ist NICHT automatisch enthalten! Dazu muss zusätzlich MinGW runtergeladen und installiert werden. Eine Anleitung dazu hat Steffen im Stud.IP hochgeladen.

printf erzeugt seltsamerweise keine Ausgabe. Warum?

Die C-Laufzeitbibliothek führt einen internen Ausgabepuffer. Wenn man eine Ausgabe veranlasst, z.B. mit printf, wird diese Ausgabe zuerst in den Puffer geschrieben. Erst, wenn das System der Ansicht ist, es sei an der Zeit, den Puffer zu leeren (d.h. einen sogenannten "flush" durchzuführen), wird die Ausgabe tatsächlich auf das jeweilige Ausgabegerät geschrieben. Unter Umständen kann es passieren, dass der Puffer nicht zu dem Zeitpunkt geflusht wird, wo man es gerne hätte, so dass nichts auf dem Bildschirm erscheint. Einen flush kann man aber manuell erzwingen. Hierzu verwendet man die Funktion fflush(FILE*) (Header stdio.h). Als Argument übergibt man einen Zeiger auf den jeweiligen Puffer, der geflusht werden soll. In unserem Fall ist das die Standardausgabe, also stdout. Der fertige Aufruf lautet dann "fflush(stdout);".

Tipp: Das Problem des nicht geleerten Puffers hat man nicht nur bei der Standardausgabe, sondern auch bei "normalen" Dateien (man erinnere sich daran, dass Unix alle Ressourcen als Dateien betrachtet). Wenn man also z.B. eine Datei geöffnet hat (mit einem Handle "FILE *pFile") und sicherstellen will, dass auch alle Daten hineingeschrieben wurden, muss man den Puffer der Datei über den Aufruf "fflush(pFile);" flushen.

Noch ein Tipp: Verschiedene C-Laufzeitbibliotheken verhalten sich auch unterschiedlich. Zum Beispiel kann es ohne weiteres sein, dass ein Programm, welches unter Linux nicht funktioniert, seinen Dienst unter Windows klaglos verrichtet. Diese Situation hat man vor allem bei der beschriebenen Pufferproblematik (unter Linux erscheint keine Ausgabe, unter Windows schon). Deswegen immer daran denken, ein Programm, das man als "plattformunabhängig" deklariert, auch wirklich unter allen relevanten Plattformen zu testen. Bereits sehr schlichte Programme funktionieren unter Umständen nicht überall! (Siehe auch Frage "Wenn ich meinem Programm ein Sternchen (*) als Argument übergebe, funktioniert es nicht. Warum?".)

Wenn ich meinem Programm ein Sternchen (*) als Argument übergebe, funktioniert es nicht. Warum?

Dies liegt an der C-Laufzeitbibliothek, die von GCC verwendet wird. Die Angabe eines Sternchens sorgt dafür, dass die Namen aller Dateien des aktuellen Verzeichnisses als Kommanmdozeilenargumente übergeben werden. Der Effekt tritt nicht mit Microsoft Visual C++ auf (der Stern wird nicht durch Dateinamen ersetzt). Wenn man auch GCC-kompilierten Programmen einen Asterisk (Sternchen) übergeben möchte, muss man es in Anführungszeichen einschließen.

In welcher Beziehung stehen Zeiger und Arrays wirklich zueinander?   bzw.   Wenn ich ein zweidimensionales Array an einen Zeiger auf einen Zeiger übergebe, gibt es Probleme. Wieso?

Der folgende Beispielcode führt zu Problemen:

void f(int** pp) {
   pp[0][0] = 1; /* Absturz */
}

int main() {
   int array[8][8];
   f(array);
}

Man beachte folgendes: Zeiger sind keine Arrays    und    Arrays sind keine Zeiger. Auch, wenn es immer wieder behauptet wird. Es ist falsch. Die Frage "wieso funktioniert es nicht?" hat meiner Erfahrung nach immer die Ursache, dass der/die Fragende (schuldlos) Arrays für Zeiger hält.

Machen wir uns klar, was ein Zeiger ist. Ein Zeiger ist eine Speicherstelle, an der eine Adresse gespeichert wird. Im Prinzip also ist ein Zeiger nichts weiter als ein int (zumindest auf 32-Bit-Maschinen), der eine Sonderbehandlung genießt. Und jetzt der Clou: Weil ja ein Zeiger eine Speicherstelle ist, hat er auch selbst eine Adresse. Für einen Zeiger int* p gilt im Allgemeinen p ≠ &p (der Zeiger ist ungleich seiner Adresse). Ist ja klar: Der Wert des Zeigers ist irgendeine Zahl, die Adresse des Zeigers seine eigene Position im Speicher.

So, und was sind jetzt Arrays? Ein Array ist eine zusammenhängende Kette von Bytes, die irgendwo im Speicher liegt, also an irgendeiner Adresse. Für ein Array int a[] gilt damit: Die Array-Variable a steht für eine Adresse im Speicher, an der das Array liegt. Und jetzt der Array-Clou: Hat a selbst eine Adresse? Nein! Während man bei Zeigern &p schreiben kann und die Adresse von p bekommt, macht das bei Arrays keinen Sinn. Denn es gibt ja keine Speicherstelle, an der die Adresse des Arrays gespeichert wäre, und deren Adresse wiederum man sich mit &a holen könnte! Zwar steht a für die Adresse des Arrays, jedoch ist a selbst keine Speicherstelle, so wie das z.B. ein int ist. a selbst hat keine eigene Adresse. Der Compiler umgeht dieses Problem, indem er &a = a setzt. Daher ist es z.B. bei scanf völlig wurscht, ob man bei einer Array-Variablen char str[100]

scanf("%s", str);

oder

scanf("%s", &str);

schreibt. Beides funktioniert, obwohl &str eigentlich ein Fehler sein müsste (und bei Arrays auch gar keinen Sinn macht). Bei Zeigervariablen wäre es jedoch ein schlimmer Fehler, &p zu schreiben, weil &p ja etwas ganz anderes ist als p. scanf würde die Daten dorthin schreiben, wo p steht, anstatt dorthin, wohin p zeigt. Das heißt, der Wert von p würde überschrieben.

Ich fasse zusammen: Arrays und Zeiger sind verwandte, aber dennoch sehr verschiedene Dinge, denn während Zeiger eine Stelle im Speicher sind und daher eine Adresse haben, gilt dies für Arrays nicht. Ich drücke das gerne so aus: Array-Variablen sind "logische" bzw. "virtuelle" Zeiger.

Wenn man ein Array durch eine Zeigervariable realisiert, ist der Zugriff darauf theoretisch langsamer, als wenn man eine Array-Variable benutzt. Denn eine Array-Variable muss (und kann) nicht gelesen werden -- sie ist ja kein Zeiger, dessen Wert man lesen und dereferenzieren müsste! Ein Zeiger jedoch ist eine Speicherstelle, dessen Wert man sich holen und dereferenzieren muss. Deswegen muss bei einem Array-Zugriff über Zeiger mehr Arbeit verrichtet werden. (Allerdings hängt das natürlich stark vom Compiler ab, den man verwendet.)

Jetzt, wo wir das haben, können wir uns auch erklären, warum der Beispielcode zum Fehler führt. Die Variable int array[8][8] ist ein "logischer" Zeiger auf einen zusammenhängenden Speicherbereich, der die Größe 8*8*sizeof(int) hat. Mehr ist nicht dahinter. Was passiert nun, wenn wir array in einen Zeigerzeiger int** pp casten? Dann denkt der Compiler, wir hätten eine Speicherstelle, die auf eine Speicherstelle zeigt, die auf einen int zeigt. Das haben wir aber nicht, wie die obigen Ausführungen darlegen! Bei *pp befindet sich ja kein Zeiger, wie der Compiler meint, sondern einfach das erste Byte des zusammenhängenden Speicherbereichs von array. Sobald der Rechner versucht, *pp zu derefenzieren, wird in den allermeisten Fällen ein Zugriffsfehler auftreten, weil *pp keine gültige Speicheradresse enthält. (Durch Zufall kann das natürlich trotzdem mal der Fall sein.) Und wenn man *pp nicht bilden kann, funktioniert **pp erst recht nicht. Das heißt also: Der Zugriff pp[0] würde funktionieren, das wäre tatsächlich das erste Element des Arrays -- allerdings jetzt vom Typ int* statt int. Wenn man nun noch einmal indiziert (über pp[0][0]), meint der Compiler, er muss den int* dereferenzieren -- und das klappt nicht, denn an dieser Stelle steht in Wirklichkeit kein gültiger Zeiger, sondern einfach irgendein int.

Hier ein Bild, das ich extra gemalt habe (!):

Das Bild soll verdeutlichen, wie die Zeiger real im Speicher liegen, während das Array nur ein "virtueller" Zeiger ist. (Die Adressen müssten eigentlich immer Vielfache von 4 sein, schließlich geht es ja um ints; das muss man sich eben denken.)

Wenn man mit Zeigerzeigern ein 2-dim. Array nachbilden will, so muss man zunächst Speicher reservieren für ein Array von Zeigern (das sind dann die "Zeilen"). Anschließend muss jedem dieser Zeiger wieder Speicher zugewiesen werden für die Spalten des Arrays. Im Bild links ist im Prinzip ein 2-dim. Array mit nur einer einzigen Zeile zu sehen (also eigentlich ein eindimensionales Array). Man darf dort z.B. pp[0][3] schreiben und bekommt damit den Wert an Adresse 61 -- also 3.

So, ich hoffe, diese Ausführlichkeit hat zum Verständnis beigetragen und nicht noch mehr Verwirrung gestiftet.

Wieso beschwert sich der Compiler, wenn ich einen int** zu const int** machen will?

C erlaubt einen impliziten Cast von int* zu const int*, wie vermutlich allseits bekannt. Ein Cast von int** zu const int** geht aber nicht (zumindest nicht ohne Warnung, in C++ sogar echter Fehler). Der Compiler warnt, weil er aus Erfahrung weiß, dass man ganz schlimme Dinge tun kann, sollte er diesen Cast erlauben. Man betrachte folgenden Beispielcode:

#include <stdio.h>

const int i = 37;

void f(const int** pp) {
   *pp = &i;   /* jetzt geht's rund */
}

int main() {
   int* p;
   int** pp;
    
   printf("i ist gleich %d\n", i);    
   pp = &p;
   f(pp);    /* gefährlicher Cast von 'int**' zu 'const int**' */
   *p = 1982;
   printf("i ist gleich %d\n", i);
    
   return 0;
}

Die Variable i ist offensichtlich ein konstanter int mit dem Wert 37, dennoch wird sie nach Ausführung des Programms den Wert 1982 haben. Sollte man versuchen, das Programm auszuführen, ist es recht wahrscheinlich, dass dabei ein Segmentation Fault auftritt, da GCC die Variable i mit hoher Wahrscheinlichkeit in ein Read-only-Speichersegment packt, so dass ein Schreibzugriff darauf einen Fehler auslöst. Wenn man aber testweise das const bei i entfernt, wird man sehen, dass sich sein Wert von 37 auf 1982 geändert hat. Und solche Scherze möchte der Compiler gerne vermieden wissen.

Diese Sache sich herzuleiten erfordert schon etwas Nachdenken. Sie ist ziemlich konterintuitiv: Normalerweise denkt man, dass das Hinzufügen des const-Attributs zu einer Variablen kein Problem sein sollte, weil man damit ja den Zugriff einschränkt. Das stimmt auch, man schränkt den Zugriff auf den Inhalt des Zeigerzeigers ein; dass man jedoch auf diese Weise quasi hintenrum den Zugriff auf ganz andere Konstanten wiederum gestatten kann, ist nicht gleich offensichtlich.

Dass die ints in der Funktion f konstant bleiben, lässt sich letztlich nur erreichen, wenn man beim Aufruf der Funktion das Feld explizit zu const int** castet. Das ist natürlich nicht unbedingt schön. Man kann überlegen, das const zu entfernen und statt dessen einen Kommentar an die Funktion zu schreiben, der darüber informiert, dass das Argument nicht verändert wird.

2010 Tim Rohlfs