ST-Computer C-Kurs Teil 3

Bitte anschnallen und das Rauchen einstellen! Heute geht es in die Vollen. In den ersten beiden Teilen des Kurses haben Sie schon soviel C-Grundlagen gesammelt, daß wir diesmal zwei etwas größere Beispiele in Angriff nehmen können. Zuvor kommen wir aber erst noch auf das Konzept zu sprechen, das C wohl am deutlichsten von anderen Programmiersprachen unterscheidet. Ich meine die konsequent verwirklichte Pointerarithmetik. Erst sie macht es möglich, nach Herzenslust auf Maschinenebene herumzuwühlen und jedes Bit im verfügbaren Speicherraum zu drehen und zu wenden.

Wer schon Pascal programmiert hat, weiß wahrscheinlich schon, was ein Pointer (oder deutsch Zeiger) ist (allerdings werden Zeiger in Pascal viel strenger gehandhabt als in C). Für geläuterte BASIC-Programmierer, die gemerkt haben, daß der „All Purpose“ Teil des Wortes BASIC doch etwas übertrieben ist, im Folgenden eine Erklärung:

Ein Zeiger ist eine Variable, wie alle, die wir bisher kennengelernt haben. Das heißt, man kann einer Zeigervariablen einen Wert zuweisen und den Wert auch wieder in irgendwelchen Ausdrücken verwenden. Der Unterschied ist, daß der Wert keine Zahl oder Zeichen repräsentiert, sondern eine Speicheradresse, unter der dann das Datum steht, auf das man eigentlich zugreifen will. Man sagt deshalb: die Zeigervariable zeigt auf eine Stelle im Speicher. An dieser Speicherstelle kann jetzt natürlich Beliebiges stehen. Vielleicht nur ein Zeichen oder ein String oder der ganze Bildschirmspeicher, wie in einem der folgenden Beispiele, oder - und da wird’s trickreich -wieder ein Zeiger.

