3.3. Variablen

Im Gegensatz zu Konstanten ist der Wert von Variablen während der Ausführung eines Programms veränderbar. Variablen müssen prinzipiell einem bestimmten Datentyp zugehören, damit von Anfang an feststeht, wieviel Speicher für eine bestimmte Variable reserviert werden muß und welche Daten in ihr gespeichert werden können. Für Variablen stehen neben beliebigen, abgeleiteten Datentypen die vier elementaren Datentypen char, int, float und double zur Verfügung. Für Pointer-Variablen existiert noch zusätzlich der Typ void. Hier fällt Ihnen vielleicht auf, daß C im Gegensatz etwa zu Pascal oder BASIC über keinen eigenen Variablentyp für Zeichenketten verfügt. Trotzdem kann auch C Zeichenketten in Variablen ablegen - dazu sind allerdings Arrays oder Pointer erforderlich.

Variablen werden dem Compiler über die Bekanntgabe ihres Namens und ihres Datentyps vorgestellt. Diesen Prozeß nennt man Deklaration bzw. Definition.

Deklaration versus Definition

Sowohl Variablen als auch Funktionen werden deklariert oder definiert. Vorerst sollen uns nur die Variablen beschäftigen; allerdings gelten für die Funktionen die gleichen Konzepte.

Der Unterschied zwischen Deklaration und Definition einer Variablen besteht darin, daß sich die Deklaration ausschließlich darauf beschränkt, die Attribute einer Variablen (Name, Datentyp) dem Compiler bekannt zu machen. Bei der Variablendefinition hingegen wird zusätzlich das Datenobjekt erzeugt, d.h. Speicher reserviert, der die Variable aufnimmt. Variablen können bei der Definition, nicht jedoch bei der Deklaration mit einem Anfangswert belegt werden. Deklarationen von Variablen beziehen sich immer auf Variablen, die bereits anderswo definiert wurden.

In der Literatur wird oft zwischen Deklaration und Definition von Variablen nicht genau unterschieden, und beide Begriffe werden mehr oder weniger synonym gebraucht. Deshalb ist bei der Definition von Variablen häufig - zu Unrecht - von Deklaration die Rede. Näheres zur Deklaration von Variablen findet sich im Abschnitt "Speicherklassen" unter dem Schlüsselwort extern.

Definition von Variablen

Die Definition von Variablen vollzieht sich in folgender Form:


Datentyp Variable_1,Variable_2,..., Variable_x;

Die Liste der zu definierenden Variablen besteht aus Variablennamen, die durch Kommas getrennt sind. Am Ende jeder Variablendefinition steht ein Semikolon. Jeder Variablen kommt derjenige Datentyp zu, der durch das Schlüsselwort vor der Variablen(liste) bezeichnet wird.


.
.
{
/* Hier werden drei Variablen vom Typ integer und eine
 * float Variable definiert. 
 * Bei dieser Schreibweise ist es allerdings schwierig, 
 * erklärende Kommentare anzubringen. Deshalb sollte man
 * besser für jede Variable eine eigene Zeile spendieren.
 */
int iDay, iMonth, iYear;
float fIncome;			/* Einkommen in DM	*/
.
.
}

In diesem Programmfragment könnte zugunsten besserer Übersichtlichkeit jede Variable separat definiert werden:


.
.
{
int iDay;			/* Anzahl der Tage */
int iMonth;			/* Anzahl der Monate */
int iYear;			/* Kalenderjahre	*/
float fIncome;			/* Einkommen in DM	*/
.
.
}

Wenn Variablen innerhalb eines Anweisungsblocks definiert werden, muß dies gleich nach der öffnenden geschweiften Klammer passieren. Das bedeutet, daß die Definition von Variablen gleich am Beginn von Funktionen und Anweisungsblöcken erfolgen muß und nicht irgendwo zwischen den Anweisungen geschehen darf. Das Einfügen von Leerzeilen und Kommentaren vor den Variablendefinitionen ist jedoch erlaubt (und im Sinne der Lesbarkeit sogar erwünscht!), denn beide werden ja vom Compiler ignoriert.

Innerhalb des gleichen Geltungsbereichs dürfen nicht mehrere Variablen mit demselben Namen definiert werden:


.
.
{
/*********************************************************
 ***		Woher soll der Compiler bei der Verwendung	  *** 
 ***		einer dieser Variablen wissen, welche        *** 
 ***		gemeint ist ?                                ***
 *********************************************************/

int count;
float count;
.
.
}

Initialisierung: Variablen erhalten Anfangswerte

Bei der Definition kann einer Variablen gleich ein Anfangswert zugewiesen werden: Man nennt diesen Vorgang Initialisieren einer Variablen. Dazu wird hinter dem Variablennamen ein Gleichheitszeichen (Zuweisungsoperator, siehe Kapitel 4) angefügt, gefolgt von dem Wert, den die Variable erhalten soll. Dieser soll natürlich mit dem Datentyp der Variablen verträglich sein. Es ist übrigens gar kein schlechter Stil, Variablen generell zu initialisieren. Werden nämlich Variablen nicht explizit initialisiert, dann weist ihnen C - im Gegensatz zu vielen interpretierenden Sprachen - keinen einheitlichen Anfangswert zu. Sie enthalten statt dessen den Wert, der gerade zufällig in den betreffenden Speicherzellen steht. Initialisierung schützt vor der Verwendung von Variablen unbekannten Inhalts.


.
.
.
{
/* nCount wird definiert und mit dem Anfangswert 1 belegt		*/
int iCount=1;

/* Die Variable fTimeUsed wird durch die Zuweisung des
 * Werts der Fließkomma-Konstanten 0.0f initialisiert */
float fTimeUsed=0.0f ;

/* cKeyPressed erhält gleich bei der Definition den
 * numerischen Wert von 'A' (bei ASCII der Wert 65) */
char cKeyPressed='A';
.
.
}

Bei der letzten der drei Variablendefinitionen scheint es, als läge eine Unverträglichkeit zwischen dem Datentyp char der Variable cKeyPressed und dem Wert vor, mit dem diese Variable initialisiert wird. Schließlich konnten wir uns ja überzeugen, daß Zeichenkonstanten allgemein der Typ integer eigen ist. Da sich aber der Compiler über diese Initialisierung nicht beschwert, müssen wir annehmen, daß C in der Lage ist, Werte, die einer Variablen zugewiesen werden, automatisch an den Variablentyp anzupassen. Tatsächlich nimmt C, wenn nötig, eine implizite Typenkonvertierung vor. Leider handelt es sich dabei aber um keinen Mechanismus, der dem Programmierer das Nachdenken über die Eigenart der einzelnen Datentypen und deren Verträglichkeit untereinander abnimmt. Eine automatische Typenkonvertierung, die Folge unbedachter Zuweisungen ist, kann ungewollte und überraschende Ergebnisse nach sich ziehen. Näheres zu den Regeln, nach denen C die Typumwandlung vornimmt, sind im Kapitel 4 beschrieben.

Modifizieren der elementaren Datentypen: short, long, signed, unsigned

Mit Ausnahme des Typs void kann die Bedeutung elementarer Datentypen durch Voranstellen der Schlüsselwörter short, long, signed und unsigned verändert werden. Es ist jedoch zu beachten, daß nicht alle Kombinationen aus Datentypen und Modifizierern zulässig sind. Es liegt auf der Hand, daß die sich widersprechenden Schlüsselwörter der Paare long - short und signed - unsigned nicht gemeinsam in einer Definition verwendet werden können.

Beispiel:


.
.
{
long int lResult;  /* Verwendung von "int" ist optional	*/
unsigned char cKeyPressed;
unsigned short int usResult=0; /* "int" ist optional */
signed short int sSalary;	 	/* identisch mit "short" */

.
.
}

long und short dienen dazu, den Wertebereich eines dafür vorgesehenen Datentyps zu erweitern bzw. zu reduzieren. long kann zusammen mit int oder double verwendet werden, short nur mit int. Ein long int hat die Größe von 4 Byte und kann als vorzeichenloser Typ ganze Zahlen bis zum maximalen Wert 4294967295 aufnehmen.

Wird der Typ double um long ergänzt, wird double um einen von ANSI nicht festgelegten Bereich erweitert; üblich für long double sind insgesamt 10 Byte.

Die Spezifizierung von int durch short erzwingt einen Datentyp in der Größe von 2 Byte.

