C++ Dateien kopieren - Meinungen zum Code

Kenobi van Gin

Brillenschlange
Registriert
14 Juli 2013
Beiträge
3.520
Ort
.\
Hallo zusammen.

Ich melde mich mal wieder mit einer Frage zu C++ :) Nachdem in meinem Referenzbuch gerade das Thema lesen und schreiben in binären Dateien behandelt wurde, wollte ich gern eine eigene Konsolenanwendung im Stil von z.B. xcopy schreiben. Der Hintergrund ist, dass ich nach der Erstellung meines Backups (mit Personal Backup 5) als externes Programm immer eine Batch aufrufe, die via xcopy zwei TrueCrypt-Container kopiert. Da das unter bestimmten Umständen nicht reibungslos funktioniert (und ich ja auch noch etwas lernen will), habe ich mir nun also einen eigenen Code zum Kopieren von Dateien geschrieben.

[src=cpp]void copyFile(string strSourcePath, string strDestinationPath)
{

ifstream readData;
ofstream writeData;

readData.open(strSourcePath, ios_base::binary | ios_base::in);
writeData.open(strDestinationPath, ios_base::binary | ios_base::out);

unsigned long long lngSourceFileSize = getFileSize(strSourcePath);
unsigned long long lngBytesRead = 0;
int intCurrPercentage = 0;
int intLastPercentage = 0;

cout << " Copying file:" << endl << " 0 % done";
char Buffer;
while (readData.read(&Buffer, sizeof(Buffer)))
{
writeData.write(&Buffer, sizeof(Buffer));
lngBytesRead += sizeof(Buffer);
intCurrPercentage = round(100.0 / lngSourceFileSize * lngBytesRead);
if (intCurrPercentage > intLastPercentage)
{
cout << '\r';
cout << " " << intCurrPercentage << " % done";
intLastPercentage = intCurrPercentage;
}

/*
if (intCurrPercentage > intLastPercentage + 1)
{
cout << '\r';
cout << intCurrPercentage << " % |";
for (int i = 0; i <= intCurrPercentage; i++)
{
if (!(i % 2))
cout << '=';
}
cout << string(100 - intCurrPercentage, ' ') << "|";
intLastPercentage = intCurrPercentage;
}
*/
}
cout << endl << " Done!" << endl;
readData.close();
writeData.close();

}[/src]

Der auskommentierte Teil sollte eine einfach Progressbar anzeigen. Das hat auch im Prinzip funktioniert, war aber noch verbuggt und hat außerdem gefühlt einiges an Performance gefressen. Darum habe ich es vorerst durch eine einfach %-Anzeige ersetzt.

Erstmal meine eigenen Gedanken zum Code:
1.) Ist diese Art, Dateien zu kopieren, prinzipiell eine einigermaßen effiziente? Oder gibt es da Möglichkeiten, die wesentlich performanter oder weniger fehleranfällig sind?
2.) Der verwendete Lesepuffer ist hier, wie man sieht, nur 1 char (= 1 Byte?) groß. Ursprünglich wollte ich einen wesentlich größeren Puffer. Leider stellt sich da natürlich das Problem, dass bei Dateien, deren Größe kein Vielfaches der Puffergröße ist, jeweils die letzten Bytes nicht mehr kopiert werden. Nun könnte ich natürlich eine zusätzliche if-Abfrage einbauen, die dann für die letzten Bytes den Puffer wieder auf 1 setzt. Würdet ihr das so machen, oder gibt es da einen eleganteren Weg?
3.) Besonders bei großen Dateien fällt auf, dass meine Methode nicht die performanteste ist. Würde hier ein größerer Buffer (z.B. 128 oder sogar 256 Bytes) Abhilfe schaffen?
4.) Im Augenblick überprüft der Code noch nicht auf Fehler beim Öffnen der Ein- und Ausgabedatei. Das ist mir bewusst. Füge ich später noch hinzu ;)