In C tauchen bei der Zeigermanipulation zwei Operatoren immer wieder auf. Das sind (lies 'Inhalt von’ oder 'zeigt auf) und (lies 'Adresse von’). Achtung: die Operatoren haben nichts mit der Multiplikation oder dem logischen „und“ zu tun. Der Compiler kann aus der Stellung im Text erkennen, was gemeint ist.

Nach viel grauer Theorie ein Beispiel:

int ival;
int *pi;

vereinbart zwei Variable. Ival ist eine ganz normale int-Variable. Pi ist ein Zeiger auf eine int-Variable. Der Stern in der Vereinbarung sagt dem Compiler, daß er nicht Platz für ein int-Objekt reservieren soll, sondern für die Adresse eines int-Objekts. Am besten liest man solche Vereinbarungen „arabisch“, das heißt von rechts nach links. In unserem Fall hieße das: wir haben ein Objekt pi, das zeigt auf (der Stern) ein Objekt vom Typ int.

Diese Regel erweist sich als besonders nützlich, wenn die Schachtelung noch weitergetrieben wird. Was bedeutet zum Beispiel:

float **ppf;

Mit obiger Regel ergibt sich: ppf ist ein Objekt, das zeigt auf ein Objekt und das zeigt auf ein Objekt vom Typ float. ppf ist also die Adresse, der Adresse einer Gleitpunkt variablen. Wer solche Spielchen mag, sollte sich das C-Puzzle-Buch (Hanser Verlag) zulegen, dort wird die Sache bis zum Exzess betrieben. Aber keine Angst, mir ist noch nie ein C-Programm vorgekommen, bei dem mehr als zwei Sterne nötig gewesen wären.

Nachdem wir einen int-Zeiger vereinbart haben, wollen wir auch etwas damit tun. Dazu sehen Sie sich bitte Beispiel 3.1 an. Zuerst noch einmal die Vereinbarung. Dann drucken wir uns zuerst die Werte unserer Variablen aus. Da Adressen auf dem 68 000 immer 32 Bit lang sind, müssen wir dem Formatelement den Zusatz 1 für long geben, x druckt die Zahl hexadezimal aus. Sie sehen, beide Variablen haben im Moment einen Undefinierten Wert. Bei ival ist das nicht so tragisch, aber wenn man pi verwendet, bevor sie einen vernünftigen Wert hat, kann das katastrophale Folgen haben (Sie wissen doch, wie die ATARI-Bömbchen aussehen?). Das bedeutet, pi muß unbedingt initialisiert werden. Dafür gibt es den &-Operator. Er liefert die Adresse von (fast) jedem in C möglichen Objekt. Die Ausnahme werden wir später noch besprechen. Durch die Zuweisung pi = &ival steht nun in pi die Adresse der int-Variablen ival. Mit Hilfe des *-Operators kann man nun der Variablen ival indirekt über pi beliebige (int) Werte zuweisen. Das passiert in der nächsten Zeile. Die Zuweisung * pi = 111 weist dem Inhalt von pi, oder anders ausgedrückt dem Objekt, auf das pi zeigt, den Wert 111 zu. Wenn Sie sich das ganz klar machen, dürften Sie im folgenden keine großen Schwierigkeiten mehr haben. Die gleiche printf-Anweisung wie vorher liefert jetzt das erwartete Ergebnis, ival hat den Wert 111 und in pi steht die absolute Speicheradresse von ival. Wir kommen nun zu einer etwas sinnvolleren Anwendung von Zeigern. Sicher ist Ihnen schon aufgefallen, daß Sie einer Funktion in Form der Parameter zwar Werte übergeben können, die Funktion aber außer dem durch return gelieferten Funktionswert keine Information zurückgeben kann. Man nennt diese Form der Parameterübergabe ’call by value’, da die Funktion nur den Wert des Parameters erhält. Im Gegensatz dazu stehen in Pascal die var-Parameter. Diese Form der Übergabe heißt ’call by reference’. Tatsache ist, daß C nur die Wertübergabe von Parametern kennt. Abhilfe ist aber leicht zu schaffen, wozu gibt es denn Zeigervariablen. Man übergibt statt einem Objekt einfach einen Zeiger darauf, und schon ist die Funktion in der Lage, über die Zeigervariable das Objekt zu verändern. Als gängiges Beispiel habe ich mir die swap-Funktion herausgegriffen, die die Werte zweier Variablen miteinander vertauscht. Wenn Sie sich bitte Beispiel 3.2 ansehen würden. Die zweite Version stellt übrigens die Lösung der Aufgabe vom letzten Teil dar. Es war eine Methode gefragt, zwei Variablen zu vertauschen, ohne eine dritte als Hilfsvariable zu benutzen. Voila! So funktioniert^. Sie sollten aber im allgemeinen doch Methode eins vorziehen, man wird Sie sonst unter Umständen für etwas sonderbar halten.

/* ST Computer C Kurs Beispiel 3.1 */
/*
 *  Zeigt die Anwendung der ' * ' — und * SC—Operatoren und zeigt die
 *  Deklaration von Zeigern 
 */

#define Wait()  gemdos(1)

main ()
{
    int ival, *pi;

    printf ("IVAL - %d, PI - %1x\n" , ival, pi);

    Pi = &ival; /* pi = Adresse von ival */
    *pi = 111;  /* Inhalt von pi - 111  */

    printf("IVAL - %d. PI - %1x\n",ival,pi);
    Wait() ;
}

Ergebnisse eines Programmlaufs:

IVAL = 794238, PI = BEF9C 
IVAL = 111, PI = F6FC6

/* Ende Beispiel 3.1 */


/* ST Computer C - Kurs Beispiel 3.2    */
/*
 *  Aequivalenz Pointer <—> Array
 *  Vier versch, Moeg1ichkeiten auf ein Array zuzugreifen 
 */

#define MAXINDEX 100 
main()
{
    char    test_arraytAXINDEX 3 ;  /*  Array mit 100 char-Elementen */
    char    *hilf;                  /*  In hilf kann die Adresse einer  char- */
                                    /* Variablen gespeichert werden */
    short   index,                  /*  Laufvariable    */
            *hilf2;

    /* Erste Moeglichkeit (like Pascal) alle Zeichen im Array zu loeschen */
    for (index = 0; index < MAXINDEX; index++)
            test_arrayCindex3 = '\0';

    /* Zweite Moeglichkeit alle Zeichen im Array zu loeschen */

    hilf = &test_arrayC03;
    for (index = 0; index < MAXINDEX; index++)
            *(hilf+index) = ’\0' ;

    /* Dritte Moeglichkeit alle Zeichen im Array zu loeschen »/
    hilf = &test_arrayC03;
    for (index = 0; index < MAXINDEX; index++)
            *hilf++ = '\0' ;

    /* Vierte (etwas anruechige Methode) alle Zeichen zu loeschen */
    /* Geht nur wenn MAXINDEX gerade ist    »/

    hilf2 = (short *) &test_arrayCO3; 
    for (index = 0; index < MAXINDEX/2; index++)
            *hilf2++ = 0;
} /* Ende Beispiel 3.2 */

Die Lösung des Problems, die vertauschten Werte wieder zurückzubekommen, ist also einfach folgende: man übergibt der Funktion die Adressen der zu vertauschenden Variablen und gibt ihr dadurch die Möglichkeit, indirekt auf die Objekte zuzugreifen. Falls Sie die &-Operatoren vergessen, können Sie - abhängig vom Wert der Variablen - wieder einige schöne Bomben sehen. Ich habe übrigens den Adressoperator im ersten Teil des Kurses schon einmal stillschweigend benutzt, um mit Hilfe der Funktion scanf() in meinem kleinen Zahlenratespiel die Benutzerantwort einzulesen. Die Parameterübergabe ist also eine nützliche Anwendung für Zeiger; eine zweite sind Reihungen oder Arrays von Werten.

Sie werden sich vielleicht schon gewundert haben, warum ich Arrays noch nicht erwähnt habe, stellen sie doch beispielsweise in BASIC praktisch das einzige Mittel dar, Daten eine Struktur aufzuprägen. Der Grund ist einfach: in C braucht man eigentlich keine Arrays, denn C-Arrays sind nur ein Sonderfall von Zeigerarithmetik. Wenn Sie zum Beispiel ein Array von long-Werten vereinbaren, etwa so:

long array[100];

reserviert der Compiler den Platz für 100 aufeinanderfolgende long-Werte und merkt sich unter array den Zeiger auf den ersten long-Wert. Sie können dann array wie einen char-Zeiger verwenden, also z. B. als Parameter an eine Funktion übergeben. Es ist nun möglich, in ihrem C-Programm wie gewohnt auf das Array zuzugreifen -array[23] würde den 24. long Wert im Array liefern (die Zählung geht mit 0 los) und array[0] den Ersten. Der Compiler wandelt aber Zugriffe über einen Index sofort in folgende Form um: *(array+ 23) d. h., er addiert 23 zur Basisadresse, array erhält dadurch eine neue Adresse und holt sich mittels *-Operator den Inhalt dieser Adresse. HALT, werden Sie jetzt rufen, da stimmt was nicht! Wenn ich die Basisadresse nehme und 23 dazuzähle, lande ich doch niemals auf dem 24 long-Wert, da beim 68 000 doch immer in Byteadressen gerechnet wird. Richtig! Und an dieser Stelle steckt auch der ganze Trick mit der Pointerarithmetik. Der Compiler zählt nämlich nicht 23 Byte auf die Basisadresse, sondern 23 mal die Länge des Objekts, auf das der Zeiger zeigt. In unserem Fall wären das long-Werte, also würden auf den Basiswert 23 * 4 = 92 Byte addiert werden, und dann stehen wir genau beim 23. long-Wert im Array.

Um das ganze noch deutlicher zu machen, habe ich Ihnen Bild 3.1 gezeichnet. Gezeigt wird jeweils der Zugriff auf das 2. Element eines Arrays. Je nachdem, wie der Zeiger basis vereinbart ist, liefert dabei die Zuweisung inhalt = * (basis +1) ganz verschiedene Werte. Dieses Weiterschalten von Zeigern funktioniert nicht nur für die Grunddatentypen, sondern genauso für jedes legale C-Objekt. Wenn Sie also ein Array von irgendwelchen Rekordtypen haben, wird beim Erhöhen des Arrayindex um Eins jedesmal um die Größe des Rekords in Bytes weitergeschaltet. Um alles noch etwas komplizierter zu machen, Beispiel 3.3. Es werden vier verschiedene Methoden gezeigt, ein char-Array auf 0 zu initialisieren. Die erste ist die in allen Programmiersprachen mit Arrays übliche Methode. Die zweite nutzt die Dualität zwischen Pointern und Arrays aus und ist unter Umständen - je nach Compiler - etwas schneller als die erste.

Bild 3.1

/* ST Computer C - Kure Beispiel 3.3 */

#define Wait() gemdos (1) 
main()
{
    int var1, var2; 
    var1 = 10;
    var2 = 20;

    printf("Vor Swap1: VAR1 - %d, VAR2 - %d\n",var1,var2);
    swap1 (&var1,&var2); /* Vertausche die Inhalte von var1 und var2 */
    printf("Nach Swap1:VAR1 - %d, VAR2 - %d\n",var1,var2);
    printf("Vor Swap2: VAR1 - %d, VAR2 - %d\n“, var1, var2) ;
    swap2(&var1,&var2); /*  Vertausche  die Inhalte von var1 und var2   */
    printf ("Nach Swap2:VAR1 - %d, VAR2 - %d\n",var1, var2) ;
    Wait();
}

/* Erste *swap'-Version — die Normale */ 
swap1(v1,v2) 
int *v1, *v2;
{

    int temp;

    temp = *v1;
    *v1 = *v2;
    *v2 = temp;

} /* SWAP1() */

/* Zweite Version— ohne Hilfsvariable */
swap2(v1,v2)
int *v1, *v2;
{
    *v1 ^= *v2;
    *v2 ^= *V1;
    *v1 ^= *v2;
} /* SWAP2() */

/* Hier das Ergebnis des Programmlaufs: */

Vor Swap1: VAR1 = 10, VAR2 = 20 
Nach Swap1:VAR1 = 20, VAR2 = 10 
Vor Swap2: VAR1 = 20, VAR2 = 10 
Nach Swap2:VAR1 = 10, VAR2 = 20

/* Ende Beispiel 3.3 */

Die dritte Methode ist die in C angemessene Methode, das Problem zu lösen. Einem Hilfszeiger wird die Adresse des ersten Arrayelements zugewiesen und dann durch Hochzählen des Hilfszeigers jedes Element adressiert und auf Null gesetzt. Diese Methode ist wesentlich schneller als die ersten beiden.

Die vierte ist noch einmal doppelt so schnell, bedingt aber gewisse Voraussetzungen und ist deshalb etwas gefährlich. Sie benutzt einen sogenannten cast, eine Typumwandlung. Ein cast ist eine sehr häufig in C auftretende Konstruktion. Man kann dadurch die Umwandlung eines Typs in einen anderen erzwingen. Dazu schreibt man den Typ, den man haben will, in runden Klammern vor das umzuwandelnde Objekt. Prinzipiell kann man alles in alles verwandeln, ist aber selbst dafür verantwortlich, daß etwas Sinnvolles dabei herauskommt. Es ist z. B. äußerst zweifelhaft, was folgende Konstruktion für ein Ergebnis hat:

float   val1;
char    c;
c = (char) val1;

Zurück zum Löschen von Arrays. Wir verwenden diesmal einen short-Zeiger mit Namen hilf2. Um an hilf2 die Adresse von test___array[0] zuweisen zu können, brauchen wir den cast, da &test__array[0] den Typ char-Zeiger hat. Durch (short *) wird eine Umwandlung in den Typ von hilf2 erzwungen. Wenn das Array 100 char-Werte lang ist entspricht das natürlich 50 short-Werten. Durch das casting auf einen short-Zeiger ist das ganze Array also nun in 50 Schritten initialisiert statt in 100. Jetzt wird auch deutlich, warum das gefährlich ist. Wenn die Länge des Arrays nicht durch zwei teilbar ist, wird das letzte Byte nicht gelöscht. Das wäre noch akzeptabel, denn man muß ja nur entsprechend aufpassen. Problematisch wird es deshalb: der 68 000 verlangt, daß Wortzugriffe - und genau das ist ein Zugriff auf einen short-Wert - nur auf geraden Adressen erfolgen dürfen, sonst gibt es einen sogenannten Buserror und damit wieder mal ein paar Bomben. Der C Standard garantiert nun nicht, daß ein statisch definiertes char Array auf einer geraden Adresse anfängt. Wenn man also bei obiger Konstruktion sicher gehen will, muß man explizit überprüfen, ob die Basisadresse durch zwei teilbar ist.

Ganz anders sieht die Sache aus, wenn der Speicherplatz dynamisch mit den Funktionen malloc(), oder calloc() angefordert wurde. Diese Funktionen liefern nämlich garantiert eine gerade Adresse, so daß jedes 68 000 Objekt ohne Problem angesprochen werden kann.

So, nachdem Sie jetzt wissen, was Zeiger sind, wie man mit ihnen umgeht und wofür man sie braucht, ein etwas größeres Beispiel. Es wird all denen bestimmt gefallen, die in Leserbriefen an die ST Computer wissen wollten, wie man von C aus direkt auf den Bildschirm zugreifen kann. Genau das wird in Beispiel 3.4 gemacht. Außerdem erkläre ich Ihnen noch, wie man zwischen mehreren Bildseiten hin-und herschalten kann und wie man den Preprozessor zur Geschwindigkeitssteigerung einsetzen kann. Die gleichen Methoden habe ich in meinem Programm NEO2MONO verwendet, das in der nächsten Ausgabe abgedruckt wird, so daß Sie zur Übung auch mal dort einen Blick hineinwerfen können. Zielsetzung war es, keine VDI- oder AES-Aufrufe zu verwenden, um den notwendigen Overhead klein zu halten.

Die main()-Funktion übernimmt die Initialisierung und den Aufruf der verschiedenen Demos. Zuerst wird an screenl die Basisadresse des ATARI Bildspeichers zugewiesen. Da der Bildspeicher - je nach Speicherausbau -an verschiedenen Stellen liegt, stellt das Betriebssystem zwei Aufrufe bereit, die als Ergebnis jeweils den Anfang des physikalischen bzw. des logischen Bildschirms liefern (xbios(2) und xbios(3)). Der Unterschied ist folgender: normalerweise sind logischer und physikalischer Bildschirm identisch. Der Grafikcontroller des ATARI erlaubt es aber, auf einen Bildschirm zu schreiben (der logische), während der andere (der physikalische) angezeigt wird. Weil dadurch der Schreibvorgang unsichtbar abläuft, kann man durch schnelles Umschalten zwischen den Seiten völlig flimmerfreie Grafiken erzeugen.

/* ST Computer - C Kurs Beispiel 3.4    */
/*
 *  Dieses Programm zeigt, wie man C-Pointer Variable vorteilhaft benutzt
 *  um hardwareabhaenige Programme nicht unbedingt in Assembler schreiben
 *  zu muessen.
 */

/* Zuerst bekommt der Preprozessor etwas Arbeit. */

#define DR_C    1   /*  Falls sie ein anderes C verwenden auf 0 setzen */
#define WEISS OL
#define SCHWARZ -1L

#define Log_base()  (short  *) xbios(3)
#define Setscreen(1,p,r) (void) xbios(5,1,p,r)
#define Clear_screen()  fill_screen(WEISS)
#define Wait()  gemdos(1)

#if DR_C    /*  Das Digital Research C kennt den Typ void nicht */
#define void int
#endif

/* Globale Variable •/

long    switch_it;  /* Wird zum Umschaiten der Bildschirmseiten */
                    /* gebraucht. */
short   *screen1,   /* 1. Bildschirmseite */
        *screen2,   /* 2. Bildschirmseite */
        *oldscreen; /* Damit wir am Schluss wieder auf die richtige */
                    /* Seite kommen. */

/* ---------------------------------------------------------------- */

main()
{

    int i;
    void    switch_screens(),   /*  Diese Funktionen liefern keinen Wert, */
            fill_screen(),      /*  deshalb werden sie als void deklariert */
            init_demo();
    char    *malloc(),          /*  malloc ordnet dem Programm waehrend der */
                                /* Laufzeit dynamisch Speicher zu. */
            *hilf,
            text150];           /*  char array fasst einen String der Laenge 49 */
                                /* und das \0~Zeichen zum Abschluss. */

    oldscreen = screen1 - Log_base(); /* screen1 enthaelt nun die Basisadresse */
                                /* der logischen Bildschirmseite */

    /* Euer die 2. Seite muss jetzt Speicher angefordert werden */ 
    if ((hilf = malloc(32511)) == (char *) 0) {
        printf ("Kann keinen Speicher fuer zweite Seite reservieren !'Nn");
        Wait() ;
        exit(1); /* Programm wird abgebrochen */
    }

    /* Wenn wir hier landen ist alles glatt gegangen und screen2 zeigt jetzt */
    /* auf einen 32511 Byte grossen Speicherbereich. */
    /* Die Basisadresse des Bildschirms muss laut Dokumentation durch 512 teil-*/
    /* bar sein. Dies erreicht man z.B. so: */

    screen2 = (short *) ((long) hilf + (512L - (long) hilf % 512));

    /* Jetzt berechnen wir noch die Variable switch_it, die wir zum Umschalten */
    /* der Bildschirmseiten benutzen wollen.    */

    switch_it = (long) screen1 ^ (long) screen2; /* Siehe switch_screens() */
    Setscreen(screen2,-1L,-1);  /* logbase wird screen2 */

    sprintf(text,"Screen1 = %1x, Screen2 = %1x, Switch_it = %1x", (long) screenl, (long) screen2, switch_it); 
    init_demo(text);

    /* Um die Geschwindigkeit zu demonstrieren schalten wir den Bildschirm */ 
    /* ein paarmal von schwarz auf weiss und umgekehrt. */

    init_demo("B I L D S C H I R M B L I T Z E"); 
    for (i = 0; i < 30; i++) {
        fill_screen(WEISS); /*  Mache logischen Bildschirm weiss */
        switch_screens();   /*  Schalte zur Anzeige um  */
        fill_screen(SCHWARZ);   /*  Mache logischen Bildschirm schwarz */
        switch_screens();   /*  und wieder umschalten.  */
    }

    init_demo("G I T T E R L I N I E N"); 
    grid();

    init_demo ("W A C H S E N D E S R E C H T E C K"); 
    growing_box();

    init_demo("Z U F A L L S P U N K T E"); 
    rnd_points();
    Wait();

    Setscreen(oldscreen,oldscreen,-1); /* Urzustand wieder herstellen   */

} /* MAIN() */

/*------------------------------------------------------------------------------ */

void init_demo(text) char *text;

/*
 *  Gibt ’text' zentriert in Bildschirmmitte weiss auf schwarz aus 
 */

{
    short tab;

    Clear_screen();
    tab = (80 - strlen(text)) / 2;  /*  Text zentrieren */
    printf("\033Y%c%c\033p*s\033q",32+12,32+tab,text); /* schreiben */
    switch_screens() ;  /*  und anzeigen */
    Clear_screen();
    Wait();
} /* INIT_DEMO() */

/* ------------------------------------------------------------------------------ */

void fill_screen(value) 
register long value;
/*
 * Schreibt gesamte logische Bildschirmseite mit ‘value’ voll 
 */

{ 
    register short i; 
    register long *help;

    help = (long *) Log_base();

    for (i=0; i < 8000; i++)    /* Der Bildschirm ist genau 8000 long-Werte lang */
        *help++ = value;
}   /* FILL_SCREEN () */

/* ------------------------------------------------------------------------------ */

void switch_screens()

/*
 *  Vertauscht die zwei Bildschirmseiten
 */
{
    screen1 = (short *) ((long) screen1 * switch_it); 
    screen2 = (short *) ((long) screen2 * switch_it);
    Setscreen(screen2,screen1,-1);

}   /* SWITCH_SCREENS() * /

/* ------------------------------------------------------------------------------ */

set_pixel(x,y)
short x,y;

/*
 * Setzt den Punkt mit den Koordinaten (x,y).
 * - 0 <- x <- 639, 0   <-  y <- 399
 * Es wird keine Bereichsueberpruefung vorgenommen.
 * Funktion nur fuer Monochrombildschirm korrekt.
 * Fuer Manipulation des Farbbildschirms siehe Programm NE02M0N0 in dieser
 * Ausgabe.
 */
{
    short *addr;
    short shift;

    addr = screen1 + y*40 + (x>>4);
    shift = 1 << (15 - (x & 15));

    *addr := shift;

} /* SET_PIXEL() */

clear_pixel(x,y) 
short x,y;

/*
 * Loescht den Punkt mit den Koordinaten (x,y).
 * 0 <- x <- 639,   0   <-  y <- 399
 * Es wird keine Bereichsueberpruefung vorgenommen.
 */
{

    short *addr; 
    short shift;

    addr = screen1 + y*40 + (x>>4); 
    shift = 1 << (15 - (x & 15));

    *addr &= ~shift;

} /* CLEAR_PIXEL() */

/* ------------------------------------------------------------------------------ */
grid()

/*
 * Zeichnet ein Gitter im Abstand von fuenf Punkten 
 */
{
    register short x,y;

    for (y=0; y<400; y +? 5) 
        for (x=0; x<640: x++) 
            set_pixel(x,y);

    for (x=0; x<640; x += 5) 
        for (y=0; y<400; y++) 
            set_pixel(x,y);
} /* GRID() */

/* ------------------------------------------------------------------------------ */

Im Beispiel wird also an screen1 die Adresse des Bildschirmanfangs zugewiesen. Jetzt brauchen wir Platz für eine zweite Seite. Dafür gibt es in der Standardbibliothek die Funktion malloc(), die als Argument die Größe des gewünschten Speicherbereichs erhält und als Ergebnis, wenn alles glatt geht, einen Zeiger auf den Bereich liefert. Wenn malloc() den Wert 0 - durch den cast * - macht man wieder eine Typumwandlung) liefert, ist entweder kein freier Speicher mehr da, oder es ist sonst etwas schiefgegangen. In diesem Fall wird das Programm durch den Aufruf exit() abgebrochen. Der Parameter von exit() wird an das aufrufende Programm geliefert; meistens wird das das Betriebssystem sein. Eine Konvention in UNIX ist, daß ein Wert ungleich 0 Fehler im Programm signalisiert. Leider wird der Rückgabewert im Moment vom TOS des Atari nicht ausgewertet.

Wenn aber alles glatt gegangen ist, zeigt jetzt hilf einen 32 511 Byte großen Speicherbereich. Daß 511 Byte mehr als benötigt angefordert wurden, (640/8 Byte * 400 Zeilen = 32 000 Byte) liegt daran, daß der Grafikcontroller verlangt, daß der Bildschirm auf einer durch 512 teilbaren Adresse anfängt. Im ungünstigsten Fall haben wir eine passende Adresse um gerade ein Byte verpaßt, das heißt, man muß maximal 511 Bytes weitergehen, um wieder auf eine durch 512 teilbare Adresse zu kommen. Diese Berechnung wird als nächstes ausgeführt und die endgültige Adresse nach einem cast auf einen (short *) an screen2 zugewiesen.

Um zwischen den Seiten umzuschalten, verwende ich noch einmal den kleinen Trick aus der Beispielfunktion swap2(). Die Variable switch___it hat, als Ergebnis der Exclusiv oder Verknüpfung der zwei Bildschirmadressen, die Eigenschaft, daß sich beim nochmaligen Verknüpfen mit einer der Bildschirmadressen die jeweils andere ergibt. Beachten Sie bitte wieder die casts. Da es verboten ist, auf Zeiger andere Operationen als Addition und Subtraktion anzuwenden, müssen sie explizit in long-Werte umgewandelt werden.

Der Aufruf Setscreen() (xbios(5)) macht dem Grafikcontroller die neue Bildschirmseite als logische Bildschirmseite bekannt. Ab jetzt sind alle Bildschirmausgaben unsichtbar, bis durch die Funktion switch__screens() logische und physikalische Bildschirmadresse vertauscht werden. Damit Sie sehen, daß alles seine Richtigkeit hat, werden nun die errechneten Werte auf den Bildschirm ausgegeben.

#define spixel(x,y) *(screen2+40*(y) + (x>>4)) |= (1<<(15-((x)&15)))
#define cpixel(x,y) *(screen2+40*(y) + (x>>4)) &= ~(1<<(15-((x)&15)))

draw_box(x1,y1,x2,y2) 
register short x1,y1,x2,y2;

/*
 *  Zeichnet ein Rechteck mit linker oberer Ecke (x1.y1) und rechter
 *  unterer Ecke (x2,y2)
 */

{
    register short index;

    for (index=x1; index<=x2; index++) { 
        spixel(index,y1);
        spixel(index,y2);
    }

    for (index=y1; index<=y2; index++) { 
        spixel(x1,index); 
        spixel(x2,index);
    }
} /* DRAW_BOX() */

/* ------------------------------------------------------------------------------ */

clear_box(x1,y1,x2,y2) 
register short x1,y1,x2,y2;

/*
 * Loescht ein Rechteck mit linker oberer Ecke (x1,y1) und rechter
 * unterer Ecke (x2,y2)
 */
{
    register short index;

    for (index=x1; index<=x2; index++) {
        cpixel(index,y1); 
        cpixel(index,y2);
    }

    for (index=y1; index <= y2; index++) { 
        cpixel(x1,index); 
        cpixel(x2,index);
    }

} /* CLEAR_BOX */

/* ------------------------------------------------------------------------------ */

growing_box()
{
    short x1,y1,x2,y2;
    x1 = x2 - 320; 
    y1 = y2 - 200;

    switch_screens();
    Clear_screen();

    while (y1 > 0) {
        draw_box(x1,y1,x2,y2);
        x1--;
        y1--; 
        x2++;
        y2++;
        switch_screens(); 
        clear_box(x1+2,y1+2,x2-2,y2-2);
    }

} /* GROWING_BOX() */

/* ------------------------------------------------------------------------------ */

#define Rnd() (long) xbios(17) /* Liefert 24-Bit Zufallszahl    */

rnd_points()

/*
 * Setzt 15000 Zufallspunkte
 */

{
    register i;

    for (i = 0; i < 15000; i++)
        set_pixel((short) (Rnd() *640), (short) (Rnd() *400));

} /* RND_POINTS() */

/* Ende von Beispiel 3.4 */

Bemerkenswert ist dabei noch die Funktion sprintf(). Sie ist vollkommen identisch mit printf(), nur gibt sie nicht auf die Standardausgabe aus, sondern legt das Ergebnis in einem String ab, dessen Adresse als erstes Argument übergeben wird. Sie ist damit auch ein Beispiel für die Äquivalenz von Zeigern und Arrays.

In der ersten Demonstration wird die logische Bildschirmseite abwechselnd weiß und schwarz gefüllt und dann die Seiten vertauscht. Das geht so schnell, daß man nur eine Folge von Blitzen sieht. Wie die Funktion fill_screen() arbeitet, können Sie nun sicher selbst ergründen. Schauen Sie sich dazu noch einmal die vierte Methode an, ein Array zu initialisieren.

Jetzt wird es noch einmal kompliziert. Im zweiten Demo soll ein Gitter auf den Bildschirm gezeichnet werden. Da keine Gem-Routinen verwendet werden sollen, müssen wir die Linien selbst machen. Das bedeutet, es müssen ganz gezielt einzelne Punkte auf den Bildschirm gesetzt werden. Da stellt sich jetzt natürlich die Frage: wie kommt man von einem Koordinatenpaar auf das zugehörige Speicherwort und innerhalb des Wortes auf das richtige Bit? Achtung: alles folgende gilt nur für den Monochrome-Bildschirm! Wer das Ganze farbig will, muß etwas mehr Gehirnschmalz hineinstecken. Rechnen wir an einem realen Beispiel; betrachten Sie dazu auch Bild 3.2.

Welches Bit im Bildspeicher muß gesetzt werden, wenn wir den Punkt (125,75) „anknipsen“ wollen? Es geht relativ einfach, wenn man sich die Aufgabe zerlegt. Zuerst wollen wir wissen, mit welcher Adresse die Zeile 75 anfängt. Eine Zeile hat 640 Punkte. 16 Punkte passen in ein Speicherwort. Also belegt eine Zeile 640/16 = 40 Worte. Damit erhalten wir als Anfangsadresse von Zeile 75: basis+ 40 *75 = basis + 3000. In welchem Wort der Zeile liegt nun der Punkt 125? Dazu teilen wir einfach 125/16 = 7 Rest 13. Um den Punkt 125 zu erreichen, müssen wir also auf die Basis nochmal 7 addieren, dann haben wir die gesuchte Adresse: das ergibt dann basis+ 3007. In dieser Adresse müssen wir nun das 13. Bit von links setzen. Da die Bits in einem Speicherwort immer von rechts nach links (0...15) gezählt werden, müssen wir die 13 noch von 15 abziehen und erhalten damit Bit Nr. 2. Zusammengefaßt erhalten wir also: um den Punkt (125,75) zu setzen, müssen wir in dem Speicherwort mit der Adresse basis+ 3007 das Bit Nr. 2 setzen. Wenn Sie das soweit verstanden haben, können Sie sich im Beispiel die Funktion set_pixel() ansehen. Dort wird genau das gemacht, was ich eben erklärt habe. Durch die Wahl von screen1 als Basis schreiben wir auf die physikalische Bildschirmseite, das heißt, jeder gesetzte Punkt wird sofort sichtbar. Das Löschen von Punkten geht genauso, nur daß das Bit nicht gesetzt, sondern eben gelöscht werden muß. Anzumerken ist noch, daß ein Rechtsshift um 4 Bit einer Division durch 16 entspricht und x%16 errechnet man schneller mit x&15. Das „Warum“ können Sie sich mit einem kleinen Beispiel schnell selbst erklären.

Bild 3.2

Das eigentliche Zeichnen des Gitters ist ziemlich trivial und wird hier mit zwei for-Schleifen erledigt.

Die Funktion set_pixel() ist ganz gut geeignet, um das Prinzip zu erklären, aber schlecht, wenn es um Geschwindigkeit geht. Denn für jeden Aufruf müssen die Parameter auf dem Stack abgelegt, Register gerettet und entsprechende Zeiger zur Verwaltung der dynamischen Aufrufstruktur, umgesetzt werden. Beim Verlassen der Funktion passiert das gleich wieder in umgekehrter Reihenfolge. Um diesen Overhead zu vermeiden, macht man sich den Preprozessor zunutze und schreibt die Berechnung als Makro. Sehen Sie sich dazu bitte die zwei Makrodefinitionen spixel() und cpixel() an. Wenn der Preprozessor bei seinem Programmdurchlauf auf einen entsprechenden Aufruf stößt, ersetzt er ihn durch die rechte Seite der define Anweisung. Gleichzeitig ersetzt er auch die formalen Parameter durch die aktuellen des Aufrufs. Das Programm wird durch diese Methode zwar etwas länger, aber auch ein ganzes Stück schneller. Damit auch gleich wieder eine Aufgabe bis zum nächstenmal:

Die Makros spixel() und cpixel() sind schon ganz schön schnell. Aber es gibt noch mindestens zwei Möglichkeiten, sie schneller zu machen, ohne auf einen Assembler zurückgreifen zu müssen. Denken Sie einmal nach, ob Ihnen dazu etwas einfällt.

Die beiden Makros werden im nächsten Beispiel benutzt, um ein wachsendes Quadrat zu zeichnen. Dabei wird durch die Funktionen draw_box() respektive clear_box() ein Quadrat gezeichnet oder gelöscht, und um das ganze flimmerfrei zu halten, wird jedesmal zwischen den Bildschirmen hin- und hergeschaltet. Wenn man bedenkt, daß dabei 40200 Punkte gezeichnet und wieder gelöscht werden müssen, ist die Geschwindigkeit doch ganz ordentlich.

Um den Aufruf des im Betriebssystem eingebauten Zufallsgenerators zu zeigen, und weil es so schön aussieht, werden im letzten Beispiel 15 000 Punkte zufällig auf den Bildschirm gemalt.

Vielleicht ist Ihnen aufgefallen, daß bei manchen Variablendefinitionen das Attribut register dabei steht. Dies ist eine Anweisung an den Compiler, eine so definierte Variable möglichst in einem Register des Prozessors zu halten. Ob die Variable tatsächlich in einem Register angelegt wird, kann man allerdings nur feststellen, wenn man den erzeugten Code untersucht. Die mir bekannten C-Compiler für den Atari lassen meist 4 normale und 5 Zeigervariablen innerhalb einer Funktion zu. Registervariablen sind die einzige Ausnahme, auf die der ’&’-Operator nicht angewendet werden kann. Warum? Register haben ja gar keine Adresse. Außerdem ist noch der Datentyp void neu hinzugekommen, void ist nur als Ergebnistyp von Funktionen gültig und bedeutet, daß die Funktion keinen Wert zurückliefert.

Es würde natürlich den Rahmen dieses Kurses bei weitem sprengen auf alle Einzelheiten ganz genau einzugehen, aber mit etwas Spürsinn und vielleicht einem guten C-Buch zur Seite sollte es möglich sein auch Dinge zu ergründen, die im Kurs nicht so explizit genannt werden. Ein ausgezeichnetes Nachschlagewerk ist übrigens ’The C Programmers Handbook, von M. I. Bolsky, erschienen 1985 bei prentice Hall’. Leider ist es nur in Englisch erhältlich. Es ist lexikonartig gegliedert, beleuchtet C wirklich von allen Seiten und enthält Unmengen von Tips wie man C Programme transparent und portabel schreibt.

Als letztes Beispiel für diesen Teil, sozusagen zur Entspannung, etwas, bei dem man den Kopf nicht so sehr anstrengen muß wie bei Zeigern und Bitfummelei auf dem Bildschirm.

Hier zeige ich Ihnen zum ersten Mal ein Programm, das auf eingebauten Grafikfähigkeiten von GEM zurückgreife. Ein mit einem Füllmuster versehener Ball (Kreis) springt auf dem Bildschirm herum und ändert jedesmal seine Richtung, wenn er an eine Berandung stößt, außerdem ertönt beim Anstoßen ein Ton. Mit den Pfeiltasten kann man jeweils die Geschwindigkeitsanteile in x- und y-Richtung verändern. Das klingt doch sehr vielversprechend. Oder? Da nur mit einer Bildebene gearbeitet wird, flimmert der Ball allerdings relativ stark. Das Einbauen einer zweiten Bildebene hätte das Programm aber zu stark anschwellen lassen, um hier im Kurs noch als Beispiel durchzugehen. Vielleicht legen Sie selbst einmal Hand an und erweitern auf mehrere Bälle, die auch miteinander kollidieren können. Daß es möglich ist, können Sie an einem von mir geschriebenen Ball-Demo sehen, das auf irgendwelchen Public-Domain-Disketten kursiert.

Vor dem Vergnügen zuerst wieder die Arbeit. Für jetzt und alle weiteren Teile des Kurses können Sie die Datei ini-gem.h eintippen. Sie muß mit #include vor allen Programmen, die das VDI- oder AES-Interface benutzen, eingelesen werden. Es werden einige wichtige Variablen definiert und ein paar Funktionen, die immer wieder Vorkommen. So spart man sich die Arbeit, bei jedem neuen Programm den ganzen Rahmen noch einmal neu eintippen zu müssen. Auf das Konzept von GEM werde ich an dieser Stelle nicht eingehen, sondern verweise Sie auf andere Artikel in dieser und anderen Zeitschriften und auf einschlägige Bücher zum Thema.

So, Ende der langen Vorrede. Wie kriegen wir den Ball zum Springen? Daß man nicht für jede Position einen Kreis zeichnen und füllen kann, wird einem ziemlich schnell klar. Von einer gleitenden Bewegung kann bei diesem Verfahren keine Rede sein. Also muß man sich etwas einfallen lassen. Wenn man beim Nachdenken den Bildschirm betrachtet und vielleicht etwas mit der Maus spielt, fällt der Groschen. Der Mauszeiger bewegt sich doch ziemlich schnell. Und auch Fenster, die verschoben werden, erscheinen meist 'sofort’ an ihrem neuen Platz. Wie macht man so etwas? Die Lösung heißt Bitblocktransfer. Das heißt, ein rechteckiger Bereich des Bildschirms wird an eine andere Stelle kopiert. Das geht unheimlich schnell. Laut Atari lassen sich 3000 16 * 16 Bitblöcke pro Sekunde kopieren. Wenn wir also unseren Ball 64 Pixel breit und 64 Pixel hoch machen, müßte man ihn immer noch rund 190 mal pro Sekunde kopieren können. Der Rest ist jetzt ein Kinderspiel. Wir zeichnen eine Titelleiste und setzen rechts und links davon zwei Bälle hin, die dem unbefangenen Betrachter nicht weiter auffallen. Sodann kopieren wir, sagen wir den Ball links oben immer an die neu errechnete Position, nachdem wir vorher den Ball an der alten Position durch Uberschreiben mit XOR gelöscht haben. Beim Errechnen der neuen Position testen wir gegen die Randbegrenzungen ab und lassen den Soundchip einen Ton ausspucken, falls eine Kollision stattgefunden hat. Wenn das alles schnell genug geschieht, erscheint dem Beobachter die ruckweise Bewegung des Balls als fließend.

Genauso habe ich es im letzten Beispiel (Beispiel 3.5) gemacht.


/* ST Computer C - Kurs Beispiel 3.5 */ /* * Generiert einen sich bewegenden Ball auf dem Bildschirm * Geschwindigkeit kann mit den Cursortasten veraendert werden. * Programmende durch SPACE Taste */ /*****************************************/ /* INCLUDE FILES */ /*****************************************/ #include "inigem.h" /*****************************************/ /* GLOBALE VARIABLE */ /*****************************************/ short xball,yball,vxball,vyball; short xmin,ymin,xmax,ymax; short pMsrc[10], pMdes[10]; /*****************************************/ /* DEFINES */ /*****************************************/ #define LEFT_ADJUST 0 #define TOP_LINE 5 #define SIZE 64 #define VEL 10 #define Rnd() (short) xbios(17) /* Liefert 16-Bit Zufallszahl */ #define Dosound(s) xbios(32,s) /* Spielt Soundstring s */ #define Constat() gemdos(11) /* Zeigt ob Taste betaetigt */ #define Logbase() (short *) xbios(3) /* Liefert BS Adresse */ #define Wvbl() xbios(37) /* Wait vertical blanking */ #define Conin() (long) gemdos(1) /* Naechstes Zeichen von Tast */ /*---------------------------------------------------*/ main() { open_vwork(); v_clrwk(handle); hide_mouse(); Ball(); show_mouse(); close_vwork(); } /* MAIN() */ /*---------------------------------------------------*/ Ball() { register short x0, y0, finish; short r(); /* * Zuerst zeichnen wir zwei Kreise auf in die linke und rechte obere * Ecke und schreiben einen Text dazwischen */ vsf_interior(handle,2); vsf_style(handle,16); v_circle(handle,SIZE/2,SIZE/2,SIZE/2); v_circle(handle,639-SIZE/2,SIZE/2,SIZE/2); center_s ("*** BOUNCING BALL DEMO ***"); /* Initialisieren der Ballposition und Geschwindigkeit */ xmin = 0; ymin = SIZE; xmax = 639; ymax = 399; xball = r(0, xmax-SIZE); /* X-Position */ yball = r(ymin, ymax-SIZE); /* Y-Position •/ vxbal1 = r(-VEL, VEL); /* Geschwindigkeit in X-Richtung */ vyball = r(-VEL, VEL); /* Geschwindigkeit in Y-Richtung */ Blt(0,0,SIZE,SIZE,xball,yball,3);/* Kopieren des Balls in der */ /* linken oberen Ecke an die */ /* Anfangsposition. */ finish = 0; while (!finish) { x0=xball; y0=yball; /* Alte Poisition merken */ xball += vxball; /* Gegen linken Rand abtesten */ if (xball < 0) { xball - 0; vxball = -vxball; click(); } else if (xball > xmax-SIZE) { /* Gegen rechten Rand abtesten */ vxball = -vxball; xball += vxball; click(); } yball += vyball; if (yball<ymin) { /* Gegen oberen Rand abtesten */ yball = ymin; vyball = -vyball; click(); yball += vyball; } else if (yball>ymax-SIZE) ( /* Gegen unteren Rand abtesten */ vyball = -vyball; yball += vyball; click(vxball * vxball+vyball * vyball); } Blt(x0,y0,SIZE,SIZE.x0,y0,6); /* Loeschen an alter Pos. */ Blt{0,0,SIZE,SIZE,xball,yball,3); /* Kopieren an neue Pos. */ /* Frage tastatur ab */ if (Constat()) switch(Conin() >> 16) < /* Teste Scancode */ case 0x4b: vxball--; if (vxba11 < -VEL) vxball = -VEL; break; /* Cursor links */ case 0x4d: vxball++; if (vxball > VEL) vxball = VEL; break; /* Cursor rechts */ case 0x48: vyball--; if (vyball < -VEL) vyball = -VEL; break; /* Cursor rauf */ case 0x50: vyball++; if (vyball > VEL) vyball = VEL; break; /* Cursor runter */ case 0x39: finish = 1; /* Space */ } } /* While */ } /* BALL() */ /* ----------------------------------- */ short r(i,j) short i,j; { short k; k = Rnd() % (j-i+1); if (k<0) return(k+j+1); else return(k+i); } /* ----------------------------------- */ center_s (string) char *string; /* * Zentriert strir-.g am oberen Bildschirmrand. Setzt Zeichenhoehe auf SIZE. */ { short xoff, ce_w, ret; /* Dummy Variable. Wert wird nicht benoetigt */ vst_height(handle,SIZE,&ret,&ret,&ce_w,&ret); vst_alignment(handle,LEFT_ADJUST,TOP_LINE,&ret,&ret); xoff = (640 - (ce_w * strlen(string)))/2; /* Offset vom linken Rand */ v_gtext(handle,xoff,0,string); /* Gibt String aus */ } /* CENTER_S() */ /* ----------------------------------- */ char ballclick[26] = {0, 0x4b, /* Kanal A Periode low */ 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0xf6, /* Kanal A Ton und Rauschen anschalten */ 6, 0x10, /* Kanal A Huellkurve an */ 11, 0x80, /* Periode Huellkurve low */ 12, 1, /* Periode Huellkurve high */ 13, 3, /* Hue1lkurvenform */ 0xff,0 }; /* Soundende */ click() /* * Erzeugt Ton wenn Ball an Wand stoesst */ { Dosound(ballclick); } /* -------------------------------------------------------- */ Bit(xsrc,ysrc,wsrc,hsrc,xdes,ydes,mode) /* * Kopiert einen rechteckigen Block innerhalb des Bildschirms */ { short pxy[8]; pxy[0] = xsrc; pxy[1] = ysrc; pxy[2] = xsrc+wsrc; pxy[3] = ysre+hsrc; pxy[4] = xdes; pxy[5] = ydes; pxy[6] = xdes+wsrc; pxy[7] = ydes+hsrc; vro_cpyform (handle, mode, pxy, &pMsrc, &pMdes) ; } /* Ende Beispiel 3.5 */

Die main() Funktion initialisiert GEM. Dieser Rahmen sieht für alle Programme ziemlich gleich aus: Öffnen der virtuellen Workstation mit open_vwork(). Abschalten der Maus, während etwas gezeichnet wird, mit hide___mouse(). Dann Aufruf des Programms. Nach dessen Beendigung wird die Maus wieder eingeschaltet und die virtuelle Workstation geschlossen show__mouse() und close_vwork(). Die interessanteren Dinge spielen sich in der Funktion ball() ab. Durch verschiedene VDI-Aufrufe werden zwei ausgefüllte Kreise gezeichnet und ein Text dazwischen geschrieben. Das darunterliegende Rechteck wird dann als Speicherplatz für den Ball markiert (xmin,ymin,xmax,ymax). Mit Hilfe des uns bereits bekannten Zufallsgenerators wird eine zufällige Startposition (xball,yball) und eine zufällige Startgeschwindigkeit (vxball, vyball) bestimmt. Dann wird in einer while-Schleife verharrt, bis irgendwann durch Betätigen der SPACE-Taste die Variable finish einen Wert ungleich 0 erhält. In der Schleife wird die neue Position des Balls bestimmt. Falls er anstößt, wird mit Hilfe des Betriebssystemaufrufs Dosound() dem Soundchip übermittelt, was er machen soll. Der Ball wird dann an der alten Position gelöscht. Dies geschieht durch Kopieren auf sich selbst und einer gleichzeitigen XOR Verknüpfung mittels der Funktion BltO danach wird er wieder mittels Blt() von links oben an seine neue Position kopiert. In einem switch Statement wird geprüft, ob der Benutzer eine Pfeiltaste gedrückt hat und je nachdem die Variablen vxball und vyball beeinflußt. Die Blt()-Funktion greift auf den VDI-Aufruf vro_cpyform() zurück. Mit diesen Informationen sollte es Ihnen nicht schwerfallen, den Ablauf des Programms zu verstehen. Durch Ändern der Konstanten SI2E und VEL können Sie die Größe und maximale Geschwindigkeit des Balls beeinflussen.

Zum Schluß möchte ich Sie noch auf zwei inhaltliche Fehler hinweisen, die sich in den ersten beiden Teilen eingeschlichen haben. Im Teil 1, Seite 73, Ende zweiter Absatz steht: „...ist das ein sicheres Anzeichen dafür, daß Sie nach einem Funktionsaufruf ein Semikolon gesetzt haben." das muß natürlich heißen „...ist das ein ziemlich sicheres Anzeichen dafür, daß Sie nach einem Funktionskopf ein Semikolon gesetzt haben.“

Im Teil 2 muß bei der Beispielfunktion error() der default Fall der Switchanweisung so lauten:

help = „Unbekannte Fehlernummer“

also kein fprintf(), sonst gibt es bei einem unbekannten Fehler einen bösen Absturz. Warum, können Sie nach der Lektüre des 3. Teils bestimmt selbst herausfinden.

Bis zum Erscheinen des 4. Teils Ende August wünsche ich Ihnen erfolgreiches Programmieren.

Th. Weinstein

/*  Dies ist INIGEM.H   */
/*  Es uebermmmt die Initialisierung von    VDI und AES */
/*  Ein Programm hat dann die folgende  allgemeine Form */
/*  */
/*  #include "inigem.h" */
/*  . */
/*  . */
/*  . */
/*  main()  */
/*  {   */
/*      open_vwork();   */
/*          Hier kommt ihr Programm */
/*  close_vwork();  */
/*  */
/**********************************************/
/*                                            */
/*         Globale Variablen für GEM          */
/*                                            */
/**********************************************/

short contrl[12];
short shortin[128];
short shortout[128]; 
short ptsin[128]; 
short ptsout[128];

short work_in[12];  /*  Parametervektoren für 'open_vwork'-Aufruf */
short work_out[57];

short handle:   /*  Workstation handle */
/**********************************************/
/* Einige nuetzlich Hilfsfunktionen           */ 

open_vwork()
{
    short i; 

    appl_init();
    for ( i = 0; i < 10; work_in[i++] = 1 ); 
    work_in[10] = 2:
    v_opnvwk(work_in, &handle, work_out );
}
/**********************************************/

close_vwork()
{
    v_clsvwk( handle );
    appl_exit();
}
/**********************************************/

static int hidden;

hide_mouse()
{
    if(! hidden){
        graf_mouse(265,0x0L);
        hidden=1;
    }
}

show_mouse()
{
    if(hidden){
        graf_mouse(257,0x0L);
        hidden=0:
    }
}

/**********************************************/
/* set clipping rectangle   */
/**********************************************/

set_clip(x.y,w,h) 
short x,y,w,h:
{
short clip[4];
    clip[0]=x;
    clip[1]=y;
    clip[2]=x+w-1;
    clip[3]=y+h-1;
    vs_clip(handle,1,clip);
}

/* Fuegen Sie hier bei Bedarf weitere haeufig vorkommende Funktionen an */
/* Ende von INIGEM.H */

Links

Copyright-Bestimmungen: siehe Über diese Seite
Classic Computer Magazines
[ Join Now | Ring Hub | Random | << Prev | Next >> ]