C programmieren: Datentypen, Deklarationen, Operatoren und Ausdrücke

Seite: 2/2

Operatoren

Tabelle 2: Operatoren in C.
Tabelle 2: Operatoren in C.
(Bild: Christian Siemers)

C verfügt über einen reichhaltigen Satz von Operatoren. Diese lassen sich nach verschiedenen Kategorien gliedern:

  • nach der Art: unäre, binäre und ternäre Operatoren
  • nach Vorrang – Präzedenz (precedence)
  • nach Gruppierung – Assoziativität: links, rechts (associativity)
  • nach Stellung: Präfix, Infix, Postfix nach Darstellung: einfach, zusammengesetzt

Die Vielfalt und oft mehrfache Ausnutzung der Operatorzeichen auch in anderem syntaktischen Zusammenhang bietet anfangs ein verwirrendes Bild. Der Compiler kann aber immer nach dem Kontext entscheiden, welche der Operatorfunktionen gerade gemeint ist. Nachfolgend eine Übersicht der Operatoren und eine ausführliche Erläuterung zu den einzelnen Operatoren, wie sie in Tabelle 2 aufgeführt sind:

Der Operator [ ] deklariert einen sogenannten Vektor bzw. Array. Auf diesen Operator wie auch auf -> werden wir in einem späteren Artikel zu Vektoren (arrays) und Zeigern (Pointer) näher eingehen.

++, -- (z.B. a++, b--) als Postin- bzw. -dekrement liefern sie den ursprünglichen Wert ihres Operanden und erhöhen bzw. erniedrigen den Wert des Operanden danach um 1. Diese Operatoren können nur auf Objekte im Speicher angewandt werden, die vom skalaren Typ sein müssen und auf die schreibend zugegriffen werden kann. Wann die tatsächliche Veränderung des Operandenwertes im Speicher eintritt (der Seiteneffekt dieser Operatoren) ist implementationsabhängig.

Der Operator sizeof arbeitet zur Compilierungszeit (sog. compile time operator) und liefert die Größe seines Operanden in Einheiten des Typs char: sizeof(char) == 1. Der Operand kann ein Typ sein, dann muss er in () stehen, oder ein Objekt im Speicher, dann sind keine Klammern erforderlich. Ist der Operand ein Arrayname, liefert er die Größe des Arrays in char-Einheiten.

Die Tilde ~ (z.B. ~a) liefert den Wert der bitweisen Negation (das Komplement) der Bitbelegung ihres Operanden, der vom integralen Typ sein muss.

! (z.B. !a) liefert die logische Negation des Wertes seines Operanden, der vom skalaren Typ sein muss. War der Wert 0, ist das Ergebnis 1, war der Wert ungleich 0 , ist das Ergebnis 0.

Die unäre Negation -, + (z.B. -a) liefert den negierten Wert ihres Operanden, der vom arithmetischen Typ sein muss. Das unäre Plus wurde nur aus Symmetriegründen eingeführt, und dient lediglich Dokumentationszwecken.

& (z.B. &a) liefert die Adresse eines Objektes im Speicher (und erzeugt somit einen Zeigerausdruck).

* (z.B. *a) erzeugt in einer Deklaration einen Zeiger auf den deklarierten Typ, in der Anwendung auf einen Zeigerwert, liefert er den Wert des so bezeigten Objekts.

(typename) ist ein Typbezeichner. Der sogenannte type cast operator liefert den in diesen Typ konvertierten Wert seines Operanden. Dabei wird versucht, den Wert zu erhalten. Eine (unvermeidbare, beabsichtigte) Wertänderung tritt ein, wenn der Wert des Operanden im Zieltyp nicht darstellbar ist, ähnlich einer Zuweisung an ein Objekt dieses Typs. Im Folgenden einige Hinweise zu erlaubten Konversionen:

  • Jeder arithmetische Typ in jeden arithmetischen Typ.
  • Jeder Zeiger auf void in jeden Objektzeigertyp.
  • Jeder Objektzeigertyp in Zeiger auf void. J
  • eder Zeiger auf ein Objekt oder void in einen Integertyp.
  • Jeder Integertyp in einen Zeiger auf ein Objekt oder void.
  • Jeder Funktionszeiger in einen anderen Funktionszeiger.
  • Jeder Funktionszeiger in einen Integertyp.
  • Jeder Integertyp in einen Funktionszeiger.

