mgr inż. Wacław Libront * Bobowa 2019

ZSO Bobowa, ul. Długoszowskich 1, 38-350 Bobowa, tel: 0183514009, fax: 0183530221, email: sekretariat@zsobobowa.eu, www: zsobobowa.eu

Lekcja 9

Pliki tekstowe w C++

  1. zapis
  2. klawiatura
  3. kolumny
  4. odczyt
  5. wiersze
  6. dopisywanie
  7. zadania
  8. rozwiązania

Dane do programów wprowadzaliśmy do tej pory z klawiatury (lub generowaliśmy liczby losowe). Wyniki obliczeń wypisywane były na ekranie komputera. Duże ilości danych trudno jednak wpisywać ręcznie i w większości przypadków pobieramy je z plików zapisanych na dysku. Podobnie jest z wynikami obliczeń – można je zapisać w postaci pliku i przeglądać w dowolnym momencie na ekranie komputera. Plik na dysku można porównać do taśmy magnetofonowej, na której wykonujemy trzy operacje: zapis taśmy, odczyt taśmy, przesunięcie taśmy o jeden element dalej (albo na początek i na koniec).

Dane do plików zapisujemy sekwencyjnie, to znaczy, że muszą być zapisywane po kolei. Aby zapisać element setny należy najpierw zapisać 99 elementów. Podobnie jest z odczytywaniem. Plik należy otworzyć (do zapisu lub odczytu) OPEN i po zakończeniu działań koniecznie go zamknąć CLOSE. Plik można otworzyć do odczytu ISTREAM, do zapisu OFSTREAM lub do zapisu lub odczytu FSTREAM. Dane do pliku wprowadza się funkcją WRITE i strumieniem <<. Dane odczytuje się z pliku funkcją READ i strumieniem >>. Funkcja logiczna EOF pozwala sprawdzić czy ciągnięto już koniec pliku. Kolejny zapis do tego samego pliku powoduje usunięcie poprzednich danych!

Sprawy z plikami zaczynają komplikować się w zależności od tego, jak chcemy potraktować nasz plik: czy jako zbiór liczb, tekstów, znaków i innych danych. Kolejną komplikacją jest możliwość obsługi plików kilkoma sposobami. Na lekcjach będziemy zajmować się wyłącznie zapisem i odczytem za pomocą strumieni, podobnie zresztą jak robiliśmy to w konsoli.


Zapis do pliku (string): napisy, liczby, konsola
Do obsługi konsoli służyła biblioteka iostream, do obsługi strumieni plikowych należy zadeklarować bibliotekę fstream.

#include <iostream>
#include <fstream>
using namespace std;

int main() {
  setlocale(LC_ALL, "");
	
  ofstream ZAPIS("dane.txt");
  int l=128;
  string t="Ala ma kota";
  ZAPIS << "piszemy do pliku" << endl;
  ZAPIS << l << endl;
  ZAPIS << t << endl;
  string tekst;
  cout << "wpisz tekst:";
  cin >> tekst;
  ZAPIS << tekst<< endl;
  ZAPIS.close();
  return 0;
}

W pliku dane.txt znajdują się 4 wiersze tekstów, które można odczytać zwykłym notatnikiem

piszemy do pliku
128
Ala ma kota
cześć

ofstream - strumień o nazwie ZAPIS kojarzymy z plikiem ”dane.txt” na dysku w katalogu bieżącym. Możemy zapisywać liczby i teksty, identycznie jak na ekranie konsoli za pomocą polecenia COUT. Bezwzględnie należy pamiętać o zamknięciu strumienia plikowego za pomocą polecenia close.

W pliku tekstowym "dane.txt" pojawią się 4 wiersze tekstu. Problemem mogą być polskie znaki diakrytyczne, zapisywane przez Windows w kodzie OEM 852.

Podczas wczytywania tekstów z konsoli posługujemy się typem string, który reaguje standardowo na spację i enter. Typu char należy używać raczej do wczytywania pojedynczych znaków.


