Vererbung, Pointer und Objekte im Speicher
-
Hallöchen alle miteinander,
ich muss grad eine Übungsaufgabe lösen, die ich selbst auch als sehr interessant empfinde, allerdings komm ich nicht wirklich weiter.
Ich habe zwei Klassen, einmal eine Fläche mit lediglich dem Attribut Farbe und einen Kreis, welcher von Fläche erbt und noch ein paar weitere Attribute besitzt.class Flaeche { private: char* farbe; public: Flaeche(char* color); void setFarbe(char* color); char* getFarbe(); }; // class Flaeche class Kreis: public Flaeche { private: double radius; public: char* Bezeichnung; char* getBezeichnung(); void setBezeichnung(char* name); Kreis(char* color); Kreis(char* name, double r, char* color); double getFlaecheninhalt(); }; // class KreisIch soll nun eine Variable vom Typ Zeiger auf einen Kreis erstellen, weiterhin eine Variable vom Typ Zeiger auf eine Fläche und diesem das Kreisobjekt in die Hand drücken.
Nun soll die Größe der Objekte ermittelt werden, anschliessend über delete den Zeiger vom Typ Fläche "löschen" und dann schauen, wie es danach aussieht. Nun kommen allerdings zwei drei kleine Fragen auf mich drauf zu (ich hab das also soweit schon programmiert).Zunächst einmal ist die Frage, ob ein Zeiger auf eine Fläche überhaupt auf ein Objekt vom Typ kreis zeigen kann.
Meiner Meinung ja, denn letztendlich sind es ja nur Speicheradressen und den Zeigern (also dem Compiler) ist es erst einmal ja egal, was an dieser Stelle liegt, sofern es ein Zeiger ist. Da Kreis ja von Fläche erbt, kann ich danach zumindest von der Fläche die Farbe ermitteln, und diese stimmt auch.Dann sollen wir "Fläche" löschen und schauen wie es mit dem Speicher aussieht.
Meiner Meinung nach dürfte doch ein delete flaeche nur den Speicher des Objektes Fläche freigeben. In meinem Fall war fläche 4 Byte groß (meine Farbe lautete "blau") und Kreis 40 Byte. Ergo sollte dieses delete doch nur die ersten 4 Byte der 40 löschen, oder? Allerdings zeigt mir sizeof nachträglich immer noch 40 bei der Größe an. Allerdings bezweifle ich, daß sizeof hier die richtige Funktion ist, denn ich glaube sizeof richtet sich nach der ursprünglichen Datengröße mit welcher eine Variable angegeben wurde, oder?Daher kommt bei mir die Frage auf, wie sieht ein Objekt eigentlich im Speicher aus. Theoretisch müsste es ja mehr sein, als nur die im Objekt gespeicherten Attribute als Summe, denn ich brauche ja zumindest immer noch Zeiger auf die Methoden des Objektes, oder? Daher die Frage, wie ermittle ich die tatsächlich im Speicher genutzte Größe eines Objektes (inklusive seiner Methoden und Attribute)?
wäre schön, wenn mir jemand bei der Beantwortung meiner Fragen helfen könnte.
mit freundlichen Grüßen
kruemeltee
-
kruemeltee schrieb:
Zunächst einmal ist die Frage, ob ein Zeiger auf eine Fläche überhaupt auf ein Objekt vom Typ kreis zeigen kann.
Meiner Meinung ja, denn letztendlich sind es ja nur Speicheradressen und den Zeigern (also dem Compiler) ist es erst einmal ja egal, was an dieser Stelle liegt, sofern es ein Zeiger ist. Da Kreis ja von Fläche erbt, kann ich danach zumindest von der Fläche die Farbe ermitteln, und diese stimmt auch.Ja,
Flaeche*darf aufKreis-Objekte zeigen. Das ist aber nicht nur so, weil Pointer auf irgendwas zeigen können. Mitreinterpret_castkann man zwar Zeiger umbiegen, aber hier steckt ein anderes Prinzip dahinter. Da ein Kreis alle Eigenschaften einer Fläche hat, kann man ihn auch nur als Fläche ansehen. Wenn du Polymorphie einsetzt, kannst du über den Basisklassenzeiger sogar spezifische Aktionen ausführen, ohne dass du den Typ des Objekts, auf den der Zeiger zeigt, explizit kennst. Das funktioniert über virtuelle Funktionen.kruemeltee schrieb:
Dann sollen wir "Fläche" löschen und schauen wie es mit dem Speicher aussieht.
Meiner Meinung nach dürfte doch ein delete flaeche nur den Speicher des Objektes Fläche freigeben. In meinem Fall war fläche 4 Byte groß (meine Farbe lautete "blau") und Kreis 40 Byte. Ergo sollte dieses delete doch nur die ersten 4 Byte der 40 löschen, oder? Allerdings zeigt mir sizeof nachträglich immer noch 40 bei der Größe an. Allerdings bezweifle ich, daß sizeof hier die richtige Funktion ist, denn ich glaube sizeof richtet sich nach der ursprünglichen Datengröße mit welcher eine Variable angegeben wurde, oder?Wie meinst du das konkret? Was soll
flaechesein? Angenommen, es ist vom TypFlaeche*, dann zerstört es dasFlaeche-Objekt, auf das es zeigt. Wenn es auch auf einKreis-Objekt zeigen soll, musst du unbedingt einen virtuellen Destruktor einrichten, damit es zu einer vollständigen Zerstörung kommt.Aber gerademal vorweg: Du kannst nicht nur den Basisklassen-Teil eines Objekts löschen - zumindest nicht ohne undefiniertes Verhalten.
kruemeltee schrieb:
Daher kommt bei mir die Frage auf, wie sieht ein Objekt eigentlich im Speicher aus. Theoretisch müsste es ja mehr sein, als nur die im Objekt gespeicherten Attribute als Summe, denn ich brauche ja zumindest immer noch Zeiger auf die Methoden des Objektes, oder?
Nein, du bräuchtest allenfalls noch eine VTable, wenn du virtuelle Funktionen einsetzt. Zudem können implementierungsabhängige Dinge wie Padding dazukommen. Also schlussendlich ist die Grösse der Klasse oft grösser als die Grösse aller Membervariablen zusammen.
kruemeltee schrieb:
Daher die Frage, wie ermittle ich die tatsächlich im Speicher genutzte Größe eines Objektes (inklusive seiner Methoden und Attribute)?
Methoden brauchen nur einmal Speicher, nicht für jedes Objekt.
sizeofgibt dir die Grösse eines Typs zur Kompilierzeit an. Eine standardkonforme Funktion, um den realen Speicherverbrauch zur Laufzeit herauszufinden, existiert nicht.Du kannst dir das aber ungefähr ausrechnen, indem du zur Objektgrösse, die dir
sizeofzurückgibt, den Speicher addierst, den du selbst angefordert hast. Falls du Klassen verwendest, die deine Speicherverwaltung abnehmen (wie etwastd::string, zu der ich dir ohnehin rate), ist das allerdings schon schwieriger.
-
Nexus schrieb:
Wie meinst du das konkret? Was soll
flaechesein? Angenommen, es ist vom TypFlaeche*, dann zerstört es dasFlaeche-Objekt, auf das es zeigt. Wenn es auch auf einKreis-Objekt zeigen soll, musst du unbedingt einen virtuellen Destruktor einrichten, damit es zu einer vollständigen Zerstörung kommt.Aber gerademal vorweg: Du kannst nicht nur den Basisklassen-Teil eines Objekts löschen - zumindest nicht ohne undefiniertes Verhalten.
Ja, flaeche ist vom Typ Flaeche* (sorry, war vielleicht etwas ungenau. Theoretisch müsste das doch bedeuten, wenn ich den Basisklassenteil (wie Du das so schön formuliert hast) mittels
delete[]flaeche;lösche, dann dürfte doch lediglich das Attribut "Farbe" verschwunden sein, richtig? Der Rest müsste noch vorhanden sein. Allerdings habe ich die Zuweisung des Objektes Kreis auf den Zeiger von Fläche folgendermaßen gemacht:
Kreis* kreis = new Kreis([...]); Flaeche* flaeche = kreis;Wenn ich nun anschliessend ein delete ausführe, geht mir das komplette Objekt kreis "flöten". Oder muss ich das mit diesem reinterpret_cast machen?
edit: dass das nicht schön ist und ich vermutlich auch nie nutzen werde ist mir klar, ich denke der Zweck der Übungsaufgabe ist lediglich das Verständnis der Objekte und der Zeiger auf solcher.
Nexus schrieb:
Eine standardkonforme Funktion, um den realen Speicherverbrauch zur Laufzeit herauszufinden, existiert nicht.
Das reicht mir ... das mit der VTable hab ich schon gehört ... meine Vermutung über die real zur Laufzeit existierende Größe ist daher die, daß ich alle Attribute zusammen rechne (bzw. deren Speicherplatzbedarf) und dann zusätzlich noch für jede Methode jeweils einen Pointer (der ja letztendlich auf die VTable bzw. einen Eintrag darin) zeigt ... daß die VTable dann zusätzlich noch Speicher belegt, ist mir klar und rechne ich erst einmal nicht mit.
Nexus schrieb:
Falls du Klassen verwendest, die deine Speicherverwaltung abnehmen (wie etwa
std::string, zu der ich dir ohnehin rate), ist das allerdings schon schwieriger.Auch wieder wahr, allerdings sind die Übungsaufgaben hier nur recht klein und darum benutze ich noch die char* Variante. Wenn es dann mal in größere Aufgaben geht, dann werd ich vermutlich auch string zurück greifen

Vielen Dank erst einmal für die Antworten!
mfg
kruemeltee
-
kruemeltee schrieb:
Theoretisch müsste das doch bedeuten, wenn ich den Basisklassenteil (wie Du das so schön formuliert hast) mittels
delete[]flaeche;lösche, dann dürfte doch lediglich das Attribut "Farbe" verschwunden sein, richtig? Der Rest müsste noch vorhanden sein.
Nein. Wie gesagt darfst du nicht einen Teil eines Objekts zerstören. Abgesehen davon ist
delete[]hier wohl nicht das Richtige, passender wäredelete.kruemeltee schrieb:
Wenn ich nun anschliessend ein delete ausführe, geht mir das komplette Objekt kreis "flöten". Oder muss ich das mit diesem reinterpret_cast machen?
Nein! Du darfst solche Dinge nicht tun. Auch
reinterpret_castführt in sehr vielen Fällen zu undefiniertem Verhalten. Also vergiss es vorerst mal, das braucht man normalerweise eh nicht. Und wenn du damit arbeitest, musst du dir wirklich im Klaren darüber sein, was du tust. Das gilt übrigens auch für C- und Funktions-Style-Casts, die zwischen verschiedenen Zeigern casten.kruemeltee schrieb:
Das reicht mir ... das mit der VTable hab ich schon gehört ... meine Vermutung über die real zur Laufzeit existierende Größe ist daher die, daß ich alle Attribute zusammen rechne (bzw. deren Speicherplatzbedarf) und dann zusätzlich noch für jede Methode jeweils einen Pointer (der ja letztendlich auf die VTable bzw. einen Eintrag darin) zeigt ... daß die VTable dann zusätzlich noch Speicher belegt, ist mir klar und rechne ich erst einmal nicht mit.
Das hast du falsch verstanden. Normale Methoden sind nichts weiter als Funktionen, die implizit den
this-Zeiger als zusätzlichen Parameter nehmen. Ein Objekt hält keine Zeiger auf seine Memberfunktionen.Bei virtuellen Funktionen sieht das anders aus, da zur Kompilierzeit nicht feststeht, welche Funktion aufgerufen werden soll. Deshalb sind solche Informationen in der VTable gespeichert. Eine nicht polymorphe Klasse, wie zum Beispiel deine
FlaecheoderKreis, besitzen keine VTable.kruemeltee schrieb:
Auch wieder wahr, allerdings sind die Übungsaufgaben hier nur recht klein und darum benutze ich noch die char* Variante. Wenn es dann mal in größere Aufgaben geht, dann werd ich vermutlich auch string zurück greifen

Das Argument höre ich leider sehr oft. Vielen Leuten ist nicht bewusst, dass
std::stringin der Handhabung massiv einfacher, komfortabler und viel weniger fehleranfällig ist. Du hast bei dir zum Beispiel weder Destruktor, Kopierkonstruktor noch Zuweisungsoperator implementiert. Dies solltest du aber tun, wenn die Klassen ihren Namen besitzen sollen. Falls du nur mit Stringliteralen arbeitest, verwende dochconst char*stattchar*.
-
Okay ... das mit dem löschen lass ich dann einfach und gehe zur Meinung über, Teile eines Objektes sollte man einfach nicht löschen. Ich denke das wird ausreichen ... da ich ohnehin soetwas nicht vorhabe. Mein eigentliches Interesse galt einfach dem "ist es möglich"

Nexus schrieb:
Das Argument höre ich leider sehr oft. Vielen Leuten ist nicht bewusst, dass
std::stringin der Handhabung massiv einfacher, komfortabler und viel weniger fehleranfällig ist. Du hast bei dir zum Beispiel weder Destruktor, Kopierkonstruktor noch Zuweisungsoperator implementiert. Dies solltest du aber tun, wenn die Klassen ihren Namen besitzen sollen. Falls du nur mit Stringliteralen arbeitest, verwende dochconst char*stattchar*.Gebe ich Dir wiederum recht, allerdings habe ich sowohl im Konstruktor eine Initialisierung des char* mittels:
Bezeichnung = new char[8]; strcpy(Bezeichnung,"nothing");und ebenfalls im Destruktor steht ein
delete[]Bezeichnung;Ist wirklich nicht schön, allerdings reicht es mir anfangs aus

vielen Dank nochmal!
mfg kruemeltee
P.S.: Typecast's (egal welcher Form) nutze ich persönlich recht ungern, da bin ich eher dazu bereit abstrakte Klassen zu nutzen. Und nur in Ausnahmefällen wirklich ein typecast, allerdings unter der Bedingung, daß ich weiß was ich tue. Allerdings sollte ein Typecast eigentlich kaum notwendig sein (es sei denn man schiebt es auf Faulheit).
-
Nexus schrieb:
Das Argument höre ich leider sehr oft. Vielen Leuten ist nicht bewusst, dass std::string in der Handhabung massiv einfacher, komfortabler und viel weniger fehleranfällig ist. Du hast bei dir zum Beispiel weder Destruktor, Kopierkonstruktor noch Zuweisungsoperator implementiert. Dies solltest du aber tun, wenn die Klassen ihren Namen besitzen sollen. Falls du nur mit Stringliteralen arbeitest, verwende doch const char* statt char*.
Gebe ich Dir wiederum recht, allerdings habe ich sowohl im Konstruktor eine Initialisierung des char* mittels:
Bezeichnung = new char[8]; strcpy(Bezeichnung,"nothing");und ebenfalls im Destruktor steht ein
delete[]Bezeichnung;Ist wirklich nicht schön, allerdings reicht es mir anfangs aus

Masochistisch veranlagt?
-
kruemeltee schrieb:
Gebe ich Dir wiederum recht, allerdings habe ich sowohl im Konstruktor eine Initialisierung des char* mittels:
Bezeichnung = new char[8]; strcpy(Bezeichnung,"nothing");und ebenfalls im Destruktor steht ein
delete[]Bezeichnung;Ist wirklich nicht schön, allerdings reicht es mir anfangs aus

Ja, möglichst konstant 8 Zeichen reservieren, damit du maximal unflexibel bist. Dafür reicht auch ein Stringliteral.
Ich habe dir einen guten Rat gegeben, aber mach es doch, wie du willst. Kopierkonstruktor und Zuweisungsoperator hast du trotzdem nicht. Es würde dich ein
#include <string>und ein paar wenige Ersetzungen kosten, damit du dich um nichts kümmern müsstest. Aber wenn du es auf Fehler so drauf an legst...
-
theta schrieb:
Masochistisch veranlagt?
nope
@Nexus ... dank Dir ja auch für den Hinweis, allerdings will ich mich tatsächlich grad erst einmal um die Aufgabenstellung kümmern und weniger um meine Strings. Ist vielleicht unschön und vielleicht hörst Du soetwas auch nur recht ungern, aber unsere Professoring nutzt auch nur char* in den Beispielen ...
Und es geht bei den Beispielen auch nicht um Strings die ich eingebe oder dergleichen, sondern die sind meist in den Beispiel fix drin, nur einfach des Daseins wegen ... (warum da unbedingt ne Bezeichnung drin sein soll weiß ich auch nicht, aber in den Vorgaben stehts halt so drin ...mfg kruemeltee
-
Wenn es sowieso immer fixe Stringliterale sind, reicht auch
const char* Text = "hallo"; // "hallo" ist ein StringliteralIch will dich ja nur davon bewahren, nachher unerklärliches Verhalten nachvollziehen zu müssen. Um mit
char*zu arbeiten, sollte man Zeiger und Speicherverwaltung wirklich gut verstanden haben. Es gibt da so viele Fallstricke und Einschränkungen, da kannst du dir das Leben viel leichter machen.Denk zumindest daran, wenn du mal nicht für deine Professorin programmierst.
