Wszystko, co program obliczy, znika, gdy się kończy, chyba że to zapisze. Interfejs plików w C mieszka w <stdio.h>, tym samym nagłówku co printf, i celowo jest do niego podobny: plik to strumień bajtów, a funkcje działające na ekranie mają bliźniaczki, które przyjmują strumień jako argument.
Typ uchwytu to FILE *. Nigdy nie zaglądasz do wnętrza FILE; to typ nieprzezroczysty (zobacz typedef) i zawsze trzymasz tylko wskaźnik do niego.
Trzy kroki
Każda operacja na pliku ma ten sam kształt: otwórz, użyj, zamknij.
Sprawdzenie NULL to nie opcjonalna paranoja. fopen zawodzi, gdy plik nie istnieje (w trybie odczytu), katalog nie jest zapisywalny, ścieżka jest błędna albo procesowi skończyły się uchwyty plików, a każdy z tych przypadków zwraca NULL. Użycie uchwytu NULL to segmentation fault.
Wysyłanie błędu na stderr zamiast na stdout to konwencja: ten strumień nie jest buforowany i można go przekierować niezależnie od właściwego wyjścia.
Tryby fopen
Drugi argument to krótki string z trybem. Pomyłka w nim to najbardziej destrukcyjny błąd w całym tym obszarze, bo "w" po cichu opróżnia istniejący plik.
| Tryb | Odczyt | Zapis | Jeśli plik istnieje | Jeśli nie istnieje |
|---|---|---|---|---|
"r" | tak | nie | otwiera od początku | zawodzi, zwraca NULL |
"w" | nie | tak | obcinany do zera | zostaje utworzony |
"a" | nie | tak | zapisuje tylko na końcu | zostaje utworzony |
"r+" | tak | tak | otwiera od początku | zawodzi, zwraca NULL |
"w+" | tak | tak | obcinany do zera | zostaje utworzony |
"a+" | tak | tak | czyta w dowolnym miejscu, zapisuje na końcu | zostaje utworzony |
Dopisz b do dowolnego z nich ("rb", "wb", "ab+"), żeby uzyskać tryb binarny; więcej o nim na końcu.
Dwie zasady, które chronią przed prawdziwą utratą danych:
- Używaj
"r", gdy chcesz czytać."r+"z literówką w nazwie pliku bezpiecznie zawodzi;"w+"tworzy pusty plik i tracisz tylko czas."w"z poprawną nazwą pliku, gdy chodziło ci o"r", niszczy dane. - Używaj
"a"do logów. Każdefprintftrafia na koniec, niezależnie od bieżącej pozycji w strumieniu, a właśnie tego chce log.
Zapis i odczyt w jednym programie
Edytor poniżej wykonuje pełną rundę: tworzy plik, zapisuje do niego rekordy, zamyka go, otwiera ponownie do odczytu i wypisuje, co znalazł.
fprintf i fscanf to printf i scanf ze strumieniem jako pierwszym argumentem; wszystko, co dotyczy ich specyfikatorów formatu, jest identyczne, łącznie z szerokością %31s, która chroni name przed przepełnieniem.
Warunek pętli to == 2, czyli liczba elementów, o które prosi format. Sprawdzanie zamiast tego EOF to klasyczny błąd: niepoprawnie sformatowana linia sprawia, że fscanf zwraca 0, a nie EOF, i pętla kręci się w nieskończoność na wejściu, którego nie potrafi zużyć.
Czytanie linia po linii przez fgets
fscanf jest wygodny dla danych o sztywnym formacie. Pliki tekstowe (konfigurację, logi, CSV, wszystko, co napisał człowiek) czytaj całymi liniami. Oto kanoniczna pętla:
Co sprawia, że fgets jest właściwym domyślnym wyborem:
- Przyjmuje rozmiar bufora, więc nie może go przepełnić. Przekaż
sizeof line, a wywołanie pozostanie poprawne, gdy zmienisz rozmiar tablicy. - Zwraca
NULLna końcu pliku albo przy błędzie, co daje czysty warunek pętli. - Zachowuje znak nowej linii, gdy linia zmieściła się w buforze. To przydatne: brak
'\n'w otrzymanym tekście oznacza, że linia była dłuższa niż bufor, a reszta wciąż czeka.strcspn(line, "\n")znajduje indeks znaku nowej linii (albo długość stringa, jeśli go nie ma), więc przypisanie tam'\0'obcina go w obu przypadkach.
Żeby odróżnić prawdziwy koniec pliku od błędu, zapytaj po pętli:
if (ferror(in)) {
fprintf(stderr, "blad odczytu\n");
} else if (feof(in)) {
/* normalny koniec */
}
Nie pisz while (!feof(fp)) jako warunku pętli. feof staje się prawdą dopiero po tym, jak odczyt już się nie powiódł, więc taka pętla przetwarza ostatnią zawartość bufora o jeden raz za dużo. Zamiast tego sprawdzaj wartość zwracaną przez funkcję odczytu, tak jak robią to obie pętle powyżej.
Znak po znaku: fgetc i fputc
Do pracy na poziomie bajtów (liczenie znaków, przekształcanie pliku, kopiowanie) służą fgetc i fputc, które obsługują jeden znak na wywołanie.
Jeden ważny szczegół: c jest zadeklarowane jako int, a nie char. fgetc zwraca int, żeby móc zwrócić każdą możliwą wartość bajtu oraz odrębnego wartownika EOF (czyli -1). Zapisanie wyniku w char sprawia, że porównanie z EOF jest albo zawsze fałszywe, albo fałszywie prawdziwe dla bajtu 0xFF, zależnie od tego, czy char na twojej platformie ma znak. To jedna z najstarszych pułapek C.
Porządne sprawdzanie błędów
Odczyt w kodzie produkcyjnym wygląda tak:
errno przechowuje kod opisujący ostatni błąd, a strerror zamienia go w zdanie; perror wypisuje twój komunikat razem z tym zdaniem w jednym wywołaniu. Wymagają odpowiednio <errno.h> i <string.h>.
fclose też może zawieść (opróżnia zbuforowane dane, a zapis może nie zmieścić się na dysku), więc przy wszystkim, co ważne, sprawdzaj wynik:
if (fclose(fp) != 0) {
fprintf(stderr, "nie udalo sie oproznic bufora i zamknac pliku\n");
}
Tryb binarny w jednym akapicie
Tryb tekstowy może tłumaczyć znaki końca linii (w Windows \n przy zapisie staje się \r\n, a przy odczycie wraca do \n) i może specjalnie traktować niektóre bajty. Dla danych, które nie są tekstem (obraz, struktura zrzucona bajt w bajt, skompresowany blob), otwieraj plik z b i używaj fread/fwrite, które przenoszą surowe bajty:
fwrite(ptr, size, count, fp) zapisuje count elementów po size bajtów i zwraca, ile udało się zapisać; fread działa lustrzanie. Pamiętaj, że plik zapisany w ten sposób jest związany z maszyną, która go zapisała (wyrównanie struktur, rozmiar liczb całkowitych i kolejność bajtów przenikają do bajtów pliku), więc nadaje się na cache albo plik roboczy, ale nie na format, który mają czytać inne programy.
Częste błędy
- Niesprawdzanie
fopenpod kątemNULL. Awarię, która następuje, zrzuca się na odczyt, a nie na otwarcie. - Otwarcie z
"w", gdy chodziło o"r". Plik zostaje opróżniony, zanim to zauważysz. - Zapomnienie o
fclose. Zbuforowane wyjście przepada, a każdy niezamknięty plik to wyciek uchwytu. while (!feof(fp)). Przetwarza ostatnią linię dwa razy. Zamiast tego sprawdzaj wywołanie odczytu.char c = fgetc(fp). Psuje porównanie zEOF. Używajint.fscanf("%s", buf)bez szerokości. To samo przepełnienie bufora co przy scanf z klawiatury.- Ścieżki względne.
fopen("data.txt", "r")szuka w katalogu roboczym, a nie obok pliku wykonywalnego. Jeśli plik "znika", zwykle właśnie dlatego.
Najczęściej zadawane pytania
Jak otworzyć plik w C?
FILE *fp = fopen("data.txt", "r"); otwiera plik do odczytu i zwraca uchwyt FILE * albo NULL, jeśli się nie udało. Zawsze sprawdzaj NULL przed użyciem uchwytu i wywołaj fclose(fp), gdy skończysz.
Jakie są tryby fopen w C?
"r" odczyt (plik musi istnieć), "w" zapis (tworzy plik albo obcina istniejący do zera), "a" dopisywanie (tworzy plik, zapisuje na końcu). Dodanie + sprawia, że każdy z nich pozwala czytać i zapisywać: "r+", "w+", "a+". Dodanie b ("rb", "wb") otwiera plik w trybie binarnym.
Jak czytać plik linia po linii w C?
Użyj fgets w pętli while: while (fgets(line, sizeof line, fp) != NULL) { ... }. Zatrzymuje się na każdym znaku nowej linii albo gdy bufor się zapełni, na końcu pliku zwraca NULL i nie może przepełnić bufora, bo przekazujesz jego rozmiar.
Dlaczego plik jest pusty po zapisie w C?
Najczęściej brakuje wywołania fclose. Wyjście jest buforowane, więc dane mogą wciąż siedzieć w pamięci, gdy program kończy się nieprawidłowo. fclose opróżnia bufor i zamyka plik; fflush(fp) opróżnia bez zamykania. Druga przyczyna to ponowne otwarcie w trybie "w", które obcina dopiero co zapisany plik.