Einführung in C++

04 - Vererbung und Tests

Prof. Dr. Malte Schilling

Autonomous Intelligent Systems Group

🚀 by Decker

Übersicht 4. Termin

  • Klassen,
  • objektorientierte Programmierung,
  • Test-basierte Entwicklung

Klassen in C++

Klassen und Objekte in C++

  • Objektorientierte Programmierung ist in C++ ähnlich zu Java realisiert.
  • Eine Klasse definiert die eine Struktur einen neuen Datentypen.
  • In Klassen werden Verhalten (Funktionen) und Eigenschaften (Zustand/Daten) gekapselt und zusammenhängend organisiert.
  • Von einer Klasse werden Objekte instanziiert, die einen konkreten Vertreter der Klasse darstellen. Die Klasse ist der Datentyp des Objektes.
  • Klassen können durch Vererbung in Hierarchien organisiert werden.

Definition Objekt

Objekt

aus Definition 8.2 (Vahrenhold 2022d), basierend auf (Echtle und Goedicke 2000):

“Ein (korrekt modelliertes) Objekt modelliert ein gedanklich abgegrenztes Gebilde mit allen seinen Eigenschaften und Verhaltensweisen.”

Klassen in C++

  • Klassen werden durch das Schlüsselwort class eingeleitet.
  • Ein Semikolon schließt die Definition der Klasse ab (leicht zu vergessen).
  • In Klassen werden Eigenschaften und Verhaltensweisen definiert:
    • Für jedes Objekt:
      • Attribute (data members)
      • Funktionen (member functions)
    • Für die gesamte Klasse:
      • Klassenattribute (static data members)
      • Klassenfunktionen (static member functions)
  • this ist ein Pointer auf das instanziierte Objekt und in jeder Klasse vorhanden.

Beispiel-Klasse

class Widget {
  int _i; // data member
  static int _j; // static data member
public:
  void setI(int i) { _i = i; } // member function
  static void setJ(int j) { _j = j; } // static member function
};

Widget::setJ( 23 ); Widget w;
w.setI( 42 );

Definition und Deklaration von Membern

  • Member können nur innerhalb der Klasse deklariert werden.
  • Definitionen können sowohl innerhalb als auch außerhalb der Klasse stehen:
class Widget { 
  int _i;
public:
  void setI(int i) { _i = i; } // Deklaration & Definition 
  int getI(); // Deklaration
};
// Definition
int Widget::getI() { // Achtung: Namen der Klasse nicht vergessen!
  return _i; 
}
  • Oft steht die Klasse mit allen Deklarationen in einer Header-Datei (.h oder .hpp) und die Definitionen in einer Source-Datei (.cpp).

Recap: Programmaufteilung auf verschiedene Dateien

Bei größeren Projekten trennt man meist Deklarationen und Definitionen in - Header-Dateien und - Implementierungs-Dateien

→ Erlaubt separate Kompilierung/Verwendung von Prebuilt-Libraries.

// max.h
// Deklaration
int max( int , int );
// max.c
// Definition
int max( int a, int b ) {
	return a>b? a : b; }
// foo.c
#include <stdio.h>
#include "max.h"

int main( void ) {
	printf( "%i\n" , max( 37, 91 ) );
}

Beim Kompilieren müssen alle Implementierungsdateien angegeben werden: gcc -Wall -o foo max.c foo.c

Recap: Wie teile ich meinen Quellcode in Header und Implementierung auf?

Die Aufeilung in Header und Implementierung ist nicht immer einfach. Generell gilt:

  • Das Interface (wie wird die Bibliothek verwendet) gehört in einen oder mehrere Header.
  • Die Implementierung (was macht die Bibliothek) gehört in Quelltextdateien.

Implementierung

#include "Header.h"

void println( int x ) {
  std::cout << x << std::endl; 
}

Sichtbarkeit

  • In Klassen können Bereiche als public, private oder protected deklariert werden.
  • Von außerhalb der Klasse kann nur auf public Member zugegriffen werden.
  • Auf private Member können nur Objekte der Klasse zugreifen.
  • Auf protected Member können auch Objekte abgeleiteter Klassen zugreifen.
  • Standard bei der Sichtbarkeit ist private.
  • In C++ kann zur Definition einer Klasse anstelle von class auch struct verwendet werden. Dann ist die Standardsichtbarkeit public.

