namespace cpp

C++ lernen, kennen, anwenden

Benutzer-Werkzeuge

Webseiten-Werkzeuge


kennen:typen

Datentypen

Das Definieren von allgemein zu gebrauchenden oder auch anwendungsspezifischen Typen ist in der Tat die wichtigste und grundlegendste Aufgabe in C++.
— Bjarne Stroustrup: Die C++ Programmiersprache

Variablen

Deklaration

Die Deklaration führt einen Namen ein und ordnet ihm einen Typ zu. Ein Bezeichner ist erst nach seiner Deklaration benutzbar ("declare before use", Geltungsbereich). Der Typ bestimmt das Verhalten des Objektes: welche Speichergröße dem Objekt im Programm zugeordnet wird, wie diese Bitfolge zu interpretieren ist und welche Operatoren auf den Namen anwendbar sind.

Durch die Definition wird dem Objekt ein Speicherplatz (Adresse) zugeordnet. Jedes Objekt muss im Programm genau einmal definiert sein ("one definition rule"). Für Variablen ist die Deklaration gleichzeitig auch eine Definition, wenn Spezifizierer nichts anderes angeben.

Syntax:

Spezifizierer Typ DeklariertesObjekt Initialisierer ;
extern int anzahl;
double x, y, z;
 
char zeichen = 'j';
const int GROESSE = 42;
float feld[GROESSE];
 
float* zeiger = &feld[0];
float& anfang = feld[0];
char* (*zeiger_auf_zeichenkettenfunktion)(char[], const char[]) = std::strcat; 

Spezifizierer (wie extern und const) und Initialisierer zur Zuweisung von Anfangswerten sind optional. Mehrere Objekte können als (kommagetrennte) Liste deklariert werden. Durch Operatoren [] * & () werden zusammengesetzte Datentypen (Felder, Zeiger und Referenzen) gebildet. Runde Klammern spielen eine Doppelrolle als Funktionsklammer und zur Vorrangregelung.

Kombinationen der Operatoren können beliebig komplex sein. Zum Verständnis sollten die Deklarationen von innen nach außen gelesen werden, d.h. vom neu eingeführten Namen ausgehend entsprechend der Rangfolge der Operatoren: Der oben definierte Bezeichner zeiger_auf_zeichenkettenfunktion ist ein *Zeiger auf eine Funktion (…) mit einem char[]-Feld und einem const char[]-Feld als Parameter und einem *Zeiger auf char als Ergebnistyp. Der zeiger_auf_zeichenkettenfunktion erhält die Einsprungadresse der Funktion strcat als Anfangswert. Die Typprüfung des Compilers akzeptiert die Zuweisung nur, weil die Funktion strcat aus der Bibliothek <<cstring>> als

char* strcat(char*, const char*);

deklariert ist (Feldparameter werden als Zeiger behandelt); Funktionsnamen sind Adressen im Maschinencode, also Zeiger auf die Funktion.

Geltungsbereich und Lebensdauer

Der Bezeichner ist ab seiner Deklaration bekannt und darf im folgenden Quelltext benutzt werden. Erfolgte die Deklaration in einem Block aus geschweiften Klammern {}, ist der Name nur lokal bis zum Ende dieses Blockes gültig. Außerhalb von Blöcken deklarierte Bezeichner sind bis zum Ende des Quelltextes (global) gültig.

Lokale Bezeichner, dazu zählen auch Funktionsparameter, sind unabhängig von äußeren Bezeichnern definierbar. Die lokalen Bezeichner verdecken in ihren Geltungsbereich gleichnamige Bezeichner außerhalb des Blockes.

Die Lebensdauer und damit der Speicherplatzbedarf globaler Variablen erstreckt sich über die gesamte Programmlaufzeit (statische Variablen).

Für lokale Variablen wird beim Eintritt in den Block automatisch Speicher auf dem Stapel reserviert und beim Verlassen des Blockes wieder freigegeben. Damit geht auch der gespeicherte Wert verloren.

Spezifizierer

Speicherklassen-Spezifizierer beeinflussen die Lebensdauer bzw. den Speicherort von Variablen.

  • Die Angabe extern macht deutlich, dass der Bezeichner an dieser Stelle nur deklariert und nicht definiert werden soll. Der deklarierte Name ist anderswo global definiert, z.B. in einer anderen Quelltextdatei (Modul) des Programms.
  • Mit register wird dem Compiler empfohlen, eine zeitkritische lokale Variable direkt im Prozessorregister zu halten statt sie auf dem Speicher abzulegen. Damit hat diese Variable auch keine Speicheradresse mehr.
  • Mit static gekennzeichnete globale Variablen sind vor dem extern-Zugriff aus in einer anderen Quelltextdatei geschützt.
  • Als static deklarierte lokale Variablen werden beim Programmstart angelegt und behalten ihren Wert auch nach dem Verlassen der Funktion. Trotz statischer Lebensdauer bleibt ihr Geltungsbereich lokal auf die Funktion begrenzt.
