C++-Typsystem

Das Konzept des Typs ist in C++ wichtig. Jede Variable, jedes Funktionsargument und jeder Rückgabewert muss über einen Typ verfügen, um kompiliert werden zu können. Außerdem werden alle Ausdrücke (einschließlich Literalwerte) implizit vom Compiler eingegeben, bevor sie ausgewertet werden. Einige Beispiele für Typen sind integrierte Typen, z int . B. zum Speichern ganzzahliger Werte, double zum Speichern von Gleitkommawerten oder Standardbibliothekstypen wie Klassen std::basic_string zum Speichern von Text. Sie können einen eigenen Typ erstellen, indem Sie einen oder structmehrere class . Der Typ gibt die Menge des Arbeitsspeichers an, der für die Variable (oder das Ausdrucksergebnis) zugewiesen ist. Der Typ gibt auch die Arten von Werten an, die gespeichert werden können, wie der Compiler die Bitmuster in diesen Werten interpretiert, und die Vorgänge, die Sie für sie ausführen können. In diesem Artikel ist eine informelle Übersicht der Hauptfunktionen des C++-Typsystems enthalten.

Terminologie

Skalartyp: Ein Typ, der einen einzelnen Wert eines definierten Bereichs enthält. Skalare enthalten arithmetische Typen (integrale oder Gleitkommawerte), Enumerationstypmember, Zeigertypen, Zeiger-zu-Member-Typen und std::nullptr_t. Grundlegende Typen sind in der Regel skalare Typen.

Zusammengesetzter Typ: Ein Typ, der kein skalarer Typ ist. Zusammengesetzte Typen umfassen Arraytypen, Funktionstypen, Klassentypen (oder Strukturtypen), Union-Typen, Enumerationen, Verweise und Zeiger auf nicht statische Klassenmber.

Variable: Der symbolische Name einer Datenmenge. Der Name kann verwendet werden, um auf die Daten zuzugreifen, auf die er sich im gesamten Bereich des Codes bezieht, auf den er definiert ist. In C++ wird die Variable häufig verwendet, um auf Instanzen von skalaren Datentypen zu verweisen, während Instanzen anderer Typen normalerweise als Objekte bezeichnet werden.

Objekt: Aus Gründen der Einfachheit und Konsistenz verwendet dieser Artikel das Ausdrucksobjekt, um auf jede Instanz einer Klasse oder Struktur zu verweisen. Wenn sie im allgemeinen Sinne verwendet wird, enthält sie alle Typen, sogar skalare Variablen.

POD-Typ (einfache alte Daten): Diese informelle Kategorie von Datentypen in C++ bezieht sich auf Typen, die skalar sind (siehe Abschnitt "Grundlegende Typen") oder POD-Klassen sind. Eine POD-Klasse enthält keine statischen Datenmember, die nicht auch PODs sind, und keine benutzerdefinierten Konstruktoren, benutzerdefinierten Destruktoren oder benutzerdefinierten Zuordnungsoperatoren. Darüber hinaus verfügt eine POD-Klasse über keine virtuellen Funktionen, keine Basisklasse und keine privaten oder geschützten nicht statischen Datenmember. POD-Typen werden häufig für externen Datenaustausch verwendet, z. B. mit einem in der Programmiersprache C (die nur über POD-Typen verfügt) geschriebenen Modul.

Angeben von Variablen und Funktionstypen

C++ ist sowohl eine stark typierte Sprache als auch eine statisch typierte Sprache. Jedes Objekt hat einen Typ und dieser Typ ändert sich nie. Wenn Sie eine Variable im Code deklarieren, müssen Sie entweder den Typ explizit angeben oder den auto Schlüsselwort (keyword) verwenden, um den Compiler anzuweisen, den Typ vom Initialisierer abzuleiten. Wenn Sie eine Funktion im Code deklarieren, müssen Sie den Typ des Rückgabewerts und jedes Arguments angeben. Verwenden Sie den Rückgabewerttyp void , wenn von der Funktion kein Wert zurückgegeben wird. Die Ausnahme ist, wenn Sie Funktionsvorlagen verwenden, die Argumente beliebiger Typen zulassen.

Nachdem Sie eine Variable zuerst deklariert haben, können Sie den Typ zu einem späteren Zeitpunkt nicht mehr ändern. Sie können jedoch den Wert der Variablen oder den Rückgabewert einer Funktion in eine andere Variable eines anderen Typs kopieren. Solche Vorgänge werden als Typkonvertierungen bezeichnet, die manchmal notwendig sind, aber auch potenzielle Datenquellen für Datenverlust oder Falschheit sind.