Ich bin gespannt auf eure Einschätzungen und vermute fast, dass ich es danach dann doch wieder komplett umschmeißen muss :D Aber ich bin ja hier, um zu lernen :)
 
Wenn du einen c++17 fähigen compiler hast schau dir mal die an.

Ansonsten ist ein 1 byte buffer eher … unzureichend, vor allem unnötig:

cppreference about the read function schrieb:
<snip>
Characters are extracted and stored until any of the following conditions occurs:
  • count characters were extracted and stored
  • end of file condition occurs on the input sequence (in which case, setstate(failbit|eofbit) is called). The number of successfully extracted characters can be queried using gcount().
<snip>
Du kannst also einfach mit folgendem code prüfen wie viel gelesen wurde und exakt so viel schreiben, sobald du weniger als sizeof(buf) gelesen hast brichst du dann ab.
[src=cpp]
ifstream readData;
ofstream writeData;

char buf[1024];
std::streamsize bytes_read = sizeof(buf);

readData.read(&buf, sizeof(buf));
// std::streamsize ist der return wert von gcount, das ist einfach ein integer typ der garantiert groß genug ist.
std::streamsize bytes_read = readData.gcount();
//do fancy shit

[/src]
Den sonderfall abfangen.


Ansonten gibt es noch diese viel zu einfach Variante:
[src=cpp]writeData << readData.rdbuf();[/src]
Wobei das natürlich keinen status printed.

Außerdem möchte ich erwähnen, das es immer weider sonderfälle gibt die recht kompliziert werden können, sachen wie alternate data streams, Berechtigungen, datei Attribute und vermutlich noch ein par sachen die mir nicht einfallen.
Die sind mit libs wie der eingangs erwähnten filesystem lib typischerweise deutlich einfacher zu Behandeln.
 
  • Thread Starter Thread Starter
  • #3
Wenn du einen c++17 fähigen compiler hast schau dir mal die an.
Da bin ich mir nicht so ganz sicher :D Ich nutze Code::Blocks mit Mingw unter Windows. Es gibt zwar in der IDE jeweils einen Haken für C++11, C++14 und C++17, aber ich war mir stellenweise schon unsicher, ob das wirklich schon unterstützt wird. Hatte allerdings bei meiner eigenen Recherche auch schon was von der besagten lib gelesen, dann werde ich mir das wohl nochmal reintun.

Ansonsten ist ein 1 byte buffer eher … unzureichend, vor allem unnötig:
Jo, klar. Und eben vermutlich ja auch mies für die Performance.

Du kannst also einfach mit folgendem code prüfen wie viel gelesen wurde und exakt so viel schreiben, sobald du weniger als sizeof(buf) gelesen hast brichst du dann ab.
Danke! Das war letztlich genau das, was ich gesucht habe :T

Ansonten gibt es noch diese viel zu einfach Variante:
[src=cpp]writeData << readData.rdbuf();[/src]
Dazu müsste ich also nur die beiden Files öffnen und dann deine Zeile einbauen? Den Buffer setzt er dann selbst auf die passende Größe? Da gibts aber dann keine Möglichkeit, eine Fortschrittsanzeige einzubauen, wenn ich dich richtig verstanden hab?

Außerdem möchte ich erwähnen, das es immer weider sonderfälle gibt die recht kompliziert werden können, sachen wie alternate data streams, Berechtigungen, datei Attribute und vermutlich noch ein par sachen die mir nicht einfallen.
Die sind mit libs wie der eingangs erwähnten filesystem lib typischerweise deutlich einfacher zu Behandeln.
Jo, klar. Berechtigungen sind noch so eine Sache. Im Zweifelsfall müsste ich dann eben, falls nötig, das Programm manuell als Admin ausführen. Ich nutze das ja sowieso nur selber.
Was mir allerdings aufgefallen ist: Offenbar kann das Programm (zumindest, wenn ich es aus der IDE heraus starte) keine Dateien öffnen, deren Pfade "besondere Zeichen" (Umlaute, Vokale mit Akzenten, ...) enthalten :unknown: Muss aber nochmal testen, ob das vielleicht einfach an der Locale liegt, die die IDE vorgibt und aus der Eingabeaufforderung heraus funktioniert.

