:~$ newsletter --subscribe
cat ~/artykuly/programowanie/obsluga-plikow-w-c-odczyt-danych.md

Obsługa plików w C i C++: odczyt i zapis danych

Pisząc programy komputerowe na pewno, każdy kiedyś zetknął się z obsługą plików. To właśnie między innymi dzięki plikom, możemy zapisywać ważne informacje wykorzystywane w naszym programie co podniesie jego użyteczność. Po co zresztą komuś aplikacja, która nie potrafi zapamiętać wcześniej wprowadzonych informacji?

Obsługa plików w C++ nie jest tak bardzo skomplikowana jak mogłoby się wydawać początkującym programistom, w niniejszym artykule zaprezentuję na praktycznych przykładach sposoby odczytu danych z plików tekstowych oraz binarnych.

Pliki możemy odczytywać za pomocą bibliotek fstream, stdio.h, które zanim zaczniemy cokolwiek robić musimy najpierw zaimportować do naszego programu:

c
#include <fstream> //import biblioteki fstream
#include <stdio.h> //import biblioteki stdio.h

Kiedy już dołączyliśmy którąś z bibliotek na przykład fstream to możemy się zabrać do dalszej pracy.

getline()

Napiszmy sobie program, który pobiera dane z pliku tekstowego linijka po linijce za pomocą funkcji getline(); i wypisuje je na ekranie. Funkcja getline(uchwytDoPliku, dane); pobiera jedną linijkę z pliku tekstowego.

cpp
#include <iostream>
#include <fstream>

using namespace std;

int main()
{
    fstream uchwyt; //obiekt typu fstream (uchwyt do pliku)

    uchwyt.open("plik.txt"); //otwieramy plik: plik.txt (plik - nazwa pliku, txt - rozszerzenie)
    string linia;

    while(getline(uchwyt, linia)) //pobieraj kolejne linijki aż do końca pliku
    {
        cout << linia << endl; //wypisz na ekranie
    }

    uchwyt.close(); //zamykamy plik

    return 0;
}

Powyższy program wyświetli nam na ekranie zawartość danego pliku tekstowego pobierając wszystkie dane linijka po linijce.

Otwieranie pliku za pomocą funkcji fopen()

Korzystając z biblioteki stdio.h będziemy korzystali z funkcji fopen() do otwarcia pliku. Zobaczmy jak się nią posługiwać:

Ogólna składnia funkcji fopen():

cpp
fopen("sciezkaDoPliku.txt", "parametr");

Za sciezkaDoPliku.txt wstawiamy ścieżkę do pliku który chcemy otworzyć względem naszego pliku z kodem, a w miejscu parametru określamy tryb dostępu do pliku:

Tryb dostępu do plikuOpis
a Otwiera plik tylko do zapisu (jeśli plik nie został stworzony, to zostanie utworzony, wszystkie nowe dane będą zapisywane na jego końcu)
a+ Otwiera plik do odczytu i zapisu (jeśli plik nie został stworzony, to zostanie utworzony, wszystkie nowe dane będą zapisywane na jego końcu, można odczytywać dowolny fragment pliku)
b Otwiera plik w trybie binarnym
r Otwiera plik tylko do odczytu (wcześniej musi on zostać stworzony)
r+ Otwiera plik do odczytu i zapisu (wcześniej musi on zostać stworzony, można odczytywać i zapisywać dowolny fragment pliku)
w Tworzy nowy plik i otwiera tylko do zapisu (uchwyt ustawiany jest na samym początku, a w przypadku kiedy dany plik już istniał to kasowana jest jego zawartość)
w+ Tworzy nowy plik i otwiera go do odczytu i zapisu (uchwyt ustawiany jest na samym początku, a w przypadku kiedy dany plik już istniał to kasowana jest jego zawartość, można odczytywać i zapisywać dowolny fragment pliku)

Przykład:

cpp
FILE* plik; //uchwyt do pliku

plik = fopen("plik.txt", "r"); //otwieramy plik w trybie tylko do odczytu - parametr "r"

fgetc()

Oczywiście możemy na przykład chcieć pobierać tekst z danego pliku znak po znaku. Problem ten można rozwiązać na przykład wykorzystując bibliotekę stdio.h i funkcję fgetc();. Osobiście wolę właśnie używać tej biblioteki do obsługi plików w C++ gdyż metoda ta jest znacznie szybsza oraz mam większą kontrolę nad pobieranymi danymi i wykonywanymi operacjami.

