Dr. Erhard Henkes, Stand: 27.07.2003


Kapselung ( engl.: information hiding oder encapsulation )

Eine der wesentlichen Konzepte der Objektorientierung ist die Kapselung. Was ist das eigentlich genau?
 
Kapselung:
Allgemein eine Technik zur Schnittstellendefinition. 
Durch Kapselung werden Realisierungsdetails hinter einer definierten Schnittstelle verborgen. Die Schnittstelle kann dadurch oft auch erhalten bleiben, wenn sich Interna ändern müssen.

Nachfolgend versuche ich, an konkreten einfachen Beispielen zu zeigen, was Kapselung in der Praxis bedeuten kann.
Hierbei geht es vorrangig um das Verständnis für die Kapselung von Objekten mit Hilfe von C++-Klassen. Sie werden aber sehen, dass die Kapselung genau genommen ein weit verbreiteter Prozess ist, der in gewisser Weise auch durch die Einführung von Variablen und Funktionen bereits statt findet.

Beginnen wir mit der C++-Programmierung. Die Basis soll folgendes einfache Konsolenprogramm bilden:
 
int main() 

 return 0; 
};

Sie benutzen hier einfach die Funktion main(...). Sie müssen dies machen, damit die Sache ins Rollen kommt.
Das ist Ihre Schnittstelle zu C/C++. Auch das ist in gewissem Sinne bereits Kapselung!

main(...) ist die Schnittstelle zum Compiler. Diese Schnittstelle ist schon über viele Jahre für Konsolen-Programme konstant, obwohl sich Editoren und Compiler gewandelt haben.

Im nächsten Schritt geben wir den String "Hallo" auf dem Bildschirm aus. Wir verwenden hierzu cout.
Daher müssen wir den C++-Header  iostream.h einbinden:
 
#include <iostream>

int main()
{
 std::cout << "Hallo" << std::endl;

 return 0;
};

So kann man typischerweise anfangen, C++ zu lernen. Sie müssen hierzu nicht wissen, was iostream genau ist und wie es intern aufgebaut ist. Sie benutzen einfach std::cout<< ... << std::endl; . Sie verwenden es, und es funktioniert. In iostream sind die notwendigen Funktionalitäten versteckt, die Zeichen auf dem Bildschirm ausgeben. Auch hier findet sich eine gewisse Art der Kapselung! Ein Anfänger muss in keiner Weise wissen, was std::cout eigentlich genau ist. Man lenkt auszugebende Einheiten einfach auf dieses Standardausgabe-Objekt. 

Wir erhöhen die Komplexität, indem wir den auszugebenden String zunächst in einer Variable speichern.
Diese Variable ist vom Typ char-Array mit der Bezeichnung Text:
 
#include <iostream>

 int main()
 {
  char Text[255];  
  char* pText = Text;
  pText = "Gruss aus der Variablen Text";
  std::cout << pText << std::endl;

  return 0;
 };

std::cout << pText << std::endl;
ist die entscheidende Zeile. std::cout << ... << std::endl "kapselt" die Ausgabe, pText "kapselt" den konkreten String (Zeichenfolge).
Diese Zeile kann unverändert bleiben, wenn der Compilerentwickler in iostream etwas ändert oder ein anderer konstanter String zugeordnet wird.

Die beiden oben durchgeführten Aktionen
1) Zuordnung eines Strings zu Text[...]
2) Ausgabe des Inhalts von Text[...] mittels std::cout
werden nachfolgend in eigene Funktionen eingebunden.
Damit "kapseln/umhüllen" Sie zum Beispiel die Ausgabe mittels std::cout:
 
#include <iostream>

 char  Text[255];  
 char* pText = Text;
 
 void SetzeText(char* txt) {pText = txt;                    };
 void Ausgabe  (         ) {std::cout << pText << std::endl;};

 int main()
 {
  SetzeText("Gruss aus der Variablen Text mittels Funktion");
  Ausgabe();

  return 0;
 };

SetzeText("Gruss aus der Variablen Text mittels Funktion");
Ausgabe();

Diesen beiden Funktionen sieht man in keiner Weise an, wie sie realisiert werden.
Das ist in gewisser Weise Kapselung.

Jetzt "kapseln/umhüllen" wir das Ganze noch einmal durch den Einsatz einer Klasse.
Die Zeiger-Variable pText wird Member-Variable. Die beiden Funktionen werden Member-Funktionen.
Der Zugriff wird zunächst sowohl direkt auf die Variable als auch auf die Funktionen gewährt.
Dafür sorgt die Anweisung "public:", die uneingeschränkten Zugriff auf alle Member der Klasse sicher stellt:
 
#include <iostream>

 class X
 {
 public:
  char* pText;

 public:
  void Ausgabe  (         ) {std::cout << pText << std::endl;};
  void SetzeText(char* txt) {pText = txt;                    };
 };

 int main()
 {
  X MyX;
  MyX.SetzeText("Gruss aus dem Inneren der Klasse MyX mittels Funktion");
  MyX.Ausgabe();

  MyX.pText = "Gruss aus dem Inneren der Klasse MyX mittels Variable";
  std::cout << MyX.pText << std::endl;

  return 0;
 };

