3 Kleine Beispiele

In diesem Kapitel werden anhand von Beispielen die wichtigsten Aspekte kurz dargestellt, um kleine C-Programme verstehen zu können.

Ein C-Programm besteht aus einem oder mehreren Quelltexten, die getrennt voneinander übersetzt werden können und dann mit den bereits vorhandenen Routinen aus Bibliotheken zusammengebunden werden zu einem hoffentlich lauffähigen Programm.

Jeder Quelltext besteht dabei aus einer Folge von Kommentaren, Praeprozessoranweisungen, Definitionen und Deklarationen.

Nach dem Durchlauf durch den Praeprozessor besteht der C-Quelltext noch aus Definitionen und Deklarationen, und zwar sowohl von Daten als auch von Programmcode.

In allen Quelltexten, die übersetzt und zusammengebunden werden, muß es genau eine Funktion mit dem Namen main() geben. Diese Funktion spielt die Rolle des Hauptprogramms bei anderen Sprachen: mit der ersten ausführbaren Zeile in main() beginnt die Programmausführung, und sie endet mit der letzten ausführbaren Zeile (oder mit einer return-Anweisung in main() oder einem Programmabbruch, beispielsweise durch einen Laufzeitfehler oder einen Aufruf von exit() oder abort()).

Eigentlich hat main() zwei Parameter, die wir aber bis auf weiteres unterschlagen wollen. Wenn eine Funktion mit Parametern aufgerufen wird, die aber gar keine Parameter verwendet, ist das in C kein Fehler.

Solange wir also die beiden Parameter, die main() bei jedem Programmlauf mit auf den Lebensweg bekommt, gar nicht benötigen, tun wir so, als gäbe es sie nicht.

Letztlich steckt in den beiden Parametern die Kommandozeile, mit der das Programm nach dem Übersetzen und Linken aufgerufen wird. Die Kommandozeile interessiert uns noch nicht, also lassen wir die Parameter einfach weg.

Als weitere Vereinfachung soll unser Programm nur aus einem Quelltext bestehen.

Wie sieht nun ein Programm aus, das einen Text auf dem Bildschirm ausgibt?

Wir wollen den Text Pause! auf den Bildschirm schreiben.

Ein String, also eine Zeichenfolge, wird in C von Gänsefüßchen " eingeklammert.

Um einen solchen String auszugeben, gibt es bereits bei jedem C-System eine Funktion namens puts() (von: put string), die man nur mit dem gewünschten String aufrufen muß.

Anstandshalber sollte man jede verwendete Funktion am Anfang des Quelltextes vereinbaren. Dies ist zwar oft nicht nötig, so auch in diesem Beispiel eigentlich nicht. Aber weggelassene Funktionsdeklarationen machen oft Ärger. Deshalb ist es eine ganz hilfreiche Angewohnheit, alles zu deklarieren, was man verwendet; einschließlich aller aufgerufenen Funktionen.

Eine Funktionsdeklaration (sogenannter Funktionsprototyp) besteht aus dem Typ des Rückgabewerts, dem Funktionsnamen, einem runden Klammerpaar, in dem die Parameter vereinbart werden (falsl vorhanden), und einem Semikolon ;.

Als Rückgabewert ist jeder Datentyp zulässig, oder das Schlüsselwort void, wenn die Funktion nichts zurückgibt. Dieser Fall entspricht einer SUBROUTINE in FORTRAN oder einer PROCEDURE in Pascal. Läßt man die Angabe des Rückgabewertes weg, dann nimmt der Compiler int als Rückgabewert an.

Die Parameter werden nacheinander mit Typ und eventuell einem Namen vereinbart. Den Namen kann man bei einer Deklaration einfach weglassen; er ist nur bei der Definition einer Funktion nötig. Die einzelnen Parameter werden mit Kommata getrennt. Gibt man zwischen den Klammern gar keine Parameter an, dann heißt das nicht, daß die Funktion keine Parameter erhalten darf! Vielmehr kontrolliert der Compiler dann einfach beim Aufruf der Funktion keine Datentypen. Will man eine Funktion deklarieren, die gar keine Parameter erhalten soll, dann gibt man anstelle der Parameterliste ebenfalls das Schlüsselwort void an. Ruft man dann die Funktion später doch mit Parametern auf, erhält man vom Compiler eine entsprechende Fehlermeldung. In C ist es (im Gegensatz zu fast allen anderen Sprachen) durchaus möglich und legal, einer Funktion bei jedem Aufruf eine unterschiedliche Anzahl von Parametern zu übergeben. Neben der Anzahl sind auch die Typen der Parameter variabel. In FORTRAN ist dies nur bei vordefinierten Funktionen möglich (z.B. MAX), in Pascal ebenso (write).

