Kapitel 4.2 Dynamische Datenstrukturen Michael Hoffmann <[email protected]> • Nächste Woche: 4. und letzte Schnellübung. • Übungsaufgaben: 71-74, 2-aus-4, Rest Bonus. Expression Funktionalität • Expression e = 1.5; • Expression f = 3 * x + 4; • Expression g = e (+-*/) f; • -e * 2; // == -3 • f(2); // == 10 • f.derive() // == 3 Expression Repräsentation class Expression { ... private: int op; // encodes operator/expression double val; type // stores constants Expression left; Expression right; }; // left subexpression // right subexpression Problem: Nicht endende Rekursion! Expression Repräsentation class Expression { ... private: int op; // encodes operator/expression double val; type // stores constants Expression* left; Expression* right; }; // left subexpression // right subexpression Null-Zeiger bricht Rekursion auf. Expression Funktionalität class Expression { public: Expression(); // POST: Set to primary expression x (symbolic variable). Expression(double v); // POST: Set to primary constant expression with value v. Expression(int o, Expression a); // PRE: o is unary operator. // POST: Set to o(a). Expression(int o, Expression a, Expression b); // PRE: o is binary operator. // POST: Set to o(a, b). }; Copy Semantik Expression::Expression(int o, Expression a) : left( &a ?), … {…} Problem: a ist temporäres Objekt! Copy Semantik Expression::Expression(int o, Expression& a) : left(&a), … {…} Expression f; // f == x Expression e(1, f, f); // f + f f = e; f == e == (1, ?, &f, &f) 1.) Nicht endende Rekursion. 2.) f = e hat e verändert. Copy Semantik • Wir brauchen eine andere Semantik als member-weises Kopieren. • „Wahre Kopie“: für einen Zeiger kopiere nicht den Wert (Adresse) sondern deren Inhalt. In C++ kann die Kopiersemantik einer Klasse frei definiert werden. Æ Copy Konstruktor. Const Initialisierung • Const Objekte müssen initialisiert werden. const int i; // error: uninitialized const object • Auch indirekt keine Änderung möglich. const int i = 0; int* iptr = &i; // error: address of const object cannot be // assigned to pointer to a non-const type Zeiger auf Const Typen • const T* • Qualification conversion: T* Æ const T* const int i = 0; const int* ciptr = &i; // OK int j = 1; ciptr = &j; // OK: int* -> const int* *ciptr = 2; // error: ciptr is pointer to // const type Const Zeiger • T* const • Qualification conversion: T* Æ T* const • const T* const int i = 1; int* const icptr = &i; // OK: int* -> int* const int* const jcptr; // error: uninitialized *icptr = 2; // OK: only icptr const const int* const cicptr = &i; // OK: int* -> const int* const cicpter = icptr; // error: cicpter unmodifiable *cicptr = 3; // error: *cicptr unmodifiable Const Referenzen • const T& • Initialisierung durch R-Wert vom Typ T1 mit T1Æ T (im Ggs. zu nicht-const Referenzen!) double d = 1.0; int& iref = d; // error: type mismatch float& fref = 1.0f; // error: needs lvalue const int& ciref = d; // OK: double -> int const float& cfref = 1.0f; // OK: initialization from rvalue Const Member Funktionen • Member Funktionen können den Wert des Objektes verändern. Was, wenn es const ist? Der Compiler lässt es nicht zu! • Für const Objekte dürfen nur const Member Funktionen aufgerufen werden. class Rational { ... // POST: return numerator int n() ;const // POST: return denominator int d() ;const ... }; Der this Zeiger • Jede Member Funktion einer Klasse T hat Zugriff auf das Objekt, für das sie aufgerufen wird. • Über einen Zeiger this vom Typ T*. • this ist R-Wert, *this ist L-Wert. class Rational { ... int n(Rational* const this); int d(Rational* const this); Rational& operator*=( Rational* const this, Rational s); ... }; Der this Zeiger • Const Member Funktion von T: this ist vom Typ const T* const. • Da this kein expliziter Parameter ist, muss sein const qualifier anderswo erscheinen Æ nach den Parametern. class Rational { ... int n(const Rational* const this); int d(const Rational* const this); Rational& operator*=( const Rational* const this, Rational s); ... }; Function Parameters • Grosse Objekte by-value zu übergeben, ist teuer. Objekte die grösser als eine Speicheradresse sind, per (const) Referenz übergeben! • Fundamentale Typen und Zeigertypen: byvalue. Const und Überladen • T versus const T • by-value Parameter: irrelevant. • Referenzen und Pointer: verschiedene Typen. void foo(int i) { ... } void foo(const int i) { ... } // error: redefinition of foo(int) void foo(int& i) { ...} void foo(const int& i) { ...} // OK: distinct ... int a; foo(a); // calls foo(int&) foo(2); // calls foo(const int&) Const Korrektheit Die konsequente Verwendung von const bei… • Referenzparametern von Funktionen, die innerhalb der Funktion nicht modifiziert werden; • Member Funktionen, die das Objekt, für das sie aufgerufen werden, nicht modifizieren. wird als Const Korrektheit eines Programms bezeichnet. Warum Const? • Dokumentiert nicht nur, der Compiler überprüft auch. • Probleme zur Compile-Zeit sind besser als Probleme zur Laufzeit. Æ Der Compiler ist des Programmierers bester Freund. • Const Referenzen sind flexibler, da von RWerten initialisierbar. void foo(const Expression& x) { ... } … foo(1); // int -> double -> Expression Dynamische Speicherverwaltung • Der „wahre“ copy Konstruktor braucht mehr als den konstanten Speicher im Expression Objekt. • Speicherbedarf eines Programms muss nicht zur Compile-Zeit bekannt sein (siehe Rekursion). • Zur Laufzeit kann Speicher vom Heap alloziert werden. int* i = new int; int* j = new int(6); delete j; // *i undefined // *j == 6 Expression Konstruktoren Expression::Expression() : op( ??)… • Kodierung der Operatoren ist mühsam. • Æ Konstanten mit passenden Namen. // Codes for different const int VAR = -1; // const int VAL = 0; // const int ADD = 1; // const int SUB = 2; // const int MUL = 3; // const int DIV = 4; // const int UMI = 5; // types of expressions symbolic variable x constant value binary addition binary subtraction binary multiplication binary division unary minus Primärausdrücke Expression::Expression() : op(VAR), val(0), left(0), right(0) {} Expression::Expression(double v) : op(VAL), val(v), left(0), right(0) {} Copy Konstruktor Expression:: Expression(const Expression& f) : op(f.op), val(f.val), left(0), right(0) { if (f.left != 0) left = new Expression(*(f.left)); if (f.right != 0) right = new Expression(*(f.right)); } Zusammengesetzte Ausdrücke Expression:: Expression(int o, const Expression& a) : op(o), val(0), left(new Expression(a)), right(0) { assert(op == UMI); } Zusammengesetzte Ausdrücke Expression:: Expression(int o, const Expression& a, const Expression& b) : op(o), val(0), left(new Expression(a)), right(new Expression(b)) { assert(op == ADD || op == SUB || op == MUL || op == DIV); } Destruktor • new und delete erscheinen immer paarweise... • Konstruktoren werden „automatisch“ aufgerufen. • Wo erscheinen die entspr. delete Ausdrücke? Cname::~Cname() { … } Destruktor Expression::~Expression() { delete left; delete right; } Zuweisungen Expression e = 1; e = 3; Copy Konstruktor Zuweisung Sollten gleiche Semantik haben! Î Zuweisungs-Operator für Expression muss auch definiert werden. Expression& Expression::operator=(const Expression&) { … } Zuweisung Ù Copy Konstruktor Copy Konstruktor: initialisiert unberührten gerade allozierten Speicher. Zuweisungs-Operator: Objekt, an das zugewiesen wird, enthält i.a. Daten. Î Zuweisungs-Operator “=“ Destruktor + Copy Konstruktor Zuweisung Expression& Expression::operator=(const Expression& f) { delete left; delete right; op = f.op; val = f.val; if (f.left != 0) left = new Expression(*(f.left)); if (f.right != 0) right = new Expression(*(f.right)); return *this; } Selbst-Zuweisung Expression e; … e = e; Problem: Erst wird gelöscht, und der Inhalt von e ist verloren! Î Verhindere Selbst-Zuweisung. Zuweisung Expression& Expression::operator=(const Expression& f) { if (this != &f) { wie vorher } return *this; } Datenkapselung • Der öffentliche Teil von Expression ist frei von Zeigern. • Die Representation ist vollständig nach aussen gekapselt. • Nach aussen präsentiert sich die Klasse mit klassischer statischer Copy Semantik. • Interna können geändert werden, ohne dass Benutzer-Code angepasst werden muss. Expression Arithmetik Expression x; Expression x2(MUL, 5, x); // 5x Expression y(ADD, x2, 1); // 5x+1 Wir wollen: Expression x; Expression y = 5 * x + 1; Arithmetische Operatoren Expression& Expression:: operator+=(const Expression& a) { return *this = Expression(ADD, *this, a); } Zweimal kopiert Æ etwas ineffizient. Expression operator+(const Expression& a, const Expression& b) { Expression f = a; return f += b; } Differenzieren class Expression { ... Expression derive() const; // POST: Returns the derivative of *this // interpreted as a real function in x. ... }; Differenzieren Expression Expression::derive() const { // no arguments if (op == VAL) return 0.0; if (op == VAR) return 1.0; assert(left != 0); Expression l = left->derive(); // unary operator if (op == UMI) return -l; … } Differenzieren Expression Expression::derive() const { … // binary operator assert(right != 0); Expression r = right->derive(); if (op == ADD) return l + r; if (op == SUB) return l - r; if (op == MUL) return l * *right + r * *left; assert(op == DIV); return (l * *right - r * *left) / (*right * *right); } Die Goldene Zeiger Regel Wann immer ein Zeiger z dereferenziert wird, fang den Fall z==0 ab! Î Assertions... Ausgabe Operator std::ostream& operator<<(std::ostream& o, const Expression& f) { … } Problem: Als nicht-Member Funktion kein Zugriff auf private Daten. Lösung: Deklariere Funktion als friend innerhalb von Expression. Ausgabe Operator class Expression { ... friend std::ostream& operator<<(std::ostream&, const Expression&); ... }; Eine als friend deklarierte Funktion hat Zugriff auf private Daten und Member Funktionen der Klasse. Ausgabe Operator std::ostream& operator<<(std::ostream& o, const Expression& f) { // no arguments if (f.op == VAL) return o << f.val; if (f.op == VAR) return o << "x"; // unary operator assert(f.left != 0); if (f.op == UMI) return o << "-(" << *(f.left) << ")"; … } Ausgabe Operator std::ostream& operator<<(std::ostream& o, const Expression& f) { … // binary operator o << "(" << *(f.left); assert(f.right != 0); if (f.op == ADD) o << "+"; else if (f.op == SUB) o << "-"; else if (f.op == MUL) o << "*"; else o << "/"; // assert … return o << *(f.right) << ")"; }