XIII. Polecenia continue; break;

13.1. Sukcesywna nauka to podstawa sukcesu

Skoro poznaliśmy już wszystkie możliwe pętle, warto w tym miejscu wtrącić słowo o instrukcjach continue; i break;. Jeśli nadal masz problemy z posługiwaniem się pętlami, wróć do wcześniejszego materiału ponieważ ten rozdział zakłada już, że znasz dobrze zasady działania każdej pętli.

13.2. Słowo kluczowe continue;

Kompilator C++, jak w większości języków programowania umożliwia nam modyfikowanie w dość specyficzny sposób działanie pętli. Słowo kluczowe continue; użyte wewnątrz pętli powoduje przerwanie wykonywania bloku instrukcji i przechodzi do sprawdzenia warunku kończącego pętlę. Jeśli jest to pętla for(...), wykonuje się jeszcze inkrementacja (lub dekrementacja) zmiennej.

#include <iostream>
#include <conio.h>
using namespace std;
int main()
{
  int i=0;
  do
  {
    cout<<"i="<<i<<endl;
    if(i==0)
    {
      i+=1;
      continue;
    }
    cout<<"koniec"<<endl;
  }while(i==0);
  for(i=0;i<5;i++)
  {
    cout<<"[for] i="<<i<<endl;
    if(i>2) continue;
    cout<<"[for]koniec"<<endl;
  }
  getch();
  return(0);
}

13.3. Słowo kluczowe break;

Drugim słowem kluczowym, które modyfikuje działanie każdej pętli to break;. Użycie tego słowa wewnątrz pętli zmusi ją do natychmiastowego jej przerwania. Słowo kluczowe break; zatrzymuje pracę tylko jednej pętli. Oznacza to, że jeżeli zagnieździmy dwie pętle, to zostanie zatrzymana pętla ta, w której zostało słowo kluczowe break; użyte. Poniższy przykład ilustruje działanie tej instrukcji w praktyce.
#include <iostream>
#include <conio.h>
using namespace std;
int main()
{
  int j;
  for(j=0;j<10;j++)
  {
    cout<<"Aktualne j to: "<<j<<endl;
    for(int i=0;i<5;i++)
    {
      cout<<"[for] i="<<i<<endl;
      if(i>2) break;
      cout<<"[for]koniec"<<endl;
    }
    if(j>6) break;
  }
  getch();
  return(0);
}

13.4. Wskazana powściągliwość

Zanim zaczniesz nagminnie używać tych poleceń, zastanów się lepiej jak można napisać kod unikając używania słów kluczowych continue; i break;. Praktycznie zawsze istnieje możliwość napisania pętli bez użycia wymienionych słów kluczowych. Używaj ich tylko w skrajnie uzasadnionych przypadkach czyli wtedy gdy naprawdę uważasz, że czytelniejszym rozwiązaniem będzie użycie jednego z wymienionych słów kluczowych, niż modyfikacja pewnej partii kodu.

XIV. Struktury danych

14.1. Organizacja danych

Jeśli próbowałeś już pisać jakąś bazę danych, np. przechowującą kontakty telefoniczne do różnych osób to zauważyłeś pewnie, że szukanie nazw tablic i posługiwanie się wieloma tablicami dla jednej osoby nie jest wygodne. Z pomocą przychodzą tu struktury, które pozwalają na zorganizowanie danych w wygodniejszy sposób. Struktury potrafią przechowywać różne typy danych. Deklaracja struktury wygląda następująco: 
//struktura dla użytkowników forum
struct Uzytkownicy_DDT //deklaracja struktury
{     //opis struktury
  std::string nik;
  std::string haslo;
  short data_zalozenia_konta;
  std:string status;
};

Elementy charakteryzujące ten typ danych to:

→ słowo kluczowe struct, które informuje kompilator, że będzie definiowana struktura,

→ opis struktury opisujący zawartość struktury,

→ jest to najprostsza postać klasy.

W przykładzie został wykorzystana biblioteka string, zapoznaj się z nią nim przejdziesz do analizy kursu!

14.2. Wykorzystanie struktury w praktyce

Ponieważ wiemy już jak utworzyć własny, bardziej złożony typ danych to dobrze by było, gdybyśmy teraz wiedzieli jak go możemy wykorzystać. Zmienną deklarujemy tak samo, jak w przypadku standardowych typów danych.

//struktura dla użytkowników forum------
#include <iostream>
#include <string>
#include <conio.h>
//struktura dla użytkowników forum
struct Uzytkownicy_DDT //deklaracja struktury
{
  //opis struktur
  std::string nik;
  std::string haslo;
  short lp;
  std::string status;
  std::string rodzaj_konta;
};