Ein vollständiges Programmbeispiel sieht also etwa so aus:

/* klpause0.c 30. 7.95 kw
 *
 * Dieses Programm gibt einen sehr wichtigen Text auf dem Bildschirm
 * aus und beendet sich dann gleich wieder.
 */

int puts( const char *s );         /* Diese Funktion wird verwendet,
                                    * also lieber vereinbaren.
                                    * puts() gibt einen String s aus
                                    * und haengt selbstaendig einen
                                    * Zeilenvorschub an.
                                    */

/* Das ist das Hauptprogramm. Die beiden Parameter von main() werden
 * ignoriert.
 */

int main()
{
  puts( "Pause!" );          /* Den gewuenschten Text ausgeben      */
  return 0;                  /* und den Wert 0 zurueckgeben.        */
}

main() ist trotz ihrer besonderen Bedeutung als Hauptprogramm eine Funktion wie jede andere auch.

Die Funktion main() liefert einen Wert vom Typ int, also eine ganze Zahl.

Was mit dem Rückgabewert der Funktion main() passiert, ist von der jeweiligen Implementation abhängig.

Bei den meisten Systemen (zumindest MSDOS, Unix und Verwandte der beiden) geben Programme einen Zahlenwert zurück. In aller Regel ist dies eine 0, wenn das Programm im wesentlichen fehlerfrei lief, oder eine 1 bei leichten Fehlern (zum Beispiel Compiler, wenn sie Warnungen ausgegeben hatten). Bei schwereren Fehlern wird entsprechend ein größerer Wert zurückgegeben. Beispielsweise gibt ein Compiler eine 0 zurück, wenn der Quelltext korrekt übersetzt wurde; eine 1, wenn beim Übersetzen Warnungen ausgegeben wurden und eine 2, wenn so schwere Fehler auftraten, daß kein lauffähiges Kompilat erstellt werden konnte.

Solche Systeme verwenden den Rückgabewert von main() dann auch als Rückgabewert des Programms. In der Regel sollte man also main() mit return 0; beenden.

main() kann jetzt aber auch wieder andere Funktionen aufrufen. Das können bereits vorhandene Funktionen aus einer Bibliothek mit fertig übersetzten Funktionen sein (wie puts()), als auch selbstgeschriebene in der selben oder einer anderen Quelldatei.

Zur Vereinfachung wird die Möglichkeit, ein Programm aus mehreren Quelldateien zusammenzusetzen, für die nächsten Beispiele nicht genutzt.

Die flexibelste Möglichkeit, Text auf dem Bildschirm auszugeben, ist die Funktion printf(). Sie erhält immer mindestens einen Parameter, nämlich einen Zeiger auf eine Zeichenkette, die auszugeben ist.

Damit kann man das erste Beispiel auch anders schreiben:

/* klpaus1.c 30. 7.95 kw
 *
 * Dieses Programm gibt einen sehr wichtigen Text auf dem Bildschirm
 * aus und beendet sich dann gleich wieder.
 */

int printf( char *format, ... );   /* gibt format und eventuell
                                    * weitere Werte auf die Standard-
                                    * ausgabe.
                                    */

int main()
{
  printf( "Pause!\n" );      /* Den gewuenschten Text ausgeben      */
  return 0;                  /* und den Wert 0 zurueckgeben.        */
}

Dabei bedeutet das Zeichen \n ein Zeilenvorschubzeichen (new line). Es ist hier nötig, da sonst die Ausgaben von aufeinanderfolgenden printf()-Aufrufen direkt hintereinander kleben.

Im Gegensatz zu puts() erzeugt printf() nämlich keinen selbsttätigen Zeilenvorschub.

Die Funktion printf() kann aber mehr. Wenn in dem auszugebenden String ein Prozentzeichen % eingestreut wird, dann wird an dieser Stelle etwas in die Ausgabe eingefügt. Mit %d kann man eine ganze Zahl (also einen Wert vom Datentyp int) ausgeben, mit %f eine gebrochene Gleitkommazahl, mit %c ein Zeichen, mit %s einen String und mehr.

Die Werte, die für die Prozentzeichenfolgen eingesetzt werden sollen, folgen beim Aufruf von printf() einfach hinter dem auszugebenden String.

Zwischen dem Prozentzeichen und dem Kennbuchstaben, der anzeigt, was denn nun ausgegeben werden soll, kann man noch verschiedene Steuerzeichen einstreuen. Mit %10.4f kann man eine gebrochene Zahl (Datentyp double) so ausgeben, daß insgesamt 10 Zeichen geschrieben werden (inklusive Vorzeichen, Dezimalpunkt und eventuellen Leerzeichen), davon 4 Nachkommastellen.