Napiszmy więc program, który będzie pobierał dane z pliku tekstowego znak po znaku.

cpp
#include <iostream>
#include <stdio.h>

using namespace std;

int main()
{
    FILE* plik; //uchwyt do pliku

    plik = fopen("plik.txt", "r"); //otwieramy plik w trybie tylko do odczytu - parametr "r"

    char znak;

    do
    {
        znak = fgetc(plik); //zapisuję jeden znak z pliku
        cout << znak << endl; //wypisuję na ekran
    }
    while(znak != EOF); //End Of File - koniec pliku

    fclose(plik); //zamykamy plik

    return 0;
}

Funkcja fopen(„plik”, „trybOtwarcia”); może otwierać pliki w różnych trybach, na przykład do odczytu lub zapisu. Sposób działania tej funkcji omówiłem w poprzednim akapicie „Otwieranie pliku za pomocą funkcji fopen()„.

fgets()

Funkcja fgets() odczytuje dane z wybranego wejścia (na przykład pliku) do momentu napotkania znaku nowej linii bądź do momentu wczytania określonej liczby znaków.

Przykład:

cpp
#include <stdio.h>

using namespace std;

int main()
{
   FILE *plik;
   char wczytaneZnaki[10];

   plik = fopen("plik.txt", "r"); //otwarcie pliku

   fgets(wczytaneZnaki, 10, plik);

   for(int i = 0;i<10;i++)
        cout << wczytaneZnaki[i];

   fclose(plik); //zamknięcie pliku

   return 0;
}

Ogólna składnia funkcji fgets():

cpp
fgets(obiektDoKtoregoZapisujemyDane, maksymalnaLiczbaZnakow, wejscie);

fscanf()

fscanf() działa bardzo podobnie do scanf(). Funkcję fscanf() można jednak wykorzystać do odczytania danych z pliku. Przyjrzyjmy się więc jak to zrobić.

Przykład:

cpp
#include <stdio.h>

using namespace std;

int main ()
{
  char wczytywaneZnaki[10];
  FILE *plik;

  plik = fopen("plik.txt", "a+");
  fscanf(plik, "%s", wczytywaneZnaki);
  fclose(plik); //zamknięcie pliku

  for(int i = 0;i<10;i++)
      cout << wczytywaneZnaki[i];

  return 0;
}

Ogólna składnia:

cpp
fscanf(wejscie, specyfikator, obiektWyjscia);

W miejscu specyfikator podajemy dane z poniżej tabelki:

SpecyfikatorOpis
%d lub %i Liczba całkowita ze znakiem (system dziesiętny) np. int
%f Liczba rzeczywista (system dziesiętny) np. float
%u Liczba całkowita bez znaku (system dziesiętny)
%e Liczba w notacji naukowej – ze znakiem e (małe litery)
%E Liczba w notacji naukowej – ze znakiem E (duże litery)
%g Liczba rzeczywista w notacji %f lub %e
%G Liczba rzeczywista w notacji %f lub %E
%o Liczba w systemie ósemkowym
%x Liczba w systemie szesnastkowym (małe litery)
%X Liczba w systemie szesnastkowym (duże litery)
%c Znak np. zmienne typu char
%s Łańcuch znaków zakończony “\0“, np. zmienne typu string
%p Adres pamięci

fread()

Napiszmy teraz program, który odczyta nam dane z pliku binarnego.

cpp
#include <iostream>
#include <stdio.h>

using namespace std;

int main()
{
    FILE* plik;

    plik = fopen("plik.dat", "rb"); //otwarcie pliku jako plik binarny - parametr "b"

    int liczba;

    fread(&liczba, sizeof(int), 1, plik); //fread(wskaźnik na zmienną do której zapisujemy dane, rozmiar elementu, ilość liczb do odczytu, plik na którym wykonujemy operację);
    cout << liczba << endl; //wypisz dane

    fclose(plik); //zamykamy plik

    return 0;
}

W powyższym programie do odczytu danych binarnych została wykorzystana funkcja fread();, której składnia wygląda następująco:

cpp
fread(wskaznik_na_zmienna_do_ktorej_zapisujemy_dane, rozmiar_elementu, ilosc_liczb_do_odczytu, plik_na_ktorym_wykonujemy_operacje);

Jak widać nie ma w tym nic trudnego.

Fot: Alexandre Buisse (Nattfodd).

Zapis danych do pliku