Wenn Sie eine Variable vom POD-Typ deklarieren, wird dringend empfohlen , sie zu initialisieren , was bedeutet, dass sie einen Anfangswert erhält. Wenn Sie eine Variable initialisieren, verfügt sie über einen "Garbage"-Wert, der aus allen Bits dessen besteht, das sich grade zuvor an diesem Speicherort befand. Es ist ein wichtiger Aspekt von C++ zu beachten, insbesondere, wenn Sie aus einer anderen Sprache stammen, die die Initialisierung für Sie behandelt. Wenn Sie eine Variable vom Typ "Non-POD" deklarieren, behandelt der Konstruktor die Initialisierung.

Im folgenden Beispiel werden einige einfache Variablendeklarationen dargestellt, jeweils mit einigen Beschreibungen. In dem Beispiel wird auch die Verwendung der Typinformationen durch den Compiler dargestellt, um bestimmte nachfolgende Vorgänge in den Variablen zuzulassen oder zu verweigern.

int result = 0;              // Declare and initialize an integer.
double coefficient = 10.8;   // Declare and initialize a floating
                             // point value.
auto name = "Lady G.";       // Declare a variable and let compiler
                             // deduce the type.
auto address;                // error. Compiler cannot deduce a type
                             // without an intializing value.
age = 12;                    // error. Variable declaration must
                             // specify a type or use auto!
result = "Kenny G.";         // error. Can't assign text to an int.
string result = "zero";      // error. Can't redefine a variable with
                             // new type.
int maxValue;                // Not recommended! maxValue contains
                             // garbage bits until it is initialized.

Grundlegende (integrierte) Typen

Im Gegensatz zu einigen Sprachen gibt es bei C++ keinen universellen Basistyp, von dem alle anderen Typen abgeleitet werden. Die Sprache enthält viele grundlegende Typen, die auch als integrierte Typen bezeichnet werden. Zu diesen Typen gehören numerische Typen wie int, , double, long, boolund die charwchar_t Typen für ASCII- bzw. UNICODE-Zeichen. Die meisten integralen grundlegenden Typen (außer bool, , doublewchar_t, und verwandte Typen) verfügen unsigned über Versionen, die den Wertebereich ändern, den die Variable speichern kann. Beispielsweise kann eine int32-Bit-ganzzahlige 32-Bit-Ganzzahl einen Wert von -2.147.483.648 bis 2.147.483.647 darstellen. Eine unsigned int, die auch als 32 Bit gespeichert ist, kann einen Wert von 0 bis 4.294.967.295 speichern. Die Gesamtzahl der möglichen Werte ist in beiden Fällen gleich, nur der Bereich ist anders.

Der Compiler erkennt diese integrierten Typen und verfügt über integrierte Regeln, die steuern, welche Vorgänge sie ausführen können, und wie sie in andere grundlegende Typen konvertiert werden können. Eine vollständige Liste der integrierten Typen und deren Größe und numerische Grenzwerte finden Sie unter integrierten Typen.

Die folgende Abbildung zeigt die relativen Größen der integrierten Typen in der Microsoft C++-Implementierung:

Diagram of the relative size in bytes of several built in types.

In der folgenden Tabelle sind die am häufigsten verwendeten grundlegenden Typen und deren Größen in der Microsoft C++-Implementierung aufgeführt:

Typ Size Kommentar
int 4 Byte Die Standardauswahl für ganzzahlige Werte.
double 8 Byte Die Standardauswahl für Gleitkommawerte.
bool 1 Byte Stellt Werte dar, die entweder wahr oder falsch sein können.
char 1 Byte Verwenden Sie sie für ASCII-Zeichen in Zeichenfolgen im älteren C-Format oder in std::string Objekten, die nie in den UNICODE konvertiert werden müssen.
wchar_t 2 Bytes Stellt "breite" Zeichenwerte dar, die in den UNICODE-Format codiert werden (UTF-16 bei Windows, andere Betriebssysteme können abweichen). wchar_t ist der Zeichentyp, der in Zeichenfolgen des Typs std::wstringverwendet wird.
unsigned char 1 Byte C++ hat keinen integrierten Bytetyp. Wird unsigned char verwendet, um einen Bytewert darzustellen.
unsigned int 4 Byte Die Standardauswahl für Bitflags.
long long 8 Byte Stellt einen viel größeren Bereich von ganzzahligen Werten dar.