Zapis do pliku (char): klawiatura-plik tekstowy
Do wczytywania znaków używamy funkcji getch() z biblioteki conio.h. Program będzie wczytywał znaki z klawiatury, zapisywał je do pliku dyskowego. Pętla zapisu kończy się w momencie naciśnięci klawisza ESC (kod 27).

ofstream klaw("klaw.txt");
char c;
do{
  c=getch();
  if (c==13) {
    cout << endl;
    klaw << endl;
  }
  if (c!=27){
    cout << c;
    klaw << c;
  }
} while (c!=27);
klaw.close();

Zwróć uwagę, w jaki sposób program obsługuje klawisz ENTER - sami musimy zadbać o przejście do nowego wiersza i klawisz ECK - nie chcemy aby program wstawiał „dziwny” znaczek na końcu.


Zapis do pliku: liczby i kolumny
Pisanie liczb do pliku wygląda tak samo, jak pisaliśmy je na ekranie w konsoli.

#include <stdlib.h>
...
ofstream Pliczby("liczby.txt");
int liczba;
for (int i=0;i<10;i++){
  liczba=rand() % 6 + 1;
  Pliczby << liczba << endl;
  cout << liczba << endl;
}
Pliczby.close();

W pliku liczby.txt znajdują się losowe liczby

6
6
5
5
6
5
1
1
5
3

Zapis w kolumnach - tabliczka mnożenia

ofstream plik("mnoz.txt");
int mnoz;
for (int i=1;i<=10;i++){
  for (int j=1;j<=10;j++){
    mnoz=i*j;
    plik.width(4);
    cout.width(4);
    plik << mnoz;
    cout << mnoz;
  }
  plik << endl;
  cout << endl;
}
plik.close();

W pliku mnoz.txt znajdują się wyniki mnożenia, w takim samym układzie, jak na ekranie konsoli


Odczyt z pliku
Jeżeli znamy dokładnie ilość danych do odczytania, możemy np. korzystać z pętli FOR. W wielu przypadkach jednak nie znamy długości pliku i posługujemy się funkcję EOF() i pętlą while, co można przeczytać: „dopóki nie osiągnęliśmy końca pliku …

ifstream PLIK("plik.txt");
while (!PLIK.eof()){
	...
}
PLIK.close();

Odczyt z pliku znak po znaku
Podczas czytania danych z plików pojawiają się problemy podobne do tych podczas zapisywania znaków – pomijane są spacja i enter (zarówno dla typu string i char).

ifstream ODCZYT("dane.txt");
char z;
while (!ODCZYT.eof()){
  ODCZYT >> z;
  cout << z;
}
ODCZYT.close();

Przedstawiony fragment programu czyta pojedyncze znaki z pliku ”dane.txt” i wyświetla na ekranie, bez spacji i przejść do nowego wiersza.

Jeżeli użyjemy funkcji GET ominiemy problemy z tzw. "białymi znakami".
ODCZYT.get(z);

ifstream ODCZYT("dane.txt");
char z;
while (!ODCZYT.eof()){
  ODCZYT.get(z);
  cout << z;
}
ODCZYT.close();


Odczyt z pliku: czytanie wierszami
Instrukcja getline odczytuje całe wiersze, łączenie ze spacjami. Jeśli odczytujemy w ten sposób liczby i teksty, konieczne będzie wyodrębnianie z tekstu fragmentów i konwersja.

ifstream Wodczyt("dane.txt");
string wiersz;
while (!Wodczyt.eof()){
  getline(Wodczyt,wiersz);
  cout << wiersz << endl; 
}
Wodczyt.close();


Odczyt z pliku: liczby na ekran i liczby do tablicy
Liczby czytamy dokładnie tak samo jak teksty. W pliku mogą być rozdzielone spacjami lub każda w nowym wierszu - potraktowane zostaną identycznie.

ifstream ODCZYT("liczby.txt");
int li;
while (!ODCZYT.eof()){
  ODCZYT >> li;
  cout << li;
}
ODCZYT.close();
int tabl[20];
int ile=0;
ifstream oplik("liczby.txt");
if(oplik.is_open()){
  while(!oplik.eof()){
    oplik >> tabl[ile];
    ile++;
  }
}
else cout<<"Brak pliku";
oplik.close();

