C/Czytanie i pisanie do plików

< C

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.

Typy metody obsługi plików

edytuj

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

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.

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.

Identyfikator pliku

  • w niskopoziomowej metodzie : 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.
  • w wysokopoziomowej metodzie: wskaźnik na strukturę typu FILE

Niskopoziomowa obsługa plików

edytuj

Niskopoziomowa metoda obsługi plików = obsługa na poziomie deskryptora[1]

Znajdują się tu funkcje do wykonywania niskopoziomowych operacji wejścia/wyjścia na deskryptorach plików[2]

  • prymitywy dla funkcji we/wy wyższego poziomu opisanych w sekcji Wejście/wyjście w strumieniach
  • funkcje do wykonywania operacji sterowania niskiego poziomu, dla których nie ma odpowiedników w strumieniach

Nazwy funkcji są typu read(), open(), write() i close().

We/wy na poziomie strumienia jest bardziej elastyczne i zwykle wygodniejsze; dlatego programiści na ogół używają funkcji na poziomie deskryptora tylko wtedy, gdy jest to konieczne. Oto niektóre z typowych powodów:

  • do odczytu plików binarnych w dużych porcjach
  • do wczytywania całego pliku do rdzenia przed jego analizowaniem.
  • do wykonywania operacji innych niż przesyłanie danych, które można wykonać tylko za pomocą deskryptora. (Możesz użyć fileno, aby uzyskać deskryptor odpowiadający strumieniowi.)
  • Aby przekazać deskryptory do procesu potomnego. (Proces potomny może utworzyć własny strumień, aby używać deskryptora, który dziedziczy, ale nie może bezpośrednio dziedziczyć strumienia)

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.

Wysokopoziomowa obsługa plików

edytuj

Nazwy funkcji z tej 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.


Nazwa pliku

edytuj

Możemy tworzyć unikalne nazwy plików programowo:[3]

for(i = 0; i < 100; i++) {
    char filename[sizeof "file100.txt"];

    sprintf(filename, "file%03d.txt", i);
    fp = fopen(filename,"w");
}

Formatowanie %03d dopełnia ciąg do 3 cyfr z początkowymi zerami. Dzieki temu pliki będą poprawnie posortowane.

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.

  • 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 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ą to 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 standardowe strumienie ( ang. Standard Streams )[4]:

  • stdin (wejście) = odczytywanie danych wpisywanych przez użytkownika
  • stdout (wyjście) = wyprowadzania informacji dla użytkownika
  • stderr (wyjście błędów) = powiadamiania o błędach

Warunki korzystania ze standardowych strumieni :

  • dołączyć plik nagłówkowy stdio.h
  • nie musimy otwierać ani zamykać strumieni standardowych ( tak jak w przypadku niestandardowych plików : fopen i fclose )



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

Inny sposób :[5]

#include<stdio.h>
#include <errno.h>

int main()
{
errno = 0;
FILE *fb = fopen("/home/jeegar/filename","r");
if(fb==NULL)
    printf("its null");
else
    printf("working");


printf("Error %d \n", errno);


}

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" */
  
 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

Plik graficzny tworzymy :

  • bezpośrednio w C ( fprintf / fwrite )
  • pośrednio za pomocą strumieni i potoku, wtedy :
    • zamiast komend zapisu do pliku ( np. fprintf ) używamy komend wysyłających do standardowego wyjścia ( np. fprint, putchar)[6]
    • zamiast przykładowej komendy : ./a.out używamy : ./a.out > anti.ppm [7]


rastrowy

edytuj
dostęp sekwencyjny
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:[8]

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

dostęp swobodny
edytuj

Jeśli chcemy mieć swobodny dostęp do danych to :

  • korzystać z funkcji: fsetpos, fgetpos oraz fseek,
  • utworzyć w pamięci tablicę
  • zapisać dane do tablicy
  • przetwarzać tablicę
  • zapisać całą tablicę na dysk w postaci pliku graficznego


Tablica może być:

  • statyczna lub dynamiczna (dla dużych plików dynamiczną)
  • jedno lub wielowymiarowa. Zależy to od
    • koloru : 8-bitowy, 24-bitowy, 32-bitowy, ... )
    • metody tworzenia, usuwanie i dostępu do tablicy


Dostęp ten pozwala na :

  • przetwarzanie danych/obrazów cyfrowych ( ang. digital image processing = DIP), jak : operacje morfologiczne ( ang. Mathematical morphology = MM)
  • przetwarzanie równoległe ( OpenMP, OpenACC, GPU )
  • szybszy ( w pamięci ) dostęp do danych

wektorowy

edytuj

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

/*  

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;
 char *filename="circle.svg";
 char *comment = "<!-- sample comment in SVG file  \n can be multi-line -->";




void draw_circle(FILE * FileP,int radius,int cx,int cy)
{
    fprintf(FileP,"<circle cx=\"%d\" cy=\"%d\" r=\"%d\" style=\"stroke:%s; stroke-width:2; fill:%s\"/>\n",
    cx,cy,radius,white,black);
}


int main(){
    
        // setup
        fp = fopen(filename,"w");
	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 %d %d \"\n"
           " xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\">\n",
           comment,iXmax,iYmax);

        // draw
	draw_circle(fp,radius,cx,cy);
	

	
    
        // end 
        fprintf(fp,"</svg>\n");
	fclose(fp);
	printf(" file %s saved \n",filename ); 
	
	return 0;
}

Przykłady

edytuj

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