Odczyt mamy już opanowany, więc naturalnym krokiem jest zapis. Zaczynamy tak samo, od fopen(), ale tym razem kluczowy jest tryb otwarcia pliku. To on decyduje, czy plik nadpisujemy, czy dopisujemy do niego dane:

  • "r" – tylko odczyt (plik musi istnieć),
  • "w" – zapis, tworzy nowy plik, a istniejący nadpisuje (kasuje dotychczasową zawartość),
  • "a" – dopisywanie na końcu pliku (ang. append), bez kasowania,
  • "r+", "w+", "a+" – warianty z jednoczesnym odczytem i zapisem.

Do samego zapisu tekstu najczęściej używamy fprintf() (działa jak printf, tyle że pisze do pliku) oraz fputs(). Poniższy przykład tworzy plik i zapisuje w nim dwie linie:

c
#include <stdio.h>

int main(void) {
    FILE *plik = fopen("dane.txt", "w");   // tryb "w" - zapis
    if (plik == NULL) {
        printf("Nie udalo sie otworzyc pliku!\n");
        return 1;
    }

    fprintf(plik, "Imie: %s, wiek: %d\n", "Anna", 30);
    fputs("Druga linia tekstu\n", plik);

    fclose(plik);   // zawsze zamykamy plik!
    return 0;
}

Zwróć uwagę na dwie rzeczy, o których łatwo zapomnieć. Zawsze sprawdzamy, czy fopen() nie zwrócił NULL (np. brak uprawnień do zapisu), i zawsze wywołujemy fclose(), bo bez tego część danych może zostać w buforze i nie trafić na dysk. Do zapisu danych binarnych (nie tekstu) służy z kolei fwrite(), będące odpowiednikiem fread().

Obsługa plików w C++ na strumieniach fstream

W C++ pracę z plikami wygodniej jest oprzeć na strumieniach z nagłówka <fstream>. Zamiast funkcji fopen/fclose używamy obiektów: ofstream do zapisu (ang. output file stream), ifstream do odczytu (ang. input file stream) oraz fstream do obu naraz. Operujemy na nich tak samo jak na cout i cin, za pomocą operatorów << i >>:

cpp
#include <fstream>
#include <iostream>
#include <string>

int main() {
    // Zapis do pliku
    std::ofstream zapis("dane.txt");
    if (zapis.is_open()) {
        zapis << "Imie: Anna, wiek: 30\n";
        zapis << "Druga linia tekstu\n";
    }   // plik zamyka sie sam, gdy obiekt wychodzi z zakresu

    // Odczyt linia po linii
    std::ifstream odczyt("dane.txt");
    std::string linia;
    while (std::getline(odczyt, linia)) {
        std::cout << linia << "\n";
    }

    return 0;
}

Największa wygoda strumieni w C++ to automatyczne zamykanie pliku. Gdy obiekt ofstream czy ifstream wychodzi z zakresu (kończy się blok { }), jego destruktor sam wywołuje close(). Nie da się więc zapomnieć o zamknięciu pliku tak, jak zdarza się to z fclose() w C. Mechanizm ten (znany jako RAII) sprawia, że w nowym kodzie C++ zwykle sięgamy właśnie po fstream, a klasyczne fopen/fread zostawiamy dla języka C i sytuacji, w których musimy współpracować z jego API.

Tryby otwarcia pliku w fstream (ios::)

Strumienie z rodziny fstream przyjmują w metodzie open (lub konstruktorze) drugi argument: tryb otwarcia pliku. Tryby można łączyć znakiem |, na przykład ios::out | ios::binary. Do wyboru mamy:

TrybDziałanie
ios::app Otwiera plik tylko do zapisu, ustawia wskaźnik na jego końcu i tam zapisuje dane.
ios::ate Ustawianie wskaźnika na koniec pliku.
ios::binary Otwarcie pliku w trybie binarnym, tzn. dane traktowane są jak binarne, a nie tekstowe.
ios::in Otwarcie pliku w trybie do odczytu.
ios::out Otwarcie pliku w trybie do zapisu.
ios::trunc Podczas otwierania plik jest czyszczony (tzn. usuwana jest jego zawartość).

W praktyce najczęściej przydaje się ios::app: otwarcie do zapisu z dopisywaniem na końcu pliku, bez kasowania dotychczasowej zawartości (odpowiednik trybu „a” z fopen opisanego wyżej). Domyślne ofstream działa jak ios::out i nadpisuje plik.

