Auf dieser Seite soll auf eine Reihe von Aspekten der Programmierung in C bzw. C++ eingegangen werden, die bei der Programmierung aus Stil-, Performance- und anderen Gründen beachtet werden sollten. Es handelt es sich hierbei nicht um ein Programmier-Tutorial. Es handelt sich lediglich um eine Sammlung von Tipps, Erklärungen und Codeschnippseln, die ich für nützlich halte. Für ein Tutorial zum Thema "Optimierung von Programmen" siehe hier.
Funktionspointer sehen zwar böse aus (z. B.: void (*funcptr)(int, float, void*);
),
aber sie sind oft sehr nützlich, um universelle Schnittstellen zu schaffen, oder
um Callback-Mechanismen zu realisieren. Da sie aber aus der Sprache C übernommen
wurden, scheint ihre Verwendung gemeinsam mit C++-Sprachkonstrukten gewissen Beschränkungen
unterworfen. Wer z. B. versucht, einen Zeiger auf eine (nicht-statische) Memberfunktion
einer Klasse zu erzeugen, wird mit diesem intuitiven Code scheitern:
struct mystruct { void Memberfunc(); }; void (*funcptr)(); // Funktionspointer einer Funktion, die nichts zurückgibt und keine Parameter nimmt. funcptr = &mystruct::Memberfunc;
Ursache für dieses Verhalten ist, dass in einer nicht-statischen Memberfunktion der this-Zeiger definiert ist und man darum auf die Membervariablen der Instanz der Klasse zurückgreifen kann, von der aus die Funktion aufgerufen wurde. Ein Funktionspointer des o. g. Typs würde man allerdings so aufrufen; die Instanz wäre dabei unbekannt:
(*ptr)();
Für Memberfunktionszeiger kann man zwar Folgendes schreiben, allerdings ist dies nicht besonders flexibel, da es an eine bestimmte Klasse gebunden und daher die Signatur der Funktion von der zuvor Genannten verschieden ist:
void (mystruct::*funcptr)() = &mystruct::Memberfunc; // Definition, Initialisierung mystruct ms; // Wir brauchen (wie oben angedeutet) eine Instanz der Klasse. (ms.*funcptr)(); // Aufruf der Funktion mit der Instanz ms.
Für dieses Problem gibt es mit dem
neuen Standard C++11 eine Lösung: std::function
und std::bind()
:
#include <functional> std::function<void(void)> funcptr; // Funktionspointer auf eine Funktion ohne Parameter und Rückgabewert mystruct ms; // Wir brauchen weiterhin eine Instanz der Klasse. funcptr = std::bind(&mystruct::Memberfunc, &ms); // Wir erzeugen einen Funktionspointer, der an die Instanz ms gebunden ist. funcptr(); // Der Aufruf erfolgt nun wie bei einem normalen Funktionspointer vom Typ void (*)(void) // Beim Aufruf der Funktion, auf die funcptr zeigt, wird ms als Instanz verwendet.
Auch komplexere Probleme sind durch std::bind lös- und beherrschbar geworden:
#include <functional> struct mystruct { void Memberfunc(int, std::string); }; mystruct ms; std::function<void(int)> funcptr = std::bind(&mystruct::Memberfunc, &ms, std::placeholders::_1, "blabla");
In diesem Beispiel wird der Parameter des Funktionspointers als erster Parameter
an Memberfunc weitergegeben, an den zweiten Parameter von mystruct::Memberfunc
wird
"blabla"
gebunden, d. h. bei jedem Aufruf der Funktion über den
Funktionszeiger übergeben.
Unter Umständen kann es nötig sein, eine ganze Datei auf einen Schlag auszulesen und im Speicher zu sichern. Die Lösung hierfür sind Iteratoren:
std::ifstream ifs("myfile"); std::string destination((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
Die zusätzlichen Klammern um den ersten Parameter des Konstruktors sind bei manchen Compilern notwendig, damit dieser Code nicht als Funktionsdeklaration fehlinterpretiert wird.
Für Funktionen:
void Func(); // Deklaration. Es wird kein Code generiert, aber der Name der Funktion ist damit bekannt gemacht. void Func() {} // Definition/Implementation. Die Funktion existiert nun. Diese Definition ist zugleich eine Deklaration.
Für Variablen:
extern int i; // Deklaration. Wie bei Funktionen wird nur der Name bekannt gemacht, aber kein Objekt erzeugt int i; // Definition. Die Variable existiert damit im Speicher. Sie ist zugleich eine Deklaration. int i = 0; // Initialisierung. Die Variable exisitert im Speicher und hat zu Beginn den Wert 0. Sie // ist zugleich Deklaration und Definition.
Während das Thema Deklarationen, Initialisierungen und Definitionen bei Funktionen
eher harmlos ist, kann es bei Variablen Probleme geben. Wenn man in einen Header
eine Definition (also int i;
oder int i = 0;
) schreibt,
werden in jeder .c/.cpp-Datei, die diesen Header inkludieren, jeweils eine dieser
Variablen erzeugt. Ab dem Punkt gibt es zwei Möglichkeiten, was passieren kann:
static
ist, entstehen auch beim Linken mehrere Variablen,
nämlich pro Übersetzungseinheit eine, die jeweils unterschiedliche Werte haben können.static
ist, hängt es vom Linker ab, was passiert. GNU ld warnt nicht,
gibt auch keinen Fehler aus, sondern baut eine gemeinsame Variable. Der Linker vom
MSVC hingegen gibt grundsätzlich einen Linkerfehler aus, dass etwas mehrfach definiert
wurde.Es kann also, Probleme geben, wenn man eine Definition in einen Header schreibt,
wenn man nicht genau weiß, was man tut. Um auch den GCC, bzw. GNU ld zu zwingen,
einen Fehler zu generieren (was an dieser Stelle empfohlen sei, um Fehler zu vermeiden),
sollte man diese Option angeben:
--warn-common
In C++ wird, im Gegensatz zu C, das Überladen von Funktionen ermöglicht:
void Func(int); void Func(float); int Func(void*);
Wie Sie sehen, ist es offenbar möglich, bei der Überladung der Parameter auch den Rückgabetyp zu ändern. Dies ist aber gewissen Beschränkungen unterworfen. Folgendes geht nicht:
void Func(int); int Func(int); float Func(int);
Dies hat zwei Gründe. Der naheliegendste ist, dass das „Name-Mangeling“, das C++-Compiler
bei Funktionsüberladungen einsetzen, den Rückgabetypen nicht zwangsläufig berücksichtigt.
Während der MSVC dies tut, vergibt beispielsweise der GCC an die Funktion void Func(int);
aus dem ersten
Beispiel das Symbol „_Z4Funci“, an die Überladung void Func(float);
„_Z4Funcf“. Ändern Sie den
Rückgabetypen einer der Funktionen, verändert sich der Symbolname nicht. Also
kann der Linker beim Auflösen des Symbols nicht unterscheiden, welche Funktion aufgerufen werden
soll, wenn man auschließlich den Rückgabetypen überlädt, denn ihr Symbolname ist identisch. Der zweite Grund ist, dass der Compiler
zumeist nur schwer entscheiden kann, welcher Typ gewünscht ist. An dieser Stelle
muss man aber nicht aufgeben, und jeder der Funktionen andere Namen geben. Eine
Lösung für das Problem können, mit gewissen Einschränkungen, templates sein: Man
könnte das erste Codebeispiel ja auch per Template, falls nötig mit entsprechenden
Spezialisierungen, umsetzen:
template<typename T> void Func(T); template<> void Func(int); template<> void Func(float); template<> int Func(void*);
Templates haben in Bezug auf beide o. g. Probleme Vorteile. Beim Name-Mangling von Templates wird der angegebene Typ T in jedem Fall in den Namen eingebaut, ob er nun als Rückgabetyp oder als Parameter genutzt wird. Außerdem kann man ohne Probleme explizit angeben, welche template-Spezialisierung man wünscht. Also kann man folgendes tun:
template<typename T> T Func(int); template<> void Func(int); template<> int Func(int); template<> float Func(int);
Der Aufruf würde so erfolgen:
float f = Func<float>(123);
Der GCC generiert für die float-Spezialisierung den Namen „_Z4FuncIfET_i“, die int-Spezialisierung bekommt „_Z4FuncIiET_i“. Eine Implementation ist übrigens nur bei den spezialisierten Funktionen nötig. Die Implementation der allgemeinen Deklaration ist nur erforderlich, wenn Typen verwendet werden, für die keine Spezialisierungen vorhanden sind.
Wenn man, z. B. für Benchmarks oder zu Optimierungszwecken, die Ausführungszeit messen will, kann man auf die Funktion
std::clock()
aus <ctime>
zurückgreifen. Diese gibt die Anzahl der Ticks (interne Zeit-Recheneinheit
des Betriebssystems) seit Programmstart zurück, sodass die Differenz zur Messung einzelner Codestücke genutzt werden kann. Die Auflösung
der Messergebnisse beträgt bei modernen Betriebssystemen i.d.R. bis zu 1 ms. Das Grundprinzip ist denkbar einfach:
std::clock_t start = std::clock(); // Hier den Code einfügen, dessen Laufzeit gemessen werden soll... std::clock_t diff = std::clock() - start; std::cout << "Laufzeit: " << diff*1000/CLOCKS_PER_SEC << " ms" << std::endl;
Dabei sollte man übrigens tunlichst vermeiden, versehentlich die Ausgabe des Ergebnisses mit zu messen, weil Textausgaben viel Zeit konsumieren können (vor allem in der langsamen Konsole von Windows).