Programowanie w języku C/C++ » Artykuły

Odczyt i zapis plików binarnych w Cpp

  • 2010-02-14 13:50
  • 8 komentarzy
  • 31499 odsłon
  • Oceń ten tekst jako pierwszy
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.

Spis treści

     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

Rebus 2012-03-19 01:58

A już sobie poradziłem np. tak:

  1. include <iostream>
  2. 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:
  1. include <iostream>
  2. 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

marcin412 2012-03-17 01:57

@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.

Rebus 2012-02-27 22:35

a jak byście zapisali do pliku binarnego tablicę booli próbowałem zrobić na wzór arytkułu i wyszło coś takiego:

  1. 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 ;/

Coldpeer 2008-12-23 16:24

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);

christow 2008-08-08 09:19

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

Coldpeer 2008-03-16 20:57

Kurczę no i mamy dubel.. 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... <url=http://4p[...]/text.php?mode=move&subject=Odczyt_i_zapis_plik%C3%B3w_binarnych_w_C%2B%2B>click</url>)

//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 <plain><windows.h></plain> (składnia Coyote) [ech, tu kolejny bug Coyote - nie widać zamknięcia znacznika plain - tak czy siak, ma to wyglądać <url=http://pastebin.4programmers.net/3724>tak</url>]

Ghostek 2008-03-16 16:27

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 <plain></plain>

heux 2008-03-16 09:30

Ś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...