Odczyt i zapis plików binarnych w Cpp

Ghostek

Niniejszy artykuł ma za zadanie zapoznać czytelnika z arcytrudną (i, wg. niektórych, wręcz 'paranormalną', cokolwiek by to miało w tym kontekście znaczyć) sztuką odczytywania i zapisywania plików binarnych przy użyciu języka C++, dostępną dotąd jedynie dla wąskiej grupki największych ekspertów w owym języku.

1 Co to jest plik binarny
2 Do czego się ich używa
3 Jak się je odczytuje
     3.1 Przykład z życia wzięty - bitmapa
     3.2 Niestandardowe wielkości pól
     3.3 Zapis
4 Końcowe uwagi

Co to jest plik binarny

Najprostsza definicja pliku binarnego to: plik, który po otworzeniu w edytorze tekstu daje tzw. 'krzaki'. Ich zawartość to po prostu 'odbitka' surowych danych zapisanych w pamięci programu, który je utworzył, bez jakiegokolwiek przetwarzania na formę odczytywalną przez człowieka.
Na przykład liczba 42 przechowywana w zmiennej typu int zostanie zapisana jako ciąg bajtów 00 00 00 2A, podczas gdy w pliku tekstowym zostałaby przed zapisaniem przetworzona na znaki '4' i '2', co w formacie ASCII odpowiada bitom 34 i 32. Edytor tekstu otwierając pliki dokonuje operacji odwrotnej, zamieniając dane binarne na znaki, które im odpowiadają. Oczywistym jest, że w wypadku plików binarnych taka zamiana nie ma sensu, stąd biorą się 'krzaki' podczas próby odczytania ich w ten sposób - np. wspomniana liczba 42 została by zamieniona na ' *'. Po to powstały tzw. hexedytory - programy pokazujące zawartość pliku liczbowo, bez konwersji na znaki. Jak jednak odczytać plik binarny z poziomu własnego programu? O tym dowiesz się z tego artykułu.

Do czego się ich używa

Może się wydawać, że używanie plików to niepotrzebne utrudnianie sobie życie. Nie lepiej byłoby zapisywać wszytko jako tekst? Otóż okazuje się, że nie. Odczyt i zapis plików binarnych jest znacznie szybszy, niż tekstowych - mają one 'sztywną' strukturę, toteż komputer nie musi zastanawiać się nad tym, co oznaczają napotkane w pliku bajty. Dlatego używa się ich do przechowywania dużych ilości danych, które i tak nie muszą być edytowalne przez przeciętnego użytkownika.

Jak się je odczytuje

Jest to stosunkowo proste. W języku C++ klasa ifstream posiada metodę read pozwalająca na odczyt surowych danych zapisanych w pliku binarnym. Na przykład, gdy chcemy odczytać z początku pliku jedną zmienną typu int i jedną typu float:

#include <iostream>
#include <fstream>
 
using namespace std;
 
int main()
{
 ifstream ifs("plik.foo", ios::binary); // otwieramy plik do odczytu binarnego
 
 char* temp = new char[sizeof(int)]; // tymczasowy bufor na dane
 ifs.read(temp, sizeof(int)); // wczytujemy dane do bufora
 int* number1 = (int*)(temp); // rzutujemy zawartość bufora na typ int
 
 // powtarzamy procedurę dla floata:
 temp = new  char[sizeof(float)];
 ifs.read(temp, sizeof(float));
 float* number2 = (float*)(temp);
 
 cout << number1 <<  " " << number2 << endl;
 delete number1;
 delete number2;
}

Można ten proces usprawnić, pisząc strukturę opisującą ów plik:

#include <iostream>
#include <fstream>
 
using namespace std;
 
struct File
{
 int number1;
 float number2;
};
 
int main()
{
 ifstream ifs("plik.foo", ios::binary); // otwieramy plik do odczytu binarnego
 
 char* temp = new char[sizeof(File)]; // tymczasowy bufor na dane
 ifs.read(temp, sizeof(File)); // wczytujemy dane do bufora
 File* file = (File*)(temp); // rzutujemy zawartość bufora na typ File
 
 cout << file->number1 <<  " " << file->number2 << endl;
 delete file;
}

