Twitter und Facebook-Anbindung
X
Tweet Follow @twitterapi
!!! Anbindung an twitter und facebook öffnen !!!

Wenn Ihnen mein Online-Buch gefällt,
dann bedanken Sie sich doch mit einer kleinen Spende...

13 Dateien

13 Dateien

So richtig macht Programmieren nur Spaß und vor allem auch Sinn, wenn man mit Dateien arbeiten kann, sei es lesend oder schreibend und genau darum soll es in diesem Kapitel gehen. Doch bevor ich anfang kann, muss ich mit Ihnen ein paar grundlegende Dinge behandeln.

Zum Seitenanfang
Zum Inhaltsverzeichnis

13.1 Allgemeines zu Dateien und Verzeichnissen

13.1 Allgemeines zu Dateien und Verzeichnissen

Ihnen dürfte klar sein, was eine Datei ist, aber wussten Sie schon, dass Verzeichnisse auch Dateien sind? In einer "Verzeichnis-Datei" befinden sich Informationen der Dateien, welche im Verzeichnis enthalten sind.

Dateien lassen sich in zwei Typen unterscheiden. Es gibt Textdateien, welche mit jedem Texteditor geöffnet werden können und es gibt Binärdateien, welche nur von zugehörigen Anwendungen geöffnet werden können. Sicher kann man diese auch mit einem Texteditor öffnen, aber man wird nur komische Symbole in ihr finden. Deshalb werde ich auf diese zwei Arten in getrennten Kapiteln eingehen.

Um auf Dateien zugreifen zu können, benötigt man die Hilfe des Betriebssystems, weil dieses weiß, wie man mit den verschiedenen Dateisystemen umgeht (FAT, NTFS, ISO9660, UDF und und und). Dies wird unter Windows über die Windows API geregelt, aber keine Angst, denn Visual Studio besitzt bereits fertige Mechanismen, um Ihnen den Umgang zu erleichtern. Dir sollten aber trotzdem wissen, dass man nicht selbst mit den Dateien arbeitet, sondern das Windows dies tut.

Zum Seitenanfang
Zum Inhaltsverzeichnis

13.1.1 Handles

13.1.1 Handles

Bevor Sie mit Dateien arbeiten, sollten Sie grob wissen, was ein Handle ist. Wie bereits erwähnt, regelt das Betriebssystem den Dateizugriff. Dem Programmierer wird beim öffnen eine Art Referenz zur Verfügung gestellt, welche dann immer mit angegeben werden muss, wenn etwas mit der Datei gemacht werden soll. Somit weiß Windows, welche Datei gemeint ist. In C wurde um diese Handles ein Mechanismus herum gebastelt, aber prinzipiell wissen Sie jetzt, dass ein Handle eine Referenz ist, mit welcher das Betriebssystem später eine Zuordnung machen kann.

Handels werden aber nicht nur für Dateien verwendet, sondern kommen überall dort zum Einsatz, wo das Betriebssystem für einen etwas machen soll. Dies betrifft z.B. die Steuerung anderer Programme, Prozesse, Bibliotheken und Fenster. Möchte man z.B. eine Funktion einer DLL benutzen, benötigt man zunächst ein Handle darauf. Will man ein Fenster in den Vordergrund holen oder ein anderes Programm steuern, wird ebenfalls ein Handle benötigt. Dies soll Sie aber jetzt nicht verwirren, sondern nur den Begriff Handle näher bringen.

Da ein Handle wie gesagt nur eine Referenz ist, handelt es sich nur um eine Zahl, welche meistens vom Typ Integer ist.

Zugriff auf eine Datei mittels Handles

Zum Seitenanfang
Zum Inhaltsverzeichnis

13.1.2 Die Tücken der Dateiarbeit

13.1.2 Die Tücken der Dateiarbeit

Der Umgang mit Dateien ist prinzipiell nicht sonderlich schwer, aber es gibt eine Menge Sachen zu beachten, auf die ich hier kurz eingehen werde.

Die erste Schwierigkeit ist dadurch begründet, dass wir heute in der Regel mit Mehrbenutzersystemen arbeiten. D.h. es kann vorkommen, dass wir auf Dateien keinen Zugriff bekommen, da sie von anderen Benutzern oder Prozessen verwendet werden. Deshalb sollten Sie vor Dateizugriffen prüfen, ob Sie auf die gewüschte Datei zugreifen kann.

Ein weiteres Problem ist, dass Sie evtl. gar nicht das Recht haben, auf eine gewisse Datei zugreifen zu können, weil sie einem anderen Benutzer gehören, welcher Ihnen das Leserecht entzogen hat. Auch hierfür gibt es prinzipiell Möglichkeiten, wie man so etwas herausfinden kann, aber da dies sehr stark vom Betriebssystem abhängt, werde ich in diesem Tutorial nicht explizit darauf eingehen. Wichtig ist nur, dass so etwas vorkommen kann, und dass ein berufstätiger Programmierer, auf solche Dinge achten muss.

Ein weiterer ungünstiger Umstand, ist der Anwender. Zum einen könnte eine eingegebene Pfadangabe nicht existieren und zum anderen könnte eine Datei, z.B. auf einem USB-Stick liegen, welche während der Dateiarbeit abgezogen wird. Auch hier sollten Vorkehrungen getroffen werden, dass das Programm nicht abstürzt bzw. sich aufhängt.