Andere C++-Implementierungen können für bestimmte numerische Typen unterschiedliche Größen verwenden. Weitere Informationen zu den Größen- und Größenbeziehungen, die der C++-Standard erfordert, finden Sie unter integrierten Typen.

Der Typ void

Der void Typ ist ein spezieller Typ. Sie können keine Variable vom Typ deklarieren, aber Sie können eine Variable vom Typ voidvoid * (Zeiger auf void) deklarieren, die manchmal erforderlich ist, wenn Sie den unformatierten (nicht typisierten) Speicher zuordnen. Zeiger, die void nicht typsicher sind und deren Verwendung in modernen C++ abgeraten wird. In einer Funktionsdeklaration bedeutet ein void Rückgabewert, dass die Funktion keinen Wert zurückgibt. Die Verwendung als Rückgabetyp ist eine häufige und akzeptable Verwendung von void. Während die C-Sprache Funktionen benötigt, die null Parameter enthalten, um sie in der Parameterliste zu deklarieren void , fn(void)wird diese Vorgehensweise in moderner C++ abgeraten. Eine parameterlose Funktion sollte deklariert fn()werden. Weitere Informationen finden Sie unter Typkonvertierungen und Typsicherheit.

const Typqualifizierer

Jeder integrierte oder benutzerdefinierte Typ kann vom const Schlüsselwort (keyword) qualifiziert werden. Darüber hinaus können constMemberfunktionen -qualifiziert und sogar const-überladen sein. Der Wert eines const Typs kann nach der Initialisierung nicht mehr geändert werden.

const double PI = 3.1415;
PI = .75; //Error. Cannot modify const variable.

Der const Qualifizierer wird umfassend in Funktions- und Variablendeklarationen verwendet, und "Const correctness" ist ein wichtiges Konzept in C++. Im Wesentlichen bedeutet es, zur Kompilierungszeit zu verwenden const , dass Werte nicht unbeabsichtigt geändert werden. Weitere Informationen finden Sie unter const.

Ein const Typ unterscheidet sich von seiner nicht-Versionconst , z const int . B. ein unterschiedlicher Typ von int. Sie können den C++const_cast-Operator in seltenen Fällen verwenden, wenn Sie die Konstität aus einer Variablen entfernen müssen. Weitere Informationen finden Sie unter Typkonvertierungen und Typsicherheit.

String-Typen

Streng genommen verfügt die C++-Sprache über keinen integrierten Zeichenfolgentyp. char und wchar_t Speichern einzelner Zeichen – Sie müssen ein Array dieser Typen deklarieren, um eine Zeichenfolge anzunähern und einen endenden Nullwert (z. B. ASCII '\0') zum Arrayelement hinzuzufügen, das über das letzte gültige Zeichen (auch als C-Style-Zeichenfolge bezeichnet wird). Für Zeichenfolgen im C-Stil ist das Schreiben von viel mehr Code oder die Verwendung externer Hilfsprogrammbibliotheken für Zeichenfolgefunktionen erforderlich. In modernen C++ verfügen wir jedoch über die Standardbibliothekstypen std::string (für 8-Bit-Zeichenzeichenfolgen char) oder std::wstring (für 16-Bit-Zeichenzeichenfolgen wchar_t). Diese C++-Standardbibliothekscontainer können als systemeigene Zeichenfolgentypen betrachtet werden, da sie Teil der Standardbibliotheken sind, die in jeder konformen C++-Buildumgebung enthalten sind. Verwenden Sie die #include <string> Direktive, um diese Typen in Ihrem Programm verfügbar zu machen. (Wenn Sie MFC oder ATL verwenden, ist die CString Klasse ebenfalls verfügbar, aber nicht Teil des C++-Standards.) Die Verwendung von Null-gekündigten Zeichenarrays (die zuvor Erwähnung ed C-Format) wird in modernen C++ abgeraten.

Benutzerdefinierte Typen