Danke jedenfalls schonmal für deine Hinweise!

[EDIT:]

So, mal fix alles eingebaut. Deine Tipps waren Gold wert :)

Mein Code sieht jetzt erstmal so aus:

[src=cpp]void copyFile(string strSourcePath, string strDestinationPath)
{

unsigned long long lngSourceFileSize = getFileSize(strSourcePath);
unsigned long long lngBytesRead = 0;
int intCurrPercentage = 0;
int intLastPercentage = 0;
int intRefreshRate = 1;

if (lngSourceFileSize < 10485760)
{
intRefreshRate = 20;
}
else if (lngSourceFileSize < 1048576000)
{
intRefreshRate = 10;
}
else if (lngSourceFileSize < 10485760000)
{
intRefreshRate = 1;
}
else
{
intRefreshRate = 0;
}

ifstream readData;
ofstream writeData;

readData.open(strSourcePath, ios_base::binary | ios_base::in);
writeData.open(strDestinationPath, ios_base::binary | ios_base::out);

cout << " Copying file:" << endl << " 0 % done";
char Buffer[1024];
streamsize bytes_read;

while (true)
{
if (readData.eof())
break;

readData.read(&Buffer[0], sizeof(Buffer));
bytes_read = readData.gcount();
writeData.write(&Buffer[0], bytes_read);

lngBytesRead += bytes_read;
intCurrPercentage = round(100.0 / lngSourceFileSize * lngBytesRead);

if (intCurrPercentage >= intLastPercentage + intRefreshRate)
{
cout << '\r';
cout << " " << intCurrPercentage << " % done";
intLastPercentage = intCurrPercentage;
}

}
cout << endl << " Done!" << endl;
readData.close();
writeData.close();

}[/src]

Die RefreshRate habe ich eingebaut, damit bei kleineren Dateien keine Performance durch zu häufige Aktualisierung der Forschrittsanzeige flöten geht. Funktioniert auch ganz gut, soweit ich das beurteilen kann.
Mit dem größeren Buffer jetzt geht es auch wesentlich schneller. Hatte ich mir ja schon gedacht :D Die eof()-Abfrage habe ich an den Anfang der Schleife gepackt, falls eine sehr kleine Datei schon nach einem Schleifendurchgang zuende ist. Ansonsten würde ja wieder irgendwelcher random RAM-Inhalt in die Datei geschrieben.

Prima! Dann fehlt jetzt eigentlich nur noch die Fehlerbehandlung! Danke :)
 
Zuletzt bearbeitet:
Hallo Kenobi van Gin,

ein paar kleine Anmerkungen zu deinen Code hätte ich auch:

1)
System Calls (insbesondere File IO) können Exceptions werfen, d.h. es ist ratsam um dein
[src=cpp]
readData.open(strSourcePath, ios_base::binary | ios_base::in);
writeData.open(strDestinationPath, ios_base::binary | ios_base::out);
[/src]
Exception Handling zu packen. Etwa so:
[src=cpp]
try { /* open ...*/ }
catch(...) { /* fängt alle ausnahmen */ }
[/src]

2)
Dein Code sieht leider noch sehr nach C und nicht nach (neuerem) C++ aus.
Neben dem vorgeschlagenen Iostream-Operatoren könntest du noch „unsigned long long“ zu „uint64_t“ umwandeln.