int noch_ein_Bier_bitte()
{
  static int biere = 0;
  return ++biere;       // Anzahl der bestellten Biere  
}

Durch const läßt sich (unabsichtliches) Ändern des Variablenwertes nach der Initialisierung verbieten. Der direkte Änderungsversuch erzeugt einen Übersetzungsfehler. C-Programmierer sagen zu const-Variablen dennoch ungern Konstante, weil

  • sie wissen, wie man const unterlaufen kann,
  • der Drang, das zu tun, beliebig groß werden kann und
  • der Begriff Konstante vom Präprozessor belegt ist und auch Literale gern als Konstanten bezeichnet werden.

Konstante Referenzen und konstante Zeiger müssen nicht unbedingt auf konstante Objekte verweisen. Sie sind vielmehr eine "freiwillige Selbstverpflichtung", den Wert nicht über diesen Bezeichner zu ändern.

Eine als volatile (flüchtig) markierte Variable muss bei jeder Auswertung neu aus dem Speicher gelesen werden. Optimierungsversuche des Compilers werden unterbunden. Das ist bei Hardwarezugriffen (Ports u.ä.) notwendig, wenn sich Speicherinhalte "von außerhalb" ändern können:

const volatile char far* COM1 = (char far*) 0x3F8; // serial port unter DOS

Eine mutable (veränderbar) gekennzeichnete Komponente eines zusammengesetzten Datentyps darf auch dann geändert werden, wenn die umgebende Struktur oder Klasse logisch konstant ist:

struct Count 
{
  mutable int counter; 
};
 
void f(const Count& c)
{
  c.counter++;
}

Anfangswerte

Variablen können, Zeiger sollten, Konstanten und Referenzen müssen bei ihrer Definition Werte erhalten. Neben der Zuweisung mit dem Gleichheitszeichen ist auch die Übergabe von Startwerten in Klammern möglich. Diese aus SmallTalk übernommene Schreibweise ist auch für Konstruktoren zusammengesetzter Typen üblich.

int i = 3;     // Variablendefinition in "C-Schreibweise"
int j(i);      // Variablendefinition in "SmallTalk-Schreibweise"
               // aber:
int f(int i);  // Funktionsdeklaration, keine Variablendefinition!

Seit C++11 kann die Übergabe von Anfangswerten auch in geschweiften Klammern erfolgen (uniform initialization). Dabei werden Typumwandlungen, die zu Datenverlust führen können (narrowing conversions), als Fehler betrachtet:

int k{9};      // ok
int l{9.0};    // Fehler: Umwandlung double -> int, möglicher Datenverlust 
int m{9.5};    // Fehler: Abrunden zu 9 ist Datenverlust.

Der Typ der Variable erschließt sich aus dem Initialisierungswert, wenn als Typ auto angegeben wird. Bei komplexen Typbezeichnern wie std::vector<double>::iterator ist das eine Erleichterung. Diese Art der Variablendefinition lässt sich (fast) durchgehend nutzen (almost always auto) — der Typ kann auch rechts vom Zuweisungsoperator erscheinen. C++17 erlaubt zudem Zugriff eines Bestandteile eines zusammengesetzten Typs unter neuen Namen.

auto n = 3;                  // int
auto p = std::pair{6, true}; // "almost always auto"-Stil 
auto  [value, success] = p;  // C++17 structured bindings, Kopie der Werte
auto& [v, done] = p;         // Referenz, Schreiben in die Bestandteile von p möglich

Typen

C++ ist eine streng typisierte Sprache. Das Typsystem enthält

Mit dem Schlüsselwort typedef sind neue Namen für Typen festlegbar.

Grundtypen

Ganzzahlige Typen

bool

Der Typ bool für Wahrheitswerte und logische Operationen nimmt einen der beiden möglichen Werte true und false an. Der Wert 0 wird als false betrachtet, jeder andere Wert (egal welchen Typs) wird zu true. Umgekehrt wird bei Typumwandlung true zum Wert 1, false zu 0.

char       signed char     unsigned char     wchar_t