Was können Sie also tun? Nun, es gibt zwei gängige Herangehensweisen, welche in enger Kombination verwendet werden sollten. Zum einen kann man ganz paranoid haufenweise Funktionen bauen, die die verschiedensten Sachen im Vorfeld prüfen und zum anderen, sollte man um Dateizugriffe "try catch Statements" bauen. Das könnte dann folgendermaßen aussehen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
					
// Wenn es die Datei gibt
if (FileExist(astrPath) && IsReadable(astrPath)) {
	try {
		// Dateizugriffe
	} catch (...) { // Wenn irgend ein Fehler/Ausnahme auftritt
		// Dieser Bereich wird ausgeführt, wenn es im Oberen zu
		// einem Absturz kommt
		printf("Beim Zugriff der Datei ist ein Fehler aufgetreten!");
	} // end of try
} // end of if
					

Sie können da sehr paranoid sein und im Zweifelsfall sollten Sie das auch, aber aus Gründen der Verständlichkeit, werde ich in folgenden Quelltexten darauf verzichten. Falls Sie im Web auf die Suche nach Codeschnipseln gehrn, dann wundern Sie sich nicht, wenn Sie sehr verschachtelte Konstrukte finden, denn diese wurden gemacht, um den von mir erwähnten Anfälligkeiten entgegen zu treten.

Zum Seitenanfang
Zum Inhaltsverzeichnis

13.2 Textdateien

13.2 Textdateien

So, das war erst einmal genügend Theorie. Fangen wir also an. Am besten dafür sind die Textdateien, da man hier schnell seine Ergebnisse kontrollieren kann. Eines sei aber vorher noch erwähnt. Wenn Sie Pfadangaben in Strings, statisch als Konstante, implementieren, dann denken Sie daran, ein doppeltes Backslash zum abtrennen von Datei und Verzeichnissen zu nehmen, da ein einzelnes Backslash ein Escapezeichen einleiten würde.

Zum Beispiel: C:\\TestVerzeichnis\\Unterverzeichnis\\Testdatei.txt

Zum Seitenanfang
Zum Inhaltsverzeichnis

13.2.3 Die Funktionen fgets() und fputs()

13.2.3 Die Funktionen fgets() und fputs()

Ein kleiner Nachteil ergibt sich durch die zwei eben genannten Funktionen. Im Falle von Strings, wird immer nur ein Wort eingelesen bzw. geschrieben. So muss man extra für Leerzeichen sorgen. Des Weiteren hat man keine Chance, Zeilenumbrüche zu erkennen.

Abhilfe schaffen die Funktionen "fgets", zum einlesen ganzer Zeilen und "fputs", zum schreiben ganzer Zeilen. Folgendes Beispiel öffnet wieder eine Datei und holt, dieses Mal Zeile für Zeile, den Inhalt heraus und packt ihn in eine neue Datei. Anschließend sieht die Zieldatei exakt so aus, wie die Quelldatei.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
					
char*	pcstrFileNameSrc	= "C:\\Testdatei_Quelle.txt";
char*	pcstrFileNameDst	= "C:\\Testdatei_Ziel.txt";

FILE*	hFileSrc		= fopen(pcstrFileNameSrc, "r");
FILE*	hFileDst		= fopen(pcstrFileNameDst, "w+");

// Wenn die Dateien geöffnet werden konnten
if (hFileSrc != NULL) {
	if (hFileDst != NULL) {
		try {
			char astrTemp[1000];
			bool bCouldRead	= true;

			// Solange das Dateiende noch nicht erreicht ist
			while (bCouldRead) {
				fgets(astrTemp, 999, hFileSrc);

				// Wenn etwas eingelesen werden konnte
				if (bCouldRead = !feof(hFileSrc)) {
					fputs(astrTemp, hFileDst);
				} // end of if
			} // end of while
		} catch (...) {
			printf("Es ist ein Fehler aufgetreten!\n");
		} // end of try
		
		fclose(hFileSrc);
	} // end of if
	
	fclose(hFileDst);
} // end of if
					
Zum Seitenanfang
Zum Inhaltsverzeichnis

13.2.1 Die Funktionen fopen(), feof() und fclose()

13.2.1 Die Funktionen fopen(), feof() und fclose()

Die Funktion "fopen" veranlasst das Betriebssystem, eine Datei für einen Zugriff zu öffnen. Dabei werden der Pfad auf die gewünschte Datei und ein s.g. Modus, mit übergeben. Zurück erhält man das bereits erwähnte Datei-Handle. Der Modus gibt an, für welchen Zugriff man die Datei öffnen will (z.B. lesen oder schreiben) und was geschehen soll, wenn die Datei noch nicht vorhanden ist. Optional kann noch angegeben werden, mit welchem Zeichensatz die Datei ausgelesen bzw. beschrieben werden soll. Folgende wichtigen Modi gibt es.

Mit der Funktion "feof" kann man testen, ob man am Dateiende angekommen ist (end of file). Wie Sie sich denken können, benötigt die Funktion lediglich das Datei-Handle als Übergabeparameter. Im Gegensatz zu anderen Sprachen, ist diese Funktion aber mit Vorsicht zu genießen, da sie erst "true" zurückgibt, wenn einmal erfolglos versucht wurde, aus einer Datei zu lesen bzw. in sie zu schreiben. Somit ließt man einmal umsonst aus einer Datei.

Die Funktion "fclose" beendet den Dateizugriff und gibt die Datei für den Zugriff durch andere Benutzer und oder Programme, frei. Auch ihr wird lediglich der entsprechende Datei-Handle übergeben. Vergisst man die Freigabe, kann die Datei bis zum nächsten Neustart des Computers, nicht mehr verwendet werden! Das Schließen einer nicht geöffneten Datei, führt zu einem Fehler bzw. zu einer Exception.