Die Zuweisung von void-Zeiger an Objektzeiger und umgekehrt geht übrigens auch ohne den Typkonversionsoperator. In allen anderen Fällen ist seine Anwendung geboten oder erforderlich, und sei es nur, um den Warnungen des Compilers zu entgehen.

% (z.B. a%b): modulo liefert den ganzzahligen Divisionsrest des Wertes seines linken Operanden geteilt durch den Wert seines rechten Operanden und lässt sich nur auf integrale Typen anwenden. Dabei sollte man Überlauf und die Division durch Null vermeiden. Bei positiven Operanden wird der Quotient nach 0 abgeschnitten. Falls negative Operanden beteiligt sind, ist das Ergebnis implementationsabhängig. Es gilt jedoch immer: X = (X/Y) * Y + (X%Y).

Die übrigen arithmetischen Binäroperatoren können nur auf Operandenpaare vom arithmetischen Typ angewandt werden, dabei geht, wie üblich und auch aus der Tabelle zu ersehen, Punktrechnung vor Strichrechnung. Bei der Ganzzahldivision wird ein positiver Quotient nach 0 abgeschnitten. Man vermeide auch hier die Null als Divisor. Wenn unterschiedliche Typen an der Operation beteiligt sind, wird selbstständig in den größeren der beteiligten Typen umgewandelt (balanciert).

<<, >> (z.B. a<<b): die Bitschiebeoperatoren schieben den Wert des linken Operanden um Bitpositionen des Wertes des rechten Operanden jeweils nach links bzw. rechts und können nur auf integrale Operandenpaare angewandt werden. Für eine n-Bit-Darstellung des promovierten linken Operanden muss der Wert des rechten Operanden im Intervall 0..n-1 liegen. Bei positivem linken Operanden werden Nullen in die freigewordenen Positionen nachgeschoben. Ob bei negativem linken Operanden beim Rechtsschieben das Vorzeichen nachgeschoben wird (meist so gehandhabt), oder Nullen, ist implementationsabhängig.

Jetzt Newsletter abonnieren

Täglich die wichtigsten Infos zu Softwareentwicklung und DevOps

Mit Klick auf „Newsletter abonnieren“ erkläre ich mich mit der Verarbeitung und Nutzung meiner Daten gemäß Einwilligungserklärung (bitte aufklappen für Details) einverstanden und akzeptiere die Nutzungsbedingungen. Weitere Informationen finde ich in unserer Datenschutzerklärung.

Aufklappen für Details zu Ihrer Einwilligung

Die Vergleichsoperatoren (z.B. a == b) können nur auf arithmetische und auf Paare von Zeigern gleichen Typs angewandt werden. Sie liefern den Wert 1, wenn der Vergleich erfolgreich war, sonst 0.

Die bitlogischen Operatoren (z.B. a&b) können nur auf integrale Typen angewandt werden und liefern den Wert der bitlogischen Verknüpfung des Wertes des linken mit dem Wert des rechten Operanden (beide als Bitmuster interpretiert).

&& (z.B. a && b): testet, ob beide Operanden ungleich Null (wahr) sind. Ist der linke Operand wahr, wird auch der rechte getestet, andernfalls hört man auf, und der rechte Operand wird nicht mehr bewertet, da das Ergebnis der logischen UND-Verknüpfung ja schon feststeht (sog. Kurzschlussbewertung, short circuit evaluation, mit Sequenzpunkt nach dem linken Operanden). Beide Operanden müssen vom skalaren Typ sein. Im Wahrheitsfall ist der Wert des Ausdrucks 1, sonst 0.

