Programmier-Aufgaben in C: erste Schritte mit Feldern und Zeigern

Es werden einfache Aufgaben besprochen, die grundlegende Eigenschaften von Feldern und Zeigern behandeln und die man nach einem ersten Durchgang durch diese Themen beherrschen sollte.

Einordnung des Artikels

Kenntnisse über Schleifen (siehe etwa Einführung in die Programmiersprache C: Schleifen mit for und while) und über Funktionen (Einführung in die Programmiersprache C: Funktionen) werden hier vorausgesetzt.

Einführung

Es werden Aufgaben zu Feldern und Zeigern gestellt, die man bearbeiten sollte, wenn man zum ersten Mal mit diesen Konzepten konfrontiert wird. Dabei sollte man vorerst nicht in den Lösungen nachschlagen, sondern immer erst selber versuchen die Aufgaben zu lösen. Denn gerade bei Feldern und Zeigern gibt es zahlreiche Missverständnisse, Spitzfindigkeiten und Fallen, die man nur kennenlernen kann, wenn man selber verschiedene Varianten ausprobiert.

Sofort in den Lösungen nachzusehen, führt selten zu einem Lernerfolg: Sie wirken meist sehr einfach und naheliegend, aber erst wenn man selber nach Lösungen sucht, wird man verstehen, wie viele Ansätze es gibt und warum viele davon nicht zum Ziel führen oder sehr umständlich sind. Einen souveränen Umgang mit Feldern und Zeigern wird man niemals allein durch das Lesen von Quelltexten erlernen, sondern man muss sich das Verständnis dieser Konzepte mit Programmier-Aufgaben erarbeiten. Die Vorgehensweise, die hier Programmier-Anfängern empfohlen wird, ist in Einführung in die Programmiersprache C: HelloWorld und erste Programme beschrieben.

Im nächsten Kapitel Programmier-Aufgaben in C: Anwendungen von Feldern, Zeigern und Funktionen werden dann umfangreichere Aufgaben gestellt, bei denen auf den ersten Blick nicht klar erkennbar ist, welche Konzepte benötigt werden und die daher eine gewisse Vertrautheit mit Funktionen, Feldern und Zeigern voraussetzen.

Aufgaben zu Feldern (ohne Zeiger)

1. Aufgabe: Skalarprodukt

In Abbildung 1 ist gezeigt, wie man das Skalarprodukt zwischen zwei dreidimensionalen Vektoren berechnet und wie man daraus ihren Zwischenwinkel erhält.

Abbildung 1: Oben: Zwei Vektoren im dreidimensionalen Raum. Unten: Berechnung des Skalarproduktes der beiden Vektoren und ihres Zwischenwinkels mit Hilfe der Kosinus-Funktion.Abbildung 1: Oben: Zwei Vektoren im dreidimensionalen Raum. Unten: Berechnung des Skalarproduktes der beiden Vektoren und ihres Zwischenwinkels mit Hilfe der Kosinus-Funktion.

Initialisieren Sie zwei Felder der Länge 3, die als drei-dimensionale Vektoren interpretiert werden sollen; sie sollen nicht parallel sein und nicht mit dem Nullvektor übereinstimmen.

Berechnen Sie das Skalarprodukt der beiden Vektoren und den Zwischenwinkel. Der Zwischenwinkel soll im Winkelmaß angegeben werden.

Wie muss man den Quelltext gestalten, damit man die Berechnungen möglichst leicht von 3 Dimensionen aus eine andere Dimension ändern kann.

Wie reagiert Ihr Programm, wenn einer der Vektoren gleich dem Nullvektor ist? Wie sollte man diesen Fall behandeln?

2. Aufgabe: Winkel zwischen den Raumdiagonalen eines Würfels

Ein Würfel besitzt 4 Raumdiagonalen. Welche Winkel schließen diese Raumdiagonalen untereinander ein? Verwenden Sie zur Berechnung das Skalarprodukt aus der letzten Aufgabe.

Eigentlich kann man bei der Auswahl von 2 Vektoren aus 4 insgesamt 6 Kombinationen bilden. Aus Symmetriegründen gibt es aber weniger als 6 unterschiedliche Winkel. Wie viele gibt es?

3. Aufgabe: Berechnung der Fibonacci-Zahlen

Die Fibonacci-Zahlen F(n) sind durch eine Rekursionsformel und zwei Anfangs-Bedingungen definiert:

F(n + 2) = F(n + 1) + F(n), F(0) = 1, F(1) = 1, n = 0, 1, 2, ...

Bei der Besprechung der Schleifen in Einführung in die Programmiersprache C: Schleifen mit for und while wurden mit folgenden Anweisungen die Fibonacci-Zahlen berechnet:

int a = 1;
int b = 1;
int c;
int max = 31;

for (int i = 2; i < max; ++i) {
    c = a + b;
    // Ausgabe:
    printf("i = %i: %i \n", i, c);

    // Vorbereitung für nächsten Durchlauf:
    a = b;
    b = c;
}

Die Fibonacci-Zahlen werden auf der Konsole ausgegeben (Zeile 9) und stehen für eine weitere Berechnung nicht zur Verfügung; dies rührt daher, dass die Variablen a, b, c mit immer neuen Werten überschrieben werden.

Schreiben Sie ein Programm, in dem die ersten 30 Fibonacci-Zahlen in einem Feld abgespeichert werden; dabei soll sich die Zahl 30 leicht durch eine andere Zahl ersetzen lassen.

Geben Sie die Fibonacci-Zahlen mit Hilfe des zuvor gebildeten Feldes aus.

