Funktionszeiger

Einführung

In den letzten Lektionen haben Sie die Funktionalität der Klasse kennen gelernt, die eine objektorientierte Programmiersprache ausmacht. In dieser Lektion gehe zunächst scheinbar einen Schritt zurück, da hier die OOP überhaupt keine Rolle spielt. Ich möchte aber nicht versäumen, die Bedeutung und die Möglichkeiten des Funktionszeigers darzulegen, da sie in der WIN32-Programmierung nicht selten verwendet werden.

Der Funktionszeiger

Bei einem Funktionszeiger handelt es sich (wie leicht zu vermuten ist) um einen Zeiger auf eine Funktion. Wozu macht das Sinn? Stellen Sie sich folgendes vor: Sie implementieren eine Funktion (nennen wir sie 'ListFiles'), die beispielsweise sämtliche Dateien eines angegebenen Ordners ausgeben kann. Jetzt könnte die Funktion natürlich einen Array anlegen und diesen zurückgeben. Derjenige, der die Funktion aufruft erhält somit die komplette Liste und kann diese weiterverwenden. Eine andere Möglichkeit wäre folgende: 'ListFiles' ermittelt Datei für Datei und gibt diese an eine Funktion (nennen wir Sie 'UseListEntries') weiter, die der Benutzer selbst angelegt hat und Eintrag für Eintrag verwaltet. In dieser Funktion kann der Benutzer selbst bestimmen, was mit jedem einzelnen Eintrag geschieht. Folgende Möglichkeiten wären u.a. denkbar:

bulletAusgabe eines jeden einzelnen Eintrags
bulletAuswerten eines jeden Eintrags (z.B. Art der Datei, Dateigröße, etc.)
bulletAusschließliches Weiterverwenden von Einträgen, die gewisse Anforderungen erfüllen
bulletAufnehmen der Einträge in eine Liste
bulletetc.

Natürlich kann man zunächst auch eine komplette Liste anlegen und diese dann auswerten und weiterverwenden, vielleicht wäre dies unter Erwägung des benötigten  Speicherbedarf etc. nicht unbedingt sinnvoll, wenn man nur wenige der Einträge verwenden möchte oder am Ende mehrere Listen braucht, die jeweils nur bestimmte Einträge aufnehmen.

Definition

Bei der Definition eines Funktionszeigers legt man fest, wie die Funktion (bzw. Funktionen) in Bezug auf Rückgabetyp, Datentypen der Parameter, etc. aussieht (bzw. aussehen). Wir überlegen uns also, wie eine Funktion wie 'UseListEntries' diesbezüglich aussehen könnte. Zunächst einmal sollte als Parameter (Datentyp 'const char*') der Eintrag übergeben werden. Ein zweiten Parameter (Datentyp 'int') bietet die Möglichkeit Zusatzinformationen weiterzugeben (Ich komme später zu der Bedeutung). Als Rückgabewert wäre 'bool' angebracht. Die Rückgabe 'false' soll der aufrufenden Funktion deutlich machen, dass keine weiteren Einträge benötigt werden. Die Deklaration der Funktion könnte wie folgt aussehen:

bool UseListEntries(char*, int);

Ein Funktionszeiger (nennen wir ihn 'pfnULE'), der eben auf diese Funktion zeigt, wird wie folgt definiert:

typedef bool pfnULE(const char*, int);

oder allgemein

typedef <Rückgabetyp> <Funktionszeiger> (<Datentyp1>, <Datentyp2>, ... ,<Datentypn>);

Korrekterweise muss man an dieser Stelle sagen, dass hier lediglich ein Datentyp nämlich 'pfnULE' (deswegen auch 'typedef') definiert wird, mit dem man Funktionszeiger anlegen kann.

Nun kann man mit Hilfe dieses neuen Datentyps Variablen deklarieren und diese wie gewohnt verwenden:

pfnULE pfn1, pfn2, pfn3;

Die Variablen können nun auf jede Funktion zeigen, die 'bool' zurückgibt, zwei Parameter besitzt, wobei der erste vom Typ 'const char*' und der zweite vom Typ 'int' ist, also auch auf die Funktion 'UseListEntries':

pfn1 = pfn2 = pfn3 = UseListEntries;

Nun könnte man anstelle die Funktion 'UseListEntries' direkt aufzurufen auch die Funktionszeiger in nahezu gleicher Weise verwenden:

pfn1("c:\\test.bmp", 0);	//entspricht: UseListEntries("c:\\test.bmp", 0);

Anwendung

Um die Funktionszeiger nutzen zu können implementieren wir zunächst die Funktion 'ListFiles': Erklärung der einzelnen Befehle (die der C++ STL entstammen) werde ich auf ein Minimum beschränken, da es weniger um die Auflistung von Dateien, als um die Verwendung von Funktionszeigern geht. Die folgende, hoffentlich nicht zu erschlagende Implementierung richtet sich nur an interessierte. Für alle anderen ist lediglich der Parameter 'pfn' in der Argumentliste der Funktion, sowie der Aufruf des Funktionszeigers interessant (beides gelb markiert)