Die möglichen Angaben in dem Formatstring werden bei printf() aufgeführt.

Mit dem Programm

/* klprfmt.c 30. 7.95 kw
 */

main()
{
  printf( "12345678901234567890\n" );
  printf( "%10.4f\n", 3.14159265354 );
  printf( "%+10.4f\n", 3.14159265354 );
  printf( "%-10.4f\n", 3.14159265354 );
  printf( "%-+10.4f\n", 3.14159265354 );
  printf( "%20.4f\n", 3.14159265354 );
  printf( "%020.4f\n", 3.14159265354 );
}
erhält man die Ausgabe:
12345678901234567890
    3.1416
   +3.1416
3.1416    
+3.1416   
              3.1416
000000000000003.1416

Der Aufruf

printf( "eine ganze Zahl %d und eine halbe %f\n", 2, 0.5 )
erzeugt die Ausgabe
eine ganze Zahl 2 und eine halbe 0.500000

Die Prozentzeichenfolgen im String und die auszugebenden Werte müssen in Reihenfolge und Anzahl zusammenpassen. Der Aufruf

printf( "eine ganze Zahl %d und eine halbe %f", 0.5, 2 )
würde nur unsinnige Werte ausgeben.

Ein anderes Beispiel für printf() ist:

/* klq100.c 30. 7.95 kw
 *
 * Dieses Programm gibt die Zahlen von 1 bis 10 und die Quadrate aus.
 */

#define IMAX        10

int printf( char *format, ... );   /* gibt format und eventuell
                                    * weitere Werte auf die Standard-
                                    * ausgabe.
                                    */

int main()
{
  int      i;                /* eine ganzzahlige Variable als
                              * Schleifenzaehler vereinbaren.
                              */

  printf( "Dieses Programm gibt die ersten %d Quadratzahlen",
          IMAX
          );
  printf( " auf dem Bildschirm aus.\n\n" );

  /* Schleife ueber alle auszugebenden Zahlen:
   */
  for( i=1; i<=IMAX; i=i+1 )
  {
    printf( "Das Quadrat von %d ist %d.\n", i, i*i );
  }

  return 0;                  /* und den Wert 0 zurueckgeben.
                              */
}

Die Ausgabe dazu ist:

Dieses Programm gibt die ersten 10 Quadratzahlen auf dem Bildschirm aus.

Das Quadrat von 1 ist 1.
Das Quadrat von 2 ist 4.
Das Quadrat von 3 ist 9.
Das Quadrat von 4 ist 16.
Das Quadrat von 5 ist 25.
Das Quadrat von 6 ist 36.
Das Quadrat von 7 ist 49.
Das Quadrat von 8 ist 64.
Das Quadrat von 9 ist 81.
Das Quadrat von 10 ist 100.

Obwohl die Obergrenze 10 effektiv an 2 Stellen im Programm verwendet wird, braucht man nur an einer Stelle die #define-Anweisung ändern, um statt der ersten 10 die sagen wir ersten 100 Quadratzahlen auf dem Bildschirm sehen zu können.

Wie weit kann man den Parameter IMAX erniedrigen?
Wie weit kann man den Parameter IMAX erhöhen?

Das hängt vom Wertebereich ab, den der Datentyp int auf dem verwendeten System hat. Das betragsmäßig größte Zwischenergebnis tritt im Ausdruck i*i auf; hier darf der Wertebereich nicht überschritten werden1.

Wenn der Wertebereich des Datentyps int nicht reicht, um besonders große Zahlen darstellen zu können, kann man stattdessen den Datentyp long int oder abgekürzt long verwenden. Allerdings muß man dann in der printf()-Anweisung statt %d die Zeichenfolge %ld verwenden.

Damit hat unser nächstes Beispielprogramm die Form:

/* klq1000.c 30. 7.95 kw
 * Dieses Programm gibt die Zahlen von 1 bis 1000 und die Quadrate
 * aus.
 */

#define IMAX        1000

int printf( char *format, ... );   /* gibt format und eventuell
                                    * weitere Werte auf die Standard-
                                    * ausgabe.
                                    */