int main()
{
  using namespace std;
  //Tworzenie obiektu struktury i wypełnianie-------
  Uzytkownicy_DDT PiotrSzawdynski =
  {
    "PiotrSzawdynski",
    "!@#$%%^",
    1,
    "Aktywny",
    "Admin"
  };
  /*WAŻNE dla niektórych kompilatorów należy
użyć zapisu static Uzytkownicy_DDT PiotrSzawdynski =*/
  Uzytkownicy_DDT Pietrzuch =
  {
    "Pietrzuch",
    "@m@m$@#@",
    24,
    "Aktywny",
    "Moderator"
  };
  /*inny sposób wprowadzania danych do
    obiektu*/
  //tworzenie obiektu
  Uzytkownicy_DDT Piotrus_Pan;
  /*wypełnianie obiektu wartościami dla
    poszczególnych typów danych*/
  Piotrus_Pan.haslo = "PanPiotr";
  Piotrus_Pan.lp = 927;
  Piotrus_Pan.nik = "Piotrus_Pan";
  Piotrus_Pan.rodzaj_konta = "Uzytkownik";
  Piotrus_Pan.status = "zablokowany";

  //wyświetlenie obiektów
  cout << "Oto lista uzytkownikow forum DDT"
  << endl << "Nik " << PiotrSzawdynski.nik
  << endl << "haslo " << PiotrSzawdynski.haslo
  << endl << "Nr usera " << PiotrSzawdynski.lp
  << endl << "Status " << PiotrSzawdynski.status
  << endl << "Rodzaj Konta "
  << PiotrSzawdynski.rodzaj_konta
  << endl
  << endl << "Nik " << Pietrzuch.nik
  << endl << "haslo " << Pietrzuch.haslo
  << endl << "Nr usera " << Pietrzuch.lp
  << endl << "Status " << Pietrzuch.status
  << endl << "Rodzaj Konta "
  << Pietrzuch.rodzaj_konta
  << endl
  << endl << "Nik " << Piotrus_Pan.nik
  << endl << "haslo " << Piotrus_Pan.haslo
  << endl << "Nr usera " << Piotrus_Pan.lp
  << endl << "Status " << Piotrus_Pan.status
  << endl << "Rodzaj Konta "
  << Piotrus_Pan.rodzaj_konta;

  getch();
  return(0);
}

14.3. Analiza kodu

Jak pewnie się zorientowałeś strukturę można wypełnić podobnie jak tablicę, gdy oczywiście przed uruchomieniem programu znamy wartości jakie ma zawierać. Co warte podkreślenia struktura Uzytkownicy_DDT posiada trzy obiekty PiotrSzawdynski,  Pietrzuch,  Piotrus_Pan. By wprowadzić bądź wyświetlić wartości danego obiektu, musimy użyć kropki( . )!!!
nazwa_obiektu.nazwa_zmiennej = wartość_wprowadzana

Większość kompilatorów po wprowadzeniu kropki z klawiatury wyświetla podpowiedz dla danego obiektu(typy danych dla tego obiektu(struktury)).

WAŻNE !!! dla niektórych kompilatorów należy użyć słowa kluczowego static Uzytkownicy_DDT PiotrSzawdynski.
static Uzytkownicy_DDT PiotrSzawdynski =
{
  "PiotrSzawdynski",
  "!@#$%%^",
  1,
  "Aktywny",
  "Admin"
};

14.4. Tablice struktur

//Tablice struktur------------------------------
#include <iostream>
#include <string>
#include <conio.h>
//struktura dla użytkowników forum
struct DDT //deklaracja struktury
{
  //opis struktury
  std::string nik;
  std::string haslo;
  std::string status;
  std::string rodzaj_konta;
};

int main()
{
  using namespace std;
  //Tworzenie obiektu struktury-------
  DDT Uzytkownicy[3];
  cout << "Wprowadz dane.\n";
  //wprowadzenie 3 uzytkowników
  for(short licz = 0; licz < 3; licz++){
    cout << "Podaj nik ";
    cin >> Uzytkownicy[licz].nik;
    cout << "Podaj haslo ";
    cin >> Uzytkownicy[licz].haslo;
  }
  //wyswietlanie wprowadzonych danych
  cout << endl
  << "Oto rezultat wprowadzonych danych:"
  << endl;
  for(short licz = 0; licz < 3; licz++){
    cout << "Uzytkownik " << licz + 1
    << " Nik " << Uzytkownicy[licz].nik
    << "   haslo " << Uzytkownicy[licz].haslo
    << endl;
  }

  getch();
  return(0);
}
//--------------------------------------------

Właściwie nie ma różnić między użyciem tablicy zmiennych a tablicy struktur. Oczywiście możemy też wypełnić tablice w ten sposób:
DDT Uzytkownicy[3] =
{
  //Użytkownik[0]
  {"PiotrSzawdynski", "!@#$%%^"},
  //Użytkownik[1]
  {"Pietrzuch", "@m@m$@#@"},
  //Użytkownik[2]
  {"Piotrus_Pan", "PanPiotr"}
};

14.5. Przypisanie struktury do struktury

Tak jak zmienną można przypisać do innej zmiennej tak samo można postąpić z strukturą danych. Wystarczy użyć operatora „=”.

//Przypisanie struktur------------------------------
#include <iostream>
#include <string>
#include <conio.h>
//struktura dla użytkowników forum
struct DDT //deklaracja struktury
{
  //opis struktury
  std::string nik;
  short wiek;
  char plec;

};

int main()
{
  using namespace std;
  DDT Monika, Mariola, Kasia;

  Monika.nik = "Mika";
  Monika.plec = 'K';
  Monika.wiek = 20;

  //przypisanie struktur
  Kasia = Mariola = Monika;

  cout << "Wyswietla uzytkownikow plci pieknej"
  << endl
  << "Nik - " << Kasia.nik << endl
  << "Nik - " << Mariola.nik << "  wiek "
  << Mariola.wiek << endl
  << "Nik - " << Monika.nik << "  plec "
  << Monika.plec << endl;
  getch();
  return(0);
}
//--------------------------------------------

14.6. Ćwiczenia

1. Farmer zwrócił się do Ciebie z prośbą byś napisał program, który będzie przechowywał informacje o rodzajach zwierząt jakie posiada. Zwierzęta to krowa, koza, kura, pies i świnia. Cechy tych zwierząt to nazwa, waga, wiek(podany w tygodniach), oraz data nabycia(zakupu). Farmer dodał, iż krowę i świnię kupił 24-04-08r od znajomego, pies przybłąkał się 6.05.02r, natomiast kozę i kurę dostał w prezencie od wójta 23.02.09. Niestety resztę cech musisz określić sam, na podstawie przyjętych średnich(google) dla tych zwierząt. Dane mają być zawarte w strukturze. Rezultatem programu ma być:

a) pierwszy program→  wyświetlenie wszystkich danych w przejrzysty i zrozumiały dla użytkownika sposób, użytkownik nic nie podaje, program wyświetla tylko dane.

b) drugi program→ program ma poprosić o wprowadzenie nazwy i wagi(w kilogramach) zwierzęcia(mają być wprowadzone trzy zwierzaki) , oraz wyświetleniu danych w odwrotny sposób do tego w jaki zostały wprowadzone, oraz  zachowaniu takiej kompozycji
Trzoda zawiera m.in.:
1.→ nazwa_zwierzęcia jego waga to waga_zwierzęcia(podana w gramach) gram.
2.→ 
3.→

XV. Wskaźniki

15.1. Odczytywanie adresu pamięci istniejących zmiennych

Język C++ w bardzo łatwy sposób umożliwia nam pobieranie adresu pamięci wybranych zmiennych. Wskaźnik zajmuje zazwyczaj 4 bajty bez względu na jaki typ danych wskazuje. Rozmiar wskaźnika może być jednak różny w zależności od użytego kompilatora (np. gdy użyjemy 64 bitowego kompilatora). Wskaźnik zwraca adres pierwszego bajta danych wybranej zmiennej. Aby pobrać adres dowolnej zmiennej wystarczy napisać: 

&nazwa_zmiennej.
#include <iostream>
#include <conio.h>
using namespace std;
int main()
{
  int zmienna1=213;
  int tablica[]={1,2,3,4,5,6,7,8,9,10};
  struct
  {
    int liczba;
    long long duzaLiczba;
  }struktura;
  cout<<"Adres zmienna1="<<&zmienna1<<endl<<endl;
  cout<<"Adres tablica="<<&tablica<<endl;
  cout<<"Adres tablica[0]="<<&tablica[0]<<endl;
  cout<<"Adres tablica[1]="<<&tablica[1]<<endl<<endl;
  cout<<"Adres struktura="<<&struktura<<endl;
  cout<<"Adres struktura.liczba="<<&(struktura.liczba)<<endl;
  cout<<"Adres struktura.duzaLiczba="<<&(struktura.duzaLiczba)<<endl;
  getch();
  return(0);
}

Zauważmy, że wskaźnik ze zmiennej tablica i ze zmiennej tablica[0] jest taki sam. Dzieje się tak dlatego, że wskaźnik ze zmiennej tablica wskazuje na początek wszystkich danych w tablicy, a pierwszym elementem jest tablica[0]. To samo dotyczy adresu zmiennej struktura i struktura.liczba. Adresy zmiennych są wyświetlane w postaci szesnastkowej.

15.2 Wskaźniki pierwsze spojrzenie.

//Wskaźniki pierwsze spojrzenie----------
#include <iostream>
#include <conio.h>
int main()
{
  using namespace std;

  int liczba = 9;
  int *wsk_liczba; /*--deklaracja wskaźnika
          na int--*/
  wsk_liczba = &liczba; /* przypisanie wskaźnikowi
                    adresu int */
  //dwa sposoby wyświetlenia wartości liczba
  cout << "Zmienna liczba = " << liczba
  << "\ni *wsk_liczba jako zmienna liczba = "
  << *wsk_liczba
  << endl;
  //dwa sposoby wyświetlenia adresu zmiennej
  cout << "Adres liczby = " << &liczba
  << "\ni wsk_liczba jako adres liczby = "
  << wsk_liczba 
  << endl;
  //zmiana wartości za pomocą wskaźnika
  *wsk_liczba = *wsk_liczba + 1;
  cout << "Liczba = " << liczba;

  getch();
  return 0;
}
//---------------------------------------

Deklaracja zmiennej wskaźnikowej jest również prosta. Aby utworzyć zmienną wskaźnikową, to po typie zmiennej dopisujemy *(gwiazdkę). Tak więc, jeśli chcemy utworzyć wskaźnik, który ma wskazywać na liczbę typu int, zapis ten będzie wyglądał tak:
Stary zapis C
// int *wsk_liczba;

Nowy zapis C++
//int* liczba, liczba1

gdzie dokładnie znajduje się *(gwiazdka) dla kompilatora nie ma znaczenia można nawet zapisać w ten sposób:
//int * liczba

Jakiego Ty będziesz używał zapisu zależy od Ciebie, uważaj jednak na taki zapis:
//int* liczba, liczba1

w ten sposób deklarujesz jeden wskaźnik liczba i jeden zmiennej typu int liczba1. Ważne !!! zmienna, która ma być wskaźnikiem musi zawierać gwiazdkę.

15.2.1 Wyświetlanie adresu wskaźnika

Jeśli wypiszemy teraz wartość zmiennej wskaźnik, otrzymamy liczbę wyświetloną szesnastkowo.

#include <iostream>
#include <conio.h>
using namespace std;
int main()
{
  long long zmienna=213;
  long long* wskaznik=&zmienna;
  cout<<"&zmienna="<<&zmienna<<endl;
  cout<<"wskaznik="<<wskaznik<<endl;
  getch();
  return(0);
}

Jak pokazuje przykład i jak można było się tego spodziewać, wartość adresu wskaźnika jest taka, jaką do niego przypisaliśmy(long long* wskaznik=&zmienna;).

15.2.2 Wyświetlanie danych, na które wskazuje adres wskaźnika

Aby wyświetlić dane jakie znajdują się pod adresem jaki mamy zapisany we wskaźniku, musimy przed nazwą zmiennej dopisać *. Tak więc, modyfikując poprzedni program, będzie to wyglądało tak:

#include <iostream>
#include <conio.h>
using namespace std;
int main()
{
  long long zmienna=213;
  long long* wskaznik=&zmienna;
  cout<<"zmienna="<<zmienna<<endl;
  cout<<"*wskaznik="<<*wskaznik<<endl;
  getch();
  return(0);
}