for (int i=0; i < ile; i++)
  cout << tabl[i]; 

Warto zwrócić uwagę na sprawdzenie poprawności otwarcia pliku. Jeśli takiego pliku nie ma - funkcja is_open(), to możemy pominąć wykonywanie programu. Problemy mogą pojawić się, jeśli plik na końcu zawiera pusty wiersz - do tablicy zostanie wczytany jeden więcej element - zero!


Dopisywanie do pliku
Możemy dopisywać na końcu pliku nowe dane

fstream dopis;
dopis.open("dane.txt", ios::app);
dopis << "Tekst dopisany na końcu";
dopis.close();

Zadania

PALINDROMY
Palindromem nazywamy słowo, które czytane od lewej i od prawej strony jest takie samo. Na przykład palindromami są słowa: JABFDFBAJ, HAJAHAJAH, ABBA, Słowo JANA nie jest palindromem.

01) Wygeneruj plik tekstowy palindrom.txt, który zawierał będzie 1000 słów o długościach od 2 do 25 znaków, każde w nowym wierszu, składających się z wielkich liter A, B, C, D, E, F, G, H, I, J.

Aby plik zawierał jakieś „ciekawe” palindromy zmodyfikuj program w następujący sposób:
- co 30 generowany wyraz sprawdź, czy jest krótszy niż 13 znaków
- jeśli tak, to doklej do niego ten sam odwrócony wyraz, np. DCEAB i doklejamy BAECD

1) Plik tekstowy palindrom.txt zawiera słowa wygenerowane przez komputer (maksymalnie 1000). Odczytaj je i sprawdź, które są palindromami, Wynik na ekranie i w pliku tekstowym.


HASŁA
02) Wygeneruj plik tekstowy hasla.txt, zawierający 200 słów, składających się z małych liter alfabetu angielskiego, każde w osobnym wierszy, których długość wynosi od 3 do 10 znaków.

2) Plik hasla.txt zawiera hasła używane w pewnej firmie (200). Odpowiedz na poniższe pytania: ekran i plik
a) ile jest haseł z parzystą - nieparzystą liczbą znaków
b) wypisz hasła będące palindromami - czytane wspak dadzą taki sam rezultat (np. kajak)
c) wypisz hasła, w których występuje obok siebie dwa identyczne znaki (np. kajjak)


CIĄGI
03) Wygeneruj plik tekstowy ciagi.txt, zawierający 1000 słów składających się z trzech liter A, B, C, każde słowo w osobnym wierszu, litery mogą się powtarzać.

3) W pliku ciagi.txt znajduje się 1000 wierszy, a w każdym wierszu ciąg o długości trzech liter ze zbioru {A, B, C} -litery mogą się powtarzać. Napisz program, który:
a) obliczy prawdopodobieństwo, że w wierszu będzie ciąg składającym się z takich samych znaków
b) obliczy prawdopodobieństwo, że w wybranym wierszu będzie palindrom


CYFRY
04) Wygeneruj plik tekstowy cyfry.txt, zawierający 1000 liczb naturalnych, mniejszych niż 10000, każda w osobnym wierszu.

4) Plik cyfry.txt zawiera liczby naturalne (maksymalnie 1000).
    Odpowiedz na poniższe pytania: ekran i plik
a) wylicz ich średnią
b) ile jest parzystych i nieparzystych liczb
c) podaj liczbę, której suma cyfr jest największa
d) która liczba powtarza się najczęściej


DWÓJKOWE
05) Wygeneruj plik tekstowy napisy.txt, zawierający 1000 napisów od 2 do 16 znaków, każdy w osobnym wierszu, składających się ze znaków ‘0’ i ‘1’.

