Abgekoppelt - Von Basic nach C, Teil 2

In diesem Teil des Kurses beschäftigen wir uns mit der Behandlung von Variablen und den wichtigen Kontrollstrukturen in beiden Sprechen. Außerdem erfahren Sie näheres über den Umgang mit Pointern und Vektoren.

Ein Ausdruck im C-Listing des ersten Beispiels aus Teil 1 unseres Kurses, der keine direkte Entsprechung im GFA-Basic-Listing zu besitzen scheint, ist »double aD;«, die Definition der Fließkommavariablen »aD«. Ihr weist der Compiler 10 Byte Speicher zu. Da GFA-Basic-Programmierer sich nicht selbst um die Anmeldung von Variablen kümmern müssen, führen wir zwei Begriffe ein, die in C große Bedeutung haben. Eine »Deklaration« legt eine Variable bestimmten Typs fest, ohne jedoch für diese Speicher anzufordern. Eine »Definition« meidet eine Variable an und reserviert gleichzeitig ausreichend Speicher.

Variablentypen

GFA-Basic kennzeichnet den Typ einer Variable durch eine entsprechende Endung. Fehlt diese, gilt der Typ »Fließkomma«. In C dienen Definitionen zur Bekanntmachung von Variablen. Wir hängen in den C-Listings in diesem Kurs Variablen zusätzlich Großbuchstaben an, die uns Hinweise auf deren Typ geben. Dies ist in C zwar nicht nötig, hat aber den Vorteil, daß wir über den Typ einer Variable Gewißheit haben, ohne in der Deklarations- oder Definitionsliste zu suchen. Das gleiche Verfahren verwendet der »Basic Konverter nach C«. Die Entsprechungen der Variablentypen entnehmen Sie der Tabelle. Außer diesen Variablen kennt C noch die Typen »unsigned int« 16 Bit (ohne Vorzeichen) und »unsigned long« 32 Bit (ohne Vorzeichen).

Variablen-Speicherklassen

In GFA-Basic gibt es zwei Speicherklassen von Variablen: »lokale« und »globale« Variablen. In C gibt es außer diesen beiden Speicherklassen noch »statische«- und »Registervariablen«. In GFA-Basic definieren Sie globale Variablen bei deren Verwendung in einem beliebigen Ausdruck, sofern sie nicht in einer Prozedur durch »LOCAL var« explizit als lokal definiert sind. In C gelten alle außerhalb von Funktionsblöcken definierte Variablen als global.

double aD;	/* Definition einer globalen Double-Variablen */
main(void)
{
	...
}

Auf diese Variablen greifen sämtliche Funktionen aller Module zu. Anmerkung: Als Modul bezeichnen wir eine Datei, die Teile des Gesamtprogrammes enthält. In C ist es möglich, Programme in viele einzelne Module aufzusplitten und so die Compilierzeiten zu verkürzen. Der Linker bindet die compilierten Module (Endung ».O«) zum fertigen Programm zusammen. Der Zugriff auf globale Variablen, die in einem anderen Modul definiert sind, erkennt der C-Compiler an dem vorangestellten Wort »extern« bei der Deklaration:

extern double aD;	/* Deklaration: aD ist an anderer Stelle definiert */

Globale Variablen erhalten sowohl in GFA-Basic wie auch in C beim Programmstart den Wert Null. Sollen sie zu Beginn andere Werte annehmen, so initialisieren Sie diese wie folgt:

double aD=1.234;	/* Definitionen und Initialisierungen */
int aI=1;

Autovariablen (lokale Variablen)

Der GFA-Basic Befehl »LOCAL a%« definiert innerhalb einer Prozedur eine lokale 32-Bit Variable, die nur in der Prozedur und deren Unterprozeduren Geltung hat. Definiert man in C Variablen zu Beginn innerhalb eines Blockes (meist Funktionsblock), heißen diese Autovariablen oder lokale Variablen. Analog zu GFA-Basic gelten diese Variablen nur im jeweiligen (Funktions-)Block und werden automatisch beim Aufruf der Funktion auf dem Stack erzeugt. Bei Namensüberschneidungen zwischen einer lokalen und globalen Variable hat die lokale Variable höhere Priorität.