Member-Funktionen und const

  • Das Schlüsselwort const kann bei Member-Funktionen angegeben werden.
  • Objekte, welche const sind (z. B. die Referenz wr), können nur konstante Member-Funktionen ausführen.
  • Innerhalb einer konstanten Member-Funktion können keine Attribute des Objektes verändert werden.

  • Faustregel: Überprüfe bei jeder Member-Funktion, ob diese als const deklariert werden kann.

Member-Funktionen und const – Beispiel

class Widget { 
public:
  int set(int i) { _i = i; }
  int get() const { return _i; }
private: 
  int _i;
};
Widget w;
const Widget& wr = w;

w.set( 42 ); // ok 
int i = w.get(); // ok
wr.set( 23 ); // compile error 
int j = wr.get(); // ok

Konstruktoren und Destruktor

  • Bei der Instanziierung eines Objektes wird der Konstruktor aufgerufen.
  • Wird ein Objekt zerstört, so wird der Destruktor aufgerufen (wenn: Objekt verlässt den Scope, delete auf dem Objekt, Objekt ist statisch und Programm terminiert).
class Widget { 
  int _i;
  int _j;

public:
  Widget(int i, int j) : _i(i), _j(j) { // Konstruktor
    std::cout << "Widget created\n"; } 
    
  Widget(int i) : Widget(i, 42) { // Konstruktor
    std::cout << "Delegating constructor\n"; } 

  ~Widget() { // Destruktor
    std::cout << "Widget destroyed\n"; }
};

Konstruktoren und Destruktor

  • Es kann mehrere Konstruktoren mit unterschiedlichen Parametern geben.
  • Es gibt immer nur einen Destruktor (ohne Parameter).
  • Konstrukten und Destruktoren werden wie Funktionen definiert, jedoch haben sie keinen Rückgabetyp.
  • Im Konstruktor können/müssen die Attribute der Klasse direkt mit einer sogenannten member initialization list initialisiert werden (_i(i), _j(j) auf der vorherigen Folie).

Faustregel:

Initialisiere alle member objects in der member initialization list.

Konstruktoren und Destruktor

  • Falls die Entwickelnden keinen Konstruktor angeben, wird automatisch ein parameterloser Standardkonstruktor (default constructor) erzeugt.
  • Dies funktioniert nur dann, wenn alle Attribute der Klasse ebenfalls über einen Standardkonstruktor verfügen.
class A { 
  int _i;

public:
  A(int i) : _i(i) {}
};

class B {
  A a; // compile error: no matching function for call to ’A::A()’
};
  • Auch der Destruktor und einige andere sogenannte special member functions werden bei Bedarf vom Compiler erzeugt.

Objekte instanziieren

  • Objekte einer Klasse werden mit einer ähnlichen Syntax wie Variablen von Basisdatentypen instanziiert.
  • In geschweiften Klammern stehen die Argumente des Konstruktors (ab C++11).
  • Anstelle der geschweiften Klammern sind auch runde Klammern möglich.

// neue Syntax (ab C++11)
Widget w1{1, 2};
// alte Syntax
Widget w2(1, 2);

Widget w3 {}; // ok

w4(); // Funktionsdeklaration
// stattdessen
Widget w5;

int i{12345};

Vererbung

  • Durch Vererbung kann eine Hierarchie von Klassen geschaffen werden, um Beziehungen zwischen Klassen zu modellieren.
  • Vererbungen sollten eine „ist-ein“-Beziehung darstellen.
  • Durch Vererbung entsteht eine starke Abhängigkeit zwischen zwei Klassen, daher sollte nicht jede Beziehung als Vererbung modelliert werden.
  • In C++ gibt es verschiedene Arten der Vererbung:
    • public-Vererbung vs. private-Vererbung.
    • Einfache Vererbung vs. Mehrfachvererbung.
  • Die Vererbung in Java entspricht der einfachen public-Vererbung in C++.

Einfache public-Vererbung

Bei einfacher Vererbung besitzt eine Klasse genau eine Oberklasse.

class Base { 
public:
  Base(int i) : _i{i} {}

  void setI(int i) {_i = i;}
  int getI() const {return _i;} 
protected: 
  int _i;
};