15.3. Modyfikacja danych, na które wskazuje wskaźnik

Mając zapisany adres do zmiennej we wskaźniku, mamy możliwość zmiany wartości zmiennej nie używając nazwy zmiennej, z której pobraliśmy adres. Przykład:

#include <iostream>
#include <conio.h>
using namespace std;
int main()
{
  long long zmienna=213;
  long long* wskaznik=&zmienna;
  cout<<"zmienna="<<zmienna<<endl;
  *wskaznik=50;
  cout<<"zmienna="<<zmienna<<endl;
  getch();
  return(0);
}

15.4. Bezpieczeństwo a użycie wskaźników.

Podczas używania wskaźników można popełnić błąd przy tworzeniu i używaniu wskaźnika. Jeżeli nie zadbamy o przypisanie wskaźnikowi adresu.
int *wsk_liczba;
*wsk_liczba = 373;

Jest to błąd, ponieważ nie wiemy gdzie(pod jakim adresem) umieszczona jest wartość 373. Kompilator będzie starał się umieścić wartość 373 bo takim właśnie adresem. Co jeżeli jednak adres ten będzie już zajęty przez program? Taka sytuacja może spowoduje, iż nie będzie można było zapisać nic w miejsce wskazane przez wsk_liczba. Błąd ten jest nie dopuszczalny i do tego trudno go wykryć. Pamiętaj gdy używasz wskaźnika zadbaj o to by zawsze miał przypisaną zmienną(miał prawidłowy adres).

15.5. Arytmetyka wskaźników.

Wskaźniki posiadają pewne podobieństwo z nazwami tablic. Wywodzi się to z arytmetyki wskaźników i sposobie przedstawienia tablic w C++. Przykład pokazuje wspomnianą analogię:

//Wskaźniki drugie starcie---------------
#include <iostream>
#include <conio.h>
int main()
{
  using namespace std;
  //tablice deklaracia inicjalizacja
  double waga[5] = {55.3, 747.8, 1001.2, 5.2, 6.4};
  short odliczanie[4] = {3, 2, 1, 0};
  //wskaźniki
  double *wsk_waga = waga; //nazwa tabeli = adres
  short *wsk_odliczanie = &odliczanie[0];

  //Wyświetlanie adresu i wartości wskaźnika wsk_waga
  cout << "wsk_waga = " << wsk_waga
  << ", *wsk_waga = " << *wsk_waga
  << endl
  << "Dodawanie wsk_waga + 1 ";
  wsk_waga += 1;
  cout << "\nTeraz wsk_waga = " << wsk_waga
  << ", *wsk_waga = " << *wsk_waga << endl
  << endl;
  //Wyświetlanie adresu i wartości wskaźnika wsk_odliczanie
  cout << "wsk_odliczanie = " << wsk_odliczanie
  << ", *wsk_odliczanie = " << *wsk_odliczanie
  << endl
  << "Dodawanie wsk_odliczanie + 1 ";
  wsk_odliczanie += 1;
  cout << "\nTeraz wsk_odliczanie = " << wsk_odliczanie
  << ", *wsk_odliczanie = " << *wsk_odliczanie << endl
  << endl;

  //Wyświetlanie zapisu tablicowego
  cout << "\nPodobienstwa tablic i wskaznikow\n"
  << "Pierwszy element tab waga[0] = "
  << waga[0] << endl
  << "Drugi element tab odliczanie[1] = "
  << odliczanie[1] << endl << endl;

  //Wyświetlanie zapisu wskaźnikowego
  cout << "Pierwszy element tab waga "
  "z uzyciem wskaznika *waga = "
  << *waga << endl
  << "Drugi element tab odliczanie "
  "z uzyciem wskaznika *(odliczanie + 1) = "
  << *(odliczanie + 1) << endl << endl << endl;

  //porównanie wielkości tablic i wskaźników
  cout << "Tablica waga wazy " << sizeof(waga)
  << " bajtow!" << endl
  << "Jednak wskaznik na ta tablice *wsk_waga "
  << "wazy tylko " << sizeof(wsk_waga)
  << " bajty!" << endl;

  getch();
  return 0;
}
//---------------------------------------

Pierwsza część programu wyświetlenia adresy wskaźników *wsk_waga i *wsk_odliczanie, oraz wartości zapisane pod adresami na które wskazują. Następnie dodajemy 1 do obu wskaźników, co powoduje przesunięcie ich adresów o 8 bajtów dla wsk_waga(ponieważ typ double to 8 bajtów) i 2 bajty dla wsk_odliczanie(typ short to 2 bajty). Przesunięcie powoduje, iż oba wskaźniki wskazują na adres drugiej wartość w obu tablicach. Wniosek dodanie do wskaźnika 1 powoduje jego przesunięcie o tyle bajtów ile ma wskazany typ danych.

Kolejna część programu pokazuje zależności między tablicą a wskaźnikiem. Dlaczego zapisy waga[0] = *waga, oraz odliczanie[1] = *(odliczanie + 1) dają ten sam rezultat? Ponieważ tak działa kompilator C++, zamienia on zapis tab[10] dla bardziej czytelny sobie zapis *(tab + 10). Stąd właśnie wskaźniki i nazwy tablic można używać zamiennie. Różnice między tab., a wsk. to:
NazwaWskaźnik = NazwaWskaźnika + 1; //prawidłowo
NazwaTabel = NazwaTabeli + 1; // błąd !!!  

oraz różnią się wielkością, którą możesz sprawdzić sam stosując operator sizeof.

15.6. Dostęp do danych struktury za pośrednictwem wskaźnika