double aD = 1;	/* Definition und Initialisierung der globalen Variablen aD */
main()
{
	printf ("%G", aD);	/* Ausgabe: 1 */
	func ();	/* Funktionsaufruf */
	printf ("7.G", aD);	/* Ausgabe: 1 */
	void funC(void)
	{
		double aD = 0;	/* Definition und Initialisierung der lokalen Variablen aD */
		printf ("%G",aD);	/*Ausgabe: 0 */
	}
}

Wichtige Unterschiede in der Behandlung lokaler Variablen in GFA-Basic und C: Während der Compiler globale Variablen beim Programmstart initialisiert, haben lokale Variablen in C (auch im Gegensatz zu GFA-Basic) keine definierten Anfangswerte - eine große Fehlerquelle, insbesondere für C-Neulinge. In C gelten lokale Variablen im Gegensatz zu GFA-Basic immer nur in dem Funktionsblock, in dem sie definiert sind - niemals in Unterprozeduren, die diese Prozedur aufruft. Ferner können in C lokale Variablen in jedem beliebigen durch »{« und »}« eingeschlossenen Block definiert sein:

{
	int aI;
	...
}

Diese Variablen heißen Blockvariablen. Sie verlieren ihre Gültigkeit beim Verlassen des Blockes. Register- und statische Variablen Registervariablen sind nur in C zugänglich. Das Programm

main(void)
{
	register long aL;
	...
}

definiert die Autovariable »aL« (lokal), die der CompiLer nach Möglichkeit in einem Register speichert. Dies führt meist zu Geschwindigkeitssteigerung. Auch statische Variablen sind nur in C vorgesehen.

static int aI;	/* Definition einer statischen 16 Bit Integer-Variablen */

Statische Variablen lassen sich als lokale und globale Variablen bilden. Global definiert, sind sie nur innerhalb des betreffenden Moduls bekannt. Dies schränkt also den Geltungsbereich einer globalen Variablen ein. Wird sie dagegen als Autovariable angelegt, gilt sie im betreffenden Block, behält jedoch ihren Wert stets bei, selbst wenn dieser Block verlassen wird. Bei erneutem Eintritt ist der Wert wieder abrufbereit. Wird eine statische Variable initialisiert, so tritt dies nur beim ersten Mal ein.

In fast keiner Sprache lassen sich Zeichenketten (Strings) so leicht handhaben wie in Basic. Dennoch hat die Basic-Stringverwaltung auch Nachteile. Strings müssen immer als ganzes angesprochen werden. Bei der Bearbeitung einzelner Zeichen eines Strings ist man gezwungen, umständliche Funktionsaufrufe durchzuführen. Zu Problemen führt auch die »Garbage Collection«, die Stringleichen aus dem Speicher verbannt und für freien Speicherplatz sorgt. Hierdurch ändern sich gelegentlich Stringadressen, was zu Fehlern führt, falls der Programmierer Maschinenprogramme oder speicherabhängigen Code eingelagert hat. GFA-Basic speichert in einer Stringvariablen beliebige Zeichen bis zu einer maximalen Länge von 32767 Byte, ohne daß der Benutzer an die Speicherplatzbeschaffung denken muß.

Die Länge des Strings schwankt demnach sehr stark - GFA-Basic sorgt für den notwendigen und optimalen Speicher. Dieses Verfahren nennt sich »dynamische Stringverwaltung«. Ein solch großartiger Komfort für Strings steht dem C-Programmierer nicht zur Verfügung. Die Verwaltung von Zeichenketten liegt hier in der Verantwortung des Programmierers. Er muß sich darüber Gedanken machen, ob ausreichend Speicherplatz für jede Zeichenkette vorhanden ist und dieser bei Kopieraktionen nicht überschritten wird. Allerdings gibt es auch Bibliotheken für C, die diesen Komfort bieten.

Vektoren und Pointer

Die Sprache C kennt zwei Arten, auf Zeichenketten zuzugreifen: Vektoren und Pointer. Ein Vektor wird wie folgt definiert:

char string_S[6];	/* Definition eines Pointers */

Hierdurch reserviert der Compiler ab der Stelle »string_S« 6 Byte Speicherplatz. Oft bezeichnet man Vektoren auch als eindimensionale Arrays oder Felder. Durch die Definition

char string_S[] = „Hallo“;