class Derived : public Base {
public:
  Derived(int i, int j) : 
    Base{i}, _j{j} {}
  void setJ(int j) {_j = j;}
  int getJTimesI() {return _j*_i;} 
private:
  int _j;
};

  • In Derived wird mit : public Base die Vererbung angegeben.
  • Im Konstruktor von Derived kann explizit ein Konstruktor von Base aufgerufen werden (ansonsten automatisch der Standardkonstruktor von Base).
  • protected- und public-Attribute einer Basisklasse behalten ihre Sichtbarkeit in der abgeleiteten Klasse.

Aufgabe 4.1 Klassen

Aufgabe: Klassen

  • Legen Sie eine Datei customer.cpp an.
  • Implementieren Sie die Klasse Account in der Datei customer.cpp gemäß dem UML-Diagramm.
code-4231a064.tex.svg

Aufgabe: Klassen

  • Überlegen Sie sich dabei sinnvolle Parameter für die Funktionen deposit, withdraw und transfer.
  • Beachten Sie, dass Überweisungen ausschließlich auf andere Konten durchgeführt werden können.
  • Fügen Sie einen Konstruktor hinzu, mit dem ein Account mit Startguthaben angelegt werden kann.
  • Implementieren Sie in der Datei customer.cpp eine main-Funktion und testen Sie die Klasse Account.

Hinweis: Kompilieren Sie Ihr Programm mit g++ -std=c++20 -Wall -Wextra -o customer customer.cpp

Aufgabe: Vererbung

  • Implementieren Sie eine Klasse Person mit einem Namen und einem Alter.
  • Implementieren Sie die Klasse Customer, die von Person erbt und dazu einen Account und eine ID (z. B. unsigned int) besitzt.
  • Fügen Sie in beiden Klassen eine Funktion print hinzu, die den Zustand des Objekts (Werte aller Attribute) ausgibt.
  • Achten Sie auf die Verwendung von virtual!
  • Sie können die Musterlösung aus der vorherigen Vorlesung als Basis nutzen.
code-2a1c4b44.tex.svg

Testen der Klasse Account

int main() {
  std::cout << std::boolalpha;

  Account a{ 25 };
  a.deposit( 75 );
  a.deposit( 50 );
  std::cout << (a.balance() == 150) << std::endl; // true 
  std::cout << a.withdraw( 25 )  << std::endl; // 25
  std::cout << (a.balance() ==  125) << std::endl; // true
  std::cout << a.withdraw( 150 ) << std::endl; // 0

  Account b{ a };
  std::cout << a.transfer( 50, a) << std::endl; // false
  std::cout << a.transfer( 126, b ) << std::endl; // false
  std::cout << a.transfer( 125, b ) << std::endl; // true 
  std::cout << (a.balance() == 0) << std::endl; // true 
  std::cout << (b.balance() == 250) << std::endl; // true 
}

Hinweis: Aufgabe 4.1

Aufgabe zum Anlegen einer Klasse und von Beispielinstanzen

Im jupyter-book ist die Aufgaben angegeben:

Jupyter-Book Link

Hierüber kann dann direkt auf dem Hub eine Umgebung gestartet werden, in der C++ interpretiert wird (wird die ersten Termine genutzt):

JupyterHub Link

Zugriffsart regeln

Access public protected private
members of the same class yes yes yes
members of derived classes yes yes no
not members yes no no

Lösung: Vererbung

class Person { 
public:
  Person( const std::string& name, unsigned int age ) : 
          _name{ name },
          _age{ age } 
  {}

  virtual ~Person() = default;

  const std::string& name() const { return _name; }
  unsigned int age() const { return _age; }

  virtual void print() const {
    std::cout << _name << ", " << _age << std::endl;
  }

private:
  std::string _name; 
  unsigned int _age;
};

Lösung: Vererbung 2

class Customer final : public Person { 
public:
  Customer( const std::string& name, unsigned int age, 
            unsigned int id ) :
      Person{ name, age }, 
      _id{ id },
      _account {}
  {}

  unsigned int id() const { return _id; }
  Account& account() { return _account; }

  void print() const override { 
    Person::print();
    std::cout << "Balance: " << _account.balance() << std::endl; 
  }

private:
  unsigned int _id; 
  Account _account;
};

Lösung: Vererbung 3

int main() {
  Customer heinz{ "Heinz", 42, 1234 };
  Customer peter{ "Peter", 24, 5678 };
  Person& hr = heinz;
  heinz.account().deposit( 150 ); 
  heinz.account().transfer( 100, peter.account() ); 
  hr.print();
  peter.print();
}

