Wskaźnik pliku
Każda klasa strumienia plików zawiera wskaźnik pliku używany do śledzenia bieżącej pozycji odczytu/zapisu w pliku. Kiedy coś jest odczytywane z pliku lub zapisywane do pliku, odczyt/zapis odbywa się w bieżącej lokalizacji wskaźnika pliku. Domyślnie podczas otwierania pliku do odczytu lub zapisu wskaźnik pliku jest ustawiony na początek pliku. Jeśli jednak plik zostanie otwarty w trybie dopisywania, wskaźnik pliku zostanie przesunięty na koniec pliku, tak aby zapis nie nadpisał żadnej bieżącej zawartości pliku.
Losowy dostęp do pliku za pomocą funkcjiseeg() iseep()
Do tej pory cały dostęp do pliku, który wykonaliśmy, był sekwencyjny — to znaczy, że czytaliśmy lub zapisywaliśmy zawartość pliku w odpowiedniej kolejności. Jednakże możliwy jest także losowy dostęp do pliku, czyli przeskakiwanie do różnych punktów pliku w celu zapoznania się z jego zawartością. Może to być przydatne, gdy plik jest pełen rekordów, a chcesz odzyskać konkretny rekord. Zamiast czytać wszystkie rekordy, aż dojdziesz do żądanego rekordu, możesz przejść bezpośrednio do rekordu, który chcesz pobrać.
Losowy dostęp do pliku odbywa się poprzez manipulację wskaźnikiem pliku za pomocą funkcji search() (dla danych wejściowych) i funkcji search() (dla danych wyjściowych). Jeśli się zastanawiasz, g oznacza „get”, a p „put”. W przypadku niektórych typów strumieni funkcja search() (zmiana pozycji odczytu) i funkcja search() (zmiana pozycji zapisu) działają niezależnie - jednak w przypadku strumieni plików pozycja odczytu i zapisu jest zawsze identyczna, więc funkcji search i search można używać zamiennie.
Funkcje search() i search() przyjmują dwa parametry. Pierwszy parametr to przesunięcie określające, o ile bajtów należy przesunąć wskaźnik pliku. Drugi parametr to flaga ios określająca, od czego powinien być przesunięty parametr przesunięcia.
| Flaga wyszukiwania Ios | Znaczenie |
|---|---|
| beg | Przesunięcie jest względne w stosunku do początku pliku (domyślnie) |
| cur | Przesunięcie jest względne w stosunku do bieżącej lokalizacji pliku wskaźnik |
| koniec | Przesunięcie jest względem końca pliku |
Przesunięcie dodatnie oznacza przesunięcie wskaźnika pliku w stronę końca pliku, natomiast przesunięcie ujemne oznacza przesunięcie wskaźnika pliku w stronę początku pliku.
Oto kilka przykładów:
inf.seekg(14, std::ios::cur); // przejdź do przodu o 14 bajtów
inf.seekg(-18, std::ios::cur); // przejdź do tyłu o 18 bajtów
inf.seekg(22, std::ios::beg); // przejdź do 22. bajtu w pliku
inf.seekg(24); // przejdź do 24. bajtu plik
inf.seekg(-28, std::ios::end); // przejdź do 28. bajtu przed końcem plikuPrzejście na początek lub koniec pliku jest łatwe:
inf.seekg(0, std::ios::beg); // przejdź na początek pliku
inf.seekg(0, std::ios::end); // przejdź na koniec plikuOstrzeżenie
W pliku tekstowym szukanie pozycji innej niż początek plik może spowodować nieoczekiwane zachowanie.
W programowaniu znak nowej linii („\n”) jest w rzeczywistości abstrakcją.
- W systemie Windows znak nowej linii jest reprezentowany jako sekwencyjne znaki CR (powrót karetki) i LF (przesunięcie wiersza) (zajmując w ten sposób 2 bajty pamięci).
- W systemie Unix znak nowej linii jest reprezentowany jako znak LF (powrót do wiersza) (zajmując tym samym 1 bajt pamięci).
Wyszukiwanie poza nową linię w dowolnym kierunku zajmuje zmienną liczbę bajtów w zależności od sposobu zakodowania pliku, co oznacza, że wyniki będą się różnić w zależności od zastosowanego kodowania.
Również w niektórych systemach operacyjnych pliki mogą być dopełniane końcowymi bajtami zerowymi (bajty o wartości 0). Szukanie końca pliku (lub przesunięcia od końca pliku) da w takich plikach różne wyniki.
Aby dać ci wyobrażenie o tym, jak one działają, zróbmy przykład z użyciem funkcji search() i pliku wejściowego, który utworzyliśmy w ostatniej lekcji. Ten plik wejściowy wygląda następująco:
This is line 1 This is line 2 This is line 3 This is line 4
Oto przykład:
#include <fstream>
#include <iostream>
#include <string>
int main()
{
std::ifstream inf{ "Sample.txt" };
// Jeśli nie mogliśmy otworzyć strumienia pliku wejściowego do odczytu
if (!inf)
{
// Wydrukuj błąd i wyjdź
std::cerr << "Uh oh, Sample.txt could not be opened for reading!\n";
return 1;
}
std::string strData;
inf.seekg(5); // przejdź do 5. znaku
// Pobierz resztę linii i wydrukuj ją, przechodząc do linii 2
std::getline(inf, strData);
std::cout << strData << '\n';
inf.seekg(8, std::ios::cur); // przenieś o 8 kolejnych bajtów do pliku
// Znajdź resztę linii i wydrukuj ją
std::getline(inf, strData);
std::cout << strData << '\n';
inf.seekg(-14, std::ios::end); // przesuń 14 bajtów przed końcem pliku
// Znajdź resztę linii i wydrukuj ją
std::getline(inf, strData); // undefined behavior
std::cout << strData << '\n';
return 0;
}Daje to wynik:
is line 1 line 2 This is line 4
Możesz otrzymać inny wynik dla trzeciej linii, w zależności od tego, jak plik jest zakodowany.
seekg() iseep() lepiej używać w przypadku plików binarnych. Możesz otworzyć powyższy plik w trybie binarnym poprzez:
std::ifstream inf {"Sample.txt", std::ifstream::binary};Dwie inne przydatne funkcje to tellg() i tellp(), które zwracają bezwzględną pozycję wskaźnika pliku. Można to wykorzystać do określenia rozmiaru pliku:
std::ifstream inf {"Sample.txt"};
inf.seekg(0, std::ios::end); // przejdź na koniec pliku
std::cout << inf.tellg();Na komputerze autora wypisuje:
64
czyli długości pliku sample.txt w bajtach (zakładając, że po ostatniej linii znajduje się znak nowej linii).
Nota autora
Wynik 64 w poprzednim przykładzie miał miejsce w systemie Windows. Jeśli uruchomisz przykład w systemie Unix, zamiast tego otrzymasz 60 , ze względu na mniejszą reprezentację nowej linii. Możesz uzyskać coś innego, jeśli plik zostanie dopełniony końcowymi bajtami zerowymi.
Jednoczesne czytanie i zapisywanie pliku przy użyciu fstream
Klasa fstream może jednocześnie czytać i zapisywać plik - prawie! Największym zastrzeżeniem jest to, że nie jest możliwe dowolne przełączanie między czytaniem i pisaniem. Po dokonaniu odczytu lub zapisu jedynym sposobem przełączania się między nimi jest wykonanie operacji modyfikującej położenie pliku (np. wyszukiwanie). Jeśli tak naprawdę nie chcesz przesuwać wskaźnika pliku (ponieważ znajduje się on już w żądanym miejscu), zawsze możesz poszukać bieżącej pozycji:
// załóżmy, że iofile jest obiektem typu fstream
iofile.seekg(iofile.tellg(), std::ios::beg); // szukanie do bieżącego pliku positionJeśli tego nie zrobisz, mogą wystąpić różne dziwne i dziwaczne rzeczy.
(Uwaga: chociaż może się wydawać, że iofile.seekg(0, std::ios::cur) też by zadziałało, wygląda na to, że niektóre kompilatory mogą to zoptymalizować).
Jeszcze jedna kwestia złożoność: w przeciwieństwie do ifstream, gdzie moglibyśmy powiedzieć while (inf) aby określić, czy jest więcej do przeczytania, w przypadku fstream to nie zadziała.
Zróbmy przykład wejścia/wyjścia pliku przy użyciu fstream. Napiszemy program, który otworzy plik, odczyta jego zawartość i zamieni znalezione samogłoski na symbol „#”.
#include <fstream>
#include <iostream>
#include <string>
int main()
{
// Uwaga: musimy określić zarówno wejście, jak i wyjście, ponieważ używamy fstream
std::fstream iofile{ "Sample.txt", std::ios::in | std::ios::out };
// Jeśli nie mogliśmy otworzyć pliku io, wydrukuj błąd
if (!iofile)
{
// Wydrukuj błąd i wyjdź
std::cerr << "Uh oh, Sample.txt could not be opened!\n";
return 1;
}
char chChar{}; // zrobimy dziesięć znaków po znaku
// Dopóki są jeszcze dane do przetworzenia
while (iofile.get(chChar))
{
switch (chChar)
{
// Jeśli znajdziemy samogłoskę
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
case 'A':
case 'E':
case 'I':
case 'O':
case 'U':
// Utwórz kopię zapasową jednej postaci
iofile.seekg(-1, std::ios::cur);
// Ponieważ przeprowadziliśmy wyszukiwanie, możemy teraz bezpiecznie wykonać zapis, więc
// piszmy # nad samogłoską
iofile << '#';
// Teraz chcemy wrócić do trybu odczytu, więc następne wywołanie
// to get() będą działać poprawnie. Będziemy szukać() do bieżącego
// lokalizacja, ponieważ nie chcemy przesuwać wskaźnika pliku.
iofile.seekg(iofile.tellg(), std::ios::beg);
break;
}
}
return 0;
}Po uruchomieniu powyższego programu nasz plik Sample.txt będzie wyglądał następująco:
Th#s #s l#n# 1 Th#s #s l#n# 2 Th#s #s l#n# 3 Th#s #s l#n# 4
Inne przydatne funkcje pliku
Aby usunąć plik, po prostu użyj funkcji usuwania().
Ponadto funkcja is_open() funkcja zwróci wartość true, jeśli strumień jest aktualnie otwarty, lub false w przeciwnym razie.
Ostrzeżenie dotyczące zapisywania wskaźników na dysk
Podczas gdy przesyłanie strumieniowe zmiennych do pliku jest dość proste, sprawa komplikuje się, gdy mamy do czynienia ze wskaźnikami. Pamiętaj, że wskaźnik po prostu przechowuje adres zmiennej, na którą wskazuje. Chociaż możliwe jest odczytywanie i zapisywanie adresów na dysku, jest to niezwykle niebezpieczne. Dzieje się tak dlatego, że adres zmiennej może różnić się w zależności od wykonania. W rezultacie, chociaż zmienna mogła znajdować się pod adresem 0x0012FF7C, kiedy zapisałeś ten adres na dysku, może już tam nie być, kiedy ponownie wczytasz ten adres!
Załóżmy na przykład, że masz liczbę całkowitą o nazwie nValue, która znajduje się pod adresem 0x0012FF7C. Przypisałeś nValue wartość 5. Zadeklarowałeś także wskaźnik o nazwie *pnValue, który wskazuje na nValue. pnValue przechowuje adres nValue 0x0012FF7C. Chcesz je zachować na później, więc zapisujesz na dysk wartość 5 i adres 0x0012FF7C.
Kilka tygodni później ponownie uruchamiasz program i odczytujesz te wartości z dysku. Wczytujesz wartość 5 do innej zmiennej o nazwie nValue, która ma numer 0x0012FF78. Wczytujesz adres 0x0012FF7C do nowego wskaźnika o nazwie *pnValue. Ponieważ pnValue wskazuje teraz na 0x0012FF7C, podczas gdy nValue ma wartość 0x0012FF78, pnValue nie wskazuje już na nValue i próba uzyskania dostępu do pnValue doprowadzi Cię do kłopotów.
Ostrzeżenie
Nie zapisuj adresów pamięci do plików. Zmienne, które pierwotnie znajdowały się pod tymi adresami, mogą znajdować się pod różnymi adresami, gdy ponownie wczytasz ich wartości z dysku, a adresy będą nieprawidłowe.