bool ListFiles(char* szDir, pfnULE pfn, int Param)
{
	//Parameter kontrollieren
	if (!szDir || szDir[0] == '\0' || !pfn)
	{return false;}

	//Sicherstellen, dass Ordner Ordnertrennzeichen '\' am Ende enthält
	//entsprechend angepasster Ordner
	char szDirX[_MAX_FILE];
	strcpy(szDirX, szDir);

	//Trennzeichen anfügen wenn nötig
	int cLen = strlen(szDirX);
	if (szDirX[cLen - 1] != '\\') 
	{szDirX[cLen] = '\\'; szDirX[cLen++ + 1] = '\0';}
	//Filter: "Alle Dateien" -> "*" anfügen
	char szFilter[_MAX_FILE];
	strcpy(szFilter, szDirX);

	strcat(szFilter, "*");

	//Struktur für STL-Funktionen '_findfirst' und '_findnext'
	_finddata_t	data = {0};

	//erste datei suchen
	intptr_t handle = _findfirst(szFilter, &data);

	//solange suchen, bis es keine Datei mehr gibt
	do
	{
		if (handle == -1) break;		//nicht gefunden?
		
		if ((data.attrib & _A_SUBDIR))		//Ordner überspringen
			continue;
		else 
		{
			//gesamten Pfad der angegeben Datei erstellen
			char sz[_MAX_FILE];
			strcpy(sz, szDirX); strcat(sz, data.name);
			
			//Funktionszeiger aufrufen und evtl. Schleife abbrechen
			if (!pfn(sz, Param)) break;
		}
	}while (_findnext(handle, &data) == 0);

	//Aufräumarbeiten
	if (handle != -1) _findclose(handle);
	//Funktionsende
	return true;	
}

Kurze Erläuterung: Zunächst werden die übergebenen Parameter auf Gültigkeit kontrolliert, dann wird die Variable 'szDirX' angelegt, die nach einigen Stringfunktionen, den als Parameter übergebenen Ordner enthält und diesen (wenn nötig) um ein Ordnertrennzeichen '\\' erweitert. Im Anschluss wird der Filter 'szFilter' erstellt, der neben dem zu durchsuchenden Ordner noch ein '*' erhält, also keine Vorauswahl getroffen, sondern nach allen Dateien gesucht wird! Mit '_findfirst' wird die erste Datei gesucht, mit '_findnext' jede weitere. Vor dem Aufruf des Funktionszeigers wird die gefundene Datei noch um den kompletten Pfad ergänzt. Das soll als Erläuterung reichen.

Der Parameter 'Param' spielt innerhalb der Funktion keine Rolle. Er wird einfach an den Funktionszeiger weitergegeben. Für eine mögliche Auswertung ist also einzig und allein die mit  'pfn' aufgerufene Funktion verantwortlich (siehe: Beispiel 2).

Anwendung am Beispiel 1

Um nun die Funktion 'ListFiles' verwenden zu können, benötigen wir noch eine Funktion, die per Funktionszeiger übergeben wird. Ich verwende zunächst die recht einfache Funktion, die wie oben als Verwendungsmöglichkeit angegeben lediglich die Einträge ausgibt:

//Ausgabe eines Eintrags
bool OutputEntry(const char* szFile, int)
{
    //Ausgabe des Eintrags
    cout << szFile << endl;

    return true;
}

Der zweite Parameter wird bei dieser Funktion nicht benötigt. Die Funktion 'ListFiles' kann nun wie folgt aufgerufen werden:

ListFiles("d:", OutputEntry, 0);

Es wird hier also tatsächlich als zweites Argument der Name einer Funktion angegeben. Erwartungsgemäß gibt der Aufruf sämtliche Dateien auf dem Bildschirm aus, die sich im Stammverzeichnis der Festplatte "D:" befinden.

Anwendung am Beispiel 2

Im zweiten Beispiel sollen die Einträge, die Textdateien (Endung ".txt") repräsentieren in einer Liste gespeichert werden. Die Liste soll hier durch eine Zeichenkette realisiert werden, die sämtliche Einträge enthält, die durch ein Zeilenwechsel ("\n") getrennt sind. Die Funktion soll im folgenden 'TextFileEntry' heißen. Dieser Funktion muss bekannt sein, wo die Einträge gespeichert werden sollen. Dies wird mit dem bisher unbenutzten zweiten Parameter erreicht. Über ihn wird eine Zeichenkette übergeben, die dann jeweils um Eintrag für Eintrag ergänzt wird. Bei der Funktion 'TextFileEntry' stellt der zweite Parameter also einen Zeiger auf eine Zeichenkette vom Datentyp 'char**' dar. Warum ein "Doppelzeiger"? Dazu im Anschluss.

Zunächst aber der mögliche Aufruf der Funktion 'ListFiles':

char* szTexts = 0;
ListFiles("d:", TextFileEntry, (int)&szTexts);