mit gleichzeitiger Vorbesetzung (Initialisierung) erhält »string_S« die Zeichenkette »Hallo«, die der Compiler automatisch mit einem Null-Byte abschließt. Hierbei darf die Angabe der Vektorgröße entfallen, da C den benötigten Speicherplatz selbst berechnet. Im Speicher steht ab Adresse string_S: »H«, »a«, »l«, »l«, »o«, »\0«. Das Zeichen 0 (» \0«) hat eine Sonderstellung, da es das Ende eines Strings markiert. Alle C-Stringfunktionen erwarten dieses Zeichen als Abbruchbedingung für ihre Aktionen. Über »string_S[x]« (mit x von 0 bis 5) haben wir direkten Zugriff auf die einzelnen Bytes des Strings. »string_S« selbst ist identisch mit &string_S[0], der Adresse des ersten Zeichens der Zeichenkette. Den Befehl »&string_S[2]« wandelt der C-Compiler automatisch in »string_S+2«. Felder lassen sich mit allen Variablentypen bilden, so definiert »int i_f[2];« ein 16-Bit-Integerfeld und reserviert 2 mal 2 Byte Speicherplatz. Wichtig: Vektoren wie »string_S« und »i_f« sind durch numerische Operationen nicht veränderbar. Näheres dazu im nächsten Teil, in dem wir uns näher mit numerischen Operationen beschäftigen.

Die Flexibilität von C liegt vor allem in der Fähigkeit, durch Angabe der Adressen (Pointer) von Variablen auf diese zuzugreifen - oder, im C-Jargon: Variablen zu »referenzieren«. Pointer und Vektoren stehen in vielerlei Hinsicht in enger Beziehung zueinander. Pointer lassen sich aber im Gegensatz zu Vektoren numerisch verändern. Ein sehr oft zu Fehlern führender Unterschied zwischen Pointern und Vektoren in C, der auch aus dem C-Standardwerk von Kernighan & Ritchie bei der Einführung des Pointerkonzepts nicht klar hervor geht: Der C-Compiler liefert bei der Definition eines Pointers nur Platz für den Pointer selbst sind nicht für dessen Inhalt (etwa einer Zeichenkette), während bei Vektoren Speicherplatz für die Feldelemente reserviert wird. Die Definition »char *stringS;« definiert »stringS« als einen Pointer auf ein Zeichen (char). Hierbei ist »*stringS« der Inhalt, auf den der Pointer »stringS« zeigt. Der Compiler reserviert also lediglich 4 Bytes im Speicher - gerade genug, um eine Adresse aufzunehmen. Nicht »*stringS«, das heißt nicht der Inhalt von »stringS« wird definiert, sondern nur »stringS« selbst. Bildlicher ausgedrückt: Durch die Definition »char *stringS« sagt man C: »Reserviere mir eine Adresse, die (später) auf eine Speicherzelle zeigen soll, in der sich genau 1 Byte befindet.« Wichtig: C setzt jetzt voraus, daß der Programmierer den Speicherplatz für dieses Byte noch beschaffen wird.

Den Zugriff auf den Inhalt einer Adresse (zum Beispiel ein Zeichen), auf die ein Pointer zeigt, realisiert der Operator »*«. Diese Operation heißt auch »Dereferenzierung«. Für Assembler-Kundige: Intern laufen jetz Zugriffe auf »stringS« mit der Assembler-Größenspezifikation ».B«. Wie bei Vektoren lassen sich Pointer auf alle Variablentypen definieren. Durch »int *i_f;« reserviert C ebenfalls nur Speicherplatz für »i_f«, führt jedoch Operationen mit der Assembler-Größenspezifikation ».W« durch. Analoges gilt für die Definition »long *l_f;«. Um sinnvoll mit Pointern arbeiten zu können, muß der Programmierer zunächst Speicherplatz für die Variable selbst beschaffen: 1. Möglichkeit: Statische Speicherplatzbeschaffung über Vektoren:

char string_S[256];
char *stringS = string_S;

Der Vektor »string_S« erhält vom C-Compiler 256 Byte Speicher zugeteilt. Der Pointer »stringS« erhält 4 Byte reserviert. Er zeigt auf »string_S« beziehungsweise »&string_S[0]«, also die Adresse des ersten Zeichens im String. Zweiter Weg: die dynamische Speicherplatzbeschaffung durch Betriebssystemfunktionen:

char *stringS;
string_S = (char *) malloc ( 256*sizeof(char));