signed und unsigned sind dazu bestimmt, einen Datentyp als vorzeichenbehaftet oder als vorzeichenlos auszuweisen. Ein als signed spezifizierter Datentyp kann im Gegensatz zu einem Typ unsigned negative Werte annehmen.

Die vier Schlüsselwörter short, long, signed und unsigned bewirken in Kombination mit den dafür vorgesehenen elementaren Datentypen folgende Modifikation:

Schlüsselwort Beschreibung
long Erweitert den Typ int auf 4 Byte und den Typ double um eine nicht einheitlich definierte Länge (mindestens aber größer als double). Kann nicht zusammen mit short verwendet werden.
short Reduziert den Typ int auf 2 Byte. Kann nicht zusammen mit long verwendet werden.
unsigned Bewirkt, daß die Variable nur positive Werte aufnehmen kann. Erlaubt sind Kombinationen mit char, long int und short int.

Die Differenz zwischen vorzeichenbehafteten und vorzeichenlosen Datentypen besteht in der unterschiedlichen Bewertung des höchstwertigen Bits. Dieses wird bei Datentypen der Sorte signed als Vorzeichenbit reserviert. Ein negatives Vorzeichen wird durch den Wert 1, ein positives durch den Wert 0 angezeigt.

Negative Zahlen werden zumeist unter Verwendung des sogenannten Zweierkomplements dargestellt. Dabei werden alle Bits einer Zahl (mit Ausnahme des Vorzeichenbits) auf ihren komplementären Wert gesetzt (also alle 0 auf 1 und umgekehrt), der gesamten Zahl wird sodann 1 hinzuaddiert und schließlich erhält das Vorzeichenbit den Wert 1.

1111111111111111 stellt in binärer Schreibweise unter Verwendung des Zweierkomplements die Integer-Zahl -1 dar. Warum?

Die Zahl 1 ohne Berücksichtigung des Vorzeichens binär anschreiben:

0000000000000001
Alle Bits mit Ausnahme des Vorzeichenbits invertieren:

0111111111111110
Den Wert 1 hinzuaddieren:

0111111111111111
Das Vorzeichenbit setzen:

111111111111111

Jetzt wird auch klar, welchen Einfluß die Verwendung des Zweierkomplements auf den Wertebereich von vorzeichenbehafteten Datentypen hat: Das höchste Bit entfällt für die Darstellung des numerischen Werts und damit verbleiben bei einer short int-Zahl dafür nur mehr 15 Bits.

Während also beim Typ unsigned short int alle 16 Bits für den numerischen Wert herangezogen werden (der sich dann bei 2 hoch 16 möglichen Zahlen auf maximal 65535 beläuft), bewegt sich der Wertebereich bei signed short int zwischen -32767 und +32767. Ensprechendes gilt natürlich auch für die Datentypen long int und char.

Die Frage, was passiert, wenn bei einer Variablen vom Typ unsigned short int der Wert 65535 um 1 erhöht wird, sollte im Wissen um die interne Repräsentation der numerischen Datentypen keine Rätsel mehr aufgeben. Da 65535 der maximale Wert für diesen Typ ist und damit alle 16 Bits bereits 1 sind, ergibt diese Addition den binären Wert 10000000000000000.

Da dieser Wert allerdings innerhalb von 2 Byte nicht mehr unterzubringen ist, wird das höchstwertige Bit abgeschnitten, und das Ergebnis dieser Operation ist dann 0.

Folgende Tabelle gibt einen Überblick über den minimalen Wertebereich der von ANSI definierten Datentypen:

Datentyp übliche Größe in Bits minimaler Wertebereich
char 8 -127 bis 127
unsigned char 8 0 bis 255
signed char 8 -127 bis 127
int 16 -32767 bis 32767
unsigned int 16 0 bis 65535
signed int 16 gleich wie int
short int 16 gleich wie int
unsigned short int 16 0 bis 65535
signed short int 16 gleich wie short int
long int 32 -2 147 483 647 bis 2 147 483 647
signed long int 32 gleich wie long int
unsigned long int 32 0 bis 4 294 967 295
float 32 auf 6 Stellen genau
double 64 auf 10 Stellen genau
long double 128 auf 10 Stellen genau

