Bei der objektorientierten Programmierung werden, vereinfacht ausgedrückt, Daten und die sie verarbeitenden Funktionen in einem Datentyp zusammengefasst. Und in C++ ist dies der Datentyp Klasse.
Dabei werden die Daten einer Klasse als deren Eigenschaften bezeichnet und die Funktionen als deren Memberfunktionen.
Alle Eigenschaften und Memberfunktionen sind dann die Member der Klasse.
Und die Instanziierung einer Klasse ist schließlich das Objekt.
C++ kennt 3 verschiedene Klassentypen: class, struct und union.
Die Definition einer Klasse wird durch das Schlüsselwort class bzw. struct eingeleitet. Nach diesem Schlüsselwort folgt der Name der Klasse und anschließend ein Paar geschweifter Klammern. Innerhalb der Klammer werden die Member definiert bzw. deklariert.
struct ClassName1
{
// Hier stehen die Eigenschaften
// und die Memberfunktionen
};
class ClassName2
{
// Hier stehen die Eigenschaften
// und die Memberfunktionen
};
Die Definition einer Eigenschaft erfolgt genauso wie die Definition einer Variable, nur jetzt innerhalb der geschweiften Klammer der Klasse.
struct Circle
{
// Eigenschaften definieren
int xpos, ypos;
unsigned int radius;
};
Eine Memberfunktion kann innerhalb oder außerhalb der Klasse definiert werden.
Wird eine Memberfunktion innerhalb der Klasse definiert, wird sie wie eine 'normale' Funktionen definiert. Der Zugriff auf die Eigenschaften ihrer Klasse erfolgt durch Angabe des Namens der jeweiligen Eigenschaft.
struct Circle
{
// Eigenschaften definieren
int xpos, ypos;
unsigned int radius;
// Memberfunktionen definieren
// Eigenschaften setzen
void SetData (int xp, int yp, unsigned int rad)
{
xpos = xp; ypos = yp;
radius = rad;
}
// Ausgabe der Eigenschaften
void Print()
{
std::cout << std::format("Kreis mit Radius {} auf ({},{})\n", radius, xpos, ypos);
}
};
Wird die Memberfunktion außerhalb der Klasse definiert, ist die Memberfunktion innerhalb der Klasse zu deklarieren.
Bei der Definition außerhalb der Klasse ist vor dem Namen der Memberfunktion der Klassenname sowie der Zugriffsoperator :: (zwei Doppelpunkte) anzugeben.
struct Circle
{
// Eigenschaften definieren
int xpos, ypos;
unsigned int radius;
// Memberfunktionen deklarieren
// Eigenschaften setzen
void SetData(int xp, int yp, unsigned int rad);
// Ausgabe der Eigenschaften
void Print();
};
// Eigenschaften setzen
void Circle::SetData(int xp, int yp, unsigned int rad)
{
xpos = xp; ypos = yp;
radius = rad;
}
// Ausgabe der Eigenschaften
void Circle::Print()
{
std::cout << std::format("Kreis mit Radius {} auf ({},{})\n", radius, xpos, ypos);
}
Vor C++20 erfolgte i.d.R. die Definition der Klasse in einer eigenen Header-Datei und die Definition der Memberfunktionen in einer entsprechenden Quellcode-Datei.
// Datei graphics.h
// ----------------
struct Circle
{
// Eigenschaften definieren
int xpos, ypos;
unsigned int radius;
// Memberfunktionen deklarieren
// Eigenschaften setzen
void SetData(int xp, int yp, unsigned int rad);
// Ausgabe der Eigenschaften
void Print();
};
// Datei graphics.cpp
// ------------------
#include <iostream>
#include <format>
#include "graphics.h"
// Eigenschaften setzen
void Circle::SetData(int xp, int yp, unsigned int rad)
{
xpos = xp; ypos = yp;
radius = rad;
}
// Ausgabe der Eigenschaften
void Circle::Print()
{
std::cout << std::format("Kreis mit Radius {} auf ({},{})\n", radius, xpos, ypos);
}
Mit der Einführung von Modulen mit C++20 ist diese Aufteilung nicht mehr notwendig. Die Definition der Klasse und deren Memberfunktionen kann in einer Datei erfolgen.
// Datei graphics.cxx
// ------------------
module;
#include <iostream>
#include <format>
export module Graphics;
// Alle Member der Klasse exportieren
export struct Circle
{
// Eigenschaften definieren
int xpos, ypos;
unsigned int radius;
// Memberfunktionen deklarieren
// Eigenschaften setzen
void SetData(int xp, int yp, unsigned int rad);
// Ausgabe der Eigenschaften
void Print();
};
// Eigenschaften setzen
void Circle::SetData(int xp, int yp, unsigned int rad)
{
xpos = xp; ypos = yp;
radius = rad;
}
// Ausgabe der Eigenschaften
void Circle::Print()
{
std::cout << std::format("Kreis mit Radius {} auf ({},{})\n", radius, xpos, ypos);
}
Memberfunktionen die keine Änderungen an den Eigenschaften vornehmen, sollten als const-Memberfunktionen definiert werden.
Eine const-Memberfunktion wird dadurch gekennzeichnet, dass nach der Parameterklammer das Schlüsselwort const angegeben wird.
Die Print()-Memberfunktion aus dem obigen Beispiel würde damit wie folgt aussehen:
module;
#include <iostream>
#include <format>
export module Graphics;
export struct Circle
{
// Eigenschaften definieren
...
// Memberfunktionen deklarieren
...
void Print() const;
};
...
// Ausgabe der Eigenschaften
void Circle::Print() const
{...}
Wird ein Datum vom Typ einer Klasse instanziiert, spricht man nicht mehr von einer Variable, sondern von einem Objekt. Bei der Definition eines Objekts kann bei Eindeutigkeit das Schlüsselwort struct bzw. class weggelassen werden.
struct Circle circle1;
Circle cicle2;
Sollen mehrere Objekte desselben Typs definiert werden, können diese zu einem Objektfeld zusammengefasst werden.
Circle circles[4];
Der direkte Zugriff auf die Member eines Objekts erfolgt in der Art, dass zuerst der Objektname angegeben wird, dann der Operator . (ein Punkt) und anschließend der Membername.
Bei einem indirekten Zugriff wird anstelle des Operators . der Operator -> (Minuszeichen+spitze Klammer zu) angegeben.
#include <iostream>
#include <format>
import Graphics; // Definiert in der Datei graphics.cxx
int main()
{
// Kreisobjekt definieren
Circle circle1;
// Kreisdaten setzen
circle1.SetData(10,10,5);
// Kreis ausgeben
circle1.Print();
// Zeiger auf ein Kreisobjekt definieren
// und mit Verweis auf circle1 initialisieren
Circle *pmyCircle = &circle1;
// Kreisdaten indirekt neu setzen und ausgeben
pmyCircle->SetData(20,20,10);
pmyCircle->Print();
}
Der Unterschied zwischen den Klassentypen class und struct liegt im voreingestellten Zugriffsrecht auf deren Member.
Auf die Member des Klassentyps struct kann standardmäßig beliebig zugegriffen werden, was auch der Grund ist, warum wir bisher nur diesen Klassentyp verwendet haben.
Auf Member des Klassentyps class dagegen kann standardmäßig nur von Memberfunktionen der gleichen Klassen aus zugegriffen werden.
#include <iostream>
#include <format>
// Klassentyp struct
// Alle Member sind standardmaessig oeffentlich
struct CircleStruct
{
// Eigenschaften int xpos,ypos;
unsigned int radius;
// Memberfunktionen
void SetData(int xp, int yp, unsigned int rad)
{...}
// Ausgabe der Eigenschaften
void Print() const
{...}
};
// Klassentyp class
// Alle Member sind standardmaessig nicht oeffentlich sondern
// der Zugriff kann nur von Memberfunktionen aus erfolgen
class CircleClass
{
// Eigenschaften
int xpos,ypos;
unsigned int radius;
// Memberfunktionen
void SetData(int xp, int yp, unsigned int rad)
{...}
// Ausgabe der Eigenschaften
void Print() const
{...}
};
int main()
{
// Objekt vom Typ struct definieren
CircleStruct circle1;
// Auf alle Member kann standardmaessig zugegriffen werden
circle1.xpos = 99;
circle1.SetData(20,20,5);
circle1.Print();
// Objekt vom Typ class definieren
// Auf alle Member kann standardmaessig nur aus
// Memberfunktionen heraus zugegriffen werden
CircleClass circle2;
circle2.xpos = 88; // FEHLER!
circle2.SetData(30,30,10); // FEHLER!
}
Dieses voreingestellte Zugriffsrecht kann durch Angabe der Zugriffsspezifizierer public: bzw. private: innerhalb der Klasse geändert werden. Auf alle Member die nach public: folgen kann beliebig zugegriffen werden während auf Member die nach private: folgen nur von Memberfunktionen heraus zugegriffen werden kann. Die folgenden beiden Klassen verhalten sich damit identisch.
// Klassentyp struct
// Alle Member sind standardmaessig oeffentlich
struct CircleStruct
{
// Eigenschaften schuetzen
private:
int xpos,ypos;
unsigned int radius;
// Memberfunktionen veroeffentlichen
public:
void SetData(int xp, int yp, unsigned int rad)
{...}
// Ausgabe der Eigenschaften
void Print() const
{...}
};
// Klassentyp class
// Alle Member sind standardmaessig nicht oeffentlich sondern
// der Zugriff kann nur von Memberfunktionen aus erfolgen
class CircleClass
{
// Eigenschaften sind geschuetzt
int xpos,ypos;
unsigned int radius;
// Memberfunktionen veroeffentlichen
public:
void SetData(int xp, int yp, unsigned int rad)
{...}
// Ausgabe der Eigenschaften
void Print() const {...}
};
Das Zugriffsrecht auf die Member kann beliebig oft geändert werden. Damit eine Klasse nicht zu unübersichtlich wird, sollte die Definition der Member strukturiert erfolgen, z.B.
Objekte können genauso wie Variablen zugewiesen werden. Dabei werden alle Eigenschaften des Quellobjekts in das Zielobjekt kopiert.
Werden Objekte an (Member-)Funktionen übergeben, sollte dies per Referenz erfolgen. Bei einer Übergabe per call-by-value werden die Eigenschaften des zu übergebenden Objekts zunächst in ein temporäres Objekt kopiert, dieses an die Funktion übergeben und nach der Rückkehr aus der Funktion wieder gelöscht. Dies kann unter Umständen sehr viel Zeit und Speicherplatz belegen.
Der Nachteil der Übergabe eines Objekts per Referenz ist, dass das Objekt innerhalb der Funktion unbeabsichtigt verändert werden kann. Soll eine Änderung des Objekts ausgeschlossen werden, ist das Objekt als const-Referenz zu übergeben. Die Funktion darf dann aber nur const-Memberfunktionen aufrufen, da ansonsten wieder Änderungen am Objekt möglich wären.
#include <iostream>
#include <format>
import Graphics;
// Objektuebergabe call-by-value
void CalledByValue(Circle param)
{
std::cout << "CalledByValue:\n";
param.SetData(88,88,11);
param.Print();
}
// Objektuebergabe als Referenz
void CalledReference(Circle& param)
{
std::cout << "CalledReference:\n";
param.SetData(99,99,22);
param.Print();
}
// Objektuebergabe als const-Referenz
void CalledConstReference(const Circle& param)
{
std::cout << "CalledConstReference:\n";
// Dies fuehrt zu einem Fehler, da eine
// Aenderung am Objekt nicht erlaubt ist
// param.SetData(77,77,33);
param.Print();
}
int main()
{
// Objekt definieren und Eigenschaft setzen
Circle circle1;
circle1.SetData(11,11,5);
std::cout << "Kreisdefinition:\n";
circle1.Print();
// Objekt call-by-value uebergeben
// Aenderungen am Objekt in der Funktion haben
// keine Auswirkung am Objekt in main()
CalledByValue(circle1);
std::cout << "main:\n";
circle1.Print();
// Objekt als Referenz uebergeben
// Aenderungen am Objekt in der Funktion haben
// Auswirkung am Objekt in main()
CalledReference(circle1);
std::cout << "main:\n";
circle1.Print();
// Objekt als const-Referenz uebergeben
// Aenderungen am Objekt in der Funktion sind
// nicht erlaubt
CalledConstReference(circle1);
std::cout << "main:\n"; circle1.Print();
}
Wird aus einer Funktion ein Objekt zurückgegeben, ist immer das Objekt selbst und niemals eine Referenz darauf zurückzugeben.
import Graphics;
// Funktion die ein Kreisobjekt erstellt und
// dieses zurueckgibt
Circle CreateCircle()
{
Circle circle1;
circle1.SetData(20,20,5);
return circle1;
}
int main()
{
// Kreis erzeugen
auto newCircle = CreateCircle();
// Kreis ausgeben
newCircle.Print();
}
Structured binding bezeichnet die Möglichkeit, mehrere Objekte oder Variablen in einer Anweisung zu definieren und dabei die public-Daten eines anderen Objekts zu übernehmen.
auto [var1,var2,...] = obj;
Die Anweisung definiert die Variablen varx und übernimmt die Eigenschaften aus obj in der Reihenfolgen ihrer Definition in die Variablen.
Außer bei Zuweisungen kann structured binding auch beim Durchlaufen von Feldern in einer range-for Schleife eingesetzt werden.
for (auto [var1,var2]: myArray)
{...}
#include <iostream>
#include <format>
// Definition eines Punkts
struct Point
{
unsigned int xpos, ypos;
};
// Feld mit Punkten
Point myPoints[] {{10,10},{20,20},{30,30}};
int main()
{
// Koordinate des 2. Punkts ausgeben
auto [xp,yp] = myPoints[1];
std::cout << std::format("XPos: {}, YPos: {}\n",xp,yp);
// Alle Koordinaten der Punkte ausgeben
for (auto [xp,yp]: myPoints)
std::cout << std::format("XPos: {}, YPos: {}\n",xp,yp);
}
Da Klassen eine zentrale Rolle spielen, einmal eine Übung aus meinem Buch "C++ Programmierung".
Es ist eine Klasse zum Rechnen mit komplexen Zahlen zu entwickeln.
Eine komplexe Zahl besitzt zwei Eigenschaften: einen Realanteil und einen Imaginäranteil. Für beide Eigenschaften sind Gleitkomma-Datentypen zu verwenden.
Damit der Anwender die Eigenschaften (Real- und Imaginäranteil) nicht direkt ändern kann, sind sie gegen den direkten Zugriff zu schützen.
Zum Setzen einer komplexen Zahl sowie für deren Ausgabe sind entsprechende Memberfunktionen zu implementieren. Die Ausgabe der Daten soll mit vier Nachkommastellen erfolgen.
Zusätzlich sind zwei Memberfunktionen zu schreiben, um komplexe Zahlen addieren und subtrahieren zu können. Werden zwei komplexe Zahlen addiert bzw. subtrahiert, werden deren Real- und Imaginäranteile jeweils getrennt addiert bzw. subtrahiert (siehe Programmausgabe).
Definieren Sie zwei Objekte, initialisieren diese und geben sie aus.
Danach ist die zweite Zahl zur ersten zu addieren und das Ergebnis auszugeben. Das so erhaltene Ergebnis ist anschließend von der zweiten Zahl zu subtrahieren und erneut auszugeben.
#include <iostream>
#include <format>
// Klassendefinition
class Complex
{
// Geschuetzte Eigenschaften
double real = 0.0; // Real-Anteil
double imag = 0.0; // Imaginär-Anteil
public:
// Memberfunktionen
void Init(decltype(real) r, decltype(imag) i);
void AddComplex(const Complex& op);
void SubComplex(const Complex& op);
void PrintComplex() const;
};
// Definition der Memberfunktionen
// Komplexe Zahl initialisieren
void Complex::Init(decltype(real) r, decltype(imag) i)
{
real = r; imag = i;
}
// 2 komplexe Zahlen addieren
void Complex::AddComplex(const Complex& op)
{
real += op.real;
imag += op.imag;
}
// 2 komplexe Zahlen subtrahieren
void Complex::SubComplex(const Complex& op)
{
real -= op.real;
imag -= op.imag;
}
// Komplexe Zahl ausgeben
void Complex::PrintComplex() const
{
std::cout << std::format("Realteil:{:.4f} / Imaginaerteil:{:.4f}\n", real, imag);
}
// main() Funktion
int main()
{
// 2 komplexe Zahlen (Objekte) definieren
Complex number1, number2;
// Objekte initialisieren
number1.Init(1.1, 2.2);
number2.Init(3.3, 4.4);
// Objekte ausgeben
std::cout << "1. Komplexe Zahl:\n";
number1.PrintComplex();
std::cout << "2. Komplexe Zahl:\n";
number2.PrintComplex();
// 2. Objekt zum 1. Objekt addieren
number1.AddComplex(number2);
// und Ergebnis ausgeben
std::cout << "\nNach Addition 2. Zahl zur 1. Zahl\nNeue 1. Zahl:\n";
number1.PrintComplex();
// 1. Objekt vom 2. Objekt subtrahieren
number2.SubComplex(number1);
// und Ergebnis ausgeben
std::cout << "\nNach Subtraktion der 1. Zahl von der 2. Zahl\n" "Neue 2. Zahl:\n";
number2.PrintComplex();
}