Zum Seitenanfang
Zum Inhaltsverzeichnis

13.2.2 Die Funktionen fscanf() und fprintf()

13.2.2 Die Funktionen fscanf() und fprintf()

Wie Sie sich vielleicht denken könnt, stehen die Funktionen für "File-ScanF" und "File-PrintF". Sie funktionieren wie die bekannten Funktionen "scanf" und "printf", nur mit dem Unterschied, dass man beim Aufruf noch das Datei-Handle mit übergibt und die Ein - bzw. Ausgabe, nichts mit der Konsole, sondern mit Dateien zu tun hat.

Im folgenden Beispiel, werde ich eine Textdatei öffnen, den Inhalt Stück für Stück auslesen und ihn in eine andere Datei schreiben. Damit wird ein (langsames) Kopieren einer Datei, realisiert.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
					
char*	pcstrFileNameSrc	= "C:\\Testdatei_Quelle.txt";
char*	pcstrFileNameDst	= "C:\\Testdatei_Ziel.txt";

FILE*	hFileSrc		= fopen(pcstrFileNameSrc, "r");
FILE*	hFileDst		= fopen(pcstrFileNameDst, "w+");

// Wenn die Dateien geöffnet werden konnten
if (hFileSrc != NULL) {
	if (hFileDst != NULL) {
		try {
			char astrTemp[100];
			bool bCouldRead	= true;

			// Solange das Dateiende noch nicht erreicht ist
			while (bCouldRead) {
				fscanf(hFileSrc, "%s", astrTemp);

				// Wenn das Einlesen funktioniert hat
				if (bCouldRead = !feof(hFileSrc)) {
					fprintf(hFileDst, "%s ", astrTemp);
				} // end of if
			} // end of while
		} catch (...) {
			printf("Es ist ein Fehler aufgetreten!\n");
		} // end of try
	
		fclose(hFileSrc);
	} // end of if

	fclose(hFileDst);
} // end of if
					

Genauso wie man das von der Funktion "scanf" kennt, kann man auch Integer und andere Typen, gezielt einlesen. Allerdings muss man dann darauf gefasst sein, dass der gelesene Inhalt nicht dem entspricht, was man erwartet (jemand könnte die Datei manipuliert haben). Deshalb ist es stets am klügsten, erst einmal Strings einzulesen und jene, nach entsprechender Prüfung, in das gewünschte Format zu Konvertieren. Wie Sie in Zeile 19 sehen, prüfe ich vor dem Schreiben, ob das Einlesen funktioniert hat. Würde ich dies nicht tun, würde in der Zieldatei, der letzte Eintrag der Quelldatei doppelt vorkommen, da "fscanf" im Fehlerfall die übergebene Variable für den Inhalt, nicht anfasst und somit noch der letzte Wert drin steht.

Alternativ, stehen auch die sicheren Funktionen "fscanf_s" und "fprintf_s" zur Verfügung, mit welchen Sie besser auf evtl. auftretende Fehler reagieren können. Mehr zu diesen Funktionen findet Sie in der Hilfe oder im MSDN.

Zum Seitenanfang
Zum Inhaltsverzeichnis

13.2.4 Testdateien mit Streams

13.2.4 Testdateien mit Streams

Eine elegantere Variante mit Dateien zu arbeiten, sind die Dateistreams. Sie nehmen Ihnen ein wenig Arbeit ab, da sie selbst Objekte sind, die viele Dinge kapseln, also im Hintergrund ausführen, um die Sie sich nicht mehr kümmern müssen. Eigentlich ist dieses Kapitel an der falschen Stelle, da ich Objekte erst im nächsten großen Abschnitt bespreche, aber auf der anderen Seite, gehört es der Vollständigkeit halber, hier her und bietet zudem eine schöne Einleitung zum Thema objektorientierte Programmierung.

Im folgenden Quelltext werde ich genau das gleiche machen, wie eben, also eine Textdatei händisch kopieren, nur dass ich jetzt Dateistreams benutzen werde. Nebenbei sei noch erwähnt, dass ich dafür die Header-Datei "fstream" benutze.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
					
char*		pcstrFileNameSrc	= "C:\\Testdatei_Quelle.txt";
char*		pcstrFileNameDst	= "C:\\Testdatei_Ziel.txt";

std::ifstream	oSrcFileStream(pcstrFileNameSrc);
std::ofstream	oDstFileStream(pcstrFileNameDst);

// Wenn die Dateien ge�ffnet werden konnten
if (oSrcFileStream.is_open() && oDstFileStream.is_open()) {
	try {
		char cValue;
		bool bCouldRead		= true;

		// Solange gelesen werden kann
		while (bCouldRead) {
			oSrcFileStream >> cValue;
		
			// Wenn das Einlesen funktioniert hat
			if (bCouldRead = !oSrcFileStream.eof()) {
				oDstFileStream << cValue;
			} // end of if
		} // end of while
	} catch (...) {
		printf("Es ist ein Fehler aufgetreten!\n");
	} // end of try
} // end of if
					