Łukasz Dudziński
Łukasz Dudziński · założyciel i autor
Pasjonat informatyki, a zwłaszcza programowania. Zwariowany maniak na tle nowych technologii oraz komputerów. W wolnych chwilach bloger... więcej-o-mnie →
ls ~/artykuly/powiazane --limit 3
komentarze --count 15
rss dla komentarzy →
Tomasz Kaczanowski2015-06-19 ·

W fopen określamy nie tylko tryb dostępu do pliku, ale również rodzaj pliku – domyślnie otwierany jest tryb tekstowy, należałoby dodac 'b’, aby był tryb binarny

Do wczytywania danych w c masz jeszcze fgets i fscanf, nie tylko fgetc
natomiast w fstream nie tylko jest getline.

Dodatkowo fstream nie do końca jest uchwytem, jest całym obiektem z mnogością metod (nie omijając operatorów <>)

lukas1242015-06-22 ·

Dzięki, w najbliższym czasie postaram się zaktualizować artykuł o podane przez Ciebie elementy. Natomiast jeśli chodzi o fstream to już poprawiłem ;)

Tomasz Kaczanowski2015-10-09 ·

Miałeś dodać 'b’, czyli powinno być np „rb” :)

lukas1242015-10-09 ·

Dodałem „b” do tabelki jako osobny tryb otwierania pliku (tryb binarny)

Tomasz Kaczanowski2015-10-09 ·

nie chodzi o tabelkę, a o uzycie:
plik = fopen(„plik.dat”, „b”); //otwarcie pliku jako plik binarny – parametr „b”

winno być:
plik = fopen(„plik.dat”, „rb”); //otwarcie pliku jako plik binarny – parametr „b”

lukas1242015-10-09 ·

Faktycznie, dzięki ;)

Pola2016-02-15 ·

Obsługa plików w C++ nie jest tak bardzo skomplikowana jak mogło by się wydawać początkującym programistOM!!

lukas1242016-02-15 ·

Ta forma też jest dopuszczalna: http://sjp.pl/programist%C4%85

Pola2016-02-16 ·

ale nie w tym kontekscie.

– Kim jestem? – Jestem programistą.

– Komu się przyglądam? – Przyglądam się pracującym programistom.

lukas1242016-02-16 ·

Ok, prawiłem ;) Tego typu błędów niestety nie wyłapuje edytor.

Justyna xD2018-01-21 ·

Czy wie ktoś jak odczytać jaki jest AUTOR/WŁAŚCICIEL pliku w C?

lukas1242018-01-21 ·

Spróbuj tak:

#include
#include
#include
#include
#include
#include
#include

using namespace std;

void printStat(const char *, struct stat *);

int main(int argc, char ** argv)
{
if (argc > 1)
{
struct stat buf;
for (int i = 1; i < argc; ++i)
{
lstat(argv[i], &buf);
printStat(argv[i], &buf);
}
}

return 0;
}

void printStat(const char * fname, struct stat *data)
{
cout << dec << "Wlasciciel: " <st_uid << "n";
}

Rudolf2023-02-07 ·

Wiem

Robert2023-02-13 ·

Może pytanie dość naiwne, ale muszę ze względu że czas mnie goni z projektem.
Do rzeczy, jaka jest różnica w odczytywaniu plików .docx a .txt. Próbuję wykonać projekt który będzie obliczał funkcje skrótu dla tekstu z pliku. Wczytuję plik .txt z tekstem „hello world” i po wywołaniu funkcji dostaję pewny ciąg (skrót tego tekstu) natomiast wynik dla tego samego tekstu w pliku .docx jest inny. Wygląda to na złe odczytanie treści pliku, więc proszę o jakieś sugestię jak obsłużyć takie rozszerzenie

Łukasz Dudzińskiautor2023-02-13 ·

Cześć Robert,

Różnica jest taka, że plik txt odczytywany podanymi w artykule metodami, zwraca dokładnie taki tekst, jaki w nim zapiszesz. Plik docx (Worda) nie składa się TYLKO z tekstu, jaki wpisałeś w edytorze. Zawiera on szereg innych danych. Aby to zobaczyć, zmień rozszerzenie docx na txt i otwórz plik dowolnym edytorem. Z tego względu dalsze operacje nie działają, tak jak oczekujesz.

Pozdrawiam,
Łukasz Dudziński, autor bloga Strefakodera.pl

dodaj-komentarz

Komentarze są moderowane. Podane dane przetwarzamy wyłącznie w celu publikacji komentarza (polityka prywatności).