C/Czytanie i pisanie do plików

Z Wikibooks, biblioteki wolnych podręczników.
< C
Skocz do: nawigacji, wyszukiwania

Pojęcie pliku[edytuj]

Na początku dobrze by było, abyś dowiedział się, czym jest plik. Odpowiedni artykuł dostępny jest w Wikipedii. Najprościej mówiąc, plik to pewne dane zapisane na dysku.

Identyfikacja pliku[edytuj]

Każdy z nas, korzystając na co dzień z komputera przyzwyczaił się do tego, że plik ma określoną nazwę. Jednak, w pisaniu programu, posługiwanie się całą nazwą niosłoby ze sobą co najmniej dwa problemy:

  • duże zużycie pamięci - przechowywanie całej nazwy pliku zajmuje niepotrzebnie pamięć,
  • ryzyko błędów (zostały szerzej omówione w rozdziale Napisy).

Programiści korzystają z identyfikatora pliku, który jest pojedynczą liczbą całkowitą. Dzięki temu kod programu jest czytelniejszy i nie trzeba korzystać ciągle z pełnej nazwy pliku. Jednak sam plik nadal jest identyfikowany po swojej nazwie. Aby "przetworzyć" nazwę pliku na odpowiednią liczbę korzystamy z funkcji open lub fopen. Różnica wyjaśniona została poniżej.

Podstawowa obsługa plików[edytuj]

Istnieją dwie metody obsługi czytania i pisania do plików:

  • wysokopoziomowa,
  • niskopoziomowa. [1]

Nazwy funkcji z pierwszej grupy zaczynają się od litery "f" (np. fopen(), fread(), fclose()), a identyfikatorem pliku jest wskaźnik na strukturę typu FILE. Owa struktura to pewna grupa zmiennych, która przechowuje dane o pliku - jak na przykład aktualną pozycję w nim. Szczegółami nie musisz się przejmować, funkcje biblioteki standardowej same zajmują się wykorzystaniem struktury FILE. Programista może więc zapomnieć, czym tak naprawdę jest struktura FILE i traktować taką zmienną jako "uchwyt", identyfikator pliku.

Druga grupa to funkcje typu read(), open(), write() i close().

Podstawowym identyfikatorem pliku jest liczba całkowita, która jednoznacznie identyfikuje dany plik w systemie operacyjnym. Liczba ta w systemach typu UNIX jest nazywana deskryptorem pliku.

Należy pamiętać, że nie wolno nam używać funkcji z obu tych grup jednocześnie w stosunku do jednego, otwartego pliku, tzn. nie można najpierw otworzyć pliku za pomocą fopen(), a następnie odczytywać danych z tego samego pliku za pomocą read().

Czym różnią się oba podejścia do obsługi plików? Otóż metoda wysokopoziomowa ma swój własny bufor, w którym znajdują się dane po odczytaniu z dysku a przed wysłaniem ich do programu użytkownika. W przypadku funkcji niskopoziomowych dane kopiowane są bezpośrednio z pliku do pamięci programu. W praktyce używanie funkcji wysokopoziomowych jest prostsze a przy czytaniu danych małymi porcjami również często szybsze i właśnie ten model zostanie tutaj zaprezentowany.

Dane znakowe[edytuj]

Skupimy się teraz na najprostszym z możliwych zagadnień - zapisie i odczycie pojedynczych znaków oraz całych łańcuchów.

Napiszmy zatem nasz pierwszy program, który stworzy plik "test.txt" i umieści w nim tekst "Hello world":

 #include <stdio.h>
 #include <stdlib.h>
 
 int main ()
 {
   FILE *fp; /* używamy metody wysokopoziomowej - musimy mieć zatem identyfikator pliku, uwaga na gwiazdkę! */
   char tekst[] = "Hello world";
   if ((fp=fopen("test.txt", "w"))==NULL) {
     printf ("Nie mogę otworzyć pliku test.txt do zapisu!\n");
     exit(1);
     }
   fprintf (fp, "%s", tekst); /* zapisz nasz łańcuch w pliku */
   fclose (fp); /* zamknij plik */
   return 0;
 }