Im nächsten Schritt kapseln wir die Member-Variable pText der Klasse X. Das erledigt das kleine Wort "private:" für uns.
Wir erhalten vom Compiler eine Fehlermeldung, daß ein Zugriff auf ein "private" Element nicht erlaubt ist.
So einfach kann man Member-Variablen, auch Attribute genannt, innerhalb einer C++-Klasse kapseln.
 
#include <iostream>

 class X
 {
 private:
  char* pText;

 public:
  void Ausgabe  (         ) {std::cout << pText << std::endl;};
  void SetzeText(char* txt) {pText = txt;                    };
 };

 int main()
 {
  X MyX;
  MyX.SetzeText("Gruss aus dem Inneren der Klasse MyX mittels Funktion");
  MyX.Ausgabe();

  MyX.pText = "Gruss aus dem Inneren der Klasse MyX mittels Variable";
  std::cout << MyX.pText << std::endl;

  return 0;
 };

 MSVC++6: error C2248: "pText" : Kein Zugriff auf private Element, dessen Deklaration in der Klasse "X" erfolgte

Im Inneren des Objektes von der Klasse X liegt nun gekapselt die Zeiger-Variable pText.
Ein direkter Zugriff mittels MyX.pText ist nicht mehr erlaubt! Dafür stehen an der Schnittstelle jetzt nur noch die Member-Funktionen bereit.

Wenn man auf diesen unerlaubten Zugriff verzichtet, wird der Quellcode natürlich problemlos kompiliert:
 
#include <iostream>

  class X
  {
  private:
   char* pText;

  public:
   void Ausgabe  (         ) {std::cout << pText << std::endl;};
   void SetzeText(char* txt) {pText = txt;                    };
  };

  int main()
  {
   X MyX;
   MyX.SetzeText("Gruss aus dem Inneren der Klasse MyX mittels Funktion");
   MyX.Ausgabe();

   return 0;
  };

X MyX;
erstellt übrigens aus der Klasse X das konkrete Objekt MyX.  Klassen sind also Baupläne/Vorlagen für konkrete Objekte.

Wozu ist das gut? Betrachten Sie folgenden Code:
 
 class X
 {
   //geheim
 };
 #include "X.h"

 int main()
 {
  X MyX;
  MyX.SetzeText("Gruss aus dem Inneren der Klasse MyX mittels Funktion");
  MyX.Ausgabe();

  return 0;
 };

Sie müssen absolut nicht wissen, wie die Klasse X intern aufgebaut ist.
Alles was Sie benötigen, ist die Information über die Verwendung der beiden Member-Funktionen

void X::SetzeText( char* )
void X::Ausgabe( ).

Hierbei ist das Wissen über die Member-Variable pText nicht wichtig. Sie können diese sowieso nicht direkt ansprechen.

Das bedeutet, dass die Aufgabe der Erstellung des main-Programmes und die Pflege der Klasse X völlig getrennt gesehen werden können.
Die Schnittstelle sind die Prototypen der beiden Member-Funktionen.

Das bedeutet auch, daß an folgenden rot gekennzeichneten Stellen Änderungen vorgenommen werden dürfen:
 
//X.h

#include <iostream>

class X
{
private:
 char* pText;

public:
 void Ausgabe  (         ) {std::cout << pText << std::endl;};
 void SetzeText(char* txt) {pText = txt;                    };
};

#include "X.h"

int main()
{
 X MyX;
 MyX.SetzeText("Gruss aus dem Inneren der Klasse MyX mittels Funktion");
 MyX.Ausgabe();

 return 0;
};
 

Die Grundidee der Kapselung ist somit sichtbar. Es geht um die Aufteilung des Programmcodes in einzeln pflegbare Module.
Damit wird Sourcecode mehrfach verwendbar und übersichtlicher.



Man könnte z.B. ein Sprachmodul verwenden, dass Strings als Sound ausgibt. Die Realisierung würde dann in Ausgabe(...) erfolgen.
Ebenso könnte man den String von hinten nach vorne ausgeben. Es gibt ja Leute, die rückwärts reden können.
Für diese Leute wäre ein solches Programm nützlich.

Normalerweise wird ein Verwender der Klasse X aber wollen, daß wir die "Funktion" der Member-Funktionen im Kern nicht verändern.
Eine gewünschte "Ausgabe rückwärts" sollte daher zu einer neuen Member-Funktion void X::AusgabeRückwaerts( ) führen.
Der innere Aufbau interessiert den Anwender hierbei nicht. Er möchte nur, dass es "funktioniert".

Der Anwender, der rückwärts lesen üben möchte, benötigt natürlich eine Eingabemöglichkeit. Daher muß die Klasse weiter wachsen.
Wir benötigen hierzu z.B. die Funktion void X::EingabeText(char*).

Das könnte auch versteckt ablaufen mit einer "polymorphen" Funktion void X::SetzeText( ). Der gleiche Name, aber ohne Parameter.
Das Einlesen eines Strings könnte dann intern erfolgen.