Praca z plikami w C i C++

Plik przechowuje dane poza pamięcią programu, więc można z nich skorzystać także po jego ponownym uruchomieniu. Niezależnie od języka, praca z plikiem składa się z czterech kroków: wybierz tryb, otwórz plik, czytaj lub zapisuj, zamknij. Każdy krok może się nie powieść, dlatego sprawdzamy jego wynik.

Ścieżka dane.txt jest liczona od bieżącego katalogu roboczego programu, a niekoniecznie od katalogu, w którym leży kod źródłowy.

Najpierw wybierz tryb

Tryb określa, czy plik musi istnieć, czy dotychczasowa zawartość zostanie usunięta i gdzie będą trafiały zapisy. Wybór w lub domyślnego zapisu przez ofstream może skasować wcześniejszą zawartość pliku.

Zamiar Tryb C dla fopen Typowy zapis w C++
Odczyt istniejącego pliku "r" std::ifstream p("dane.txt");
Zapis od początku; utworzenie lub wyczyszczenie "w" std::ofstream p("dane.txt");
Dopisywanie na końcu; utworzenie, jeśli brak "a" std::ofstream p("dane.txt", std::ios::out \| std::ios::app);
Odczyt i zapis istniejącego pliku "r+" std::fstream z in | out
Odczyt i zapis pliku wyczyszczonego "w+" std::fstream z in | out | trunc
Odczyt i dopisywanie "a+" std::fstream z in | out | app

W tabeli in, out, trunc i app oznaczają odpowiednio std::ios::in, std::ios::out, std::ios::trunc i std::ios::app. Flagi C++ łączy się operatorem |. W C do trybu można dodać b, np. "rb"; w C++ służy do tego std::ios::binary.

Tryb binarny przekazuje bajty bez tekstowej interpretacji końców wierszy, ale sam nie określa przenośnego formatu liczb czy struktur. W trybie app każdy zapis trafia na koniec. Jeśli chcesz zachować wcześniejszy tekst, a potem dodać nowy, to właśnie app jest właściwym wyborem.

Pliki w C

Funkcje C są zadeklarowane w <stdio.h>. fopen zwraca wskaźnik FILE* albo NULL, gdy otwarcie się nie powiedzie. Nie wykonuj operacji na pliku przed sprawdzeniem wyniku:

~~~c

include

int main(void) { FILE *plik = fopen("wejscie.txt", "r"); if (plik == NULL) { perror("Nie można otworzyć wejscie.txt"); return 1; }

/* Miejsce na odczyt z pliku. */

if (fclose(plik) != 0) {
    perror("Nie udało się zamknąć pliku");
    return 1;
}

} ~~~

Tryb "r" wymaga istniejącego pliku; "w" utworzyłby plik, ale skasowałby jego wcześniejszą zawartość. perror wypisuje własny opis i komunikat systemowy o przyczynie błędu. fclose wywołujemy tylko dla poprawnie otwartego pliku.

Odczyt i koniec pliku

Chcemy wypisać zawartość pliku znak po znaku. fgetc zwraca typ int, bo musi reprezentować zarówno dowolny znak, jak i odrębny znacznik EOF. Po pętli EOF może oznaczać zwykły koniec pliku albo błąd odczytu; ferror rozstrzyga, czy wystąpił błąd.

~~~c

include

int main(void) { FILE *plik = fopen("wejscie.txt", "r"); if (plik == NULL) { perror("Nie można otworzyć wejscie.txt"); return 1; }

int znak;
while ((znak = fgetc(plik)) != EOF) {
    putchar(znak);
}

if (ferror(plik)) {
    fprintf(stderr, "Błąd odczytu pliku\n");
    fclose(plik);
    return 1;
}
if (fclose(plik) != 0) {
    perror("Nie udało się zamknąć pliku");
    return 1;
}

} ~~~

Dla pliku zawierającego tekst Ala i znak końca wiersza program wypisze te znaki, a przy normalnym końcu pętli ferror(plik) pozostanie fałszywe. Częsta pomyłka to przypisać wynik fgetc do char. Znak o pewnej wartości mógłby wtedy zostać pomylony z EOF.

Zapis i sprawdzanie

fputc zapisuje jeden znak, a fprintf tekst z formatowaniem. Sprawdź wynik zapisu i zamknięcia, ponieważ błąd może ujawnić się dopiero, gdy fclose opróżnia bufor:

~~~c

include

int main(void) { FILE *plik = fopen("raport.txt", "w"); if (plik == NULL) { perror("Nie można otworzyć raport.txt"); return 1; }

int status = 0;
if (fprintf(plik, "Wynik: %d\n", 42) < 0) {
    fprintf(stderr, "Błąd zapisu do raport.txt\n");
    status = 1;
}
if (fclose(plik) != 0) {
    perror("Nie udało się zapisać lub zamknąć raport.txt");
    status = 1;
}
return status;

} ~~~

Po udanym zapisie plik zawiera wiersz Wynik: 42. W C programista sam odpowiada za fclose. Przy wielu możliwych wyjściach z funkcji zaplanuj sprzątanie tak, by plik nie został otwarty po błędzie.

Inne funkcje: fgets czyta wiersz do bufora o ograniczonym rozmiarze; fscanf czyta wartości według formatu; fread i fwrite czytają lub zapisują określoną liczbę elementów. Sprawdzaj liczbę elementów faktycznie przeczytanych lub zapisanych oraz wynik fscanf.

Pliki w C++