5) W pliku napisy.txt znajduje się 1000 napisów o długościach od 2 do 16 znaków, każdy napis w osobnym wierszu. W każdym napisie mogą wystąpić jedynie dwa znaki: „0” lub „1”. Odpowiedz na poniższe pytania: ekran i plik
a) ile jest napisów o parzystej długości
b) ile jest napisów, które zawierają taką samą liczbę zer i jedynek
c) ile jest napisów składających się z samych zer, z samych jedynek
d) dla każdej liczby k = 2, 3, ...16 podaj liczbę napisów o długości k znajdujących się w pliku, tzn. ile jest napisów 2-znakowych, 3-znakowych itd.
e) potraktuj każdy napis, jak liczbę dwójkową i wykonaj polecenia z zadania 4


Przykładowe rozwiązania zadań
Jedynie ciężka umysłowa praca i zrozumienie działania programu może nauczyć programowania!

//zadanie 1 - PALINDROM
// Tworzenie pliku
cout << "PALINDROMY" << endl;
ofstream PALzapisz("palindrom.txt");
for (int i=1; i <= 1000; i++){
	//ile znaków w wyrazie
	int ile=rand() % 24 + 2;
	string pal="";
	//sklejamy wyrazy z ile znaków
	for (int j=1; j <= ile; j++){
		//losowe znaki o kodach 65-74
		char zna=char(rand() % 10 +65);
		pal=pal+zna;		
	}
	//co 30 wyraz i jeśli krótszy niż 1 znaków
	//robimy z wyrazu palindrom
	int dl=pal.length();
	if (i%30 == 0 && dl <= 12){
		for (int k=0; k < dl; k++)
			pal=pal+pal[dl-k-1];
	}
	//zapisujemy wyrazy w pliku
	PALzapisz << pal << endl;
	cout << pal << endl;
}
PALzapisz.close();

// odczytywanie pliku i sprawdzanie palindromów
ifstream PALodczyt("palindrom.txt");
string wyraz;
while (!PALodczyt.eof()){
	PALodczyt >> wyraz;
	//całkowita połowa długości
	int dl=wyraz.length();
	int dl2=dl/2;
	bool palindrom=true;
	//sprawdzamy od początku do połowy
	//i porównujemy ze znakami od końca
	for (int i=0; i < dl2; i++)
		//jeśli którakilwiek litera się nie zgadza
		//to nie jest palindrom
		if (wyraz[i] != wyraz[dl-1-i]) 
			palindrom=false;
	if (palindrom) 
		cout << wyraz << endl;
}
PALodczyt.close();
//zadanie 2
//generator pliku z hasłami
//3-10 małych liter (ASCII 97-122)
ofstream HASzapis("hasla.txt");
string napis;
for (int i=1;i<=200;i++){
	napis="";
	//ile znaków od 3 do 10
	int ile=rand() % 8 +3;
	for  (int j=1;j<=ile;j++)
		//losujemy małe litery kodów ASCII
		napis+=rand() % (122-97+1)+97;
	HASzapis << napis << endl;
}
HASzapis.close();

//parzysta-nieparzysta
int PA=0;//parzyste
int NP=0;//nieparzyste
ifstream odczytA("hasla.txt");
string napisA;
while (!odczytA.eof()){
	odczytA >> napisA;
	//sprawdzamy długości napisów za pomocą modulo
	if (napisA.length() % 2 ==0) 
		PA++; else NP++;
}
odczytA.close();
cout << "   parzyste: " << PA << endl;
cout << "nieparzyste: " << NP << endl;

//palindromy
cout << "PALINDROMY" << endl;
ifstream odczytB("hasla.txt");
string napisB;
while (!odczytB.eof()){
	odczytB >> napisB;
	int dl=napisB.length();
	int dl2=dl/2;
	//na początku każdy wyraz jest palindromem
	bool palindrom=true;
	for (int i=0; i < dl2; i++)
		//porównujemy znak z początku ze znakiem od końca
		if (napisB[i]!=napisB[dl-1-i]) 
			//jeśli jakikolwiek znak różny to
			//nie jest palindromem
			palindrom=false; 
	// jest palindromem, bo porównało
	//i wszystkie znaki były zgodne
	if (palindrom) 
		cout << napisB << endl;
}
odczytB.close();

