Hilfestellungen zu C++

Diese Veranstaltung ist das Praktikum Bildbearbeitung, nicht eine vertiefte Beschäftigung mit C++. Da die OpenCV API für C++ einem das Leben gegenüber dem Arbeiten mit den C-Bindings aber massiv erleichtert, bekommt Ihr hier einen kurzen "Crashkurs" zu C++. Meldet euch bitte bei Unklarheiten zu hier nicht erwähnten Details der Sprache, ihr sollt euch vornehmlich mit verschiedenen Aspekten der Bildbearbeitung beschäftigen.

Die für diese Übung relevanten Unterschiede (also Vorteile) von C++ gegenüber C sind vor allem:

  • Es ist keine explizite Speicherverwaltung mit malloc und free (bzw new und delete in C++) erforderlich. Für Strings lässt sich der eingebaute Datentyp std::string verwenden.
  • Arbeiten mit Zeigern (und Prüfungen auf NULL) werden durch die Verwendung von Referenzen überflüssig.
  • Die C++ Standardbibliothek verfügt über die "typischen" Containerdatenstrukturen. Ihr müsst nicht manuell mit Arrays oder eigenen Listen hantieren.

Verwendet als primäres Nachschlagewerk bitte cppreference.com, dort finden sich sinnvolle Beispiele für alle relevanten Funktionen und Methoden. Die Masse an wirklich schlechten C++ Tutorials und Nachschlagewerken ist leider schier endlos. Sofern euch ein Aspekt von C++ auf diesen Seiten nicht ausreichend erklärt scheint wäre ich für einen kurzen Hinweis dankbar. Abgesehen von der eingangs erwähnten Dokumentationen der C++Standardbibliothek solltet ihr keinerlei externe Quellen benötigen.

Optional: Arbeiten mit dem QtCreator

Maßgeblich für die Abnahme ist nur das einzucheckende Makefile. Wer schon mit C++ gearbeitet hat darf gerne eine beliebige IDE verwenden, ich persönlich empfehle die Verwendung des QtCreator. Er bietet die "typischen" Features die man von einer IDE erwartet, also Code Completion, Syntax Highlighting, einen integrierten Debugger und ist dabei noch einigermaßen schlank.

Der QtCreator sieht standardmäßig die Verwendung von "großen" Buildsystemen qmake, cmake oder den GNU Autotools vor, der Umgang mit einem "normalen" Makefile ist aber ebenfalls problemlos möglich. Nach einem Klick auf "File -> New File or Project ..." muss entsprechend des Screenshot rechts "Import Existing Project" ausgewählt werden. Wenn man das Programm aus dem QtCreator heraus laufen oder debuggen möchte, muss man dazu einmalig nach dem Kompilieren den Pfad zum ausführbaren Programm angeben.

Der QtCreator speichert seine Einstellungen in den folgenden, für den Benutzer sichtbaren Dateien:

  • projectname.creator speichert globale Projektoptionen.
  • projectname.creator.user speichert lokale Projektoptionen wie Pfade und sollte nicht mit eingecheckt werden.
  • projectname.config für #defines die beim Kompilieren verwendet werden sollen.
  • projectname.files für Dateien die im Projektbaum des QT Creator angezeigt werden sollen. Die Datei kann direkt im integrierten Editor bearbeitet werden, beim Speichern wird dann der Projektbaum die Änderungen wiedergeben.

Ein-/Ausgabe und Namespaces

Namespaces (Namensräume) werden in C++ zur Zusammenfassung von Funktionen und Klassen zu Kategorien und zur Vermeidung von Namenskonflikten verwendet. Man gibt sie explizit mit :: vor der jeweiligen Funktion, Klasse etc. angeben. Die Standardbibliothek befindet sich im Namespace std, OpenCV im Namespace cv.

Die Ein- und Ausgabe erfolgt in C++ über die Streams cout, cin und cerr unter Verwendung der Operatoren >> (aus dem Stream in eine Variable lesen) bzw << (in Stream schreiben). Diese sind für die eingebauten Datentypen überladen und können daher problemlos für Strings und Zahlen verwendet werden. Einen Zeilenumbruch samt Flush erreicht man durch die Verwendung von std::endl. Die für die Ein- und Ausgabe relevanten Operation sind im Header iostream definiert.

Der Import von Bezeichnern in den globalen Namensraum mit using namespace xyz; ist möglich, im Rahmen dieser Veranstaltung aber unzulässig!

#include <iostream>
#include <string>

int main(int argc, char** argv)
{
  std::string name;
  int alter;
  std::cout << "Ihr Name : ";
  std::cin >> name;
  std::cout << "Ihr Alter: ";
  std::cin >> alter;

  std::cout << "Sie heißen " << name << " und sind "
            << alter << " Jahre alt" << std::endl;
}