Die Folge der Quotienten aufeinanderfolgender Fibonacci-Zahlen F(n + 1) / F(n) konvergiert gegen die positive Nullstelle des Polynoms

x2 - x - 1.

Berechnen sie die Differenz von F(n + 1) / F(n) und dem Grenzwert mit Hilfe des Feldes der Fibonacci-Zahlen. Geben Sie diese Differenzen mit 12 Nachkommastellen an.

Lösungen zu den Aufgaben zu Feldern (ohne Zeiger)

Lösung zur 1. Aufgabe: Skalarprodukt

Die Berechnung des Skalarproduktes zweier Vektoren erfolgt in einer Schleife (Zeile 15 bis 19):

Um den Zwischenwinkel der Vektoren zu berechnen benötigt man die Beträge der beiden Vektoren. Einfacher zu berechnen sind die Quadrate der Beträge, denn dazu muss der Vektor mit sich selbst multipliziert werden (gemäß Skalarprodukt).

Daher werden drei double-Variablen vorbereitet (initialisiert mit 0), die diese drei Summen berechnen (Zeile 11 bis 13).

Die Berechnung des Zwischenwinkels ergibt sich dann mit Hilfe der Arcus-Kosinus-Funktion acos() (Zeile 21). Die Umrechnung des Winkels vom Bogenmaß ins Winkelmaß erfolgt mit Hilfe der Kreiszahl π, die man durch acos(-1) erhält (Zeile 24).

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

int main(void) {
    double pi = acos(-1);

    double a[3] = {3, 5, -1};
    double b[3] = {-3, 1, 2};

    double skp = 0;             // für Skalarprodukt
    double a_norm_2 = 0;        // für |a|^2
    double b_norm_2 = 0;        // für |b|^2

    for (int i = 0; i < 3; ++i) {
        skp += a[i] * b[i];
        a_norm_2 += a[i] * a[i];
        b_norm_2 += b[i] * b[i];
    }

    double phi = acos(skp / sqrt(a_norm_2 * b_norm_2));

    printf("\n SKP: %f\n", skp);
    printf("\n Winkel: %f\n", phi * 180 / pi);
    return EXIT_SUCCESS;
}

Dass die Berechnungen im drei-dimensionalen Raum stattfinden, geht an zwei Stellen ein:

Möchte man das Programm so abändern, dass man die Berechnungen in einer anderen Dimension durchführt, muss man hier die neue Dimension einsetzen. Es ist einfacher eine Konstante DIM (als Präprozessor-Direktive) zu definieren, die für die Dimension steht. Anstelle der 3 wird dann an den genannten Stellen DIM eingesetzt. Man muss dann nur noch darauf achten, die Initialisierungslisten der Vektoren an die jeweilige Dimension anzupassen. Wenn das Programm für eine andere Dimension verwendet werden soll, muss lediglich die Konstante DIM geändert werden. Der Quelltext lautet dann:

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
# define DIM 3

int main(void) {
    double pi = acos(-1);

    double a[DIM] = {3, 5, -1};
    double b[DIM] = {-3, 1, 2};

    double skp = 0;             // für Skalarprodukt
    double a_norm_2 = 0;        // für |a|^2
    double b_norm_2 = 0;        // für |b|^2

    for (int i = 0; i < DIM; ++i) {
        skp += a[i] * b[i];
        a_norm_2 += a[i] * a[i];
        b_norm_2 += b[i] * b[i];
    }

    double phi = acos(skp / sqrt(a_norm_2 * b_norm_2));

    printf("\n SKP: %f\n", skp);
    printf("\n Winkel: %f\n", phi * 180 / pi);
    return EXIT_SUCCESS;
}

Ist einer der Vektoren a oder b gleich dem Nullvektor, ist auch der Betrag gleich null und der Zwischenwinkel verliert seine Bedeutung. Da man nicht durch 0 dividieren darf, muss man den Fall, dass der Betrag von a oder b gleich null ist abfangen und eine entsprechende Warnung ausgeben; die Berechnung des Zwischenwinkels sollte dann nicht mehr ausgeführt werden.

Lösung zur 2. Aufgabe: Winkel zwischen den Raumdiagonalen eines Würfels

In Abbildung 2 erkennt man, dass es nur zwei unterschiedliche Winkel gibt:

Abbildung 2: Darstellung eines Würfels mit seinen 4 Raumdiagonalen. Verbindet man die 4 Eckpunkte einer Würfelfläche mit dem Schnittpunkt der Raumdiagonalen, entsteht eine quadratische Pyramide. In ihr kann man leicht die möglichen Schnittwinkel der Raumdiagonalen ablesen. Berechnen kann man sie entweder mit elementarer Geometrie oder mit den Richtungsvektoren der Raumdiagonalen und dem Skalarprodukt.Abbildung 2: Darstellung eines Würfels mit seinen 4 Raumdiagonalen. Verbindet man die 4 Eckpunkte einer Würfelfläche mit dem Schnittpunkt der Raumdiagonalen, entsteht eine quadratische Pyramide. In ihr kann man leicht die möglichen Schnittwinkel der Raumdiagonalen ablesen. Berechnen kann man sie entweder mit elementarer Geometrie oder mit den Richtungsvektoren der Raumdiagonalen und dem Skalarprodukt.

Man muss jetzt die Richtungsvektoren der entsprechenden Raumdiagonalen bestimmen und wie in der letzten Aufgabe den Zwischenwinkel berechnen.

Für den kleineren der beiden Winkel lauten die beiden relevanten Richtungsvektoren etwa:

(1, 1, 1) und (-1, 1, 1).

Für den größeren Winkel:

(1, 1, 1) und (-1, -1, 1).