Die C-Bibliotheksfunktion »malloc()« reserviert für »stringS« 256 Byte. Damit zeigt der Pointer »stringS« auf diesen Speicherbereich, der bis zur Speicherfreigabe durch »free(stringS)« geschützt ist. Da malloc() einen typenlosen (generischen) Zeiger (void *) zurückliefert, steht der »Castoperator (char *)« vor der Funktion, um C die korrekte Typumsetzung der Pointer zu ermöglichen (näheres zum Thema Castoperatoren in Teil 4). In Kürze: Der Typ »void *« steht für »ein Zeiger auf irgendeinen Typ«. Anmerkung: Da Pointerarithmetik in der Regel schneller als Vektorarithmetik arbeitet, sollten Sie mit Pointern statt mit Vektoren arbeiten.

Tips zur Vermeidung von Fehlerquellen:

Verwechseln Sie niemals die Definitionen »char *stringS = 'Hallo';« und »char string_S[] = 'Hallo‘;«. Im 1. Fall definieren Sie lediglich einen Pointer auf eine beliebig verfügbare, konstante Zeichenkette, die der C-Compiler in den DATA-Bereich legt. Im 2. Fall melden Sie ein genügend großes Feld an, in das der Compiler die Zeichenkette »Hallo« inklusive des abschließenden Nullbytes einkopiert. Jede konstante Zeichenkette im ersten Fall könnte durch Zusammenlegung mehrerer Strings auch an andere Pointer vergeben werden. Änderungen betreffen dann alle anderen Stellen - die Ausmaße sind verheerend. Achten Sie bei Strings immer darauf, daß auch für das abschließende Nullbyte Speicherplatz reserviert ist.

Operationen mit Pointern und Vektoren

Pointer lassen sich durch numerische Operationen verändern. Statt durch Vektorindizes auf Elemente eines Feldes zuzugreifen, sprechen wir Feldelemente durch Erhöhen beziehungsweise Erniedrigen des Pointers an. In GFA-Basic erhöht der Operator »INC a%« die Variable a% um den Wert 1, »DEC a%« erniedrigt a% um diesen. Entsprechende Operatoren sind auch für C vorhanden: »aL++« beziehungsweise »aL--«. Bei Verwendung dieser Operatoren auf Pointer paßt C automatisch die Änderung des Pointers an den Variablentyp des Feldelements an. Voraussetzung dafür ist, daß Pointer und Vektor gleichen Typs sind und der Speicherbereich nicht verlassen wurde. Die viel verwendete Schleife »WHILE...WEND« in GFA-Basic, die wir in einem der nächsten Beispiele benötigen, sieht in C wie folgt aus:

while (bedingung)	/* Operationen mit Vektoren und Pointern */
	char a_S[6] = „Hallo“;	/* oder char a_S[] = „Hallo“; */
	char *aS, z;
	aS = &a_S[0];	/* bzw. aS = a_S; --aS zeigt jetzt auf das Zeichen 'H' */
	z = *aS;	/* 'H' wird nach z kopiert -- auch möglich: z = a_S[0]; */
	z = *(as + 1);	/* ‚a‘ wird nach z kopiert -- auch möglich: z = a_S[1]; */
	z = *as + 1;	/* 'H' + 1, d.h. 'I' wird nach z kopiert */

Im Ausdruck »*aS++« dereferenzieren wir zunächst auf den Wert *aS und erhöhen anschließend den Pointer um 1 Byte. Da C die Operatoren von links nach rechts bewertet, müssen wir bei diesem Ausdruck keine Klammern setzen. Möchte man dagegen den Wert *aS erhöhen, sind Klammern erforderlich:

z = (*aS) ++;	/* erhöht den Wert *aS um 1, z.B. von ‚a‘ nach ‚b‘ */
Stringoperationen in GFA-Basic und C:
! GFA-BASIC:
a$ = “Hallo“
b$ = a$
PRINT b$
~INP(2)
/* Umsetzung nach C durch Vektoren */
#include <stdio.h>
char a_S[] = „Hallo“;	/* Reservierung von 5+1 Byte Speicher */
char b_S[6];		/* Reservierung von 6 Byte Speicher */
main(void)
{
	int iI = 0;	/* Laufindex */
	while (a_S[iI])	/* Schleife bis a_S ohne die ‚/0‘ */
		b_S[iI] = a_S[iI++];	/* nach b_S kopiert wurde */
		b_S[iI] = a_S[iI];	/* nun das Nullbyte kopieren */
		printf ("%s", b_S);
		getchar();	/* wartet auf ein Zeichen: vgl. INP (2) */
	return (0);	/* Programmende */
}
/ * Umsetzung nach C durch Pointer * /
#include <stdio.h>
char a_S[] = “Hallo“;	/* Reservierung von 5+1 Byte Speicher */
char *aS;
char b_S[6];		/* Reservierung von 6 Byte Speicher */
char *bS;
main(void)
{
	aS = a_S;		/* aS wird auf den Vektor a_S gerichtet */
	bS = b_S;		/* bS wird auf den Vektor b_S gerichtet */
	while (*a)	/* Schleife bis a_S ohne die ‚\0‘ */
		*bS++ = *aS++;	/* nach b_S kopiert wurde */
		*bS = *aS;	/* nun das Nullbyte kopieren */
		printf ("%s", b_S);
		getchar();	/* wartet auf ein Zeichen: vgl. INP(2) */
	return(0);	/* Hier wird das Programm beendet */
}

Einen dem Basic entsprechenden Stringzuweisungsoperator »=« gibt es in C nicht. Strings kopieren wir in C immer byteweise. Die Operation »bS = aS« hätte lediglich den Pointer »bS« auf den Vektor »a_S« gerichtet, nicht jedoch die Zeichenkette nach »b_S« kopiert. Der Ausdruck »*bS++ = *aS++« arbeitet sich Byte für Byte vor, da »bS« und »aS« beides Pointer auf eine Stringvariable sind. Hätten Sie fälschlicherweise »bS« als »int *bS«definiert, würde der C-Compiler eine Warnung ausgeben. Einfacher geht‘s mit der Bibliotheksfunktion »strcpy()«. Sie nimmt uns die Kopierarbeit ab.


/* Umsetzung nach C durch Vektoren und die Kopierfunktion strcpy () */
#include <stdio.h>
#include <string.h>
char a_S[] = "Hallo";	/* Reservierung von 5+1 Byte Speicher */
char b_S[6];	/* Reservierung von 6 Byte Speicher */
main(void)
{
	strcpy (b_S, a_S);	/* kopiert a_S nach b_S */
	printf ("%s", b_S);
	getchar();	/* wartet auf ein Zeichen: vgl. INP(2) */
	return (0);	/* Hier wird das Programm beendet */
}

Bitte beachten Sie, daß sowohl »aS« wie auch »bS« nach der Kopieraktion hinter den Speicherbereich der Vektoren zeigen. Eine Ausgabe von »aS« oder »bS« über »printf()« hätte somit wahrscheinlich fatale Folgen, denn »printf()« würde nach dem abschließenden Nullzeichen fahnden, das jedoch gerade überlesen wurde - und ob sich in erreichbarer Nähe eine Null befindet, wissen die Götter. Zum Schluß zwei »Hausaufgaben«:

  1. Aufgabe: Realisieren Sie eine Stringkopierfunktion, die gleichzeitig den Ergebnisstring umdreht.
  2. Aufgabe: Realisieren Sie den GFA-Basic-Befehl INSTR (a$,b$) mit Hilfe von Pointern und Vektoren.

Die Umsetzung dieser sehr einfachen Stringoperationen erhebt selbstverständlich nicht den Anspruch, auch nur annähernd soviel Komfort zu schaffen, wie sie GFA- Basic oder die Funktionen des »Basic nach C Konverters« bieten. (ah)

Variablentypen von GFA-Basic und C

GFA

C

Typ

Datenbreite

Integervariablen:

!

B

char

8 Bit(mit Vorzeichen)

1

U

unsigned char

8 Bit(ohne Vorzeichen)

&

1

(short) int

16 Bitfmit Vorzeichen)

%

L

long (int)

32 Bitfmit Vorzeichen)

Fließkommavariablen:

#

F

float

32 Bit Fließkommaformat

#

D

double

64/80 Bit Fließkommaformat

Stringvariablen:

$

s

char *,char ...[]

variabel (String)

Tabelle: Die Endungen der Variablen in C sind zur Hilfestellung gedacht


Martin Hanser
Aus: TOS 06 / 1991, Seite 90

Links

Copyright-Bestimmungen: siehe Über diese Seite