//dwa identyczne znaki
cout << "ASCII 220"<< endl;
ifstream odczytC("hasla.txt");
string napisC;
while (!odczytC.eof()){
	odczytC >> napisC;
	int dl=napisC.length();
	//sprawdzamy do przedostatniego znaku
	//aby nie zawiesił się na ostatnim [i+1]
	for (int i=0;i < dl-2;i++)
		//dwa znaki obok siebie takie same
		if (napisC[i] == napisC[i+1])
			cout << napisC << endl;
}
odczytC.close();
//zadanie 3 - CIĄGI ABC
//tworzenie pliku do zadania - jednorazowo
ofstream CIAGzapis("ciagi.txt");
string napis;
for (int i=1;i<=1000;i++){
	napis="";
	//losowe znali ABC
	napis=rand() % 3+65; 
	napis+=rand() % 3+65;
	napis+=rand() % 3+65;
	CIAGzapis << napis << endl;
}
CIAGzapis.close();

//takie same litery w ciągu
ifstream CIAGodczytA("ciagi.txt");
int licznikA=0;//liczymy identyczne
int licznikB=0;//liczymy palindromy
int licznikR=0;//liczymy łącznie
//string napis;
while (!CIAGodczytA.eof()){
  CIAGodczytA >> napis;
  licznikR++;
  //A - trzy znaki takie same
  //sprawdzamy zerowy z pierwszym i zerowy z drugim
  //nie potrzeba sprawdzać pierwszego z drugim
  if (napis[0]==napis[1] && 
      napis[0]==napis[2] 
	  //&& napis[2]==napis[1]
	  )
    licznikA++;
  //B - czy palindrom
  // wystarczy sprawdzić pierwszy z zostatnim
  if (napis[0]==napis[2])
  	licznikB++;
}
CIAGodczytA.close();
//takie "cuda" trzeba robić w c++
//aby całkowite zamieniło na rzeczywiste
//bo zwyczajne dzielenie nie potrafi!
double P_A=(licznikA*1.0)/(licznikR*1.0);
double P_B=(licznikB*1.0)/(licznikR*1.0);
cout << "P[A]=" << P_A << endl;
cout << "P[B]=" << P_B << endl;
//zadanie 4
//generujemy liczby z zakresu 1..10000
ofstream CYFRYzapis("cyfry.txt");
for (int i=1; i <= 1000; i++){
	int c=rand() % 10000 + 1;
	CYFRYzapis << c;
	//nowy wiersz bez ostatniego
	if (i < 1000) CYFRYzapis << endl;
}
CYFRYzapis.close();
//na końcu generuje pusty wiersz, który potem jest czytany !!!
//i dlatego trzeba dodatkowy warunek

//średnia z cyfr
ifstream CYFRYodczytA("cyfry.txt");
int liczbaA;
int suma=0;
while (!CYFRYodczytA.eof()){
  CYFRYodczytA >> liczbaA;
  suma=suma+liczbaA;
}
CYFRYodczytA.close();
//dzielenie przez rzeczywiste 1000.0 
//aby wynik był rzeczywisty
cout << "Średnia=" << suma/1000.0 << endl;

//parzyste z cyfr
ifstream CYFRYodczytB("cyfry.txt");
int liczbaB;
int CP=0;//licznik parzystych
int CN=0;//i nieparzystych
while (!CYFRYodczytB.eof()){
  CYFRYodczytB >> liczbaB;
  	if (liczbaB % 2 == 0) CP++;
  	else CN++;
}
CYFRYodczytB.close();
cout << "Parzyste=" << CP << endl;
cout << "nieParzyste=" << CN << endl;

//zadanie CYFRY c
//korzystamy z funkcji SUMACYFR
ifstream CYFRYodczytE("cyfry.txt");
int liczbaE,sumaE,liczbaEmax;
int sumaEmax=0;
while (!CYFRYodczytE.eof()){
	CYFRYodczytE >> liczbaE;
	sumaE=SUMACYFR(liczbaE);
	if (sumaE > sumaEmax){
		liczbaEmax=liczbaE;
		sumaEmax=sumaE;
	}
}
CYFRYodczytE.close();
cout << "SUMA CYFR max " << liczbaEmax << "  " << sumaEmax << endl;