Jeśli chcemy odczytać lub zapisać dane do struktury za pomocą wskaźnika wskazującego na nią, postępujemy prawie tak samo jak w przypadku zwykłej zmiennej - poprzedzamy wskaźnik znakiem *. Wskaźnik ten musimy jednak umieścić w okrągłe nawiasy, żeby kompilator wiedział czego się tyczy symbol *. Kolejny przykład:

#include <iostream>
#include <conio.h>
using namespace std;
int main()
{
  struct daneST
  {
    int liczba;
    char znak;
  };
  daneST dane;
  dane.liczba=55;
  dane.znak='a';
  daneST* wskaznik=&dane;
  cout<<"(*wskaznik).liczba="<<(*wskaznik).liczba<<endl;
  (*wskaznik).liczba=99;
  cout<<"dane.liczba="<<dane.liczba<<endl;
  getch();
  return(0);
}

15.7. Wskaźniki i struktury po raz drugi

Oprócz przedstawionej wyżej metody uzyskiwania dostępu do danych istnieje również drugi, który jest równoważny pierwszemu. Jest on moim zdaniem wygodniejszy w użyciu, jednak chciałem pokazać Ci różne zapisy ponieważ starsi programiści, którzy 'przesiedli' się z C na C++ korzystają zazwyczaj z pierwszego zapisu. Zamiast poprzedzać zmienną wskaźnikową gwiazdką i wstawiać ją w nawiasy, wystarczy kropkę zastąpić zapisem takim zapisem: ->. Przykład z poprzedniego podrozdziału ze zmodyfikowanym zapisem przedstawiam poniżej.

#include <iostream>
#include <conio.h>
using namespace std;
int main()
{
  struct daneST
  {
    int liczba;
    char znak;
  };
  daneST dane;
  dane.liczba=344;
  dane.znak='a';
  daneST* wskaznik=&dane;
  cout<<"wskaznik->liczba="<<wskaznik->liczba<<endl;
  wskaznik->liczba=221;
  cout<<"dane.liczba="<<dane.liczba<<endl;
  getch();
  return(0);
}

15.8. Podsumowanie

Przeanalizuj dokładnie cały materiał, jaki znalazł się w tym rozdziale. Dobra znajomość całej teorii o wskaźnikach będzie niezbędna, gdy dojdziesz do rozdziału poświęconemu dynamicznemu zarządzaniu pamięcią.

15.9 Ćwiczenia

1. Popraw błędy w następującym kodzie:
//Wskaźniki pierwsze zadanie----------
#include <iostream>
#include <conio.h>
int main()
{
  using namespace std;

  short zmienna = 213;
  short long* wskaznik = zmienna;

  //Wyświetlanie adresu wskaźnika
  cout << "&zmienna=" << zmienna << endl
  cout <<"wskaznik=" << wskaznik << endl;
  //Wyświetlanie danych, na które wskazuje adres wskaźnika
  cout  "Adres zmienna=" << *zmienna <<endl;
  cout << "*wskaznik=" << wskaznik
  //Modyfikacja danych, na które wskazuje wskaźnik
  cout << "zmienna="<<zmiena<<endl;
  *wskaznik = &50;
  cout << "zmienna=" << zienna <<endl;

  getch();
  return 0;
}
//---------------------------------------

XVI. Obsługa plików

16.1. Biblioteka odpowiedzialna za obsługę plików

Pisząc nasze programy, prędzej czy później zajdzie potrzeba zapisywania danych na dysku. Z pomocą przychodzi tu biblioteka fstream, dzięki której uzyskujemy funkcje pozwalające nam zarówno zapisywać pliki jak i je odczytywać.

16.2. Typ zmiennej fstream

Zanim zaczniemy odczytywać, bądź zapisywać dane z/do pliku, musimy posiadać zmienną, dzięki której będziemy mogli wykonywać operacje na wybranym pliku. W tym celu utworzona została klasa fstream. Klasa ta jest umieszczona w przestrzeni nazw std::. Klasa ta udostępnia nam cały interfejs, dzięki któremu będziemy mogli obsłużyć dowolny plik znajdujący się na dysku lub innym nośniku danych.
std::fstream plik;

16.3. Otwieranie pliku

Zmienna, którą utworzyliśmy aktualnie nie wskazuje na żaden plik. Aby przypisać konkretny plik do zmiennej wywołujemy funkcję open(), której definicja wygląda następująco:
void open(const char* nazwa_pliku,ios_base::openmode tryb_otwarcia_pliku);

Pierwszy parametr funkcji (nazwa_pliku) określa ścieżkę dostępu i nazwę pliku do jakiego chcemy uzyskać dostęp. Drugi parametr funkcji, czyli tryb_otwarcia_pliku służy do poinformowania kompilatora w jakim trybie dany plik chcemy otworzyć. Lista dostępnych trybów wraz z opisami w poniższej tabeli.
Tryb	
Opis trybu

ios::app	
(append - dopisywanie danych do pliku) Ustawia wewnętrzny wskaźnik zapisu pliku na jego koniec. Plik otwarty w trybie tylko do zapisu. Dane mogą być zapisywane tylko i wyłącznie na końcu pliku.

ios::ate	
(at end) Ustawia wewnętrzny wskaźnik pliku na jego koniec w chwili otwarcia pliku.

ios::binary	
(binary) Informacja dla kompilatora, aby dane były traktowane jako strumień danych binarnych, a nie jako strumień danych tekstowych.

ios::in	
(input - wejście/odczyt) Zezwolenie na odczytywanie danych z pliku.

ios::out	
(output - wyjście/zapis) Zezwolenie na zapisywanie danych do pliku.

ios::trunc	
(truncate) Zawartość pliku jest tracona, plik jest obcinany do 0 bajtów podczas otwierania.