In Zeile 4 und 5 sehen Sie, dass bereits das öffnen einer Datei mit Streams, anders aussieht. Dies liegt daran, dass man die Datei nicht selbst öffnet, sondern dies der Kontrolle der Objekte überlässt. In den beiden Zeilen werden also lediglich zwei Streamobjekte erzeugt und es wird ihnen mitgeteilt, welche Dateien sie benutzen sollen. Normalerweise muss man auch hier noch einen Modus mit angeben, allerdings gibt es für jene Parameter entsprechende Defaultwerte, welche genau so definiert sind, wie ich sie brauche, um in Textdateien zu lesen, bzw. zu schreiben.

Einen weiteren Unterschied zu eben sehen Sie in Zeile 8. Vorhin habe ich getrennt geprüft, ob die Dateien geöffnet werden konnten, damit ich sie auch nur in diesem Fall schließe. Auch hier kommen Ihnen die Streamobjekte entgegen. Da sie, wie erwähnt, den kompletten Dateizugriff Kapseln (auch das Schließen der Dateien), brauche ich keine getrennten Prüfungen und spare mir damit ein paar Zeilen Quelltext.

In Zeile 15 wird nun aus der Datei bzw. aus dem Stream, Zeichenweise gelesen. Entsprechend ähnlich verhält sich die Sache in Zeile 19, nur mit dem Unterschied, dass hier das Schreiben in die Datei realisiert wird.

Leider sind Streams keine Allzweckwaffe. Zum einen muss man trotzdem immer prüfen, ob das Einlesen funktioniert hat und zum anderen sieht das Ergebnis, also die Zieldatei, nicht so aus wie gewünscht. Es fehlen wieder sämtliche Leerzeichen und auch alle Zeilenumbrüche. Wie man dies beheben kann, sehen Sie dann, wenn ich zeige, wie man Binärdateien mit Streams bearbeitet. Abschließend sei nochmals erwähnt, dass man die Dateien nicht wieder freigeben braucht, da dies geschieht, sobald man den Gültigkeitsbereich der Objekte verlässt und sie im Zuge dessen, freigegeben werden (es passiert im Destruktor der Streams).

Zum Seitenanfang
Zum Inhaltsverzeichnis

13.4 Durchsuchen von Verzeichnissen

13.4 Durchsuchen von Verzeichnissen

Bisher habe ich Ihnen zwar gezeigt, wie man auf Dateien zugreifen kann, aber nicht, wie man eine Datei findet. Genau darum geht es in diesem Unterkapitel. Bevor wir loslegen können, muss ich Ihnen ein paar Worte zur Windows API erzählen.

Zum Seitenanfang
Zum Inhaltsverzeichnis

13.4.2 Hinweise zu den Variablentypen der Windows API

13.4.2 Hinweise zu den Variablentypen der Windows API

Ein weiterer Stolperstein der Windows API ist, dass es sehr viele Typdefinitionen, also Aliasnamen, für bekannte Datentypen gibt und das diese auch oftmals verlangt, bzw. zurückgegeben werden. Dies wurde aber nicht aus reiner Schikane gemacht, sondern um eine Vereinheitlichung vorzunehmen. Ich hatte ja mal erwähnt, dass ein Integer früher nur zwei Byte groß war und heute meist vier Byte groß ist. Der durch Windows spezifizierte Datentyp "DWORD" steht für "double Word" und entspricht einem heutigen Integer. Früher wusste man durch das "double", dass der Datentyp größer war als ein Integer. Genauso tauchen Datentypen wie "LPCSTR" auf, was heißt "large pointer of const string", also ein heutiger "const char*". Sie fragen sich jetzt bestimmt wieder, warum "large pointer"? Die Begründung fällt wie eben aus. Pointer waren schon immer Integervariablen und da heute die Integer größer sind, sind auch die Pointer größer (streng genommen ist es anders herum, da man mehr Arbeitsspeicher adressieren können wollte). Es gibt noch unzählige andere Datentypen, aber wenn man schaut wo sie definiert sind, sieht man was dahinter steckt und man hat wieder die bisher bekannten Typen vor sich.

Zum Seitenanfang
Zum Inhaltsverzeichnis

13.4.4 Vorbemerkung zur Verzeichnissuche

13.4.4 Vorbemerkung zur Verzeichnissuche

Wie schon erwähnt, sind Verzeichnisse auch nur Dateien. Jene haben aber ein ganz bestimmtes Dateiattribut (Bitmaske). Des Weiteren bekommt man keine Information darüber, wie viele Verzeichnisse und/oder Dateien sich in einem Verzeichnis befinden. Möchte man dies wissen, muss man es wie gleich gezeigt durchlaufen und selber die gefundenen Einträge zählen.

Ein weiterer Irrglaube ist, dass man alle Einträge in alphabetischer Reihenfolge bekommt. Oftmals ist dem so, aber eine Garantie dafür gibt es nicht. Des Weiteren werden auch Dateien und Verzeichnisse gemischt ermittelt. Falls man also diese Angaben trennen möchte, muss man wieder selbst Hand anlegen.

Am wichtigsten jedoch ist, dass ein Durchsuchen viele Gemeinsamkeiten mit dem Öffnen und Schließen einer Datei hat. Deswegen ist es extrem wichtig, dass jede Suche mit einem speziellen Befehl wieder beendet wird. Andernfalls bekommt man mit diesem Verzeichnis so lange Probleme (gerade wenn man es löschen oder umbenennen will), bis Windows neu gestartet wurde.

Wenn ein Verzeichnis durchsucht wird, bekommt man meist zu Beginn zwei Einträge, die oft nicht benötigt werden. Es sind die Einträge "." und "..", welche für DOS eine große Rolle spielen. Der einzelne Punkt steht für das aktuelle Verzeichnis und der doppelte für das übergeordnete Verzeichnis. Ggf. müssen Sie diese zwei Einträge also ausfiltern.