/* Ausgabe: 
Heinz , 42 
Balance: 50 
Peter , 24 
Balance: 100 */

Objektorientierte Programmierung und Klassen in C++

Wichtige Konzepte der Objektorientierung

  • Ein Programm besteht aus Klassen, die die Komponenten beschreiben.
  • Objekte werden aus Klassen erzeugt.
  • Es können mehrere Objekte einer Klasse erzeugt werden.
  • Alle Objekte einer Klasse haben die selbe Struktur.
  • Objekte anderer Klassen haben eine andere Struktur.
  • Der Zustand jedes Objektes einer Klasse kann unterschiedlich sein.
  • Objekte besitzen Methoden (die über Botschaften aufgerufen werden können).
  • Methoden können Parameter und/oder Rückgabewerte (Botschaften) haben.

Class Declaration und Definition

Eine Klasse wird üblicherweise auch aufgeteilt auf 2 Dateien: .h Header und die Implementierung in der .cpp Sourcedatei.

Interface

Account.h

  • Offen für Nutzer
  • zeigt Methoden und Variablen an

Implementierung

Account.cpp

  • geschrieben vom Programmierer
  • implementiert (verdeckt) die Methoden

Virtuelle Funktionen

  • Durch virtuelle Funktionen kann Derived eine Funktion von Base spezialisieren.
  • Es wird die am meisten spezialisierte Funktion aufgerufen.
  • In Java sind alle Methoden virtuelle Funktionen.
class Base { 
public:
  virtual void foo() { 
    std::cout << "Base::foo\n";
  } 
};
class Derived : public Base { 
public:
  virtual void foo() {
    std::cout << "Derived::foo\n";
  } 
};

Virtuelle Funktionen 2

Base        b;   b.foo();    // Base::foo
Derived     d;   d.foo();    // Derived::foo
Base* bp = &b;   bp->foo();  // Base::foo
      bp = &d;   bp->foo();  // Derived::foo
Base& br = b;    br.foo();   // Base::foo
Base& dr = d;    dr.foo();   // Derived::foo
  • Bei virtuellen Funktionen bestimmt der dynamische Typ einer Variable, welche Funktion aufgerufen wird.

  • Enthält eine Klasse eine virtuelle Funktion und ist damit als Oberklasse vorgesehen, sollte auch der Destruktor virtuell sein.
  • Nur von Klassen erben, deren Destruktor virtuell ist.

Virtuelle Funktionen

  • Funktionen müssen explizit in Base als virtuell gekennzeichnet werden!

  • Niemals eine Funktion in Derived definieren, die den gleichen Namen wie eine nicht-virtuelle Funktion in Base hat.
  • Insbesondere muss der Destruktor von Base virtuell sein, damit beim Zerstören eines Derived-Objektes der richtige Destruktor aufgerufen wird.

Problem: Slicing

Beim Zuweisen eines Derived-Objektes an ein Base-Objekt wird der Zuweisungsoperator von Base aufgerufen.

Base b{1}; Derived d{2, 3}; // b ist kein Pointer oder Referenz
b = d; // Zuweisungsoperator von Base

Zu beachten:

  • Nur die Attribute der Klasse Base können daher kopiert werden.
  • Die Attribute der Klasse Derived gehen verloren.

Dies bezeichnet man als Slicing und sollte unbedingt vermieden werden.

Derived d1{2, 3};
Derived d2{4, 5};
Base& baseRef = d2;
baseRef = d1; // Zuweisungsoperator von Base

Achtung: d2 enthält nun Attribute von d1 (für den Anteil, der in Base existiert) und d2!

Abstrakte Klassen

  • Mithilfe von rein virtuellen Funktionen können abstrakte Klassen definiert werden, die ein Interface definieren (ähnlich zu interface in Java).
  • Eine rein virtuelle Funktion wird am Ende mit = 0 markiert.
  • Von Klassen mit rein virtuellen Funktionen kann kein Objekt erzeugt werden.
  • Abgeleitete Klassen implementieren diese Funktionen und können über das gemeinsame Interface verwendet werden.