Wszystkie wymienione tryby możemy łączyć ze sobą - oznacza to, że jeśli chcemy otrzymać plik do odczytu i zapisu wystarczy oddzielić je pojedynczym operatorem |.
std::fstream plik;
plik.open("nazwa_pliku.txt",std::ios::in|std::ios::out);

16.3.1. Czy udało otworzyć się plik?

Zdecydowana większość kursów pomija ten bardzo ważny krok, jaki należy implementować we własnych kodach źródłowych - sprawdzanie czy plik został otwarty prawidłowo. Operacja jest bardzo prosta, jednak prawie zawsze zaniedbywana nawet w książkach poświęconych programowaniu!

Po wykonaniu operacji otwarcia pliku, wewnątrz klasy ustawiane są odpowiednie flagi, które informują o tym, czy otrzymaliśmy dostęp do pliku czy też nie. Funkcje, jakie umożliwiają nam sprawdzenie tego stanu to good() oraz is_open(). Definicja tych funkcji wygląda następująco:
bool good();
bool is_open();

Obie funkcje zwrócą wartość true, jeśli uzyskano dostęp do pliku, w przeciwnym wypadku otrzymamy wartość false.

std::fstream plik;
plik.open("nazwa_pliku.txt",std::ios::in|std::ios::out);
if(plik.good()==true)
{
  std::cout<<"Uzyskano dostep do pliku!"<<std::endl;
  //tu operacje na pliku
}else std::cout<<"Dostep do pliku zostal zabroniony!"<<std::endl;

16.3.2. Kiedy nie uzyskamy dostępu do pliku

Próba odczytu:
Plik nie istnieje na dysku;
Nie posiadamy uprawnień odczytu do pliku.

Próba zapisu:
Nie posiadamy uprawnień pozwalających nam modyfikować plik;
Nie posiadamy uprawnień do katalogu w którym chcemy utworzyć plik;
Nośnik, na którym chcemy dokonać zapisu jest tylko do odczytu.
16.4. Zamykanie pliku

Każdy plik należy zamykać po zakończeniu pracy z nim. Jeśli plik ma być używany tylko przez jednego użytkownika szkodliwość jest stosunkowo mała - klasa fstream sama zamknie plik przed usunięciem zmiennej z pamięci. Jeśli natomiast zapomnisz zamknąć plik, którego dane mają być współdzielone przez kilku użytkowników, automatycznie uniemożliwisz im dostęp do tego zasobu. Funkcja odpowiedzialna na zamykanie pliku nosi nazwę close(). Deklaracja wygląda następująco:
void close(void);

16.4.1. Przykład

Poniższy przykład pokazuje jak należy prawidłowo posługiwać się otwartym plikiem.

#include <fstream>
int main()
{
  std::fstream plik;
  plik.open("nazwa_pliku.txt",std::ios::in|std::ios::out);
  if(plik.good()==true)
  {
    //tu operacje na pliku (zapis/odczyt)
    plik.close();
  }
  return(0);
}
16.5. Odczytywanie danych z pliku

Jeśli uzyskamy już dostęp do pliku w trybie do odczytu, możemy rozpocząć odczytywanie danych z pliku. Język C++ oferuje więcej niż jedną metodę odczytu danych z pliku.

16.5.1. Pobieranie danych za pomocą strumienia

Pierwszą, a zarazem bardzo wygodną metodą odczytywania danych z pliku jest strumień. Ponieważ zapis jest analogiczny do strumienia std::cin>>, przedstawiam tylko formę zapisu.
nazwa_zmiennej_plikowej>>zmienna_do_ktorej_dane_maja_zostac_zapisane;

Co należy wiedzieć o strumieniu:
Dane odczytywane za pomocą strumienia są zawsze traktowane jako tekst, niezależnie czy podczas otwierania użyliśmy trybu ios::binary czy nie.
Strumień działa analogicznie do std::cin>>, co w konsekwencji oznacza, że za pomocą tej funkcji nie odczytamy żadnej informacji o białych znakach (tj. enter, tabulacja, spacja itp).

16.5.2. Pobieranie danych wierszami

Kolejną metodą na odczytanie danych, to użycie funkcji getline(). Funkcja ta została omówiona w rozdziale XVIII. Biblioteka <string>. Przykład:

std::fstream plik("nazwa_pliku.txt",std::ios::in);//zakładamy, że plik istnieje
std::string dane;
getline(plik,dane);//wczytanie CAŁEGO jednego wiersza danych

Istnieje również druga funkcja służąca do wczytywania danych wierszami, jednak wydaje się ona mniej wygodna w użyciu. Jest nią funkcja getline(), zaszyta wewnątrz klasy fstream.
istream& getline (char* odczytane_dane, streamsize ilosc_danych, char znak_konca_linii);

Parametry oznaczają kolejno:

(odczytane_dane) wskaźnik zmiennej, do której mają zostać wczytane dane z pliku;
(ilosc_danych) maksymalna ilość znaków jakie mogą zostać zapisane do zmiennej;
(znak_konca_linii) parametr jest opcjonalny. Umożliwia zmianę znaku końca linii.

Przykład wykorzystania tej funkcji:

std::fstream plik("nazwa_pliku.txt",std::ios::in);//zakładamy, że plik istnieje
char dane[255];
plik.getline(dane,255);//wczytanie jednego wiersza danych (lub częśći wiersza jeśli sie nie zmieści)

Co należy wiedzieć o obu funkcjach getline():

Dane odczytywane za pomocą funkcji getline() są zawsze traktowane jako tekst, niezależnie czy podczas otwierania użyliśmy trybu ios::binary czy nie.

16.5.3.  Pobieranie danych blokami

