Menu

Obsługa plików w C: fopen, fclose i czytanie pliku linia po linii

Jak czytać i zapisywać pliki w C: tabela trybów fopen, sprawdzanie NULL, zapis przez fprintf i fputc, odczyt przez fgets, fscanf i fgetc, kanoniczna pętla linia po linii i kiedy używać trybu binarnego.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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.

TrybOdczytZapisJeśli plik istniejeJeśli nie istnieje
"r"taknieotwiera od początkuzawodzi, zwraca NULL
"w"nietakobcinany do zerazostaje utworzony
"a"nietakzapisuje tylko na końcuzostaje utworzony
"r+"taktakotwiera od początkuzawodzi, zwraca NULL
"w+"taktakobcinany do zerazostaje utworzony
"a+"taktakczyta w dowolnym miejscu, zapisuje na końcuzostaje 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żde fprintf trafia 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 NULL na 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 fopen pod kątem NULL. 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 z EOF. Używaj int.
  • 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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