Jeśli wielkość struktury nie jest podzielna przez 4, kompilator doda do niej puste bajty dla ułatwienia adresowania. Nie ma to znaczenia dla normalnego użytkowania struktur, lecz w tym wypadku może ci uniemożliwić odczytanie pliku. By się tego pozbyć, dodaj '#pragma pack(push, 1)' przed i '#pragma pack(pop)' po definicji struktury. Będzie wtedy zajmować dokładnie tyle pamięci, co suma jej składowych.

Przykład z życia wzięty - bitmapa

Jeśli programujesz pod Windowsem, w wypadku bitmap można skorzystać z gotowych struktur zawartych w pliku nagłówkowym <windows.h>. Jeśli nie, skopiuj do programu następujące struktury:

#pragma pack(push, 1)
struct BITMAPFILEHEADER
{ 
  short bfType; 
  int bfSize; 
  short bfReserved1; 
  short bfReserved2; 
  int bfOffBits; 
};
 
struct BITMAPINFOHEADER
{
  int biSize; 
  int biWidth; 
  int biHeight; 
  short biPlanes; 
  short biBitCount; 
  int biCompression; 
  int biSizeImage; 
  int biXPelsPerMeter; 
  int biYPelsPerMeter; 
  int biClrUsed; 
  int biClrImportant; 
};
#pragma pack(pop)

Bitmapę odczytuje się w następujący sposób:

#include <iostream>
#include <fstream>
#include <windows.h> // zastąp przez powyższe definicje struktur jeśli nie posiadasz tego pliku
 
using namespace std;
 
#pragma pack(push, 1)
struct Pixel
{
 unsigned char b, g, r;
};
#pragma pack(pop)
 
int main()
{
 ifstream ifs("foo.bmp", ios::binary);
 
 char* temp = new char[sizeof(BITMAPFILEHEADER)];
 ifs.read(temp, sizeof(BITMAPFILEHEADER));
 BITMAPFILEHEADER* bfh = (BITMAPFILEHEADER*)(temp);
 
 temp = new char[sizeof(BITMAPINFOHEADER)];
 ifs.read(temp, sizeof(BITMAPINFOHEADER));
 BITMAPINFOHEADER* bih = (BITMAPINFOHEADER*)(temp);
 
 ifs.seekg(bfh->bfOffBits, ios::beg); // bfOffBits wskazuje początek danych obrazka
 
 int width = bih->biWidth;
 if(width % 4) width += 4 - (width % 4); // piksele w bitmapie są wyrównywane do 4 bajtów
 
 Pixel** pixs = new Pixel*[bih->biHeight];
 for(int i=0; i<bih->biHeight; i++)
 {
   temp = new char[3*width];
   ifs.read(temp, 3*width);
   pixs[i] = (Pixel*)(temp); // uwaga - nigdy nie czytaj z tej tablicy więcej niż bih->biWidth pixeli
  }
 
 /* robisz z bitmapą co tam chcesz */
 
 delete bfh;
 delete bih;
 for(int i=0; i<bih->biHeight; i++) delete[] pixs[i];
 delete[] pixs;
}

Niestandardowe wielkości pól

Jeśli plik zawiera pola o wielkości innej niż dostępne typy danych, można posłużyć się następującą konstrukcją:

struct File
{
 int foo : 4; // pole 4-bitowe
 int bar : 20;
 int baz : 3;
 int gazonk : 5;
};

Struktura taka będzie miała rozmiar jednego inta, czyli 4 bajty.

Zapis

Pliki binarne zapisuje się w sposób analogiczny do odczytywania, tyle że odwrotny. Dla przykładu:

#include <iostream>
#include <fstream>
 
using namespace std;
 
struct File
{
 int number1;
 float number2;
};
 
int main()
{
 ofstream ofs("plik.foo", ios::binary); // otwieramy plik do zapisu binarnego
 
 File* file = new File;
 cin >> file->number1 >> file->number2 ;
 ofs.write((char*)(file), sizeof(File)); // zapisujemy dane do pliku
 
 delete file;
}

Końcowe uwagi

Mam nadzieję, że artykuł okaże się pomocny. Domyślam się, że i tak ludzie będą dalej o to pytać na forum, ale przynajmniej będzie ich gdzie odsyłać ;)

8 komentarzy

A już sobie poradziłem np. tak:
#include <iostream>
#include <fstream>

using namespace std;