//zadanie CYFRY d najczęściej
//zwiększamy liczniki w komórkach tablicy
ifstream CYFRYodczytD("cyfry.txt");
int liczbaD;
int ileD[10001];
//zerowanie tablicy liczników cyfr
for (int i=0; i <= 10001; i++)
	ileD[i]=0;
while (!CYFRYodczytD.eof()){
	CYFRYodczytD >> liczbaD;
	ileD[liczbaD]++;
}
CYFRYodczytD.close();
// a teraz trzeba wyszukać max w tablicy
int maxD=0;
int licD;
for (int i=1; i <= 10001; i++)
	if (ileD[i] > maxD){
		maxD=ileD[i];
		licD=i;
	}
cout << "Najczęściej: " << licD << "  " << maxD << " razy" << endl;
//zadanie 5 
//napisy dwójkowe 2..16 znaków 01
ofstream NDzapis("napisy.txt");
int ile;
int znak;
for (int j=1;j<=1000;j++){
	ile=rand() %15 +2; //2-16
	string napis="";
	for (int i=1;i<=ile;i++){
		znak=rand() % 2;//0 lub 1 losowo
		//znak ASCII '0' lub '1'
		napis=napis+char(znak+48); 
	}
	NDzapis << napis << endl;
}
NDzapis.close();

//zadanie a
ifstream NDodczytA("napisy.txt");
string NDnapisA;
int parz=0;
while (!NDodczytA.eof()){
	NDodczytA >> NDnapisA;
	if (NDnapisA.length() % 2 == 0) parz++;	
}
NDodczytA.close();
cout << "parzyste: " << parz << endl;

//zadanie b taka sama ilość zer i jedynek
ifstream NDodczytB("napisy.txt");
string NDnapisB;
//licznik wyrazów z taką samą iloścoą zer i jedynek
int suma01=0;
while (!NDodczytB.eof()){
	NDodczytB >> NDnapisB;	
	int dl=NDnapisB.length();
	//liczniki zer i jedynek w napisie
	int ile0=0;
	int ile1=0;
	//przelatujemy po napisie i sprawdzamy znaki
	for (int i=0; i < dl; i++)
		if (NDnapisB[i] == '0') ile0++; 
		else ile1++;
	//gdy taka sama ilość
	if (ile0 == ile1) suma01++;
}
NDodczytB.close();
cout << "tyle samo 0 i 1: " << suma01 << endl;

//zadanie c same zera lub same jedynki
ifstream NDodczytC("napisy.txt");
string NDnapisC;
int ile0=0;
int ile1=0;
while (!NDodczytC.eof()){
	NDodczytC >> NDnapisC;
	int dl=NDnapisC.length();
	int suma=0;
	for (int i=0; i < dl; i++)
		suma=suma+int(NDnapisC[i])-48;
	// gdy suma zer jest równa zero, to same jedynki
	if (suma == 0) ile0++;
	//gdy suma jest równa długości, to same jedynki
	if (suma == dl) ile1++;
}
NDodczytC.close();
cout << "same 0: " << ile0 << endl;
cout << "same 1: " << ile1 << endl;

//zadanie d ile napisów każdej długości
//tablica na liczniki napisów
int tablica[17];//od 0 do 16
for (int i=0; i < 17; i++) tablica[i]=0;
ifstream NDodczytD("napisy.txt");
string NDnapisD;
while (!NDodczytD.eof()){
	NDodczytD >> NDnapisD;
	int dl=NDnapisD.length();
	tablica[dl]++;
}
NDodczytD.close();
//wyniki
for (int i=2;i<17;i++) 
	cout << i << " " << tablica[i] << endl;

//SZABLON
#include <stdlib.h> //system
#include <iostream> //cout
#include <iomanip> //fixed
#include <cmath> //pow
#include <fstream> //ofstream
#include <ctime>//time

using namespace std;
int main(){
  setlocale(LC_ALL, "");


   system("pause");
}