Zum Seitenanfang
Zum Inhaltsverzeichnis

13.4.5 Implementierung der Dateisuche

13.4.5 Implementierung der Dateisuche

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
					
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

// ...

WIN32_FIND_DATAA	sFileInfo;
HANDLE			hFileHandle	= FindFirstFileA("C:\\*.*", &sFileInfo);
bool			bGoOn		= hFileHandle != INVALID_HANDLE_VALUE;

// Solange Einträge gefunden werden
while (bGoOn) {
	// Wenn ein Verzeichnis gefunden wurde
	if (sFileInfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
		// Nur relevante Verzeichnisse
		if ((strcmp(sFileInfo.cFileName, ".") != 0) &&
		    (strcmp(sFileInfo.cFileName, "..") != 0)) {

			printf("Verzeichnis:  %s\n", sFileInfo.cFileName);
		} // end of if
	} else {
		printf("Datei:        %s\n", sFileInfo.cFileName);
	} // end of if

	bGoOn				= FindNextFileA(hFileHandle, &sFileInfo);
} // end of while

FindClose(hFileHandle);
					

Wie Sie sehen, gibt es drei wichtige Funktionen, nämlich "FindFirstFileA", "FindNextFileA" und "FindClose". Erstere Startet die Suche und dort gibt man auch das Verzeichnis an, welches durchsucht werden soll. Wenn man kein Pfad angibt, wird das Verzeichnis durchsucht, in welchem das Programm liegt. Das "*.*" bedeutet, dass alle Einträge beachtet werden sollen. Würde man z.B. "*.jpg" angeben, würden alle JPEG Dateien (Bilder) gefunden werden, wobei die Groß - und Kleinschreibung nicht berücksichtigt wird.

Zum Seitenanfang
Zum Inhaltsverzeichnis

13.4.6 Auflisten aller vorhandener Laufwerke

13.4.6 Auflisten aller vorhandener Laufwerke

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
					
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

// ...

char* pstrDrive		= new char[4];
pstrDrive[1]		= ':';
pstrDrive[2]		= '\\';
pstrDrive[3]		= '\0';		// Terminierungszeichen

// Durchlaufe alle möglichen Laufwerke
for (char cDrive = 'A'; cDrive <= 'Z'; cDrive++) {
	pstrDrive[0]	= cDrive;

	// Je nachdem, um welches Laufwerk es sich handelt
	switch (GetDriveTypeA(pstrDrive)) {
		case DRIVE_REMOVABLE:
			printf("%c: - Auswerfbares Laufwerk\n", cDrive);
			break;
		case DRIVE_FIXED:
			printf("%c: - Festplatte\n", cDrive);
			break;
		case DRIVE_REMOTE:
			printf("%c: - Netzlaufwerk\n", cDrive);
			break;
		case DRIVE_CDROM:
			printf("%c: - Optisches Laufwerk\n", cDrive);
			break;
	} // end of switch
} // end of for

delete [] pstrDrive;
					

Da die Funktion "GetDriveTypeA" Angaben der Art "C:\" benötigt, muss ich mir zunächst in den Zeilen 8 bis 10 und 14, diese Pfadangabe zusammen bauen. Der Rest ist ganz einfach. Man klappert alle möglichen Laufwerksbuchstaben von A bis Z ab und schaut, welche Informationen man bekommt. Hierbei sei aber noch erwähnt, dass hier alle Laufwerke aufgelistet werden, egal ob z.B. in einem CD-ROM Laufwerk eine CD liegt oder nicht. Des Weiteren gilt z beachten, dass die Laufwerkstypen etwas schwammig sind. "DRIVE_CDROM" heißt auch DVD oder BluRay und "DRIVE_REMOVABLE" kann für einen USB-Stick (wird allerdings nur erkannt, wenn er am Rechner angeschlossen ist) oder ein Kartenleser stehen.

Die Windows API stellt noch weitere nützliche Funktionen bereit (Freier Speicherplatz oder Pfadangaben zum Windowsverzeichnis oder dem Benutzerverzeichnis), auf welche ich jetzt nicht im Detail eingehen möchte. Schauen Sie also ins MSDN und erkunden Sie die anderen Möglichkeiten.

Zum Seitenanfang
Zum Inhaltsverzeichnis

13.4.2 Bitmasken

13.4.2 Bitmasken

Ein weiterer Aspekt ist, dass oft Masken übergeben werden sollen. Es handelt sich hierbei wieder um einfache Integer, welche aber durch Bitoperationen mehrere Werte bzw. Flags aufnehmen können. Ich hatte bereits über Bitfelder gesprochen, wobei ich aber keine richtigen Bitfelder gezeigt habe, sondern einen neuen Mechanismus gezeigt habe, welcher wesentlich komfortabler war. Jetzt kommen wir aber nicht mehr umher, dies nachzuholen.

Die große Frage also lautet, wie kann ich jedes der 32 Bit eines Integers einzeln abfragen und setzen? Die Antwort ist ganz einfach. Zum Lesen verwendet man eine AND Operation, zum Setzen eine OR Operation und zum Entfernen eine XOR Operation. Wie und warum das Funktioniert, sollen folgende Grafiken zeigen.

Bitweise Verknüpfung mit AND