main()
{bool a=1;
ofstream ofs("plik.bin", ios_base::binary | ios_base::app );
for(int x=0;x<36;x++)
ofs.write((char*) &a, sizeof(a));

a=0; ofs.write((char*) &a, sizeof(a));
}

prosto i o to mi chodziło kodzik zapisuje boola do plik.bin w pątli for idzie 36 jedynek na końcu bool a =0 i też idzie to do pliku razem 37bitów
odczyt podobny tylko z ofs.read oczywiście i zamiast pojedyńczego boola można zrobić tabelkę i działa:
#include <iostream>
#include <fstream>

using namespace std;

int main()
{bool a[37];
ifstream ofs("plik.bin", ios_base::binary);
ofs.read((char*) &a, sizeof(a));
cout<<a[0]<<a[1]<<a[2]<<a[3]<<a[4]<<a[5]<<a[6]<<a[7]<<a[8]<<a[9]<<a[10]<<a[11]<<a[12]<<a[13]<<a[14]<<a[15];
cout<<a[16]<<a[17]<<a[18]<<a[19]<<a[20]<<a[21]<<a[22]<<a[23]<<a[24]<<a[25]<<a[26]<<a[27]<<a[28]<<a[29]<<a[30];
cout<<a[31]<<a[32]<<a[33]<<a[34]<<a[35]<<a[36];}

proste pozdrawiam

@Rebus
Spróbuj dodać
#pragma pack(push, 1)
i
#pragma pack(pop))
tak jak to jest w przykładzie. Może to jest właśnie problem z zaokrąglaniem do 4.

Kurczę no i mamy dubel.. http://4programmers.net/C/Artykuły/Odczyt_i_zapis_plików_binarnych_w_Cpp
Dobra usunąłem powyższy (ten jest aktualniejszy). To przekleję jeszcze swój komentarz :P

Oj, bedzie trzeba poprawić błąd z tymi plusami w Coyocie... Teraz nie mogę nawet przenieść czy edytować artykułu...
(zastąpienie + na %2B zdaje się na nic, bo przy zapisywaniu się sypie... click)

//edit: ok, udało się z wtyczką Developer Console do Opery (tudzież Webdeveloper dla FF)

--
Ghostek: w tych pierwszych kodach brakuje using namespace std i nie ios:binary a ios::binary - ktoś tu chyba pisał kody z palca ;)
Ponadto <windows.h> zapisuj jako <code><windows.h>


 (składnia Coyote) [ech, tu kolejny bug Coyote - nie widać zamknięcia znacznika plain - tak czy siak, ma to wyglądać <a href="http://pastebin.4programmers.net/3724">tak</a>]

a jak byście zapisali do pliku binarnego tablicę booli próbowałem zrobić na wzór arytkułu i wyszło coś takiego:
#include<fstream>
using namespace std;

main()
{
struct bool1
{bool a[10];};
bool1 adam={{1,0,1,0,1,0,0,1,0,1}};

ofstream bin.open("a",ios_base::binary);
bin.write((bool*)(adam), sizeof(bool1));

return 0;}
ale nie działa ;/

swoją drogą, można zapisać to prościej, bez tworzenia tempa:

BITMAPFILEHEADER* bfh = new BITMAPFILEHEADER;
ifs.read( (char*)& bfh, sizeof(BITMAPFILEHEADER));
 
BITMAPINFOHEADER* bih = new BITMAPINFOHEADER;
ifs.read( (char*)& bih, sizeof(BITMAPINFOHEADER));
// ...
   pixs[i] = new Pixel;
   ifs.read( (char*)pixs[i], 3*width);

We wszystkich napisanych wyżej programach można jeszcze na końcu zwolnić zasoby i zamknąć plik funkcją close().

Dzięki za poprawienie błędu. Artykuł i tak musiałem zedytować, ponieważ jak się okazało tutejsza składnia boczy się na zapis typu < [bez spacji] windows.h [bez spacji] >, więc dodałem ````

Świetne. Właśnie tego mi było trzeba. Dzięki :)
Miałeś drobny błąd przy rozmiarze tablicy temp, ale już poprawiłem.
//Dodane: Edytuj ten artykuł i zamień gdzieś tam jedną literkę czy coś, bo mi głupio, że po małej modyfikacji już widnieję jako autor...