class Shape { 
public:
  // pure virtual function
  virtual double getArea() = 0; 
  void setW(double w) {_w = w;} 
  void setH(double h) {_h = w;}
protected:
  double _w; double _h;
};
class Rectangle : public Shape{ 
public:
  double getArea() { 
    return _w * _h; }
};

class Triangle : public Shape { 
public:
  double getArea() { 
    return (_w * _h)/2; }
};

Mehrfachvererbung

  • C++ erlaubt Mehrfachvererbung, d. h. eine Klasse kann von mehreren Oberklassen erben.
class List { ...
};

class Serializable {
... };

class SerializableList 
  : public List ,
    public Serializable { ...
};
  • Bei Mehrfachvererbung können Mehrdeutigkeiten auftreten, wenn Attribute von unterschiedlichen Oberklassen den gleichen Namen haben.
  • Mehrfachvererbung sollte nur mit Vorsicht eingesetzt werden!

Deadly Diamond of Death

  • Ein besonderes Problem tritt auf, wenn eine Klasse mehrfach von der selben Oberklasse erbt.
class A { int data; }; 
class B : public A { ... }; 
class C : public A { ... };

class D
  : public B,
    public C { ...
};

Frage: Hat D das Attribut data einmal oder zweimal?

Antwort: Das Attribut ist zweimal vorhanden (als B::data und C::data). Bei virtueller Vererbung (wird hier nicht besprochen) wäre das Attribut nur einmal vorhanden.

Objekte kopieren und zuweisen

C++

Widget w1{1, 2}; 
Widget w2 = w1; 
w2.modify();
// w1 != w2

Java

Widget w1 = new Widget(1, 2); 
Widget w2 = w1;
w2.modify(); 
// modifiziert w1 // w1 == w2

Objekte kopieren und zuweisen

  • Es wird zwischen kopieren und zuweisen unterschieden:
Widget original {};
Widget copy1{original}; // Kopie 
Widget copy2 = original; // Kopie 
Widget assigned {};
assigned = original; // Zuweisung
  • Der Kopierkonstruktor (copy constructor) definiert das Verhalten beim Erzeugen einer Kopie: Widget(const Widget& w) : _i{w._i}, _j{w._j} { /* ... */ }

Automatisch generierte Funktionen

C++ generiert besondere Funktionen (special member functions) einer Klasse automatisch, wenn man diese nicht explizit selber definiert:

  1. Standardkonstruktor (wenn es keinen anderen Konstruktor gibt)
  2. Kopierkonstruktor
  3. Destruktor
  4. Zuweisungsoperator
  5. move constructor und move-assignment operator (hier nicht weiter betrachtet)

Beide Versionen der Klasse A sind äquivalent:

class A { 
  int data;
};

class A{
  int data;
public:
  A() : data{} {} // (1)
  A(const A& other) : data{other.data} {} // (2) 
  ~A() {} // (3)
  A& operator=(const A& rhs) { // (4)
    if (this != &rhs) { data = rhs.data; } 
    return *this; }
};

Überladen von Operatoren

  • Neben dem Zuweisungsoperator können in C++ fast alle vorhanden Operatoren überladen werden.
  • Ausnahme: ::, ., .* und ? können nicht überladen werden.
  • Es können keine neuen Operatoren definiert werden (wie z. B. <> oder &|).
  • Operatoren sind Funktionen mit speziellem Namen, z. B. operator! für ! und operator[] für [].

Überladen von Operatoren

Expression As member function As non-member function Example
@a (a).operator@ () operator@ (a) std::cin calls std::cin.operator!()
a@b (a).operator@ (b) operator@ (a,b) std::cout << 42 calls std::cout.operator<<(42)
a=b (a).operator= (b) cannot be non-member std::string s; s='abc'; calls s.operator=('abc')
a[b] (a).operator[](b) cannot be non-member std::map<int, int> m; m[1] = 2; calls m.operator[](1)
a-> (a).operator-> () cannot be non-member std::unique_ptr<S> ptr(new S); ptr ->bar() calls ptr.operator->()
a@ (a).operator@ (0) operator@ (a, 0) std::vector<int>::iterator i = v.begin(); i++ calls i.operator++(0)

In the table, @ is a placeholder representing all matching operators: all prefix operators in @a, all postfix operators other than -> in a@, all infix operators other then = in a@b.