Wie Sie sehen können, kann man mit dem "&" Operator eine bitweise Verknüpfung machen und nur wenn der kleinere Wert im größeren enthalten ist, erhält man als Ergebnis den kleineren Wert. Ansonsten bekommt man einen anderen. Da ein Flag meistens nur durch ein oder maximal 2 benachbarte Bits signalisiert wird, kann man getrost davon ausgehen, dass das Ergebnis der Operation 0 ist, wenn das Flag nicht enthalten ist. Beachten Sie, dass dieses einzelne "&" an dieser Stelle nichts mit dem Referenzierungsoperator zu tun hat und verwechseln Sie es bitte auch nicht mit dem Verknüpfungsoperator "&&", welcher bei Bedingungen zum Einsatz kommt.

Bitweise Verknüpfung mit OR

Den Operator "|" müssen Sie sich wie ein intelligentes Plus vorstellen. Sie können damit ein Flag zu einer Maske dazu addieren und falls es schon enthalten ist, stört das nicht weiter.

Bitweise Verknüpfung mit XOR

Mit dem "^" Operator können Sie wieder Flags von einer Maske entfernen, aber wie Sie sehen, müssen Sie aufpassen. Dieser Operator verhält sich wie ein Plus und ein Minus. Falls Sie also ein Flag entfernen wollen, was gar nicht gesetzt war, setzen Sie es. Bevor Sie diese Operation anwenden, müssen sie also mit "&" prüfen, ob es enthalten ist. Und falls Sie sich wundern, dass ich zu Beginn behauptet hatte, dass es in C kein XOR gibt, dann weiße ich Sie nochmal auf den Unterschied zwischen einem Bitoperator und einem Verknüpfungsoperator hin. In einer Bedingung kann man zwei Ausdrücke nicht mit XOR koppeln, aber bei Bitoperationen steht einem ein XOR zur Verfügung.

Zum Seitenanfang
Zum Inhaltsverzeichnis

13.4.1 Funktionshinweise zur Windows API

13.4.1 Funktionshinweise zur Windows API

Zunächst einmal sollten Sie an dieser Stelle nicht zurück schrecken, denn wenn man sich ein paar dieser Funktionen anschaut, wird man ein wenig verunsichert. Ich möchte hier ein wenig Licht ins Dunkle bringen.

Als erstes muss man bedenken, dass die heutigen Windowsversionen weitestgehend abwärtskompatibel sind. Das heißt jetzt nicht unbedingt, dass man unter Windows 7 noch ein Windows 95 Programm ausführen kann. Vielmehr meine ich den internen Aufbau. Das hat zur Folge, dass es viele Funktionen in mindestens zwei Versionen gibt. Einmal die Klassische und dann die Moderne. Meistens macht sich dies dadurch bemerkbar, dass man zum einen ganz normale Strings übergibt bzw. zurück bekommt und das andere Mal bekommt man "WideStrings" im Unicode 16 Format. Letztere benötigen nicht nur ein Byte pro Zeichen, sondern zwei (damit kann man z.B. auch deutsche Umlaute und kyrillische Buchstaben darstellen). Dazu kommt noch, dass es meistens noch eine allgemeine Version der Funktion gibt, welche im Hintergrund nur eine der anderen beiden anspricht (je nach Systemvoreinstellung). Hier mal ein Beispiel.

Da der Umgang mit den "WideChars" etwas gewöhnungsbedürftig ist (zumal man für alle bekannten Stringfunktionen äquivalente andere aufrufen muss, welche oft mit "w" oder "_t" beginnen), werde ich mich ausschließlich auf die ASCII Funktionen beschränken. Falls Sie jetzt also mal eine Funktion mit einem großen A oder W am Ende sehen, wissen Sie jetzt, was dies zu bedeuten hat.

Zum Seitenanfang
Zum Inhaltsverzeichnis

13.3 Binärdateien

13.3 Binärdateien

Gerade wenn man wichtige Informationen, wie Strukturen und Einstellungen, speichern und laden möchte, sind Textdateien nicht der beste Weg (mal abgesehen von INI oder XML Dateien). Wie eben erwähnt, kann man nicht davon ausgehen, auch das zu finden, was man erwartet, weil andere die Datei händisch manipuliert haben könnten. Dies ist zwar bei Binärdateien prinzipiell auch möglich, aber nicht so verlockend wie bei Textdateien.

Zudem ergeben sich aus der Verwendung von Binärdateien weitere Vorteile. Zum einen können in ihr ganze Strukturen abgelegt bzw. aus ihr entnommen werden. Daraus ergibt sich wiederum, dass man zumindest grundlegend erst einmal testen kann, ob die Datei konsistent ist. Dies könnte man so anstellen, dass man prüft, ob die Dateigröße ein vielfaches des Speicherbedarfs des zu erwarteten Typs ist (Ist der einzulesende Typ 2 KB und die Datei 11 KB groß weiß man schon vor dem öffnen der Datei, dass etwas faul ist).

Der Inhalt der Datei sieht dann so aus, wie man die Variablen im Hauptspeicher des Rechners vorfinden würde. Ein Integer würde dann also genau vier Byte (Zeichen) einnehmen. Für kleine werte (<1000) erscheint dies zwar als Speicherverschwendung, dafür spart man aber Platz bei großen werden. In der Regel spart man durchschnittlich, zumal man kleinere Werte in kleineren Datentypen unterbringen kann.

Zum Seitenanfang
Zum Inhaltsverzeichnis

13.3.1 Die Funktionen fread() und fwrite()

13.3.1 Die Funktionen fread() und fwrite()

Das Öffnen und Schließen von Binärdateien funktioniert grundlegend genauso wie mit Textdateien, nur mit dem Unterschied, dass man beim Öffnen einen speziellen Modus angibt.