Redundanzen ergeben sich, wenn eines dieser vier modifizierenden Schlüsselwörter den Normalfall eines Datentyps bestätigt; so haben die Typen char und integer von Haus aus ein Vorzeichen, was nicht durch zusätzliches Voranstellen von signed bei der Definition bekräftigt werden muß (signed int ist idententisch mit int, signed char identisch mit char).

Da short nur in Kombination mit int definiert ist, kann als Abkürzung für short int nur short geschrieben werden; umgekehrt ist long alleine gleichbedeutend mit long int. Allerdings schadet es nicht, wenn man im Sinne besserer Lesbarkeit auf die Kurzschreibweise verzichtet und einem short oder long ein int hinzufügt.

Eine doppelte Bestimmung eines int-Datentyps ergibt sich bei der Verwendung entweder von short oder long immer: Je nach der Beschaffenheit von int führt eines der beiden Schlüsselwörter zu keiner Änderung des elementaren Datentyps. Auf 16-Bit-Systemen wie DOS oder Windows 3.x belegt int 2 Byte im Speicher, die Spezifizierung von int durch short bliebe daher in diesen Umgebungen folgenlos. Unter den meisten UNIX-Systemen umfaßt int hingegen 4 Byte, was identisch mit long int ist. Wenn man freilich sichergehen will, daß ein ganzzahliger Datentyp über die Größe von 2 Byte verfügt, sollte das Schlüsselwort short nicht fehlen: Beim Portieren auf ein anderes System bleiben Ihnen dann unnötige Anpassungsarbeiten erspart. Analoges gilt natürlich auch für long.

Änderung der Zugriffsbedingungen: const und volatile

Beide Schlüsselwörter wurden erst mit dem ANSI-Standard eingeführt. Ihre Verwendung bei der Definition oder Deklaration von Variablen teilt dem Compiler mit, daß bei der Veränderung der Werte für solche Variablen besondere Bedingungen gelten.

Das Schlüsselwort const bewirkt, daß der Wert einer Variablen vom Programm nicht geändert werden darf.

Dem C-Programmierer eröffnet sich auf diese Weise eine weitere Möglichkeit, eine Konstante zu definieren. Eine solcherart definierte Konstante kann innerhalb des Programms wie eine Variable verwendet werden, es darf nur nicht versucht werden, ihr einen Wert zuzuweisen.

Da jede Variable in C nach ihrer Definition einen Zufallswert enthält, muß es natürlich einen Weg geben, auch einer const-Variablen zumindest den gewünschten unveränderlichen Wert zuzuweisen. Deshalb ist die Initialisierung einer solchen Variablen gleich bei der Definition zulässig.


.
.
{
const unsigned char uchRichtig = 'X';

/********************************************************
 ***** Diese folgende Zuweisung ist unzulässig. Der Anfangs- *****
 ***** wert 'Y' könnte der Variablen uchFalsch nur   ***** 
 ***** bei der Definition zugewiesen werden!!       *****
 ********************************************************/
const unsigned char uchFalsch;
uchFalsch = 'Y';
.
.
}

Die Verwendung des Schlüsselwortes volatile bei der Definition bzw. Deklaration von Variablen informiert den Compiler darüber, daß deren Wert sich verändern kann, ohne daß im Programm explizit eine Änderung des Variablenwerts vorgenommen wird.

Die meisten optimierenden Compiler gehen nämlich davon aus, daß der Wert einer Variablen so lange unverändert bleibt, bis eine Zuweisung innerhalb des Programms für eine Änderung sorgt. Diese Annahme könnte einen Compiler bei entsprechend kritischen Variablen zu unerwünschten Optimierungen veranlassen.

Ein Anwendungsfall für volatile liegt zum Beispiel dann vor, wenn beim PC mit Hilfe einer Pointer-Variable über Portadressen Werte von einem Peripheriegerät eingelesen werden. Änderungen in diesen Speicherzellen werden dann nicht durch das Programm herbeigeführt, sondern durch den Betrieb des entsprechenden Peripheriegeräts.

Beide Schlüsselwörter müssen bei der Definition der jeweiligen Variablen vor der Angabe des Datentyps und dem Variablennamen stehen. Die gemeinsame Verwendung von const und volatile innerhalb einer Definition ist zulässig, weil const nur die Änderung des Variablenwerts durch eine explizite Anweisungsoperation verbietet.