Skip to content

Cpp Tipps

Ario Dastmaltschi edited this page Apr 19, 2017 · 14 revisions

Eine tolle Übungsseite hier

Inhaltsverzeichnis

Klassen erstellen

Die Klassen wird in zwei Dateien aufgeteilt:

  1. Die Headerdatei:
    In diese Datei gehören die Definitionen der Klasse und ihrer Member (Attribute und Methoden)
    Bsp.:
// foo.h

#pragma once

class Foo {
public:
   Foo();       // Konstruktor
   ~Foo();    // Destruktor
private:
   // Membervariablen (Attribute)
   int x;
   int y;
public:
   // Memberfunktionen (Methoden)
   void doSomething(int someValue);

   // kleine get-Funktionen können auch in der Headerdatei definiert werden
   int getX() { retrun x; }
   int getY() { return y; }
};
  1. Die Quelldatei:
    In diese Datei gehören die Deklarationen der Memberfunktionen (und evtl. auch die Initialisierung der statischen Membervariablen)
    Bsp.:
// foo.cpp

#include "foo.h"

void Foo::doSomething(int someValue)
{}


Sobald ihr __extra Dateien__ wie z.B. `iostream` einbeziehen (`#include`) wollt, müsst ihr das in der Headerdatei, unter der Zeile `#pragma once` machen.


Bitte vergesst außerdem nicht eine kurze Beschreibung der Klasse anzuführen. (siehe unser Ehrenkodex)

Vererbung

Vererbung wird in C++ ähnlich wie in Java implementiert.


Statt dem keyword extends in Java wird in C++ ein : verwendet.
Desweiteren muss man in C++ angeben welchen Zugriffsmodifikator (private, ...) die vererbten Sachen haben sollen. Dies wird folgendermaßen implementiert.


Dieses Beispiel verwendet zwar nur Variablen, aber bei Funktion verhält es sich im Grunde genauso

class Base {
private:      // private variablen werden nicht weitervererbt
   int x;
protected:    // protected variablen werden weitervererbt
   int y;
public:       // public variablen werden weitervererbt
   int z;
};

class Derived : public Base {
public: 
    void print() {
        z = 3;     // OK
        y = 3;     // OK
        x = 3;     // ERROR!
    }
};


Außerdem auch wichtig: Destruktor


Eine bessere Dokumentation hier.

Abstrakte Klassen

Das Prinzip einer abstrakten Klasse und warum wir sie brauchen sollte euch aus dem Informatikunterricht bekannt sein, deshalb werde ich nicht mehr darauf eingehen. Hier wird nur auf die Implementierung dieser eingegangen.
Um eine abstrakte Klasse in Java zu erstellen muss man der Klasse lediglich das keyword abstract voranstellen.
In C++ ist das ein kleines bisschen komplizierter.


Damit eine Klasse in C++ abstrakt sein kann muss sie mindestens eine "rein virtuelle Funktion" besitzen.


Eine virtuelle Funktion(keine rein virtuelle) ist - stark vereinfacht gesagt - eine abstrakte Memberfunktion, die jedoch eine Definition besitzt und somit nicht in der abgeleiteten Klasse definiert werden muss.(bitte benutzt diese Definition NIEMALS, denn sie ist im Grunde genommen so falsch wie eine Aussage nur sein kann)
Das heißt im Umkehrschluss, dass jede nicht virtuelle Memberfunktion nicht in der abgeleiteten Klasse aufgerufen werden kann, ohne dass sie darin definiert wurde.
Außerdem ist eine Klasse, die eine virtuelle Memberfunktion besitzen nicht gleich abstrakt.


Eine rein virtuelle Funktion ist eine virtuelle Fuktion, die keine Definition besitzt und somit in der abgeleiteten Klasse definiert werden muss. Also im Prinzip eine uns aus Java bekannte abstrakte Methode.

Aber wie implementiert man das ganze?

// baseClass.h
#pragma once

class BaseClass
{
public:
   void doSomething() {}      // eine normale Funktion. Muss in dieser Klasse definiert werden.
   virtual void doThis() {}   // eine virtuelle Funktion. Muss ebenfalls in dieser Klasse definiert werden.
   virtual void doThat() = 0; // eine rein virtuelle Funktion. Muss in der abgeleiteten Klasse definiert werden.
};
// derivedClass.h
#include "baseClass.h"