Teraz omówimy najważniejsze elementy programu. Jak już było wspomniane wyżej, do identyfikacji pliku używa się wskaźnika na strukturę FILE (czyli FILE *). Funkcja fopen zwraca ów wskaźnik w przypadku poprawnego otwarcia pliku, bądź też NULL, gdy plik nie może zostać otwarty. Pierwszy argument funkcji to nazwa pliku, natomiast drugi to 'tryb dostępu - w oznacza "write" (pisanie). Zwrócony "uchwyt" do pliku będzie mógł być wykorzystany jedynie w funkcjach zapisujących dane. I odwrotnie, gdy otworzymy plik podając tryb r ("read", czytanie), będzie można z niego jedynie czytać dane. Funkcja fopen została dokładniej opisana w odpowiedniej części rozdziału o bibliotece standardowej. Jak zatem uprościć nazwę typu FILE*? Używając typedef:

typedef FILE* plik;
plik fp;

Po zakończeniu korzystania z pliku należy plik zamknąć. Robi się to za pomocą funkcji fclose. Jeśli zapomnimy o zamknięciu pliku, wszystkie dokonane w nim zmiany zostaną utracone!

Pliki a strumienie[edytuj]

Można zauważyć, że do zapisu do pliku używamy funkcji fprintf, która wygląda bardzo podobnie do printf - jedyną różnicą jest to, że w fprintf musimy jako pierwszy argument podać identyfikator pliku. Nie jest to przypadek - obie funkcje tak naprawdę robią tak samo. Używana do wczytywania danych z klawiatury funkcja scanf też ma swój odpowiednik wśród funkcji operujących na plikach - jak nietrudno zgadnąć, nosi ona nazwę fscanf.

W rzeczywistości język C traktuje tak samo klawiaturę i plik - są to źródła danych, podobnie jak ekran i plik, do których można dane kierować. Jest to myślenie typowe dla systemów typu UNIX, jednak dla użytkowników przyzwyczajonych do systemu Windows albo języków typu Pascal może być to co najmniej dziwne. Nie da się ukryć, że między klawiaturą i plikiem na dysku zachodzą podstawowe różnice i dostęp do nich odbywa się inaczej - jednak funkcje języka C pozwalają nam o tym zapomnieć i same zajmują się szczegółami technicznymi. Z punktu widzenia programisty, urządzenia te sprowadzają się do nadanego im identyfikatora. Uogólnione pliki nazywa się w C strumieniami.

Każdy program w momencie uruchomienia "otrzymuje" od razu trzy otwarte strumienie:

  • stdin (wejście)
  • stdout (wyjście)
  • stderr (wyjście błędów)

(aby z nich korzystać należy dołączyć plik nagłówkowy stdio.h)

Pierwszy z tych plików umożliwia odczytywanie danych wpisywanych przez użytkownika, natomiast pozostałe dwa służą do wyprowadzania informacji dla użytkownika oraz powiadamiania o błędach.


Warto tutaj zauważyć, że konstrukcja:

fprintf (stdout, "Hej, ja działam!");

jest równoważna konstrukcji:

printf ("Hej, ja działam!");

Podobnie jest z funkcją scanf().

fscanf (stdin, "%d", &zmienna);

działa tak samo jak:

scanf("%d", &zmienna);

Obsługa błędów[edytuj]

Jeśli nastąpił błąd, możemy się dowiedzieć o jego przyczynie na podstawie zmiennej errno zadeklarowanej w pliku nagłówkowym errno.h. Możliwe jest też wydrukowanie komunikatu o błędzie za pomocą funkcji perror. Na przykład używając:

fp = fopen ("tego pliku nie ma", "r");
if( fp == NULL )
  {
  perror("błąd otwarcia pliku");
  exit(-10);
  }

dostaniemy komunikat:

błąd otwarcia pliku: No such file or directory

Zaawansowane operacje[edytuj]

Pora na kolejny, tym razem bardziej złożony przykład. Oto krótki program, który swoje wejście zapisuje do pliku o nazwie podanej w linii poleceń:

#include <stdio.h>
#include <stdlib.h>
/* program udający bardzo prymitywną wersję programu tee(1) */
 
int main (int argc, char *argv[])
{
   FILE *fp;
   int c;
   if (argc < 2) {
      fprintf (stderr, "Uzycie: %s nazwa_pliku\n", argv[0]);
      exit (-1);
   }
   fp = fopen (argv[1], "w");
   if (!fp) {
      fprintf (stderr, "Nie moge otworzyc pliku %s\n", argv[1]);
      exit (-1);
   }
   printf("Wcisnij Ctrl+D+Enter lub Ctrl+Z+Enter aby zakonczyc\n");
   while ( (c = fgetc(stdin)) != EOF) {
      fputc (c, stdout);
      fputc (c, fp);
   }
   fclose(fp);
   return 0;
}

Tym razem skorzystaliśmy już z dużo większego repertuaru funkcji. Między innymi można zauważyć tutaj funkcję fputc(), która umieszcza pojedynczy znak w pliku. Ponadto w wyżej zaprezentowanym programie została użyta stała EOF, która reprezentuje koniec pliku (ang. End Of File). Powyższy program otwiera plik, którego nazwa przekazywana jest jako pierwszy argument programu, a następnie kopiuje dane z wejścia programu (stdin) na wyjście (stdout) oraz do utworzonego pliku (identyfikowanego za pomocą fp). Program robi to tak długo, aż naciśniemy kombinację klawiszy Ctrl+D (w systemach Unixowych) lub Ctrl+Z(w Windows), która wyśle do programu informację, że skończyliśmy wpisywać dane. Program wyjdzie wtedy z pętli i zamknie utworzony plik.

Rozmiar pliku[edytuj]

Dzięki standardowym funkcjom języka C możemy m.in. określić długość pliku. Do tego celu służą funkcje fsetpos, fgetpos oraz fseek. Ponieważ przy każdym odczycie/zapisie z/do pliku wskaźnik niejako "przesuwa" się o liczbę przeczytanych/zapisanych bajtów. Możemy jednak ustawić wskaźnik w dowolnie wybranym miejscu. Do tego właśnie służą wyżej wymienione funkcje. Aby odczytać rozmiar pliku powinniśmy ustawić nasz wskaźnik na koniec pliku, po czym odczytać ile bajtów od początku pliku się znajdujemy. Użyjemy do tego tylko dwóch funkcji: fseek oraz fgetpos. Pierwsza służy do ustawiania wskaźnika na odpowiedniej pozycji w pliku, a druga do odczytywania na którym bajcie pliku znajduje się wskaźnik. Kod, który określa rozmiar pliku znajduje się tutaj:

#include <stdio.h>

int main (int argc, char **argv)
{
  FILE *fp = NULL;
  fpos_t dlugosc;
  if (argc != 2) {
    printf ("Użycie: %s <nazwa pliku>\n", argv[0]);
    return 1;
    }
  if ((fp=fopen(argv[1], "rb"))==NULL) {
    printf ("Błąd otwarcia pliku: %s!\n", argv[1]);
    return 1;
    }
  fseek (fp, 0, SEEK_END); /* ustawiamy wskaźnik na koniec pliku */
  fgetpos (fp, &dlugosc);
  printf ("Rozmiar pliku: %d\n", dlugosc);
  fclose (fp);
  return 0;
}

Znajomość rozmiaru pliku przydaje się w wielu różnych sytuacjach, więc dobrze przeanalizuj przykład!

Przykład - pliki graficzne[edytuj]

Przykład użycia tej techniki, sekwencyjny dostęp do danych (kod źródłowy)
Przykład użycia tej techniki, swobodny dostęp do danych ( kod źródłowy)