Zeichen haben den Typ char. Dieser hat meistens 8 bit Breite und kann damit einen von 256 verschiedenen Werten aufnehmen. Es existieren verschiedene Standards (EBCDIC, ASCII, ISO 8859-1, ISO 10646, Unicode, UCS32, …), den einzelnen Zeichen Zahlenwerte zuzuordnen. Welcher Zeichensatz verwendet wird, ist maschinen- und betriebssystemabhängig. Für Zeichensätze wie Unicode existiert ein Datentyp wchar_t.

Zeichentypen werden als Ganzzahltypen aufgefasst, weil mit ihnen auch arithmetische Operationen (mit begrenzten Werteumfang) möglich sind. Ob ein char vorzeichenbehaftet (signed -128…127) oder nicht vorzeichenbehaftet (unsigned 0…255) behandelt wird, ist compilerabhängig. Ist der Unterschied wichtig, können signed char und unsigned char genutzt werden. C++17 definiert in <cstddef> den Datentyp std::byte über unsigned char mit eingeschränktem Operationsumfang zur Bitmanipulation.

int        unsigned
short      unsigned short     
long       unsigned long

Der Ganzzahltyp int nimmt typisch die Verarbeitungsbreite eines Maschinenworts an. Diese kann mit dem sizeof-Operator ermittelt werden: n = 8*sizeof(int); Der Wertebereich der Ganzzahl umfasst (Zweierkomplementdarstellung vorausgesetzt) die Werte -pow(2,n-1) bis pow(2,n-1)-1.

Die Modifikationen short int und long int können einen kleinere bzw. größere Breite besitzen. Typisch sind 16 bit und 32 bit bzw. 64 bit. Zu jeder int-Variante (short, int, long) gibt es eine unsigned-Variante, deren Wertebereich 0 bis pow(2,n)-1 umfasst.

In der Bibliothek <cstdint> definierte Typen wie uint8_t, int_least16_t oder int_fast64_t kommen zum Einsatz, wenn exakte oder Mindestwerte für Speicherbedarf oder Wertebereich bzw. Verarbeitungsgeschwindigkeit von Bedeutung sind.

Die Bibliothek <climits> definiert Konstanten mit den Grenzen der Wertebereiche. Die Schablone numeric_limits<T> aus <limits> bietet eine einheitliche Schnittstelle für Größeninformationen.

Gleitkommatypen

float     double      long double

Fließkomma- oder Gleitkommatypen nehmen gebrochene Zahlen mit begrenztem Wertebereich und beschränkter Genauigkeit auf. Typischerweise sind nur 6 führende Ziffern einer float-Zahl exakt darstellbar. Doppelt genaue Zahlen (double) erlauben einige Stellen mehr, long double (erweitert genaue Zahlen) evtl. noch mehr. Angaben zum Wertebereich und zur möglichen Genauigkeit sind aus den Bibliotheken <cfloat> und limits> entnehmbar. Der Wertebereich der einfach genauen ist eine Untermenge der doppelt genauen Gleitkommazahlen.

Nicht jede gebrochene Zahl ist als endlicher Dualbruch darstellbar. Beim Rechnen mit Gleitkommazahlen kommt es deshalb zu Rundungsfehlern. Die Auslöschung signifikanter Stellen bei der Differenzbildung lässt Genauigkeit verloren gehen. Einige numerische Rechenverfahren reagieren empfindlich auf geringfügige Änderungen von Startwerten (Instabilität oder Chaos).

Die Auswahl der richtigen Genauigkeit für ein Problem, bei dem die Auswahl relevant ist, erfordert ein weitgehendes Verständnis von Gleitkommaberechnungen. Falls Sie dieses Wissen nicht haben, holen Sie sich Hilfe, nehmen Sie sich die Zeit zum Lernen oder benutzen Sie double und hoffen das beste.
— Bjarne Stroustrup: Die C++ Programmiersprache

Typumwandlung

Werte der Grundtypen sind trotz strenger Typprüfung nahezu beliebig ineinander umwandelbar. Führt die Umwandlung zu Genauigkeitsverlust oder ganz aus dem Wertebereich des Zieltyps hinaus, kann der Compiler Warnungen ausgeben. Der Programmierer sollte diese Warnungen ernst nehmen, da sie zu subtilen Laufzeitproblemen führen können. Typumwandlungen können automatisch (bei Wertzuweisungen, Rückgabe von Funktionswerten und in Rechenoperationen) oder auch ausdrücklich durch einen Typumwandlungsoperator erfolgen. Die ausdrückliche Konversion schaltet die Compiler-Warnung ab.

int  i = 65;
char c = char(i);
// char d{i}; // C++11 Fehler: narrowing