Standard-Template-Library

Die C++-Standard-Template-Library (STL) bietet komplexe Datentypen, um gängige Problemstellungen einfach lösen zu können. Wir werden u.a. möglicherweise die Datentypen std::vector<T> und std::map<TKey, TValue> nutzen, welche dynamische Arrays bzw. den Map-Datentypen repräsentieren. Diese können als Elemente beliebige Datentypen beinhalten, welche (analog zu Java) in spitzen Klammern nach dem Typnamen angegeben werden.

#include <iostream>
#include <vector>

int main(int argc, char** argv)
{
  const int NUM = 5;
  std::cout << "Geben sie " << NUM << " Zahlen ein." << std::endl;
  std::vector<int> numbers;

  for (int i = 0; i < NUM; ++i)
  {
    int num;
    std::cin >> num;
    // Eine neue Zahl am Ende anhängen, std::vector "wächst"
    // automatisch mit.
    numbers.push_back(i);
  }

  int sum = 0;
  // foreach-artige Iteration
  for (const int i : numbers)
  {
    sum += i;
  }

  std::cout << "Die Summe ist " << sum << std::endl;
  // Zugriff auf Elemente wie bei Arrays
  std::cout << "Der mittlere Wert ist " << numbers[NUM / 2] << std::endl;
}

Referenzparameter

In C++ gibt es gegenüber C eine weitere Alternative, wie der Aufrufer einer Methode die benötigten Daten übergeben kann. Anstatt eines Zeigers kann man auch eine Referenz als Parameter verwenden. Eine Referenz unterscheidet sich bei der Parameterdeklaration syntaktisch dadurch, dass statt des Sterns * ein "kaufmännisches und" & vorangestellt wird. Inhaltlich unterscheiden sich Zeigervariablen und Referenzen dadurch, dass bei einer Referenz nicht die Adresse kopiert, sondern direkt auf die Variable verwiesen wird. Innerhalb der Funktion wird auf die übergebene Variable also nicht über einen Zeiger zugegriffen, sondern die Variable kann direkt bearbeitet werden. Konsequenterweise wird beim Aufruf auch nicht die Adresse, sondern die Variable selbst übergeben. Dadurch können, im Gegensatz zu Zeigern, Nullzeiger schon vom Typsystem ausgeschlossen werden.

Für den von euch zu schreibenden Code beachtet ihr daher bitte folgende Regeln:

  • Strukturierte Datentypen werden nach Möglichkeit als konstante Referenz (const T&) übergeben. Da man auch die Containertypen ohne Probleme an Funktionen übergeben muss man hier besonders sorgfältig arbeiten: Die Übergabe eines ganzen std::vector "by Value" wird tatsächlich den gesamten Speicherbereich kopieren. Soll der Parameter in der aufzurufenden Funktion verändert werden lasst ihr natürlich das const weg.
  • Primitive Datentypen hingegen (also Zahlen und boolesche Werte) werden typischerweise by Value übergeben. Von einer konstanten Referenz wird man bei diesen Typen aufgrund der kleinen Größe nicht profitieren.
  • Die beiden obigen Regeln gelten auch für die foreach-artige Iteration. Sofern ihr hier keine Referenz benutzt wird der Wert für jeden Durchlauf der Schleife einmal kopiert.
  • Eine Eigenart von C++ ist, dass diese Regeln leider nicht für Rückgabewerte gelten: Der Ergebnistyp einer Funktion sollte im Normalfall niemals eine Referenz sein. Der Compiler ist jedoch, anders als bei Parametern, in der Lage die unnötige Kopie durch return value optimization bzw. copy elision zu umgehen.

Und falls ihr lieber Quelltext statt menschlicher Sprache lest, komme ich dem natürlich gerne nach:

// Unnötiger Kopieraufwand, strukturierte Datentypen werden
// bitte per Referenz übergeben
void doMagic(std::string s);

// Zulässig, sofern s von der Funktion verändert werden soll
void doMagic(std::string& s);

// "Normalfall" für strukturierte Datentypen
void doMagic(const std::string& s);

// "Normafall" für primitive Datentypen
void doMagic(int i);

// Zulässig, sofern i von der Funktion verändert werden soll
void doMagic(int& i);

// Unnötig, eine Kopie von primitiven ist mindestens
// genau so schnell gemacht wie das übergeben einer Referenz
void doMagic(const int& i);

// Korrekt, die theoretisch entstehende Kopie wird vom Compiler
// verhindert.
std::vector<int> createMagic();

// Gefährlich, da garantiert falsch wenn die Variable "hinter"
// dem Rückgabewert auf dem Stack allokiert wurde.
std::vector<int>& createMagic();