Klassen-Templates

  • Template-Funktionen beschreiben Algorithmen unabhängig von Datentypen.
  • Mit Klassen-Templates können generische Datentypen implementiert werden, unabhängig von den zu speichernden Daten.

Beispiel: Generische Klasse Pair

template <typename T1, typename T2>

class Pair { 
public:
  Pair(const T1& first,
       const T2& second) :
    _first(first),
    _second(second) {} 
  const T1& first()
    { return _first; } 
  const T2& second()
    { return _second; }
private:
  T1 _first;
  T2 _second; 
};
  • template kündigt die Template-Klasse an,
  • dann Argumente T1 und T2 jeweils mit typename,
  • T1 und T2 stehen für Datentypen und können (innerhalb) wie normale Datentypen verwendet werden,
  • Template-Argumente werden bei der Instanziierung angegeben: Pair<int, float> p1{23, 4.2f}; Pair <bool , char > p2{true , ’a’};

Klassen-Templates

  • Mit Klassen-Templates wird nicht eine einzelne Klasse definiert, sondern mehrere (z. B. sowohl Pair<int, float> als auch Pair<bool, char>).
  • Ohne Angabe der Template-Argumente kann kein Objekt instanziiert. werden: Pair p; //NICHT möglich
  • Wird ein Klassen-Template verwendet, indem TemplateArgumente angegeben werden, so instanziiert der Compiler das Template und erzeugt die Klasse.
Pair<int, float> p1{23, 4.2f}; 
// ~~~~~~~~~~~~~~~~
// Instanziierung des Templates => Generierung von Pair <int , float >
  • Die instanziierten Klassen (Pair<int,float> und Pair<bool,char>) haben keine besondere Beziehung zueinander, sie teilen sich nur einen ähnlichen Namen.

Test-getriebene Entwicklung

Aus Einführung in die Programmierung …

Kennzeichen von Programmier-Expertinnen und -Experten

  • Verfügbarkeit effizient organisierter und spezialisierter Wissensschemata.
  • Wissensorganisation gemäß funktionaler Charakteristiken.
  • Anwenden allgemeiner Problemlösungsmuster und spezieller Strategien.
  • Anwendung spezialisierter Schemata für das Zerlegen und Verstehen von Programmen. Bereitschaft, mehrere Strategien zu erproben und Hypothesen zu verwerfen.
  • Verfügbarkeit korrespondierender Test- und Fehlersuchstrategien.

Übersicht aus Grundlagen der Programmierung

Vorstellung verschiedener Paradigmen zur Modellierung und Programmierung.

  • Einführung in datengetriebene Modellierung.
  • Einführung in testgetriebene Programmierung.
  • Formalisierung des Entwicklungsprozesses. Erlernen des Handwerkszeugs.
  • (Programmiersprachen als konkrete Beispiele für die betrachteten Paradigmen.)
  • Hilfe bei der Entwicklung hin zur Expertin/zum Experten.

Testgetriebene Entwicklung (test-driven development)

  • Methode der agilen Software-Entwicklung (→ Vorlesung “Software Engineering”)

Grundidee:

  1. Tests formulieren,
  2. dann Programm implementieren.

Ansatz Testgetriebene Entwicklung

Angabe von Beispielen, d. h. ”Was sollte die Funktion für Eingabe X berechnen?“

Beispielergebnisse

  • Nicht: Programmierung der Lösung.
  • Stattdessen: Ausrechnen der Beispiele von Hand.
    • Vorteil: Man macht sich selbst nochmals klar, was man eigentlich programmieren möchte.
    • Vorteil:Vermeiden des “Henne und Ei”-Problems.

Sicht auf Programmierung und Softwareentwicklung

../data/01/software_engineering_overview_01.png ../data/01/software_engineering_overview_02.png ../data/01/software_engineering_overview_03.png ../data/01/software_engineering_overview_04.png ../data/01/software_engineering_overview_05.png ../data/01/software_engineering_overview_06.png

Entwurf Programm

  • Datenanalyse und -definition.
  • Signatur, Zweck und Funktionskopf angeben.
  • Beispiele erstellen = Tests
  • Funktionsrumpf erstellen.
  • Funktionsweise überprüfen.

Entwurf Programm – (1) Datenanalyse und -definition

Ziel

Erstellen einer Datendefinition.