class DerivedClass : public BaseClass
{
public:
   void doThat() {}
};

Der Destruktor muss immer virtuell sein.


Erklärung anhand der Klassen BaseClass und DerivedClass (erbt von BaseClass):

// main.cpp

#include "baseClass.h"
#include "derivedClass.h"

int main()
{
   BaseClass* b = new DerivedClass();   // b ist ein pointer auf eine Instanz von DerivedClass
                                        // Das können wir machen weil DerivedClass von BaseClass erbt (Polymorphie)

   delete b;     // Wenn wir nun die Instanz löschen wollen wird jedoch nicht der Destruktor von DerivedClass, 
                 // sondern der von BaseClass aufgerufen und das ist undefiniertes Verhalten. Ihr könnt euch bestimmt vorstellen warum das schlecht ist... 

   return 0;
}


Mir ist klar, dass das ein wenig unübersichtlich ist. Eine bessere Dokumentation findet ihr hier.

Namensräume

Wir benutzen Namensräume oder namespaces um Klassen, Funktionen, etc. einem bestimmten etwas - einer Gruppierung - zuzuordnen. Dies machen wir in erster Linie um zu verhindern, dass es zwei oder mehr im Namen identische Funktionen, etc. gibt. Bsp.:

#include <iostream>

using namespace std;

ostream& put (char c);   //  Fehler: Diese Funktion gibt es schon im namespace std!!

...

Um den Fehler im obigen Beispiel zu verhindern, machen wir folgendes:

#include <iostream>

using namespace std;

namespace myFunctions
{
   ostream& put(char c);    //  Kein Fehler: Wir sind im eigenen namespace
};

// So wird auf die Funktion zugegriffen
myFunctions::put(100);

Operatoren überladen

Operatoren sind: +, -, +=, &&, ->, ==, usw. Wir kennen das Prinzip von überladenen Funktionen schon aus dem Informatikunterricht. Das sind Funktionen, die den gleichen Namen haben, sich aber in den Parametern unterscheiden Bsp.:

#include <iostream>

using namespace std;

void foo() { cout << "keine Parameter"; }
void foo(const char* s) { cout << s; }

int main()
{
   foo();                     //  gibt aus: "keine Parameter"
   foo("Ein Parameter");      //  gibt aus: "ein Parameter"
}

Operatoren zu überladen ist eigentlich sehr ähnlich dazu, denn eine Operation mit einem Operator kann man auch in Form einer Funktion schreiben. Bsp.:

// in main
string a = "Ko";
string b = "ra";
string c = "mu";

a += b;
a.operator+=(c);

cout << a << endl;       //  gibt aus: "Koramu"

Anmerkung: Dies Funktioniert nicht direkt bei elementaren Datentypen.

Also müssen wir um einen Operator für eine Klasse zu überladen nur eine neue Methode in dieser hinzufügen. (Der Operator gilt dann auch nur für die Klasse, in der er definiert wurde. i.e. Es gelten die klassischen Scope-Regeln) Diese Methode könnte ungefähr so aussehen.

#include <iostream>

class foo
{
private:
    int x;
public:
    foo(int y) : x(y) {}
    void operator+=(const foo& f)      // const foo& ist eine Referenz auf den zweiten Summanden 
    {
        this->x += f.x;    
    }
        
    int getX() { return this->x; }
};

int main()
{
    foo a(10);
    foo b(20);
    
    a += b;
    
    std::cout << a.getX() << std::endl;    // gibt aus: 30
    
}

Hierbei muss man sich überlegen was der Operator zurückgeben soll und was er als Parameter nehmen soll. Dies ist immer fallspezifisch zu betrachten und kann manchmal verwirrend sein, also im Zweifelsfall StackOverflow oder Ario fragen.

Forward Declaration

http://cpp-tip-of-the-day.blogspot.de/2013/11/forward-declarations-vs-includes-in.html

Summary
These are the basic rules for inclusion:

  • include the headers of classes you inherit from
  • always include the headers of the STL containers
  • forward declare the types of static variables
  • forward declare the types of the variables held by reference or by pointer
  • include the header of all variables held by value
  • forward declare the function parameter types and the function return types