Pobieranie danych blokami jest jedną z najszybszych metod na odczytywanie danych. Co więcej jest to bezpieczna metoda dla danych binarnych (pod warunkiem włączenia trybu ios::binary). Minusem tej metody jest niezbyt poręczna forma w jakiej otrzymujemy dane. Budowa tej funkcji wygląda następująco:
istream& read(char* bufor, streamsize rozmiar_bufora);

Pierwszym parametrem przekazywanym do funkcji jest wskaźnik do którego mają zostać wczytane dane. Drugi parametr określa rozmiar bufora. Pamiętaj, że bufor może nie być wypełniony do końca danymi. Aby sprawdzić ile bajtów danych zostało faktycznie wczytanych do bufora, należy posłużyć się tu funkcją gcount(). Przykład:
std::fstream plik("nazwa_pliku.txt",std::ios::in);//zakładamy, że plik istnieje
char bufor[1024];
plik.read (bufor,1024);//wczytuje tyle danych ile się zmieści do bufora
std::cout<<"Wczytano "<<plik.gcount()<<" bajtów do bufora"<<std::endl;

Co należy wiedzieć o funkcji read():
Dane odczytywane za pomocą funkcji read() są traktowane jako dane binarne, jeśli użyliśmy trybu ios::binary.

16.5.4. Inne metody na odczytywanie danych

C++ oferuje również inne metody odczytywania danych. Nie będą one jednak tu omówione ponieważ te, które zostały poruszone w tym rozdziale są wystarczające do pełnego wykorzystywania możliwości wczytywania danych z plików.

16.6. Zapisywanie danych do pliku

Zapisywanie danych do pliku jest równie proste jak ich odczytywanie. Po otwarciu pliku do zapisu możemy korzystać z kilku technik umożliwiających zapisywanie danych. Zanim jednak je poznasz musisz zdać sobie sprawę, że dane do pliku można albo dopisywać tylko i wyłącznie na końcu pliku albo nadpisywać dane jeśli nie jesteśmy na jego końcu. Nie można dopisywać tekstu pomiędzy istniejące dane jak to często robimy w edytorach tekstowych. Pamiętaj więc, jeśli chcesz otworzyć plik do zapisu zastanów się conajmniej dwa razy, bo tutaj błąd może kosztować nawet utratę całej zawartości pliku. Jeśli chcesz testować działanie funkcji służących do zapisu danych polecam utworzyć najpierw pusty plik i wpisać ręcznie do niego jakieś nieistotne dane lub pracować na kopii pliku, zawierającego ważne dane. W razie wykonania jakiegoś rażącego błędu będziesz mógł przywrócić szybko dane.

16.6.1. Zapisywanie danych za pomocą strumienia

Zapisywanie danych za pomocą strumienia jest analogicznym działaniem do std::cout<<. Jedyną istotną różnicą, jaka ma tu miejsce to fakt, że wyjściem jest teraz plik, a nie konsola.
nazwa_zmiennej_plikowej<<zmienna_ktora_ma_zostac_zapisana_do_pliku;

Tak samo jak w przypadku odczytywania danych za pomocą strumienia, zapisywane dane tą techniką są zawsze traktowane jako tekst niezależnie od ustawienia trybu ios::binary. Każdorazowe zapisanie danych powoduje przesunięcie wskaźnika o tyle znaków ile zostało zapisanych do pliku.

16.6.2. Zapisywanie danych blokami

Gdy zapisywanie danych w postaci tekstu jest dla nas niewystarczające (a przy profesjonalnym podejściu do większości projektów tak właśnie jest) z pomocą przychodzi nam kolejna funkcja klasy fstream i jest to write(). Definicja tej funkcji wygląda następująco:
ostream& write(const char* bufor, streamsize ilosc_danych_do_zapisu );

Pierwszy parametr (bufor) to wskaźnik bufora, w którym znajdują się dane jakie chcemy zapisać do pliku. Drugim parametrem (ilosc_danych_do_zapisu) informujemy kompilator ile danych ma zostać zapisanych do pliku z bufora. Wraz z wykonaniem tej operacji wskaźnik wewnętrzny pliku przesuwa się do przodu o ilość bajtów zapisanych do pliku.
std::fstream plik("nazwa_pliku.txt",std::ios::out);//zakładamy, że nie wystąpił błąd (plik otwarto/utworzono)
std::string napis;
getline(std::cin,napis);
plik.write (&napis[0],napis.length());//zapisuje dane poczynając od 0 indeksu

16.6.3. Zapisywanie danych w szczegółach

Jeśli napiszesz sobie program, który będzie zapisywał do pliku wczytywane wiersze z klawiatury aż do napotkania pustego wiersza pewnie zauważysz, że rozmiar pliku się nie zmienia zaraz po dopisaniu danych. Dzieje się tak dlatego, że klasa fstream ma wewnętrzny bufor, który ma na celu przyśpieszenie operacji dyskowych. Każdorazowy dostęp do wybranego obszaru dysku wymaga bardzo dużego czasu w porównaniu do szybkości pamięci podręcznej. Dane zanim trafią na dysk są umieszczane najpierw w buforze, a następnie gdy bufor się zapełni zostają zapisywane na dysk. Dzięki takiemu podejściu do zapisywania danych w pliku proces jest dużo szybszy. Przykładowo, jeśli jednorazowe ustawienie głowicy dysku na określonej pozycji zajmuje np. 2ms, to zapisanie długiego zdania znak po znaku zajęłoby: 2ms*ilość_znaków czasu. Wbudowany system buforowania danych zamiast zapisywać tak często dane, zapisze je najpierw do bufora, a później wyśle je na dysk oszczędzając jednocześnie mnóstwo zasobów sprzętowych komputera. System ten jest zawsze sprawny niezależnie od tego czy skaczesz po pliku w różne miejsca, czy dopisujesz stale dane na jego końcu.

16.6.4. Kontrola bufora zapisu