Aktivität(en)

  • Untersuche Aufgabenstellung auf Hinweise, wie viele verschiedene Arten von Objekten es gibt.
  • Führe diese Arten in einer Datendefinition auf.
  • Erstelle für jede Art von Objekten eine eigene Strukturdefinition und eine (umgangssprachliche) Datendefinition.
  • Formuliere ggfs. “Oberbegriffe” für Objektarten in einem Kommentar.

Entwurf Programm – (2) Signatur, Zweck und Funktionskopf angeben

Ziel

Benennung der Funktion.

Angabe, welche Art von Eingabe(n) und Ausgabe(n) verwendet wird.

Beschreibung des Zwecks der Funktion.

Angabe des Funktionskopfs.

Aktivität(en)

Wähle einen passenden Namen.

Wähle einen formalen Parameter pro Unbekannter; verwende möglichst Namen aus Aufgabenstellung

Beschreibe unter Verwendung dieser Namen, was die Funktion berechnet.

Formuliere Signatur und Funktionskopf.

Entwurf Programm – (3) Beispiele erstellen = testgetriebene Programmierung

Ziel

Charakterisierung des Zusammenhangs von Eingabe und Ausgabe anhand von Beispielen.

Aktivität(en)

  • Schaue in der Aufgabenstellung nach Beispielen.
  • Vollziehe diese Beispiele nach.
  • Überprüfe die Beispiele manuell.
  • Erstelle mindestens ein Beispiel pro Situation bzw. Art von Objekt.

Entwurf Programm – (4) Funktionsrumpf erstellen

Ziel

Definition der Funktion.

Aktivität(en)

Definiere die Funktion in C++.

Entwurf Programm – (5) Funktionsrumpf überprüfen

Ziel

Erkennen von logischen und syntaktischen Fehlern.

Aktivität(en)

  • Wende Funktion auf Eingaben aus den Beispielen an.
  • Überprüfe, ob Funktionswerte mit erwarteten Ausgaben übereinstimmen.
  • Neben den Unit Tests ist dazu dann häufig das Programm im Gesamtzusammenhang noch zu prüfen.

Unit Testing

Unit Testing ist eine Methode zum Testen kleiner Teile oder “Einheiten” des Quellcodes in einer größeren Software.

  • Jeder Test wird normalerweise als einzelne Funktion dargestellt.
  • Grundidee: Jeder Test sollte einen Teil der Funktionalität isoliert untersuchen.

Vorteile

  • Begrenzt Code / Funktionen auf nur das, was notwendig ist
  • Findet Fehler frühzeitig
  • Erhält die Funktionalität, wenn der Code geändert wird

Unit Tests

Program testing can be used to show the presence of bugs, but never to show their absence!

Test Frameworks für C++

Testframeworks: Boost.Test, Google Test, Catch

  • Boost.Test: Teil der Boost Libraries, plattformübergreifend
  • Google Test: Umfangreiches Framework, gut für große Projekte
  • Catch: Einfach zu verwenden, unterstützt BDD (Behavior-Driven Development)

Hinweis: Aufgabe 4.2

Aufgabe zu Unit Testing über Nutzen der Boost-Library

Im jupyter-book ist die Aufgaben angegeben:

Jupyter-Book Link

Hierüber kann dann direkt auf dem Hub eine Umgebung gestartet werden, in der C++ interpretiert wird (wird die ersten Termine genutzt):

JupyterHub Link

References

Dijkstra, Edsger. 1970. „Notes on Structured Programming“. Technological University Eindhoven.
Echtle, Klaus, und Michael Goedicke. 2000. Lehrbuch der Programmierung mit Java. dpunkt-Verlag.
Kölling, Michael, und John Rosenberg. 2001. „Guidelines for teaching object orientation with Java“. ACM SIGCSE Bulletin 33 (3). ACM New York, NY, USA: 33–36.
Vahrenhold, Jan. 2022a. „Informatik I: Grundlagen der Programmierung, 1 Einführung“. Lecture Notes, University of Münster.
———. 2022b. „Informatik I: Grundlagen der Programmierung, 2 Ausdrücke und Funktionen“. Lecture Notes, University of Münster.
———. 2022c. „Informatik I: Grundlagen der Programmierung, 4 Zusammengesetzte Daten“. Lecture Notes, University of Münster.
———. 2022d. „Informatik I: Grundlagen der Programmierung, 8 Objekte und Klassen“. Lecture Notes, University of Münster.