Wenn Sie ein class, struct, , oder unionenum, dieses Konstrukt im restlichen Code so verwenden, als wäre es ein grundlegender Typ. Es verfügt über eine bekannte Größe im Arbeitsspeicher und bestimmte Regeln zur Verwendung gelten zur Kompilierzeitüberprüfung und zur Laufzeit für die Lebensdauer des Programms. Die wichtigsten Unterschiede zwischen den grundlegenden integrierten Typen und den benutzerdefinierten Typen sind wie folgt:

  • Der Compiler verfügt über kein integriertes Wissen zu einem benutzerdefinierten Typ. Er lernt den Typ, wenn er beim ersten Mal auf die Definition während des Kompilierungsprozesses trifft.

  • Sie geben an, welche Vorgänge auf dem Typ ausgeführt werden können und wie er in andere Typen konvertiert werden kann, indem Sie (durch Überladen) die entsprechenden Operatoren entweder als Klassenmember oder als Funktionen definieren. Weitere Informationen finden Sie unter Funktionsüberladung

Zeigertypen

Wie in den frühesten Versionen der C-Sprache ermöglicht C++ weiterhin, eine Variable eines Zeigertyps mithilfe des speziellen Deklarators * (Sternchen) zu deklarieren. Ein Zeigertyp speichert die Adresse des Speicherorts im Arbeitsspeicher, in dem der tatsächliche Datenwert gespeichert wird. In modernen C++ werden diese Zeigertypen als unformatierte Zeiger bezeichnet, und sie werden in Ihrem Code über spezielle Operatoren aufgerufen: * (Sternchen) oder -> (Gedankenstrich mit größer als, häufig als Pfeil bezeichnet). Dieser Speicherzugriffsvorgang wird als Dereferencing bezeichnet. Welcher Operator Sie verwenden, hängt davon ab, ob Sie einen Zeiger auf einen Skalar oder einen Zeiger auf ein Element in einem Objekt ableiten.

Das Arbeiten mit Zeigertypen ist seit Langem einer der schwierigsten und verwirrendsten Aspekte bei der Programmentwicklung mit C- und C++. In diesem Abschnitt werden einige Fakten und Methoden beschrieben, die ihnen bei Bedarf bei der Verwendung von unformatierten Zeigern helfen. In modernen C++ ist es jedoch nicht mehr erforderlich (oder empfohlen), rohe Zeiger für den Objektbesitz überhaupt zu verwenden, da die Entwicklung des intelligenten Zeigers (mehr am Ende dieses Abschnitts erläutert) ist. Es ist immer noch nützlich und sicher, unformatierte Zeiger zum Beobachten von Objekten zu verwenden. Wenn Sie sie jedoch für den Objektbesitz verwenden müssen, sollten Sie dies vorsichtig und sorgfältig prüfen, wie die Objekte, die ihnen gehören, erstellt und zerstört werden.

Als Erstes sollten Sie wissen, dass eine unformatierte Zeigervariablendeklaration nur genügend Arbeitsspeicher zum Speichern einer Adresse zuweist: der Speicherspeicherort, auf den der Zeiger verweist, wenn er abgeleitet wird. Die Zeigerdeklaration weist den zum Speichern des Datenwerts erforderlichen Arbeitsspeicher nicht zu. (Dieser Speicher wird auch als Sicherungsspeicher bezeichnet.) Anders ausgedrückt: Durch das Deklarieren einer unformatierten Zeigervariable erstellen Sie eine Speicheradressenvariable, keine tatsächliche Datenvariable. Wenn Sie eine Zeigervariable ableiten, bevor Sie sichergestellt haben, dass sie eine gültige Adresse für einen Sicherungsspeicher enthält, verursacht sie ein nicht definiertes Verhalten (in der Regel ein schwerwiegender Fehler) in Ihrem Programm. Im folgenden Beispiel wird die Verwendung dieses Fehlertyps veranschaulicht.

int* pNumber;       // Declare a pointer-to-int variable.
*pNumber = 10;      // error. Although this may compile, it is
                    // a serious error. We are dereferencing an
                    // uninitialized pointer variable with no
                    // allocated memory to point to.

Das Beispiel dereferenziert einen Zeigertyp, ohne dass Arbeitsspeicher zum Speichern der tatsächlichen Ganzzahldaten belegt ist oder dass ein gültiger Speicherort zu zugewiesen wurde. Der folgende Code korrigiert diese Fehler:

    int number = 10;          // Declare and initialize a local integer
                              // variable for data backing store.
    int* pNumber = &number;   // Declare and initialize a local integer
                              // pointer variable to a valid memory
                              // address to that backing store.
...
    *pNumber = 41;            // Dereference and store a new value in
                              // the memory pointed to by
                              // pNumber, the integer variable called
                              // "number". Note "number" was changed, not
                              // "pNumber".