int main()
{
  long int      i;           /* eine ganzzahlige Variable als
                              * Schleifenzaehler vereinbaren.
                              */

  printf( "Dieses Programm gibt die ersten %d Quadratzahlen",
          IMAX
          );
  printf( " auf dem Bildschirm aus.\n\n" );

  /* Schleife ueber alle auszugebenden Zahlen:
   */
  for( i=1; i<=IMAX; i=i+1 )
  {
    printf( "Das Quadrat von %ld ist %ld.\n", i, i*i );
  }

  return 0;                  /* und den Wert 0 zurueckgeben.    */
}

Als nächstes wollen wir nicht nur Quadrate (2. Potenzen), sondern auch die 3. und 4. Potenzen der ersten 10 Zahlen auf dem Bildschirm haben.

Da wir jetzt von jeder Zahl gleich drei verschiedene Potenzen brauchen, lohnt es sich fast, dafür eine eigene Funktion zu schreiben.

Sinnigerweise geben wir ihr den Namen potenz()2.

Das Programm sieht nun so aus:

/* klqp4.c 30. 7.95 kw
 *
 * Dieses Programm gibt die Zahlen von 1 bis 10 und die ersten 4
 * Potenzen derselben aus.
 */

#define IMAX        10

int printf( char *format, ... );   /* gibt format und eventuell
                                    * weitere Werte auf die Standard-
                                    * ausgabe.
                                    */

/* Diese Funktion liefert zu einem Wert die n-te Potenz:
 */
long int potenz( long wert, int n )
{
  long    ergebnis;
  int     i;

  ergebnis = 1;
  for( i=1; i<=n; i=i+1 )
    ergebnis = ergebnis * wert;

  return ergebnis;
}

int main()
{
  int      i;                /* eine ganzzahlige Variable als
                              * Schleifenzaehler vereinbaren.
                              */

  printf( "Dieses Programm gibt die ersten 4 Potenzen zu den" );
  printf( " ersten %d Zahlen aus.\n\n",
          IMAX
          );

  /* Schleife ueber alle auszugebenden Zahlen:
   */
  for( i=1; i<=IMAX; i=i+1 )
  {
    printf( "%d hoch 2 ist %ld, hoch 3 ist %ld, hoch 4 ist %ld\n",
            i,
            potenz( i, 2 ),
            potenz( i, 3 ),
            potenz( i, 4 )
            );
  }

  return 0;                  /* und den Wert 0 zurueckgeben.
                              */
}

Definitionen und Deklarationen: Neben den Praeprozessordirektiven und Kommentar finden sich in einem C-Quelltext meist noch Deklarationen und Definitionen für Daten und Programmcode.

Deklarationen vereinbaren nur das ,,Aussehen`` von Daten oder Unterprogrammen, während Definitionen auch Platz im Programm dafür schaffen. Dies gilt gleichermaßen für Daten als auch für Unterprogramme (Funktionen).

Der Programmteil

        int summe( int a, int b );
ist nur eine Deklaration. Das heißt für den Compiler, daß es irgendwo eine Funktion namens summe gibt, die zwei Argumente vom Typ int erhält und einen Wert vom Typ int liefert. Wo die Funktion steht, weiß der Compiler damit noch nicht. Die Definition dafür kann später im Quelltext kommen, in einer anderen Datei stehen oder nirgends.

Der Quelltext

        int summe( int a, int b )
        {
            return a + b;
        }
enthält die gleiche Deklaration wie oben, aber zusätzlich die Definition der Funktion, nämlich: liefere beim Aufruf die Summe der beiden Argumente zurück.

Beachten muß man dabei, daß eine Funktionsdeklaration mit einem Semikolon abgeschlossen wird; eine Funktionsdefinition dagegen nicht!

Daten können ähnlich vereinbart werden.

        int a, b;
vereinbart zwei Variablen namens a und b, beide vom Typ int. Der Compiler legt gleich im Programm Platz dafür an und merkt sich die beiden Namen (a und b) als Synonyme für die beiden Speicherplätze. Dies ist also eine Definition und eine Deklaration gleichzeitig. Mit
        extern int a, b;
dagegen erfolgt nur eine Deklaration. Das heißt, daß der Compiler keinen Speicherplatz reserviert, sondern sich nur die Namen als Variablen vom Typ int merkt, aber davon ausgeht, daß die Platzreservierung (also die Definition) anderswo im Programm, eventuell in einer anderen Quelldatei, erfolgt. Der Programmierer ist selbst dafür verantwortlich, daß die zugehörige Definition in der anderen Quelltextdatei identisch ist und nicht möglicherweise mit einem anderen Typ versehen wird. Siehe dazu auch Aufteilung auf mehrere Quelltexte.

Anweisungen: Einzelne Anweisungen in einer Funktion werden mit einem Semikolon (;) abgeschlossen.3

AnyWare@Wachtler.de