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); // move forward 14 bytes
inf.seekg(-18, std::ios::cur); // move backwards 18 bytes
inf.seekg(22, std::ios::beg); // move to 22nd byte in file
inf.seekg(24); // move to 24th byte in file
inf.seekg(-28, std::ios::end); // move to the 28th byte before end of the filePrzejście na początek lub koniec pliku jest łatwe:
inf.seekg(0, std::ios::beg); // move to beginning of file
inf.seekg(0, std::ios::end); // move to end of fileOstrzeż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" };
// If we couldn't open the input file stream for reading
if (!inf)
{
// Print an error and exit
std::cerr << "Uh oh, Sample.txt could not be opened for reading!\n";
return 1;
}
std::string strData;
inf.seekg(5); // move to 5th character
// Get the rest of the line and print it, moving to line 2
std::getline(inf, strData);
std::cout << strData << '\n';
inf.seekg(8, std::ios::cur); // move 8 more bytes into file
// Get rest of the line and print it
std::getline(inf, strData);
std::cout << strData << '\n';
inf.seekg(-14, std::ios::end); // move 14 bytes before end of file
// Get rest of the line and print it
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); // move to end of file
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:
// assume iofile is an object of type fstream
iofile.seekg(iofile.tellg(), std::ios::beg); // seek to current file 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()
{
// Note we have to specify both in and out because we're using fstream
std::fstream iofile{ "Sample.txt", std::ios::in | std::ios::out };
// If we couldn't open iofile, print an error
if (!iofile)
{
// Print an error and exit
std::cerr << "Uh oh, Sample.txt could not be opened!\n";
return 1;
}
char chChar{}; // we're going to do this character by character
// While there's still data to process
while (iofile.get(chChar))
{
switch (chChar)
{
// If we find a vowel
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
case 'A':
case 'E':
case 'I':
case 'O':
case 'U':
// Back up one character
iofile.seekg(-1, std::ios::cur);
// Because we did a seek, we can now safely do a write, so
// let's write a # over the vowel
iofile << '#';
// Now we want to go back to read mode so the next call
// to get() will perform correctly. We'll seekg() to the current
// location because we don't want to move the file pointer.
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.

