C/Czytanie i pisanie do plików

Z Wikibooks, biblioteki wolnych podręczników.

< C

Spis treści

[edytuj] Pojęcie pliku

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.

[edytuj] Identyfikacja pliku

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ło by ze sobą co najmniej dwa problemy:

  • pamięciożerność - przechowywanie całego (czasami nawet 255-bajtowego łańcucha) zajmuje niepotrzebnie pamięć
  • ryzyko błędów (owe błędy szerzej omówione zostały w rozdziale Napisy)

Aby uprościć korzystanie z plików programiści wpadli na pomysł, aby identyfikatorem pliku stała się liczba. Dzięki temu kod programu stał się czytelniejszy oraz wyeliminowano konieczność ciągłego korzystania z łańcuchów. 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 jest poniżej.

[edytuj] Podstawowa obsługa plików

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

  • wysokopoziomowa,
  • niskopoziomowa.

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

[edytuj] Dane znakowe

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.

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!

[edytuj] Pliki a strumienie

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

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

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łedzie 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

[edytuj] Zaawansowane operacje

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 dotąd, 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.

[edytuj] Rozmiar pliku

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. Wiem, brzmi to strasznie, ale działa wyjątkowo prosto i skutecznie. 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!


[edytuj] Przykład - pliki graficzny

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 :

  • nagłówka pliku używana jest funkcja fprintf,
  • tablicy do pliku używana jest funkcja fwrite.
 #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.

[edytuj] Co z katalogami?

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.

Utwórz książkę
W innych językach