|| (z.B. a || b): testet, ob mindestens einer der beiden Operanden ungleich Null (wahr) ist. Ist der linke Operand gleich Null (falsch), wird auch der rechte getestet, andernfalls hört man auf, und der rechte Operand wird nicht mehr bewertet, da das Ergebnis der logischen ODER-Verknüpfung ja schon feststeht (sog. Kurzschlussbewertung, short circuit evaluation, wie oben) Beide Operanden müssen vom skalaren Typ sein. Im Wahrheitsfall ist der Wert des Ausdrucks 1, sonst 0.

X?Y:Z X muss vom skalaren Typ sein und wird bewertet. Ist X ungleich Null (wahr), wird Y bewertet, andernfalls wird Z bewertet. Y und Z können fast beliebige Ausdrücke sein, auch void ist möglich, sollten aber kompatibel sein. Zwischen der Bewertung von X und der Bewertung von entweder Y oder Z befindet sich ein Sequenzpunkt (sequence point ). Der Wert des Ausdrucks ist dann der Wert des (evtl. im Typ balancierten) Wertes des zuletzt bewerteten Ausdrucks.

= Der Zuweisungsoperator bewertet seine beiden Operanden von rechts nach links, so sind auch Zuweisungsketten in der Art von a = b = c = d = 4711 möglich. Der Wert des Zuweisungsausdrucks ist der Wert des Zugewiesenen, der in den Typ des linken Operanden transformierte Wert des rechten Operanden. Der linke Operand muss ein Objekt im Speicher darstellen, auf das schreibend zugegriffen werden kann. Aufgrund der speziellen Eigenheit von C, dass die Zuweisung ein Ausdruck und keine Anweisung ist, sowie seiner einfachen Wahr-Falsch-Logik, taucht die Zuweisung oft als Testausdruck zur Schleifenkontrolle auf. Ein markantes Beispiel:

while (*s++ = t++); /* C-Idiom für Zeichenketten kopie */

Die Verbund- oder Kombinationszuweiser bestehen aus zwei Zeichen, deren rechtes der Zuweiser ist. Sie führen, kombiniert mit der Zuweisung verschiedene arithmetische, bitschiebende und bitlogische Operationen aus. Dabei bedeutet a op= b soviel wie a = a op b, mit dem Unterschied, dass a, also der linke Operand, nur einmal bewertet wird.

Der Komma- oder Sequenzoperator (z.B. a,b) gruppiert wieder von links nach rechts und bewertet erst seinen linken, dann seinen rechten Operanden. Dazwischen liegt ein Sequenzpunkt, das heißt, alle Seiteneffekte sind garantiert eingetreten. Der Wert des Ausdrucks ist das Resultat der Bewertung des rechten Operanden. Der Nutzen des Operators besteht darin, dass er einen Ausdruck erzeugt und folglich überall stehen kann, wo ein Ausdruck gebraucht wird. Seine Hauptanwendungen sind die Initialisierungs- und Reinitialisierungsausdrücke in der Kontrollstruktur der for-Schleife, wo ja jeweils nur ein Ausdruck erlaubt ist, und manchmal mehrere gebraucht werden.

Einige Operationen erzeugen implementationsabhängige Typen, die in stddef.h definiert sind. size_t ist der vom sizeof-Operator erzeugte vorzeichenlose integrale Typ. ptrdiff_t ist der vorzeichenbehaftete integrale Typ, der vom Subtraktionsoperator erzeugt wird, wenn dieser auf Zeiger (gleichen Typs!) angewandt wird..

Ausdrücke in C

C ist eine Ausdrucks-orientierte Sprache. Der Compiler betrachtet die Ausdrücke und bewertet sie. Ein Ausdruck (expression) in C ist:

  • eine Konstante (constant)
  • eine Variable (variable)
  • ein Funktionsaufruf (function call )
  • eine beliebige Kombination der obigen 3 Elemente mittels Operatoren