Mit der Funktion "fread" kann man nun aus der Datei lesen und mit "fwrite" in sie schreiben. Den Funktionen wird jetzt, neben der Variable mit dem Inhalt bzw. der Variable in welcher der Inhalt kommen soll und dem Datei-Handle, noch zwei Weitere Werte übergeben, welche angeben, wie groß der einzulesende Wert ist und wie viele davon einzulesen sind. So ist es theoretisch möglich, gleich ein ganzes Array mit einem Schlag zu speichern bzw. es auszulesen.

Im folgenden Beispiel werde ich durch den Anwender Personenangaben einlesen und jene in einer Struktur speichern. Jenes wird dann in eine Datei gespeichert und anschließend wieder ausgelesen. Zunächst implementiere ich eine Struktur, welche verschiedene Personeninformationen aufnehmen kann.

 1
 2
 3
 4
 5
 6
					
struct SPerson {
	char	astrVorname[80];
	char	astrNachname[80];
	int	iAlter;
	float	fGewicht;
};
					

Dieser Typ soll verwendet werden. Er hat eine Größe von 168 Byte. Die Dateigrößen werden also immer ein Vielfaches dieser Zahl sein. Kümmern wir uns nun um die Eingabe der Werte und das Abspeichern in die Binärdatei.

 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
					
char*	pcstrFileName		= "C:\\Test.bin";
bool	bGoOn			= true;
FILE*	hFile			= fopen(pcstrFileName, "w+b");

// Wenn die Datei erstellt werden konnte
if (hFile != NULL) {
	printf("== Eingabe ========================\n");

	// Solange Personen hinzugefügt werden sollen
	while (bGoOn) {
		SPerson sPerson;
		char	cSign;

		printf("Vorname:  ");
		fflush(stdin);
		scanf("%s", &(sPerson.astrVorname));
		
		printf("Nachname: ");
		fflush(stdin);
		scanf("%s", &(sPerson.astrNachname));
		
		printf("Alter:    ");
		scanf("%i", &(sPerson.iAlter));
		
		printf("Gewicht   ");
		scanf("%f", &(sPerson.fGewicht));

		try {
			fwrite(&sPerson, sizeof(SPerson), 1, hFile);
		} catch(...) {
			printf("Beim Speichern ist ein Fehler aufgetreten!\n");
		} // end of try
		
		printf("\n\nNoch eine Person einlesen? (j/n)");
		fflush(stdin);
		scanf("%c", &cSign);
		bGoOn		= (cSign == 'j' || cSign == 'J');
	} // end of while

	fclose(hFile);
} // end of if
					

Als nächstes folgen das Auslesen der eben erstellten Datei und die Ausgabe auf dem Bildschirm. Dabei werde ich dieselben Variablen benutzen, wie eben. Besonders auffällig ist nun das doppelte Prüfen auf "end of file". Sie können sich ja mal überlegen, was passiert, wenn man die innere Prüfung weg lässt.

50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
					
hFile			= fopen(pcstrFileName, "rb");

// Wenn die Datei geöffnet werden konnte
if (hFile != NULL) {
	printf("== Ausgabe ========================\n\n");

	// Solange es noch etwas zum auslesen gibt
	while (!feof(hFile)) {
		SPerson sPerson;

		try {
			fread(&sPerson, sizeof(SPerson), 1, hFile);

			// Nur wenn noch was eingelesen werden konnte
			if (!feof(hFile)) {
				printf("Vorname:  %s\n", sPerson.astrVorname);
				printf("Nachname: %s\n", sPerson.astrNachname);
				printf("Alter:    %i\n", sPerson.iAlter);
				printf("Gewicht:  %g\n\n", sPerson.fGewicht);
			} // end of if
		} catch (...) {
			printf("Beim Einlesen ist ein Fehler aufgetreten!\n");
		} // end of try
	} // end of while

	fclose(hFile);
} // end of if						
					

So könnte nun eine Ausgabe des Programms aussehen.

== Eingabe ========================
Vorname:  Hans
Nachname: Müller
Alter:    20
Gewicht   80.5

Noch eine Person einlesen? (j/n)j

Vorname:  Paul
Nachname: Meier
Alter:    21
Gewicht   75.25

Noch eine Person einlesen? (j/n)n

== Ausgabe ========================
Vorname:  Hans
Nachname: Müller
Alter:    20
Gewicht:  80.5

Vorname:  Paul
Nachname: Meier
Alter:    21
Gewicht:  75.25
		
Zum Seitenanfang
Zum Inhaltsverzeichnis

13.3.2 Binärdateien mit Streams

13.3.2 Binärdateien mit Streams

Nun werde ich noch abschließend zeigen, wie man eben gezeigten Quelltext mit Streams realisieren kann. Ich werde dabei den gleichen Datentyp benutzen wie eben.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
					
char*		pcstrFileName		= "C:\\Test.bin";
bool		bGoOn			= true;

std::ofstream	oOutFileStream(pcstrFileName, std::ios_base::binary);