Passt der Wert eines Ausdrucks in den Wertebereich des Zieltyps, bleibt der Wert erhalten, auch Vorzeichen werden korrekt behandelt. Bei der Umwandlung von Gleitkommazahlen in Ganzzahlen wird der gebrochene Anteil abgeschnitten, d.h. zur Null hin abgerundet. Passt der Wert nicht in den Wertebereich eines Zieltyps, werden bei ganzzahligen Typen höherwertige Bits weggelassen (Moduloarithmetik beim Überlauf), bei Gleitkommatypen ist das Ergebnis nicht definiert. Die ab C++11 eingeführte, "uniform initialization" genannte Schreibweise behandelt die Einschränkung des Wertebereichs (narrowing) als Fehler.

Zusammengesetzte Typen

Felder

Wird ein Objekt als Name[Anzahl] deklariert, so handelt es sich um ein Feld mit der Anzahl typgleicher Elemente. Die Anzahl der Elemente (Dimension oder Feldgröße) muss eine ganzzahlige Konstante sein. Die Feldelemente belegen einen zusammenhängenden Speicherbereich. Der Zugriff auf die Feldelemente Name[0] bis Name[Anzahl-1] erfolgt ohne Bereichsprüfung. Schreibzugriffe außerhalb der Feldgrenzen haben schwer lokalisierbare, häufig fatale Folgen.

Mehrdimensionale Felder sind Felder von Feldern. Sie werden durch mehrere aufeinander folgende eckige Klammern beschrieben. Zwischen benachbarten Speicherplätzen ändert sich der rechteste (innerste) Feldindex am schnellsten ("zeilenweise" Anordnung).

Felder können mit Anfangswerten belegt werden:

int rechteck[2][3] = { {1, 2, 3}, {4, 5, 6} };

Die inneren geschweiften Klammern können bei der Initialisierungsliste weggelassen werden. Auch eine unvollständige Initialisierung ist möglich:

int primzahlen[100] = { 2, 3 }; // weitere werden später berechnet
int y[2][3] = { 1, 2, 3  };     // nur Zeile  y[0][j] belegt  
int z[2][3] = { {1}, {4} };     // nur Spalte z[i][0] belegt

Fehlt die äußerste, am weitesten links stehende Felddimension, wird sie vom Compiler aus der Anzahl der Elemente in der Initialisiererliste bestimmt. Feldparameter benötigen ebenfalls die äußerste Dimensionsangabe nicht, da Feldparameter als Zeiger auf den Feldanfang behandelt werden. Weitere Dimensionen müssen jedoch angegeben sein.

Zeiger

Ein Zeiger (engl. pointer) enthält die Adresse eines Objektes im Hauptspeicher. Die Deklaration Typ *zeiger kann auf zwei Weisen gelesen werden:

  1. zeiger ist ein Zeiger auf eine Typ-Variable.
  2. Der Ausdruck *zeiger besitzt den Typ (sozusagen als "Muster" für die Verwendung).

Der Inhaltsoperator * erlaubt den Zugriff auf den Wert im Hauptspeicher, dessen Adresse im Zeiger hinterlegt ist.

Wird ein Zeiger in der Form *zeiger benutzt, muss er vorher eine gültige Speicheradresse erhalten haben (z.B. über den Adressoperator &). Nicht initialisierte "baumelnde" Zeiger sind eine gefürchtete Fehlerquelle. Nichtgenutzte Zeiger sollten auf NULL gelegt ("geerdet") werden. Vor dem Zugriff auf den Inhalt kann dann geprüft werden, ob der Zeiger auf etwas zeigt:

  int i;
  int *ip = &i; // Zeiger ip erhält Adresse von i (ip zeigt auf i)
 
  if (ip)       // ip sollte gültig sein, 
  {
    *ip = 10;   // ehe über ip der Wert von i angesprochen wird
  }
  ip = NULL;    // ip wird nicht mehr genutzt

Referenzen

Referenzvariablen Typ& aliasname = variable definieren feste Aliasnamen, die für ihre Lebensdauer fest mit der Speicheradresse einer anderen Variable verbunden sind. Referenzvariablen werden relativ selten "solo" verwendet, um mehrfache umständliche Ausdrücke zu vermeiden.

int matrix[3][3];
int &mitte = matrix[1][1];
mitte = 15;                // statt matrix[1][1] = 15;

Häufiger werden sie als Referenzparameter eingesetzt.