Diese Vektoren muss man in der letzten Aufgabe für die Felder a und b einsetzen und erhält die Winkel, die in Abbildung 2 angegeben sind:

// (1, 1, 1) und (-1, 1, 1).
double a[3] = {1, 1, 1};
double b[3] = {-1, 1, 1};
// Winkel: 70.528779
// (1, 1, 1) und (-1, -1, 1).
double a[3] = {1, 1, 1};
double b[3] = {-1, -1, 1};
// Winkel: 109.471221

Lösung zur 3. Aufgabe: Berechnung der Fibonacci-Zahlen

Die Anzahl der zu berechnenden Fibonacci-Zahlen wird als Konstante L in einer Präprozessor-Direktive abgespeichert (Zeile 3). Dann muss später nur an einer Stelle in den Quelltext eingegriffen werden, wenn sich diese Anzahl ändert.

Das Feld für die Fibonacci-Zahlen wird als int-Feld der Länge L deklariert, aber noch nicht initialisiert (Zeile 8) – bis auf die nullte und erste Komponente, die die Anfangs-Bedingung beschreiben (Zeile 11 und 12).

Erst in einer for-Schleife werden die weiteren Komponenten initialisiert (Zeile 15 bis 17).

Die Ausgabe der Fibonacci-Zahlen erfolgt in einer weiteren Schleife, in der der printf()-Befehl je eine Komponente ausgibt (Zeile 20 bis 22).