Damit man eine Zeichenkette als drittes Argument übergeben kann, muss man natürlich den Datentyp anpassen, da 'ListFiles' ein 'int' und kein 'char**' erwartet. Dies wird in der Windowsprogrammierung häufig angewandt um Parameter weiterzugeben, deren Inhalt die Funktion selber nicht auswertet bzw. zu deuten weiß aber für gewisse Zwecke übernehmen muss. Hier ist es ebenso: 'ListFiles' gibt den Parameter nur weiter, da dieser in der über den Funktionszeiger aufgerufenen Parameter ausgewertet wird. So bleibt 'ListFiles' allgemeingültig. Die Verwendung des Datentyps 'int' bietet sich an, da er neben Ganzzahlen auch (32-Bit-) Zeiger repräsentieren kann.

Nun aber zu der Funktion 'TextFileEntry'. Ihre Implementierung könnte wie folgt aussehen:

//Speichern eines Txt-Eintrags in der per Param übergebenen Zeichenkette
bool TextFileEntry(const char* szFile, int Param)
{
	//Argumente auswerten
	if (!szFile || !Param) 
	{return false;}

	//Positionszeiger auf vorletztes Zeichen des Eintrags
	const char* pPos = szFile + strlen(szFile) - 2; 

	//Zeichenkette auf "." durchsuchen
	while (pPos > szFile && *pPos != '.')
	{pPos--;}

	//Punkt gefunden?
	if (*pPos != '.') return true; //keine Dateiendung
	else pPos++; //Zeigt nun auf Dateiendung

	//Dateiendung Textdatei?
	if (stricmp(pPos, "txt") != 0) 
	return true; //kein Text

	//Zugriff auf Zeichenkette
	char** pTxtList = (char**)Param;

	//Länge der Liste
	int cLen = ((*pTxtList == 0)? 0 : strlen(*pTxtList));

	//neue Zeichenkette erstellen
	char* sz = new char [cLen + 
	strlen(szFile) + 2 /*"\n\0"*/];

	//alte Daten kopieren
	if (cLen > 0)
	{
		strcpy(sz, *pTxtList);
		strcat(sz, "\n");
		strcat(sz, szFile);
	}
	else strcpy(sz, szFile);

	//alte Liste löschen
	delete[] *pTxtList; *pTxtList = sz; sz = 0;


	return true;
}

Zur Erläuterung: Zunächst werden die Argumente auf Gültigkeit überprüft. Im Anschluss wird mit 'pPos' ein Zeiger deklariert, der anfangs auf das vorletzte Zeichen des Dateinamens zeigt. Im folgenden wird der Zeiger solange dekrementiert bis er auf einen '.' zeigt und somit auf die Dateiendung der Datei zeigt. Danach wird überprüft, ob es sich um eine Textdatei handelt ('stricmp' kontrolliert zwei Zeichenketten ohne Berücksichtigung der Groß- und Kleinschreibung).

Im nächsten Aufruf wird der Parameter ausgewertet. Die Funktion, "weiß", dass es sich dabei um einen Zeiger auf eine Zeichenkette handelt und führt aus diesem Grunde ein "Typecasting" durch um per 'pTxtList' vereinfachten Zugriff auf eben diese Zeichenkette erhalten.

Danach wird eine neue Liste 'sz' erstellt, die der alten entspricht aber um die neue Datei und dem Trennzeichen '\n' ergänzt wird. Zum Schluss wird die alte Liste gelöscht und der übergebene Zeiger 'pTxtList' zeigt auf die neue Liste. Hier wird deutlich, warum ein Doppelzeiger übergeben werden musste: Angenommen man hätte einen einfachen Zeiger 'char*' verwendet. In diesem Falle hätte man nur die Adresse der Zeichenkette übergeben und somit direkten Zugriff auf die Zeichenkette erhalten. Beim Erstellen der neuen Zeichenkette erhält man aber auch eine neue Adresse. Diese wiederum müsste man zurückgeben. Daher benötigt man eben nicht die Adresse der Zeichenkette, sondern die Adresse des Variablen, der eben die Adresse der Zeichenkette gespeichert hat um eben dort die neue Adresse anlegen zu können.

Ruft man nun die Funktion 'ListFiles' unter Angabe dieser neuen Funktion wie oben bereits geschehen auf, dann erhält man eine Liste, die sämtliche Textdateien enthält, die sich im angegebenen Ordner befinden.

Zum Herumprobieren der Quelltext zum Herunterladen: funcpointers_src.rar

Für die nächsten Lektionen

In dieser Lektion sollten Sie die Funktionszeiger kennen lernen. Sicherlich ist das Thema hier nicht erschöpfend behandelt worden. In der "einfachen" Windowsprogrammierung tauchen sie immer mal wieder auf und aus diesem Grunde habe ich sie hier vorgestellt. Es sollte Sie also in der Windowsprogrammierung zukünftig nicht mehr wundern, wenn Sie als Argument für irgendeine Funktion oder aber als Feld in einer Struktur ein Funktionsnamen angeben müssen.

Zurück Nach oben