Najprostszym przykładem rastrowego pliku graficznego jest plik PPM. Poniższy program pokazuje jak utworzyć plik w katalogu roboczym programu. Do zapisu:[2]

  • nagłówka pliku używana jest funkcja fprintf, która zapisuje do plików binarnych lub tekstowych
  • tablicy do pliku używana jest funkcja fwrite, która zapisuje do plików binarnych,
 #include <stdio.h>
 int main() {
        const int dimx = 800; 
        const int dimy = 800;
        int i, j;
        FILE * fp = fopen("first.ppm", "wb"); /* b - tryb binarny */
        fprintf(fp, "P6\n%d %d\n255\n", dimx, dimy);
        for(j=0; j<dimy; ++j){
          for(i=0; i<dimx; ++i){         
                        static unsigned char color[3];
                        color[0]=i % 255; /* red */
                        color[1]=j % 255; /* green */
                        color[2]=(i*j) % 255; /* blue */
                        fwrite(color,1,3,fp);
                }
        }
        fclose(fp);
        return 0;
 }

W powyższym przykładzie dostęp do danych jest sekwencyjny. Jeśli chcemy mieć swobodny dostęp do danych to :

  • korzystać z funkcji: fsetpos, fgetpos oraz fseek,
  • utworzyć tablicę (dla dużych plików dynamiczną), zapisać do niej wszystkie dane a następnie zapisać całą tablicę do pliku. Ten sposób jest prostszy i szybszy. Należy zwrócić uwagę, że do obliczania rozmiaru całej tablicy nie możemy użyć funkcji sizeof.

Zamiast bezpośredniego tworzenia pliku możemy wykorzystać również potok, wtedy :

  • zamiast komend zapisu do pliku ( np. fprintf ) używamy komend wysyłających do standardowego wyjścia ( np. fprint, putchar)[3]
  • zamiast przykładowej komendy : ./a.out używamy : ./a.out > anti.ppm [4]

Bardzo łatwo również utworzyć plik SVG[5]

/*  
 
c console program based on :
cpp code by Claudio Rocchini
 
http://commons.wikimedia.org/wiki/File:Poincare_halfplane_eptagonal_hb.svg
 
 
http://validator.w3.org/ 
The uploaded document "circle.svg" was successfully checked as SVG 1.1. 
This means that the resource in question identified itself as "SVG 1.1" 
and that we successfully performed a formal validation using an SGML, HTML5 and/or XML 
Parser(s) (depending on the markup language used). 
 
*/
 
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
 
 
 
const double PI = 3.1415926535897932384626433832795;
 
const int  iXmax = 1000,
           iYmax = 1000,
           radius=100,
           cx=200,
           cy=200;
const char *black="#FFFFFF", /* hexadecimal number as a string for svg color*/
           *white="#000000";
 
 FILE * fp;
 
void draw_circle(FILE * FileP,int radius,int cx,int cy)
{
    fprintf(FileP,"<circle cx=\"%f\" cy=\"%f\" r=\"%f\" style=\"stroke:%s; stroke-width:2; fill:%s\"/>\n",
    cx,cy,radius,white,black);
}
 
 
int main(){
    FILE * fp;
    char *filename="circle.svg";
    fp = fopen(filename,"w");
	char *comment = "<!-- sample comment in SVG file  \n can be multi-line -->";
 
	fprintf(fp,
		    "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
		    "%s \n "
           "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \n"
           "\"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n"
           "<svg width=\"20cm\" height=\"20cm\" viewBox=\"0 0 %f %f \"\n"
           " xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\">\n",
           comment,iXmax,iYmax);
 
 
	draw_circle(fp,radius,cx,cy);
 
 
 
 
 
    fprintf(fp,"</svg>\n");
	fclose(fp);
	printf(" file %s saved \n",filename ); 
	getchar();
	return 0;
}

Co z katalogami?[edytuj]

Faktycznie, zapomnieliśmy o nich. Jednak wynika to z tego, że specyfikacja ANSI C nie uwzględnia obsługi katalogów. Dlatego też aby dowiedzieć się więcej o obsłudze katalogów w języku C zapraszamy do podręcznika o programowaniu w systemie UNIX.

Przypisy[edytuj]

  1. Jakub Narębski :Zapis wyników do pliku w C, C++ i Javie
  2. is a collection of C routines for creating and reading Portable Bit Map files (PBM).
  3. Przykład programu wykorzystującego potok
  4. Ultimate Anti-Buddhabrot by Claude Heiland-Allen
  5. Tworzenie pliku SVG - Claudio Rocchini