Der Abstand der Verhältnisse F(n + 1 / F(n) zum Grenzwert (1 + sqrt(5)) / 2 wird ebenfalls in einer Schleife ausgegeben (Zeile 27 bis 29). Diese for-Schleife läuft nur bis L - 1 , da man sonst auf die Komponente fibo[L] zugreifen würde, die es aber nicht gibt – die letzte Komponente von fibo ist fibo[L - 1] .

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
# define L 30

int main(void) {
    // Deklaration des Feldes:
    int fibo[L];

    // Anfangsbedingungen:
    fibo[0] = 1;
    fibo[1] = 1;

    // Schleife:
    for (int i = 2; i < L; ++i) {
        fibo[i] = fibo[i - 1] + fibo[i - 2];
    }

    // Ausgabe:
    for (int i = 0; i < L; ++i) {
        printf("i = %i: %i \n", i, fibo[i]);
    }
    
    // Abstand zu (1 + sqrt(5)) / 2:
    double limit = (1 + sqrt(5)) / 2;
    printf("\nGrenzwert: %f\n", limit);
    for (int i = 0; i < L - 1; ++i) {
        printf("i = %i: %.12f \n", i, (double) fibo[i+1] / fibo[i] - limit);
    }
    
    return EXIT_SUCCESS;
}

Bei der Division (Zeile 28) ist zu beachten, dass die Komponenten des Feldes fibo den Datentyp int besitzen. Ohne Typumwandlung wird dann mit fibo[i+1] / fibo[i] die Ganzzahl-Division durchgeführt.

Das Programm erzeugt die Ausgabe:

i = 0: 1 
i = 1: 1 
i = 2: 2 
i = 3: 3 
i = 4: 5 
i = 5: 8 
i = 6: 13 
i = 7: 21 
i = 8: 34 
i = 9: 55 
i = 10: 89 
i = 11: 144 
i = 12: 233 
i = 13: 377 
i = 14: 610 
i = 15: 987 
i = 16: 1597 
i = 17: 2584 
i = 18: 4181 
i = 19: 6765 
i = 20: 10946 
i = 21: 17711 
i = 22: 28657 
i = 23: 46368 
i = 24: 75025 
i = 25: 121393 
i = 26: 196418 
i = 27: 317811 
i = 28: 514229 
i = 29: 832040

Grenzwert: 1.618034
i = 0: -0.618033988750 
i = 1: 0.381966011250 
i = 2: -0.118033988750 
i = 3: 0.048632677917 
i = 4: -0.018033988750 
i = 5: 0.006966011250 
i = 6: -0.002649373365 
i = 7: 0.001013630298 
i = 8: -0.000386929926 
i = 9: 0.000147829432 
i = 10: -0.000056460660 
i = 11: 0.000021566806 
i = 12: -0.000008237677 
i = 13: 0.000003146529 
i = 14: -0.000001201865 
i = 15: 0.000000459072 
i = 16: -0.000000175350 
i = 17: 0.000000066978 
i = 18: -0.000000025583 
i = 19: 0.000000009772 
i = 20: -0.000000003733 
i = 21: 0.000000001426 
i = 22: -0.000000000545 
i = 23: 0.000000000208 
i = 24: -0.000000000079 
i = 25: 0.000000000030 
i = 26: -0.000000000012 
i = 27: 0.000000000004 
i = 28: -0.000000000002

Methodischer Hinweis:

Die Schleifen sind sehr fehleranfällig, was ihren ersten und letzten Durchlauf betrifft. Es kann sehr leicht passieren, dass

Gerade als Programmier-Anfänger sollte man Schleifen, die über Felder iterieren, immer zuerst den ersten und letzten Durchlauf trocken überprüfen – also tatsächlich Papier und Bleistift zur Hand nehmen und aufschreiben, welche Werte die Zählvariable annimmt und auf welche Komponenten zugegriffen wird.

Aufgaben zu Zeigern (ohne Felder)

1. Aufgabe: Berechnung der Winkel im allgemeinen Dreieck

Bei einem allgemeinen Dreieck können aus den gegebenen Seitenlängen die drei Innenwinkel eindeutig berechnet werden (zu den Bezeichnungen siehe Abbildung 3).

Abbildung 3: Oben: Bezeichnungen der Seiten und Winkel im allgemeinen Dreieck. Damit drei gegebene Seiten ein Dreieck bilden können, müssen die Dreiecksungleichungen erfüllt sein. Unten: Wird eine der Dreiecksungleichungen zur Gleichheit, lässt sich aus den Seitenlängen kein Dreieck erzeugen.Abbildung 3: Oben: Bezeichnungen der Seiten und Winkel im allgemeinen Dreieck. Damit drei gegebene Seiten ein Dreieck bilden können, müssen die Dreiecksungleichungen erfüllt sein. Unten: Wird eine der Dreiecksungleichungen zur Gleichheit, lässt sich aus den Seitenlängen kein Dreieck erzeugen.

Implementieren Sie eine Funktion, die die drei Seitenlängen als Eingabewerte besitzt und die drei Innenwinkel als "Rückgabewerte" – Rückgabewerte sind hier im Sinne von call by reference zu verstehen.

Testen Sie Ihre Funktion für:

  1. Gleichseitiges Dreieck: a = b = c = 1.
  2. Rechtwinkliges Dreieck: a = 3, b = 4, c = 5.
  3. Stumpfwinkliges Dreieck: a = 2 = b = 4, c = 5.

Implementieren Sie zusätzlich eine geeignete Prüfung der Eingabewerte!

2. Aufgabe: näherungsweise Integration

In Einführung in die Programmiersprache C: Funktionen wurde eine Funktion integrate() entwickelt, die die näherungsweise Integration einer Funktion ausführt. Sie war so implementiert, dass drei Näherungswerte berechnet werden (für Untersumme, Obersumme und Trapezregel) und diese drei Werte ausgegeben werden. Dies ist unvorteilhaft, wenn man mit den Ergebnissen weiterrechnen möchte.

Schreiben Sie die Funktion integrate() so um, dass die drei Näherungswerte im Sinne von call by reference Rückgabewerte sind.

Lösungen zu den Aufgaben zu Zeigern (ohne Felder)

Lösung zur 1. Aufgabe: Berechnung der Winkel im allgemeinen Dreieck

Die Berechnung der Winkel erfolgt mit dem Mechanismus call by reference. Man benötigt dazu eine Funktion angles(), die insgesamt 6 Eingabewerte besitzt:

  1. Die drei Seitenlängen des Dreiecks a, b, c als double.
  2. Die drei Winkel des Dreiecks alpha, beta, gamma als Adressen der entsprechenden Variablen.

Da die Funktion nur die drei Winkel berechnen soll, benötigt sie keinen Rückgabewert. Der Funktionskopf lautet somit:

// Berechnet Winkel zu gegebenen Dreiecksseiten a, b, c:
void angles(double a, double b, double c, double * alpha, double * beta, double * gamma);

Für die Implementierung gibt es mehrere Möglichkeiten; hier werden die ersten beiden Winkel mit Hilfe des Kosinus-Satzes berechnet (Zeile 6 und 7):

a2 + b2 - 2 a b cos γ = c2.

Der dritte Winkel kann dann durch die Winkelsumme im Dreieck bestimmt werden (Zeile 8).

Da die Winkel im Winkelmaß berechnet werden sollen, die Arcus-Kosinus-Funktion aber Bogenmaß liefert, wird mit dem Faktor 180 / π umgerechnet. Die Kreiszahl π kann man mit Hilfe von acos(-1) berechnen (wegen cos π = -1; siehe Zeile 5).

void angles(double a, double b, double c, double * alpha, double * beta, double * gamma){
    //TODO: Prüfung der Eingabewerte
    // a, b, c >= 0; 3 Dreiecksungleichungen: a + b > c, ...
    
    double pi = acos(-1);
    * gamma = acos( (a * a + b * b - c * c) / (2 * a * b) ) * 180 / pi;
    * beta = acos( (a * a + c * c - b * b) / (2 * a * c) ) * 180 / pi;
    * alpha = 180 - * gamma - * beta;
    return;
}

In der Implementierung werden anstelle der Winkel alpha, beta, gamma die Zeiger auf die Winkel eingesetzt, wie * alpha .

Zur Prüfung der Eingabewerte werden 2 "fehlerhafte" Eingaben untersucht:

  1. Die Seitenlängen dürfen nicht negativ oder null sein.
  2. Die Dreiecksungleichungen dürfen nicht verletzt sein.

Damit lautet die Implementierung:

void angles(double a, double b, double c, double * alpha, double * beta, double * gamma){
    // Prüfung der Eingabewerte
    // a, b, c >= 0; 3 Dreiecksungleichungen: a + b > c, ...
    if (a <= 0 || b <= 0 || c <= 0) {
        puts("\n negative Seiten!");
        return;
    }
    if (a + b <= c || a + c <= b || b + c <= a) {
        puts("\n Dreiecksungleichung verletzt!");
        return;
    }

    double pi = acos(-1);
    * gamma = acos( (a * a + b * b - c * c) / (2 * a * b) ) * 180 / pi;
    * beta = acos( (a * a + c * c - b * b) / (2 * a * c) ) * 180 / pi;
    * alpha = 180 - * gamma - * beta;
    return;
}

Der Aufruf erfolgt dann innerhalb von main(), etwa durch:

double alpha;
double beta;
double gamma;

angles(3, 4, 5, & alpha, & beta, & gamma);
printf("\n Winkel: %f, %f, %f \n", alpha, beta, gamma);

angles(2, 2, 5, & alpha, & beta, & gamma);
printf("\n Winkel: %f, %f, %f \n", alpha, beta, gamma);

Mit der Ausgabe:

Winkel: 36.869898, 53.130102, 90.000000
 
 Dreiecksungleichung verletzt!
 
 Winkel: 36.869898, 53.130102, 90.000000

Man beachte den Effekt, der durch die Verwendung von Zeigern entsteht:

Derartige Nebeneffekte müssen bei der Verwendung von Zeigern beachtet werden und es liegt in der Verantwortung es Programmierers, dass sie nicht zu fehlerhaften Ergebnissen führen.

Lösung zur 2. Aufgabe: näherungsweise Integration

Die früher entwickelte Funktion integrate() hat den Funktionskopf:

void integrate(double a, double b, int N);

Damit sie über Zeiger auf die drei Zahlenwerte zugreifen kann, die Näherungswerte für Untersumme, Obersumme und Trapezregel speichern, benötigt sie drei zusätzliche Eingabewerte, die als Zeiger auf double in die Liste der Eingabewerte aufgenommen werden:

void integrate(double a, double b, int N, double * integral_min, double * integral_max, double * integral_trapez);

Innerhalb der Funktion integrate() werden wie bisher die drei Näherungswerte der Integrale berechnet. Neu ist dann, dass mit Hilfe von Zeigern diese Näherungswerte an Variablen außerhalb der Funktion übergeben werden kann. Dies ist gleichwertig dazu, dass die Funktion integrate() drei Rückgabewerte hat.

Als Implementierung bietet sich an:

void integrate(double a, double b, int N,
                        double * integral_min, double * integral_max, double * integral_trapez){
    double sum_min = 0;
    double sum_max = 0;
    double sum_trapez = 0;

    // [x; y] Teilintervall der Länge (b - a) / N
    double delta = (b - a) / N;
    double x = a;
    double y = a + delta;

    //Funktionswerte an den Intervallgrenzen x, y:
    double fx;
    double fy;

    // für jedes Teilintervall wird eine Fläche berechnet -> es muss N Schleifendurchläufe geben!!
    for (int i = 0; i < N; ++i) {
        // Funktionswerte an Intervallgrenzen berechnen:
        fx = f(x);
        fy = f(y);

        // Flächen zu Teilintervall berechnen
        sum_min = sum_min + delta * fmin(fx, fy);
        sum_max = sum_max + delta * fmax(fx, fy);
        sum_trapez = sum_trapez + delta * ( fx + fy ) / 2;

        // x, y für nächsten Schritt vorbereiten
        x = y;
        y = a + (i+1) * delta;
    }

    * integral_min = sum_min;
    * integral_max = sum_max;
    * integral_trapez = sum_trapez;
}

Bis auf die letzten drei Zuweisungen (Zeile 32 bis 34) stimmt diese Implementierung mit der aus Einführung in die Programmiersprache C: Funktionen überein.

Der Aufruf der Funktion integrate() innerhalb von main() kann jetzt wie folgt realisiert werden:

double sum_min, sum_max, sum_trapez;
integrate_ref(-5, 3, 1000, & sum_min, & sum_max, & sum_trapez);
printf("Untersumme: %f \n", sum_min);
printf("Obersumme: %f \n", sum_max);
printf("Trapezregel: %f \n", sum_trapez);

Der Vorteil sollte klar sein: in der alten Version wurden die Näherungswerte lediglich ausgegeben, jetzt kann mit ihnen in main() weitergerechnet werden.

Aufgaben zu Feldern und Zeigern

1. Aufgabe: Adressen eines Feldes beziehungsweise einer Matrix

1. Erzeugen Sie ein short-Feld namens a mit 5 Komponenten, die alle den Wert 1 annehmen.

Geben Sie die Adressen der 5 Komponenten von a aus. Wie groß ist die Differenz der Adressen benachbarter Komponenten?

Erzeugen Sie ein weiteres int-Feld namens b mit ebenfalls 5 Komponenten, die alle den Wert -1 annehmen. Wie kann man a einsetzen, um b zu initialisieren?

Geben Sie die Adressen der 5 Komponenten von b aus. Wie groß ist hier die Differenz der Adressen benachbarter Komponenten?

2. Erzeugen Sie eine 2 × 5-Matrix c (also 2 Zeilen und 5 Spalten vom Datentyp int), deren erste Zeile mit a und deren zweite Zeile mit b übereinstimmt.

Wie kann man a und b zur Initialisierung von c einsetzen?

Geben Sie die Adressen aller Komponenten von c aus. Beschreiben Sie, wie die Komponenten von c im Speicher angeordnet sind.

3. Erzeugen Sie eine 5 × 2-Matrix d, deren erste Spalte mit a und deren zweite Spalte mit b übereinstimmt.

Bearbeiten Sie die entsprechenden Aufgaben wie in 2.

Lösung zur 1. Aufgabe: Adressen eines Feldes beziehungsweise einer Matrix

1. Erzeugen und Ausgeben der Felder a und b

Es bietet sich an die Zahl 5 in einer Konstante als Präprozessor-Direktive zu definieren; dies geschieht nach den include-Anweisungen mit:

# define L 5

In den folgenden Listings werden die Befehle gezeigt, die sich innerhalb von main() befinden; die Funktion main() wird nicht gezeigt.

Die Deklarationen und die Initialisierungen für die Felder a und b lauten:

// Deklaration der Felder a, b:
short a[L];
int b[L];

// Initialisierung a, b:
for (int i = 0; i < L; ++i) {
    a[i] = 1;
    b[i] = -a[i];
}

// b = -a;          // Compiler-Fehler

Das Feld a ist vom Datentyp short, das Feld b vom Datentyp int; beide besitzen L Komponenten (Zeile 2 und 3).

Die Initialisierungen können in diesem einfachen Fall auch mit einer Initialisierungsliste erfolgen. Um später die Ähnlichkeit mit den Initialisierungen der Matrizen zu verdeutlichen, wird hier eine Schleife über die Komponenten der Felder gewählt (Zeile 6 bis 9).

Zeile 8: Da alle Komponenten von b gleich -1 sind, könnt man hier – deutlich einfacher – schreiben: b[i] = -1; . In der Aufgabe wurde aber ausdrücklich gefordert, dass man b auf a zurückführen soll. Dies sollte verdeutlichen, dass die naheliegende Zuweisung b = -a (außerhalb der Schleife, siehe Zeile 11, auskommentiert) nicht möglich ist.

Aufgabe: Testen Sie, welche Fehlermeldung durch die Zuweisung in Zeile 11 erzeugt wird. Formulieren Sie in eigenen Worten, warum diese Zuweisung nicht möglich ist.

♦ ♦ ♦

Die Ausgabe von a und b könnte man wie folgt realisieren:

// Ausgabe a:
// (diese Variante ist vorzuziehen)
puts("Vektor a:");
for (int i = 0; i < L; ++i) {
    printf("a[%i] = %i || Adresse von a[%i]: %p \n", i, a[i], i, a + i);        
}

// Ausgabe b:
// (unnötig: b ist ein Zeiger!)
puts("\nVektor b:");
for (int i = 0; i < L; ++i) {
    printf("b[%i] = %i || Adresse von b[%i]: %p \n", i, b[i], i, & b[i]);       
}

Zur Iteration über die Komponenten wird jeweils eine for-Schleife mit Zählvariable i verwendet, die von 0 bis L - 1 läuft (Zeile 4 und 11).

Zeile 5: Die printf()-Anweisung enthält 4 Format-Platzhalter und entsprechend eine Liste mit 4 Variablen, nämlich

Erklärungsbedürftig ist vermutlich nur der vierte Platzhalter:

Zur Ausgabe der Adresse wird der Platzhalter %p eingesetzt. Auf den ersten Blick enthält die zugehörige Variable a + i keinen Zeiger. Da aber a der Zeiger auf die nullte Komponente des Feldes ist, zeigt a + i auf die i-te Komponente von a. Denn der Summand i steht dafür, dass man um i Speicherzellen voranschreitet; und die Größe einer Speicherzelle ist durch den Datentyp des Feldes a gegeben.

Zeile 12: Die printf()-Anweisung für b stimmt bis auf eine Kleinigkeit mit der für a überein. Jetzt wird zur Ausgabe der Adresse von b[i] tatsächlich der Adress-Operator eingesetzt: & b[i] . Dies mag auf den ersten leichter verständlich sein, ist aber unnötig, da b bereits ein Zeiger ist und hier die bei a erläuterte Zeiger-Arithmetik eingesetzt werden kann.

Man beachte, dass man diese Zeiger-Arithmetik nicht verwenden kann, um die Werte des Feldes auszugeben. Mit printf("Adresse als Dezimalzahl: %i\n", b + i); erhält man nicht den Wert von b[i] , sondern die Adresse b[i] umgerechnet in eine Dezimalzahl.

Die Ausgabe der obigen Anweisungen lautet:

Vektor a:
a[0] = 1 || Adresse von a[0]: 0061FEE2 
a[1] = 1 || Adresse von a[1]: 0061FEE4 
a[2] = 1 || Adresse von a[2]: 0061FEE6 
a[3] = 1 || Adresse von a[3]: 0061FEE8 
a[4] = 1 || Adresse von a[4]: 0061FEEA 

Vektor b:
b[0] = -1 || Adresse von b[0]: 0061FECC 
b[1] = -1 || Adresse von b[1]: 0061FED0 
b[2] = -1 || Adresse von b[2]: 0061FED4 
b[3] = -1 || Adresse von b[3]: 0061FED8 
b[4] = -1 || Adresse von b[4]: 0061FEDC

Man erkennt, dass für das short-Feld 2 Bytes pro Komponente verwendet werden und für das int-Feld b 4 Bytes. Diese Angaben müssen nicht auf allen Plattformen übereinstimmen.

Man beachte, dass die Adressen nicht reproduzierbar sind.

2. und 3. Initialisierung und Ausgabe der Matrizen c und d

Die Matrix c hat 2 Zeilen und 5 Spalten, die Matrix d hat 5 Zeilen und zwei Spalten, folglich lauten ihre Deklarationen:

// Deklaration von c, d:
int c[2][L];
int d[L][2];

Die einfachste Initialisierung von c:

// Initialisierung von c:
for (int i = 0; i < L; ++i) {
    c[0][i] = a[i];         // 0. Zeile
    c[1][i] = b[i];         // 1. Zeile
}

Ähnlich wie oben der Vektor b initialisiert wurde, wird die Matrix c zeilenweise mit den Komponenten von a beziehungsweise b initialisiert.

Alternativ kann man dies über Zeiger realisieren – für jemanden, der noch nicht Zeigern vertraut ist, sind dies weniger naheliegende Anweisungen:

for (int i = 0; i < L; ++i) {
    c[0][i] = * (a + i);            // 0. Zeile
    c[1][i] = * (b + i);            // 1. Zeile
}

Entsprechend wird d initialisiert, wobei Zeilen und Spalten ihre Rollen tauschen:

// Initialisierung von d:
for (int i = 0; i < L; ++i) {
    d[i][0] = a[i];         // 0. Spalte
    d[i][1] = b[i];     // 1. Spalte
}

Die Ausgaben der Werte und Adressen der Komponenten von c und d kann man ähnlich wie oben realisieren:

// Ausgabe c:
puts("\nMatrix c");
for (int i = 0; i < 2; ++i) {
    for (int j = 0; j < L; ++j) {
        printf("c[%i][%i] = %i || Adresse von c[%i][%i]: %p \n", i, j, c[i][j], i, j, & c[i][j]);
    }
}

// Ausgabe d:
puts("\nMatrix d:");
for (int i = 0; i < L; ++i) {
    for (int j = 0; j < 2; ++j) {
        printf("d[%i][%i] = %i || Adresse von d[%i][%i]: %p \n", i, j, d[i][j], i, j, & d[i][j]);
    }
}

Die Ausgaben lauten:

Matrix c
c[0][0] = 1 || Adresse von c[0][0]: 0061FEA4 
c[0][1] = 1 || Adresse von c[0][1]: 0061FEA8 
c[0][2] = 1 || Adresse von c[0][2]: 0061FEAC 
c[0][3] = 1 || Adresse von c[0][3]: 0061FEB0 
c[0][4] = 1 || Adresse von c[0][4]: 0061FEB4 
c[1][0] = -1 || Adresse von c[1][0]: 0061FEB8 
c[1][1] = -1 || Adresse von c[1][1]: 0061FEBC 
c[1][2] = -1 || Adresse von c[1][2]: 0061FEC0 
c[1][3] = -1 || Adresse von c[1][3]: 0061FEC4 
c[1][4] = -1 || Adresse von c[1][4]: 0061FEC8

Da sowohl c als auch d Felder vom Datentyp int sind und daher pro Komponente 4 Byte belegen, kann man leicht ablesen, in welcher Reihenfolge die Komponenten im Speicher abgelegt werden.

2. Aufgabe: Erzeugen von Multiplikationstabellen

Die folgende Tabelle zeigt alle Produkte der Art

n · m, wobei n, m = 0, 1, 2, ..., 10.

Sie wird auch als Multiplikationstabelle bezeichnet.

0 0 0 0 0 0 0 0 0 0 0
0 1 2 3 4 5 6 7 8 9 10
0 2 4 6 8 10 12 14 16 18 20
0 3 6 9 12 15 18 21 24 27 30
0 4 8 12 16 20 24 28 32 36 40
0 5 10 15 20 25 30 35 40 45 50
0 6 12 18 24 30 36 42 48 54 60
0 7 14 21 28 35 42 49 56 63 70
0 8 16 24 32 40 48 56 64 72 80
0 9 18 27 36 45 54 63 72 81 90
0 10 20 30 40 50 60 70 80 90 100

Im folgenden Programm werden die Zeilen einer Multiplikationstabelle ausgegeben (gezeigt sind wieder nur die Anweisungen, die innerhalb von main() stehen):

int NR = 11;        // Anzahl der Zeilen
int NC = 11;        // Anzahl der Spalten

// Multiplikationstabelle: NR Zeilen, NC Spalten
int a [NR] [NC];
for (int i = 0; i < NR; ++i) {
    for (int j = 0; j < NC; ++j) {
        a[i][j] = i * j;
    }
}

// Zugriff auf die Zeilen von a durch Zeiger:
int * p_row [NR];

for (int i = 0; i < NR; ++i) {
    p_row[i] = a[i];
}

puts("Zeilen der Multiplikationstabelle: \n");
for (int i = 0; i < NR; ++i) {
    printf("Multiplikation mit: %d\n", i);
    for (int j = 0; j < NC; ++j) {
        printf("\%d ", p_row[i][j]);
    }
    puts("\n");
}

Die Ausgaben lauten:

Zeilen der Multiplikationstabelle: 
Multiplikation mit: 0
0 0 0 0 0 0 0 0 0 0 0 

Multiplikation mit: 1
0 1 2 3 4 5 6 7 8 9 10 

Multiplikation mit: 2
0 2 4 6 8 10 12 14 16 18 20 

Multiplikation mit: 3
0 3 6 9 12 15 18 21 24 27 30 

Multiplikation mit: 4
0 4 8 12 16 20 24 28 32 36 40 

Multiplikation mit: 5
0 5 10 15 20 25 30 35 40 45 50 

Multiplikation mit: 6
0 6 12 18 24 30 36 42 48 54 60 

Multiplikation mit: 7
0 7 14 21 28 35 42 49 56 63 70 

Multiplikation mit: 8
0 8 16 24 32 40 48 56 64 72 80 

Multiplikation mit: 9
0 9 18 27 36 45 54 63 72 81 90 

Multiplikation mit: 10
0 10 20 30 40 50 60 70 80 90 100

Die Matrix a mit den Werten der Multiplikationstabelle, wird in Zeile 6 bis 10 initialisiert. Mit Hilfe des Feldes von Zeigern int * p_row [NR]; kann auf die Zeilen der Matrix a zugegriffen werden.

Erklären Sie die relevanten Anweisungen für diese Zeiger:

Klären Sie dazu insbesondere: Welches Objekt ist a[i] in Zeile 16?

Schreiben Sie das Programm so um, dass man damit die Zeilen der Multiplikationstabelle für Hexadezimalzahlen aus Abbildung 4 ausgeben kann (kleines Einmaleins im Hexadezimalsystem, die Multiplikationen mit 0 werden weggelassen).

Abbildung 4: Multiplikationstabelle im Hexadezimalsystem für Zahlen von 1 bis 16.Abbildung 4: Multiplikationstabelle im Hexadezimalsystem für Zahlen von 1 bis 16.

Zeichenketten

Aufgabe: Erzeugen einer HTML-Tabelle

Die folgende Tabelle zeigt die Zahlen von 1 bis 5 (linke Spalte) sowie deren Quadrate (rechte Spalte):

1 1
2 4
3 9
4 16
5 25

Der HTML-Quelltext dazu lautet wie folgt:

<table>
    <tr>
        <td>1</td>
        <td>1</td>
    </tr>
    <tr>
        <td>2</td>
        <td>4</td>
    </tr>
    <tr>
        <td>3</td>
        <td>9</td>
    </tr>
    <tr>
        <td>4</td>
        <td>16</td>
    </tr>
    <tr>
        <td>5</td>
        <td>25</td>
    </tr>
</table>

Eine Erklärung der verwendeten HTML-Elemente findet sich in C++ Programmier-Aufgaben: Anwendungen aus Numerik, Finanzmathematik, Kombinatorik, Auszeichnungssprachen.

Aufgabe:

Schreiben Sie ein Programm, das diesen Quelltext erzeugt. Dabei soll der Quelltext der gesamten Tabelle in einer Zeichenkette abgespeichert werden. (Es reicht, wenn Sie diese Zeichenkette auf der Konsole ausgeben; man könnte ihn auch in einer Datei abspeichern.)

Verwenden Sie dabei die Funktionen aus string.h .

Lösung zur Aufgabe: Erzeugen einer HTML-Tabelle

Die Tabelle kann mit folgendem C-Programm erzeugt werden (wie immer sind nur die Anweisungen innerhalb von main() gezeigt):

// Konstanten:
const char * table [] = {"<table>\n", "</table>\n"};
const char * tr [] = {"\t<tr>\n", "\t</tr>\n"};
const char * td [] = {"\t\t<td>", "</td>\n"};

// Initialisierung:
char fullTable [1000];
strcpy(fullTable, table[0]);
char number[3];

// Schleife:
for (int i = 1; i < 6; ++i) {
    * fullTable = *strcat(fullTable, tr[0]);

    * fullTable = *strcat(fullTable, td[0]);
    sprintf( number, "%d", i);
    * fullTable = *strcat(fullTable, number);
    * fullTable = *strcat(fullTable, td[1]);

    * fullTable = *strcat(fullTable, td[0]);
    sprintf( number, "%d", i*i);
    * fullTable = *strcat(fullTable, number);
    * fullTable = *strcat(fullTable, td[1]);

    * fullTable = *strcat(fullTable, tr[1]);
}

* fullTable = *strcat(fullTable, table[1]);

// Ausgabe:
printf("%s\n", fullTable);

Als Präprozessor-Direktive wird hier #include &lt;string.h&gt; benötigt.

Zeile 2 bis 4: Es werden drei Felder von Zeigern der Länge 2 angelegt, die die Textbausteine für den HTML-Quelltext einer Tabelle enthalten:

  1. Komplette Tabelle table .
  2. Tabellenzeile tr .
  3. Tabellendaten td .

Es handelt sich jeweils um das Anfangs- und Endelemente; Letztere sind am slash erkennbar. Dabei werden die tr-Elemente um einen Tabulator eingerückt \t und die td-Elemente um zwei Tabulatoren \t\t . Die Endelemente werden um einen Zeilenumbruch \n ergänzt.

Zeile 7: Der in der Aufgabe gezeigte Quelltext soll in der Zeichenkette namens fullTable abgespeichert werden; er wird mit ausreichender Länge deklariert.

Zeile 8: Initialisiert wird fullTable zunächst mit der ersten Zeile des Quelltextes (Anfangs-Element der HTML-Tabelle). Dazu wird die Funktion strcpy() aus string.h eingesetzt. (Man beachte, dass hier keine Zuordnung nötig ist.)

Zeile 9: Für die Zahlen, die später in der Tabelle stehen sollen, wird ein Feld von char deklariert. Denn Zahlen können nicht als int-Objekte in die Zeichenkette aufgenommen werden. Und da sie mehrere Stellen haben können, reicht eine char-Variable nicht aus.

Zeile 12 bis 26: Die for-Schleife zum Erzeugen des Inhaltes der HTML-Tabelle.

Zeile 13 und 25: Das Anfangs- und Endelement der Tabellenzeile wird am Anfang beziehungsweise am Ende der Schleife hinzugefügt. Dazu wird mit Hilfe von *strcat(fullTable, tr[0]) das tr-Anfangselement an die Zeichenkette fullTable gehängt; am Ende dann das Endelement.

Man beachte dabei, dass fullTable ein char-Feld ist. In der Zuweisung darf aber der Name des Feldes nicht auf der linken Seite stehen. Daher wird ein Zeiger auf die Anfangskomponente des Feldes eingesetzt, nämlich * fullTable . Durch die Verwendung des Zeigers und dadurch dass * strcat() ebenfalls einen Zeiger zurückgibt, werden die nachfolgenden Komponenten automatisch gesetzt; man muss hier nicht über die Komponenten des char-Feldes iterieren. (Testen Sie dies: wenn auf der linken Seite der Zuweisung nur der Feldname fullTable steht, erhalten Sie eine Fehlermeldung!)

Zeile 15 bis 18: Die linke Zelle der Tabelle (jeweils erstes td-Element). Um die int-Zahl i in eine Zeichenkette zu verwandeln, wird sprintf() eingesetzt; damit wird die Variable number gesetzt (Zeile 16).

Zeile 20 bis 23: Analog wird die zweite Tabellenzelle erzeugt.

Zeile 31: Ausgabe des char-Feldes fullTable. Man beachte: Obwohl die 1000 Zeichen nicht ausgeschöpft wurden, erhält man die gewünschte Zeichenkette. Die Zeichenkette bricht nämlich ab, wenn das Zeichen für ihr Ende erscheint.