Nagłówek <fstream> udostępnia std::ifstream do odczytu, std::ofstream do zapisu i std::fstream do obu tych operacji. Strumień jest obiektem pamiętającym stan operacji; warunek if (!plik) pozwala wykryć nieudane otwarcie albo późniejszy błąd.

Odczyt wierszami

std::getline(plik, linia) pobiera znaki do końca wiersza, usuwa znak nowej linii ze strumienia i zapisuje pozostały tekst do linia:

~~~cpp

include

include

include

int main() { std::ifstream plik("wejscie.txt"); if (!plik) { std::cerr << "Nie można otworzyć wejscie.txt\n"; return 1; }

std::string linia;
while (std::getline(plik, linia)) {
    std::cout << linia << '\n';
}

if (plik.bad()) {
    std::cerr << "Błąd wejścia/wyjścia podczas odczytu\n";
    return 1;
}

} ~~~

Jeśli plik zawiera wiersze Ala oraz ma kota, pętla wykona się dwa razy i wypisze:

~~~text Ala ma kota ~~~

Przy zwykłym końcu pliku getline zwraca fałsz, co kończy pętlę. To normalny koniec odczytu, a nie awaria; bad() służy do wykrywania poważnego błędu I/O.

Stan strumienia

Metoda Znaczenie
good() Nie ustawiono flag błędu.
eof() Odczyt dotarł do końca pliku. Sam koniec może być normalny.
fail() Operacja nie dostarczyła oczekiwanych danych, np. tekstu nie dało się zamienić na liczbę. Może być ustawione przy końcu pliku.
bad() Poważny błąd wejścia/wyjścia.

Warunek if (plik) jest prawdziwy, gdy nie ustawiono failbit ani badbit. Nie pisz while (!plik.eof()). Flaga końca zwykle pojawia się dopiero po próbie odczytu za końcem pliku; taka pętla może więc ponownie użyć poprzedniej wartości. Umieść odczyt w warunku pętli:

~~~cpp int liczba; while (plik >> liczba) { // Liczba została poprawnie odczytana. } if (plik.bad()) { // Poważny błąd I/O. } else if (!plik.eof()) { // Niepoprawny tekst, np. „abc” zamiast liczby. } ~~~

Dla danych 10 20 30 pętla odczyta trzy liczby, a następna próba trafi na koniec pliku. Dla tekstu 10 abc 30 odczyt zatrzyma się przy abc: fail() będzie prawdziwe, ale koniec pliku jeszcze nie został osiągnięty.

Zapis i kontrola błędów

operator<< zapisuje tekst i wartości do strumienia. Sprawdzamy stan po zapisie; jawne zamknięcie pozwala też wykryć błąd opróżniania bufora:

~~~cpp

include

include

int main() { std::ofstream plik("raport.txt"); // Tworzy albo czyści plik. if (!plik) { std::cerr << "Nie można otworzyć raport.txt\n"; return 1; }

plik << "Wynik: " << 42 << '\n';
if (!plik) {
    std::cerr << "Błąd zapisu do raport.txt\n";
    return 1;
}

plik.close();
if (!plik) {
    std::cerr << "Błąd podczas zamykania pliku\n";
    return 1;
}

} ~~~

Po poprawnym zapisie w pliku znajdzie się Wynik: 42. Zwykle nie musisz pisać close(): strumień zamknie plik automatycznie przy wyjściu obiektu z zakresu. W przykładzie robimy to jawnie, aby sprawdzić końcowy stan.

Dopisywanie i tryb binarny

Jeśli plik log.txt zawiera już Start, tryb std::ios::app dopisze nowy tekst na końcu zamiast skasować wcześniejszy:

~~~cpp

include

int main() { std::ofstream plik("log.txt", std::ios::out | std::ios::app); if (!plik) return 1; plik << "Nowe zdarzenie\n"; } ~~~

Po zapisie plik zawiera oba wiersze: Start i Nowe zdarzenie. Flaga std::ios::binary wybiera pracę z bajtami i można ją łączyć z std::ios::in lub std::ios::out. Sama flaga nie czyni formatu przenośnym: reprezentacja liczby zależy m.in. od architektury.

Do odczytu i zapisu służy std::fstream. Flagi in i out wybierają kierunek, app wymusza dopisywanie na końcu, trunc czyści plik przy otwarciu. Przy przeplataniu odczytu i zapisu na jednym strumieniu trzeba poprawnie przechodzić między tymi operacjami; w prostych programach łatwiej użyć osobnych plików wejściowego i wyjściowego.

C i C++: to samo zadanie, inny sposób zarządzania

Zadanie C C++
Otwarcie fopen zwraca FILE* albo NULL. Tworzymy ifstream, ofstream lub fstream i sprawdzamy if (!plik).
Odczyt wiersza fgets zapisuje do bufora o ustalonym rozmiarze. std::getline zapisuje do std::string.
Zapis tekstu fprintf lub fputs; sprawdź wynik. operator<<; sprawdź stan strumienia.
Zamknięcie Jawne fclose; sprawdź wynik. Destruktor strumienia zamyka plik automatycznie; close() umożliwia sprawdzenie końcowego błędu.
Błędy Kody zwrotu funkcji, ferror, perror. Flagi stanu strumienia; wyjątki są opcjonalne.

W C programista musi dopilnować fclose na każdej ścieżce wyjścia. W C++ strumień stosuje RAII: zamyka plik, kiedy obiekt wychodzi z zakresu, także przy wcześniejszym wyjściu z funkcji lub rozwijaniu stosu po wyjątku. RAII zwalnia zasób, ale nie wykrywa za programistę błędów odczytu i zapisu. Te nadal trzeba sprawdzać.