Jeder Ausdruck hat einen Typ und einen Wert. Bei der Bewertung von Ausdrücken gelten folgende Regeln: Daten vom Typ char oder short werden sofort in den Typ int umgewandelt (integral promotion). Bei der Kombination von Ausdrücken wird balanciert, d.h. der dem Wertebereich oder Speicherplatz nach kleinere Typ wird in den beteiligten, dem Wertebereich oder Speicherplatz nach größeren Typ umgewandelt. Dabei wird versucht, den Wert zu erhalten (value preservation).

Die Bewertung der einzelnen Elemente von Ausdrücken folgt Vorrang und Assoziativität der Operatoren. Bei Gleichheit in diesen Eigenschaften ist die Reihen-folge der Bewertung (order of evaluation) gleichwohl bis auf wenige Ausnahmen undefiniert, denn der Compiler darf sie auf für ihn günstige Weise ändern, wenn das Ergebnis aufgrund der üblichen mathematischen Regeln gleichwertig wäre. In der Theorie gilt (a * b)/c = (a/c) * b, also darf der Compiler das nach seinem Gusto umordnen, und auch Gruppierungsklammern können ihn nicht daran hindern. Das kann aber bei den Darstellungs-begrenzten Datentypen im Computer schon zu unerwünschtem Überlauf etc. führen.

Soll dies wirklich verhindert werden, d.h., soll der Compiler gezwungen werden, eine bestimmte Reihenfolge einzuhalten, muss die entsprechende Rechnung aufgebrochen und in mehreren Teilen implementiert werden. Die Codesequenzen

x = (a * b) / c;

und

x = a * b;
x = x / c;

bewirken tatsächlich nicht automatisch das gleiche, denn im ersten Fall darf der Compiler umsortieren, im zweiten nicht, da das Semikolon einen so genannten Sequenzpunkt (sequence point) darstellt, den der Compiler nicht entfernen darf.

Manche Operatoren bewirken sog. Seiteneffekte (side effects), d.h. sie können den Zustand des Rechners verändern, z.B. den Wert von Speichervariablen oder Registern oder sonstiger Peripherie. Dazu gehören neben den Zuweisern auch die Post- und Präinkrement und -dekrement-Operatoren und Funktionsaufrufe. Das Eintreten der Wirkung dieser Seiteneffekte sollte niemals abhängig von der Reihenfolge der Bewertung sein! Während durch Komma separierte Deklarations- und Definitionslisten strikt von links nach rechts abgearbeitet und bewertet werden, gilt das z.B. für die Reihenfolge der Bewertung in Parameterlisten beim Funktionsaufruf nicht.

Anweisungen bzw. Statements in C

In C gibt es folgende Anweisungen (statements):

  • Leeranweisung ; (empty statement)
  • Audrucksanweisung expression; (expression statement)
  • Blockanweisung { ... } (block statement)
  • markierte Anweisung label: statement (labeled statement)
  • Auswahlanweisung if else switch ... case (selection statement)
  • Wiederholungsanweisung for while do ... while (iteration statement)
  • Sprunganweisung goto break continue return (jump statement)

Dies sind schon einmal die wesentlichen Grundaspekte der Syntax von C. Allerdings umfasst diese noch weitere Elemente, die es sich genauer zu betrachten lohnt. Im nächsten Beitrag zu den syntaktischen Elementen von C gehen wir näher auf Kontrollstrukturen und Funktionen in der Programmiersprache ein.

Hinweis: Dieser Beitrag ist ein Auszug aus dem Handbuch „Embedded Systems Engineering“ . Dieses ist auch als kostenlose PDF-Version in voller Länge auf ELEKTRONIKPRAXIS.de verfügbar.

Dieser Beitrag, mit Ausnahme der ersten beiden und des letzten Absatzes, ist Copyright © Bernd Rosenlechner 2007-2010. Dieser Text kann frei kopiert und weitergegeben werden unter der Lizenz Creative Commons – Namensnennung – Weitergabe unter gleichen Bedingungen (CC – BY – SA) Deutschland 2.0.

* Prof. Dr. Christian Siemers lehrt an der Technische Universität Clausthal und arbeitet dort am Institut für Elektrische Informationstechnik (IEI).

(ID:45405485)