Im korrigierten Codebeispiel wird lokaler Stapelarbeitsspeicher zum Erstellen von Sicherungsspeicher, auf den pNumber verweist, verwendet. Wir verwenden der Einfachheit halber einen grundlegenden Typ. In der Praxis sind die Sicherungsspeicher für Zeiger am häufigsten benutzerdefinierte Typen, die dynamisch in einem Speicherbereich zugeordnet werden, der als Heap (oder kostenloser Speicher) bezeichnet wird, mithilfe eines new Schlüsselwort (keyword) Ausdrucks (bei der Programmierung im C-Stil wurde die ältere malloc() C-Laufzeitbibliotheksfunktion verwendet). Nach der Zuordnung werden diese Variablen normalerweise als Objekte bezeichnet, insbesondere, wenn sie auf einer Klassendefinition basieren. Speicher, der zugeordnet ist, muss durch eine entsprechende delete Anweisung gelöscht werden (oder wenn Sie die malloc() Funktion zum Zuordnen new der Funktion verwendet haben, die C-Laufzeitfunktionfree()).

Es ist jedoch leicht zu vergessen, ein dynamisch zugeordnetes Objekt zu löschen – insbesondere im komplexen Code, was zu einem Ressourcenfehler führt, der als Speicherverlust bezeichnet wird. Aus diesem Grund wird die Verwendung von Rohzeigern in modernen C++ abgeraten. Es ist fast immer besser, einen unformatierten Zeiger in einen intelligenten Zeiger umzuschließen, der den Speicher automatisch freigibt, wenn der Destruktor aufgerufen wird. (Das heißt, wenn der Code außerhalb des Gültigkeitsbereichs für den intelligenten Zeiger steht.) Durch die Verwendung intelligenter Zeiger beseitigen Sie praktisch eine ganze Klasse von Fehlern in Ihren C++-Programmen. Im folgenden Beispiel wird angenommen, dass MyClass ein benutzerdefinierter Typ ist, der eine öffentliche Methode DoSomeWork(); umfasst.

void someFunction() {
    unique_ptr<MyClass> pMc(new MyClass);
    pMc->DoSomeWork();
}
  // No memory leak. Out-of-scope automatically calls the destructor
  // for the unique_ptr, freeing the resource.

Weitere Informationen zu intelligenten Zeigern finden Sie unter "Intelligente Zeiger".

Weitere Informationen zu Zeigerkonvertierungen finden Sie unter Typkonvertierungen und Typsicherheit.

Weitere Informationen zu Zeigern im Allgemeinen finden Sie unter "Zeiger".

Windows-Datentypen

In der klassischen Win32-Programmierung für C und C++ verwenden die meisten Funktionen Windows-spezifische Typedefs und #define Makros (definiert in windef.h), um die Typen von Parametern und Rückgabewerten anzugeben. Diese Windows-Datentypen sind hauptsächlich spezielle Namen (Aliase) für integrierte C/C++-Typen. Eine vollständige Liste dieser Typedefs und Präprozessordefinitionen finden Sie unter Windows-Datentypen. Einige dieser Typedefs, z HRESULT . B. und LCID, sind nützlich und beschreibend. Andere, z INT. B. , haben keine besondere Bedeutung und sind nur Aliase für grundlegende C++-Typen. Weitere Windows-Datentypen haben Namen, die seit den Tagen der C-Programmierung und der 16-Bit-Prozessoren beibehalten werden. Sie haben keinen Zweck oder keine Bedeutung mehr bei moderner Hardware oder Betriebssystemen. Es gibt auch spezielle Datentypen, die der Windows-Runtime Bibliothek zugeordnet sind, die als Windows-Runtime Basisdatentypen aufgeführt sind. In modernen C++ ist die allgemeine Richtlinie, die grundlegenden C++-Typen zu bevorzugen, es sei denn, der Windows-Typ kommuniziert etwas zusätzliche Bedeutung darüber, wie der Wert interpretiert werden soll.

Weitere Informationen

Weitere Informationen zum C++-Typsystem finden Sie in den folgenden Artikeln.

Werttypen
Beschreibt Werttypen zusammen mit Problemen im Zusammenhang mit deren Verwendung.

Typumwandlungen und Typsicherheit
Beschreibt allgemeine Typkonvertierungsprobleme und zeigt, wie sie vermieden werden.

Siehe auch

Willkommen zurück bei C++
C#-Programmiersprachenreferenz
C++-Standardbibliothek