3)
Falls du weiterhin den Old School C Weg mit dem Buffer gehen willst könntest du die Performance Probleme auch damit lösen, dass du nicht bei jeder Iteration ein read/write machst sondern für „MAX_BUFF_SIZE“ guckst, ob genug Bytes in der Quelldatei vorliegen und wenn ja, kopierst du dann eben MAX_BUFF_SIZE Bytes in einem read/write (beim letzten Durchlauf eben weniger).
 
Zuletzt bearbeitet:
  • Thread Starter Thread Starter
  • #5
@FirleFranz: Danke für die Tipps :)
Try/catch "kann" ich bei C++ noch nicht. Aber vielleicht wäre es trotzdem gut, mich damit mal zu beschäftigen. (Bei VB hatte ich das seinerzeit schon benutzt.)

Dass es für die Datentypen inzwischen neue Bezeichnungen gibt, war mir nicht bewusst. Wie gesagt, ich lerne aus einem Buch (nicht mehr ganz aktuell, das ist zum C++11-Standard erschienen), aber sooo alt ja auch noch nicht :D Das behalte ich mal im Hinterkopf.

Das mit dem größeren Buffer mache ich ja jetzt auch schon. Würdest du einen noch größeren wählen? Soweit ich weiß wird doch aber bei C++ sowieso nicht bei "write()" sofort in die Datei geschrieben, sondern wiederum gepuffert, bis der Puffer voll ist oder die Datei geschlossen wird, oder? Hatte ich jedenfalls so verstanden.
 
Try/Catch sollte auch nur als Sicherheitsnetz verwendet werden, nicht als Ersatz für mögliche Validierung im Vorfeld, à la ON ERROR GO NEXT. ;)
 
  • Thread Starter Thread Starter
  • #7
@Asseon: Grad mal son bisschen durch die Referenz zur Filesystem lib gescrollt. Das liest sich alles sehr interessant! Danke für den Tipp. Da werde ich mich in den nächsten Tagen mal mehr mit beschäftigen :)
...und hoffen, dass meine IDE bzw. der Compiler das unterstützt :D
 
Dass es für die Datentypen inzwischen neue Bezeichnungen gibt, war mir nicht bewusst.
Die gibt es auch nicht.
Der unterschied ist, dass uint64_t eine klar definierte Länge hat "unsigned long long" dagegen nicht, letztere muss "nur" mindestens 64 bit groß sein.
In vielen fällen brauch man die Garantie exakter größe aber gar nicht, daher ist es vollkommen in Ordnung auch die typen, short, int, long int und long long int weiterhin zu verwenden.
 
  • Thread Starter Thread Starter
  • #9
Wollte mich nun die Tage mal näher mit der Filesystem-Library beschäftigen, aber mein Compiler schmeißt immer einen "directory not found"-Fehler. Sieht ja so aus, als würde der das noch nicht unterstützen?! Dabei ist das doch schon von 2017 :unknown:
 
Du brauchst eine Standard-Library, die std::filesystem unterstützt.

MinGW ist ja ein Windows-Port der GNU-Toolchain, also GCC (Compiler) und libstdc++ (Standardlib). std::filesystem ist für libstdc++ natürlich erstmal für die unix-basierten Plattformen implementiert worden. MinGW hatte trotz aktueller GCC-Version noch keine Unterstützung. Inzwischen scheint sich das geändert zu haben.

Ohne die exakte Fehlermeldung lässt sichs schwer 100% beurteilen, sieht mir aber so aus, als wäre dein MinGW zu alt. Updates gibts . I.d.R. willst du 64bit, POSIX-Threads und SEH-Exceptions. Das heißt im Moment .

Alternativ: boost::filesystem ist sehr ähnlich und brauch iirc nicht mal C++11.
 
  • Thread Starter Thread Starter
  • #11
Ja, hatte mich auch schon gewundert, dasss es für die gesamte IDE so selten Updates gibt. Aber wenn ich den Compiler manuell updaten muss, dann erklärt es das natürlich. Danke schonmal für den Hinweis!
 
Zurück
Oben