Weiterleitende Referenzen (ab C++11) Typ&& aliasname = ausdruck können sowohl Referenzen auf "Linkswerte" (lvalue) als auch auf "Rechtswerte" (rvalue) übernehmen. Bei Linkswerten kollabiert die universelle Referenz zu Typ&, bei Rechtswerten wird der Wert durch Verschieben (Verschiebesemantik) oder wenn das nicht möglich ist, durch Kopieren für den Sichtbarkeitsbereich des Aliasnamens am Leben erhalten.

Aufzählungen

Aufzählungen (engl. enumerations) sind Sammlungen benannter ganzzahliger (int-)Konstanten, denen ein gemeinsamer Typname zugeordnet werden kann. Die Konstanten werden mit 0 beginnend aufsteigend nummeriert, falls ihnen kein anderer Wert zugeordnet wird.

Werte vom Aufzählungstyp lassen sich problemlos einer int-Variable zuweisen, umgekehrt jedoch nicht (explizite Typumwandlung erforderlich).

Syntax:

enum Typname { Konstantenliste } Variablenliste;
enum Woche { SO, MO, DI, MI, DO, FR, SA };
enum Muenzen { PFENNIG=1, FUENFER=5, GROSCHEN=10, FUFFZIGER=50 };

Typsichere Aufzählungen erlauben keine implizite Umwandlung in Ganzzahlen:

enum class TriState : char { unknown, no, yes };
TriState s = TriState::unknown;
// char c = s;    // nicht erlaubt
char c = char(s); // ok

Die Angabe eines Grundwertebereichs ist möglich. (Das ist keine Vererbung!)

Klassen und Strukturen

Vorwärtsdeklarationen können benutzt werden, solange der Compiler keine Information über den Strukturinhalt benötigt, z.B. um einen Zeiger oder eine Referenz zu deklarieren (unvollständige Typdefinition).

Syntax:

struct Typname ;

Inhaltlich zusammengehörende Daten auch verschiedener Typen (Attribute, engl. member data) und zugehörige Funktionen (Methoden, engl. member functions) lassen sich als Komponenten einer Struktur (Schlüsselwort struct) oder Klasse (Schlüsselwort class) zusammenfassen, siehe auch Klassenkonzept.

Die Instanzen werden bei Zuweisung, Parameterübergabe und Funktionsrückgabe als Einheit behandelt, d.h. Byte für Byte kopiert.

Syntax:

struct Typname
{
Zugriffsspezifikation:
Komponentendeklarationen

} Variablenliste ;

Der Zugriff auf die Bestandteile einer Instanz erfolgt als variable.komponente oder zeiger→komponente. Zugriffsspezifikationen legen fest, von wo aus der Zugriff auf die folgenden Komponenten erlaubt ist. Ohne Zugriffsspezifikation, sind die Komponenten einer Struktur öffentlich (public), die einer Klasse nur den Methoden der Klasse zugänglich (private). Abgesehen davon sind struct und class gleichwertig.

Besitzt die Struktur keine selbstdefinierten Konstruktoren, können die Variablen Anfangswerte durch eine Initialisiererliste in geschweiften Klammern erhalten.

struct Atom
{
  char symbol[3];
  float masse;
};
 
const Atom wasserstoff = { "H", 1.0079 };
Atom atom1, atom2;
atom2 = atom1 = wasserstoff;
 
float masse = atom1.masse + atom2.masse;

Vereinigungen und Bitfelder

Vereinigungen (Schlüsselwort union) überlagern einem Speicherbereich mehrere Bezeichner. Das Konzept entspricht den varianten Records in PASCAL oder EQUIVALENCE-Bereichen in FORTRAN. Je nach Typ der Komponente werden die gleichen Bits unterschiedlich interpretiert. Der Speicherbedarf der Vereinigung entspricht dem größten enthaltenen Typ. Die Nutzung zur Typkonversion ist gefährlich, weil nicht portabel.

Syntax:

union Typname
{
Komponentendeklarationen

} Variablenliste ;

Mit Bitfeldern können kleine Informationseinheiten in ein Maschinenwort gepackt werden. Hinter jeder Komponente wird die Anzahl der zu reservierenden Bits angegeben. Die Bitfeldkomponenten verhalten sich wie kleine Ganzzahlen mit oder ohne Vorzeichen. Für Zwischenräume können unbenannte Bitfeldkomponenten eingeschoben werden. Fast alles an Bitfeldern ist implementierungsabhängig (Beispiel Gleitkommaformat).

struct IEEE754Single
{
  unsigned mantisse : 23;
  unsigned exponent : 8;
  unsigned sign     : 1;
} bits;
kennen/typen.txt · Zuletzt geändert: 2019-01-13 14:19 von rrichter