// Wenn die Dateien geöffnet werden konnten
if (oOutFileStream.is_open()) {
	try {
		printf("== Eingabe ========================\n");

		// Solange Personen hinzugefügt werden sollen
		while (bGoOn) {
			SPerson sPerson;
			char	cSign;

			printf("Vorname:  ");
			fflush(stdin);
			scanf("%s", &(sPerson.astrVorname));
			
			printf("Nachname: ");
			fflush(stdin);
			scanf("%s", &(sPerson.astrNachname));
			
			printf("Alter:    ");
			scanf("%i", &(sPerson.iAlter));
			
			printf("Gewicht   ");
			scanf("%f", &(sPerson.fGewicht));

			try {
				oOutFileStream.write((char*)&sPerson, sizeof(SPerson));
			} catch(...) {
				printf("Beim Speichern ist ein Fehler aufgetreten!\n");
			} // end of try

			printf("\n\nNoch eine Person einlesen? (j/n)");
			fflush(stdin);
			scanf("%c", &cSign);

			bGoOn		= (cSign == 'j' || cSign == 'J');
		} // end of while
	} catch (...) {
		printf("Es ist ein Fehler aufgetreten!\n");
	} // end of try
} // end of if

oOutFileStream.close();

std::ifstream	oInFileStream(pcstrFileName, std::ios_base::binary);

// Wenn die Dateien geöffnet werden konnten
if (oInFileStream.is_open()) {
	try {
		printf("== Ausgabe ========================\n\n");

		// Solange es noch etwas zum auslesen gibt
		while (!oInFileStream.eof()) {
			SPerson sPerson;

			try {
				oInFileStream.read((char*)&sPerson, sizeof(SPerson));

				// Nur wenn noch was eingelesen werden konnte
				if (!oInFileStream.eof()) {
					printf("Vorname:  %s\n", sPerson.astrVorname);
					printf("Nachname: %s\n", sPerson.astrNachname);
					printf("Alter:    %i\n", sPerson.iAlter);
					printf("Gewicht:  %g\n\n", sPerson.fGewicht);
				} // end of if
			} catch (...) {
				printf("Beim Einlesen ist ein Fehler aufgetreten!\n");
			} // end of try
		} // end of while
	} catch (...) {
		printf("Es ist ein Fehler aufgetreten!\n");
	} // end of try
} // end of if
					

Die Ausgabe sieht wieder genauso aus, wie vorhin, aber vom Quelltext her hat sich einiges geändert. Es geht schon in Zeile 4 los. Genauso wie bei den Textdateien, habe ich ein Streamobjekt erzeugt, jedoch mit einem zusätzlichen Parameter, um zu signalisieren, dass die zu bearbeitende Datei eine Binärdatei ist.

In Zeile 22 wird jetzt das schreiben in die Datei angestoßen. Allerdings benutze ich dieses mal nicht den Serialisierungsoperator, da jener nur mit reinem Text funktioniert bzw. wenn man mit Textdateien arbeitet. Sehr merkwürdig ist hier, dass ich die zu schreibende Variable in einen char Pointer caste. Dies ist notwendig und hat folgenden Hintergrund. Der Stream schreibt und ließt immer Byteweise. Jeder Datentyp ist ja ohne Rest in n Bytes teilbar (auch für ein bool wird intern ein Byte Speicher benötigt). Diese eingelesenen Bytes, werden also in den RAM bzw. die Datei geschrieben, ohne wirkliche Chars zu sein (ein Char ist nun mal ein elementarer Datentyp, der genau ein Byte groß ist). Da die Methode "write" ein Pointer erwartet, muss ich nicht die Struktur selber, sondern seine Adresse übergeben, was das "&" erklärt.

In Zeile 37 schließe ich jetzt händisch die Datei, da ich sie sonst in Zeile 38 nicht wieder mit dem anderen Streamobjekt öffnen kann (auch hier wieder im Binärmodus).

In Zeile 51 wird das Lesen aus der Datei veranlasst und wie eben schon+ angedeutet, ist auch hier wieder ein Casten auf einen Char-Pointer notwendig und man kann ebenfalls nicht die Serialisierungsoperatoren verwenden.

Wenn man dieses Beispiel adaptiert, kann man auch Textdateien händisch kopieren und bekommt dann auch alle Sonderzeichen, wie Leerzeichen und Zeilenumbrüche, mit. Als Variable muss man lediglich ein einzelnes Char benutzen (niemals eine Zeichenkette, da sonst am Ende der Datei, unter Umständen, etwas fehlen könnte).

Zum Seitenanfang
Zum Inhaltsverzeichnis

13.5 Übungsaufgaben X

13.5 Übungsaufgaben X

  1. Lassen Sie vom Benutzer seinen Nachnamen, Vornamen und Geburtsdatum eingeben und speichern Sie die Eingaben in einer HTML-Datei, wobei die Daten in einer Tabelle gespeichert werden sollen. Der Benutzer soll außerdem noch angeben können, wo und unter welchem Namen die Datei gespeichert werden soll.
  2. Schreiben Sie ein Programm, welches ein Verzeichnis rekursiv durchsucht und alle gefundenen Dateien in eine HTML-Datei speichert. Beim Programmstart soll der Benutzer nach einem Verzeichnis (leere Eingabe ist aktuelles Verzeichnis), einer Suchmaske (leere Eingabe ist „*.*“) und eine Rekursionstiefe angeben. Zuletzt soll er wieder angeben können, wo die erstellte Datei gespeichert werden soll.
  3. Schreiben Sie ein kleines Programm, welches aus einer Textdatei Integer einließt (Datei besteht nur aus zahlen, durch Leerzeichen getrennt), daraus den durchschnitt berechnet und diesen errechneten Wert ans Ende dieser Datei schreibt (in eine neue Zeile).
Zum Seitenanfang
Zum Inhaltsverzeichnis

© Copyright by Thomas Weiß, 2009 - 2012