Klasa fstream umożliwia nam 'kontrolowanie' wewnętrznego bufora zapisu. Cała ta kontrola sprowadza się do zmuszenia klasy fstream, aby zapisała całą obecną zawartość bufora na dysk bez względu na to czy jest on zapełniony czy nie. W tym celu utworzono funkcję flush(). Poniżej zamieszczam przykład demonstrujący użycie tej funkcji.

#include <fstream>
using namespace std;
int main ()
{
  fstream plik("plik.txt",ios::out);
  if(plik.good())
  {
    for(int i=1;i<=100;i++)
    {
      plik<<i<<", ";
      plik.flush();
    }
    plik.close();
  }
  return(0);
}

Pamiętaj jednak, że takie zapisywanie danych jak tu zostało zaprezentowane nie jest wydajne. Funkcja flush() pomimo iż wydaje się w obecnym świetle dla Ciebie bezużyteczna znajduje ona swoje praktyczne zastosowanie chociażby w serwerach profesjonalnych baz danych.

16.7. Poruszanie się po pliku z danymi

Do tej pory odczytywaliśmy (zapisywaliśmy) dane z (do) pliku zawsze od tego miejsca na którym skończyliśmy operację odczytu (zapisu) ostatnim razem. Taka forma odczytu i zapisu danych jest bardzo wygodna, jednak czasem zachodzi potrzeba poruszania się po pliku w bardziej nietypowy sposób. Z pomocą przychodzą tu funkcje seekg() i seekp(). Obie funkcje służą do ustawiania nowej pozycji wewnętrznego wskaźnika pliku. Jest jednak między nimi jedna zasadnicza różnica:

seekg() ustawia wewnętrzny wskaźnik pliku dla funkcji odczytujących dane;
seekp() ustawia wewnętrzny wskaźnik pliku dla funkcji zapisujących dane.

Parametry obu tych funkcji są analogiczne:

istream& seekg (streamoff offset, ios_base::seekdir kierunek);
ostream& seekp (streamoff offset, ios_base::seekdir kierunek);

Pierwszy parametr (offset) to przesunięcie, które informuje o ile bajtów ma zostać przesunięty wewnętrzny wskaźnik pliku. Drugi parametr (kierunek) jest opcjonalny i informuje klasę fstream względem czego ma zostać dokonane przesunięcie wskaźnika. Domyślną wartością, jaka jest przyjmowana za zmienną kierunek, to ios_base::beg. Kierunki jakie mamy do wyboru to:
Kierunek	
Opis

ios_base::beg	
Przesunięcie względem początku pliku (domyślne)

ios_base::cur	
Przesunięcie względem aktualnej pozycji

ios_base::end	
Przesunięcie względem końca pliku

16.7.1. Odczytywanie aktualnej pozycji wewnętrznego wskaźnika pliku

Jeśli będziemy mieli potrzebę odczytania aktualnej pozycji wewnętrznego wskaźnika pliku, możemy to zrobić za pomocą funkcji tellg() i tellp(). Definicja obu funkcji wygląda następująco:
streampos tellg();
streampos tellp();

Obie funkcje wyglądają tak samo, różnią się jednak działaniem.
Funkcja tellg() zwraca aktualną pozycję wewnętrznego wskaźnika pliku od której będzie następowało wczytywanie danych z pliku.
Funkcja tellp() zwraca aktualną pozycję wewnętrznego wskaźnika pliku od której będzie następowało zapisywanie danych do pliku.

16.7.2. Gdy wyjdziemy poza zasięg pliku

Aby sprawdzić, czy skok na nową pozycję zakończył się sukcesem możemy dokonać tego na dwa sposoby:
Sprawdzić aktualną pozycję pliku i porównać z tą, którą chcieliśmy otrzymać;
Wywołać funkcję fail(), należącą do klasy fstream.

Pierwsza metoda jest logiczna, druga wymaga krótkiego omówienia. Definicja funkcji fail() wygląda następująco:
bool fail();

Jeśli wystąpi błąd podczas wykonywania skoku (i nie tylko skoku) funkcja ta zwróci wartość true informując nas, że ostatnia operacja na pliku nie powiodła się. Przykładowo:
std::fstream plik("plik.txt",std::ios::in);//zakładamy, że plik udało się otworzyć
plik.seekg(+2,std::ios_base::end);//skok do przodu o 2 względem końca pliku
if(plik.fail()) std::cout<<"Error! Nie udalo sie przesunac wewnetrznego wskaznika pliku"<<std::endl;

16.8. Pozostałe funkcje, wykorzystywane podczas pracy z plikami

Ostatnią funkcją, jaką chciałbym omówić jest eof(). Funkcja ta służy do sprawdzania, czy wskaźnik pliku znajduje się na końcu pliku. Definicja funkcji:
bool eof();

Funkcja zwróci wartość true wtedy, gdy nie będzie już w pliku więcej danych do odczytu. Dzięki tej funkcji możemy w bardzo łatwy sposób odczytać zawartość całego pliku. Poniższy przykład wyświetli zawartość całego pliku na ekran konsoli.

#include <iostream>
#include <fstream>
#include <conio.h>
using namespace std;
int main()
{
  fstream plik;
  plik.open("dane.txt",ios::in);
  if(plik.good())
  {
    string napis;
    cout<<"Zawartosc pliku:"<<endl;
    while(!plik.eof())
    {
      getline(plik,napis);
      cout<<napis<<endl;
    }
    plik.close();
  }else cout<<"Error! Nie udalo otworzyc sie pliku!"<<endl;
  getch();
  return(0);
}

16.8.1. Dokumentacja

Dobrym źródłem dostępnych funkcji w klasie fstream jest dokumentacja. Dokumentację dla klasy fstream znajdziesz pod adresem: http://www.cplusplus.com/reference/iostream/fstream/