Eine Klasse ist ein Datentyp und ein Objekt eine Variable, deren Datentyp eine Klasse ist. Deshalb kann man ein Objekt wie eine Variable eines einfachen Datentyps definieren. Es enthält dann alle Elemente der Klasse. Diese kann man unter dem Namen des Objekts ansprechen, auf den ein Punktoperator „ . “ und der Name des Elements folgt.
Beispiel 1: Mit der Klasse C2DPunkt erhält man durch die folgenden Definition ein Objekte p dieser Klasse:
C2DPunkt p;
Diese Objekte enthalten dann die Datenelemente
p.x und p.y
Solange eine Klasse keine virtuellen Elementfunktionen oder statischen Elemente (mehr darüber später) enthält, ergibt sich der Speicherplatzbedarf für ein Objekt nur aus dem für seine Datenelemente. Die Elementfunktionen tragen nicht dazu bei. Falls der Compiler für die Elemente eines Objekts nicht mehr Platz als notwendig reserviert, belegt ein Objekt genauso viel Speicherplatz wie alle Datenelemente seiner Klasse zusammen:
sizeof(C2DPunkt) = sizeof(double) + sizeof(double) = 16
Mit den Zugriffsrechten private, protected und public kann man für jedes Klassenelement explizit festlegen, ob man über ein Objekt darauf zugreifen kann.
Diese Spezifizierer definieren ab ihrer Angabe einen Abschnitt mit Zugriffsrechten, die für alle folgenden Elemente bis zum nächsten solchen Spezifizierer oder bis zum Ende der Klasse gelten. Ein Element aus einem private, protected oder public Abschnitt heißt auch private, protected oder public Element.
Ohne die Angabe eines Zugriffsrechts sind alle Elemente einer mit class definierten Klasse private, während alle Elemente einer mit struct definierten Klasse public sind. Dieses voreingestellte Zugriffsrechte ist der einzige Unterschied zwischen einer mit class und einer mit struct definierten Klasse.
Beispiel 2a: Alle Elemente der Klassen C0 und C1 haben dieselben Zugriffsrechte:
class C0 {int x; int f() { return x; } }; struct C1 {private: int x; int f() { return x; } };
Jeder Zugriff auf diese Elemente über ein Objekt führt zu einer Fehlermeldung:
void test(C0 a, C1 b) { a.x=1;//Fehler: Zugriff auf 'C0::x' nicht möglich a.f();//Fehler: Zugriff auf 'C0::f()' nicht mögl. b.x=1;//Fehler: Zugriff auf 'C1::x' nicht möglich b.f();//Fehler: Zugriff auf 'C1::f()' nicht mögl. }
Mit dem Zugriffsrecht public sind alle diese Zugriffe zulässig:
class C0 {public: int x; int f() { return x; } }; struct C1 {int x; int f() { return x; } };
Bemerkung 1: Der Compiler prüft das Zugriffsrecht auf ein Klassenelement allerdings nur bei der Verwendung seines Namens. Wenn man den Speicherbereich eines private Elements über seine Adresse (via Zeiger) anspricht, kann man die Zugriffsrechte umgehen. Von solchen Manipulationen kann aber nur abgeraten werden.
Bemerkung 2: Eine Klasse kann eine beliebige Anzahl von Abschnitten mit verschiedenen Zugriffsrechten in einer beliebigen Reihenfolge enthalten. Die Reihenfolge der Abschnitte ist dabei ohne Bedeutung. Es wird aber gelegentlich empfohlen, sie in der Reihenfolge public, protected und private aufzuführen. Dann kommen die Elemente zuerst, die für einen Anwender der Klasse von Bedeutung sind, und dieser muss dann den Rest der Klasse überhaupt nicht mehr anschauen, der nur für einen Entwickler von abgeleiteten Klassen (protected Elemente) oder dieser Klasse (private Elemente) von Bedeutung ist.
Beispiel 2b: Die beiden Klassen C1 und C2 sind gleichwertig. Oft werden die verschiedenen Abschnitte wie bei C1 in der Reihenfolge private, protected und public aufgeführt. Wenn man sie aber wie bei C2 in der umgekehrten Reihenfolge anordnet, kommen die Elemente, die für das breiteste Publikum interessant sind, am Anfang:
class C1 {int x; public: int f(C p); }; class C2 {public: int f(C p); private: int x; };
Ein Benutzer einer Klasse ist dadurch charakterisiert ist, dass er eine Variable des Klassentyps definiert (ein Objekt) und dann auf ihre Elemente zugreift (z.B. Elementfunktionen aufruft). Da man über ein Objekt nur auf die public Elemente zugreifen kann, werden diese auch als Schnittstelle der Klasse bezeichnet.
Beispiel 3a: Die Schnittstelle der Klasse Datum_1 besteht aus den Datenelementen Tag, Monat und Jahr, und die der Klasse Datum_2 aus den Funktionen setze, Tag, Monat und Jahr. Über ein Objekt der Klasse Datum_2 ist kein Zugriff auf die Elemente Tag_, Monat_ und Jahr_ möglich.
class Datum_1 {public: int Tag, Monat, Jahr; }; class Datum_2 { public: bool gueltigesDatum(int Tag,int Monat,int Jahr) {int MaxTag=31; if ((Monat==4)||(Monat==6)||(Monat==9)||(Monat==11)) MaxTag=30; else if (Monat==2) {bool Schaltjahr =((Jahr%4 == 0) && (Jahr%100 != 0)) || (Jahr%400 == 0); if (Schaltjahr) MaxTag=29; else MaxTag=28; } return ((1<=Monat) && (Monat<=12) && (1<=Tag) && (Tag<=MaxTag)); } void setze(int Tag, int Monat, int Jahr) {if (gueltigesDatum(Tag, Monat, Jahr)) {Tag_=Tag; Monat_=Monat; Jahr_=Jahr; } else Fehlermeldung("Ungültiges Datum"); } int Tag() { return Tag_;} int Monat() { return Monat_;} int Jahr() { return Jahr_;} private: int Tag_, Monat_, Jahr_; };
Obwohl es auf den ersten Blick unsinnig erscheinen mag, den Zugriff auf Datenelemente zu beschränken (Datenkapselung, information hiding), können Schnittstellen ohne Datenelemente gravierende Vorteile haben:
Beispiel 3b: Um mit Kalenderdaten rechnen zu können, stellt man ein Datum oft durch die Anzahl der Tage seit einem bestimmten Stichtag dar. Bei einer Klasse wie Datum_2 kann man die interne Darstellung und die Implementation der Funktionen ändern, so dass ein Anwender den bisher geschriebenen Code weiterverwenden kann. Bei einer Klasse wie Datum_1 ist das dagegen nicht möglich.
Obwohl solche Änderungen nach einer vollständigen Problemanalyse eigentlich nicht vorkommen dürften, sind sie in Praxis nicht selten: Oft erkennt man erst während der Entwicklung eines Systems alle Anforderungen, bzw. nach der Fertigstellung, dass Algorithmen zu langsam sind und optimiert werden müssen. Bei großen Projekten ändern sich die Anforderungen oft während ihrer Realisierung (z.B. durch neue Gesetze).
Beispiel 3c: In der Klasse Datum_2 können die Datenelemente nur in der Funktion setze verändert werden. Falls ein Objekt dieser Klasse ein ungültiges Datum darstellt, muss die Ursache dieses Fehlers in der Funktion setze sein. Mit der Klasse Datum_1 kann ein ungültiges Datum dagegen durch jedes Objekt dieser Klasse verursacht werden.
Beispiel 3d: Da die Datenelemente der Klasse Datum_2 nur in der Funktion setze verändert werden können, und in dieser Funktion die Konsistenz der Daten geprüft wird, ist sichergestellt, dass der Anwender nicht mit inkonsistenten Daten arbeiten kann.
Mit public Datenelementen oder globalen Variablen kann der Entwickler dem Anwender dagegen keine solche Garantie geben. Datenkapselung bietet dem Entwickler also die Möglichkeit, den Zugriff auf die Datenelemente so zu beschränken, dass der Anwender überhaupt keine Möglichkeit hat, solche Fehler zu machen.
Beispiel 3e: In der Klasse Datum_2 können die einzelnen Datenelemente nur gelesen, aber nicht geändert werden.
Deshalb wird oft empfohlen, alle Datenelemente einer Klasse private zu deklarieren, und den Zugriff auf die private Elemente nur über public Elementfunktion zur ermöglichen. Falls durch den Zugriff auf Datenelemente eine Konsistenzbedingung verletzt werden kann, ist das aber nicht nur eine gut gemeinte Empfehlung, sondern ein Muss.
Oft sind auch private Elementfunktionen sinnvoll. Das sind meist Hilfsfunktionen, die nur in den Elementfunktionen aufgerufen werden, aber einem Benutzer der Klasse ausdrücklich nicht zur Verfügung stehen sollen.