15. fejezet: Adatstruktúrák

 

Sok más programozási nyelvhez hasonlóan itt is létre lehet hozni különféle önálló adatstruktúrát, így nem vagyunk lekorlátozva pár alapvető elemre és típusra. Gyakorlatban az adatstruktúra az egyes típusú elemek azonos nevű csoportja. Ezek az egyes adatelemek (vagy tagok) különféle típusúak és eltérő hosszúak lehetnek.

Szintaxisuk a következő:

struct struktúra_név {

típus_1 tag_neve_1;

típus_2 tag_neve_2;

típus_3 tag_neve_3;

.

.

} objektum_neve;

 

A legfontosabb egy adatszerkezet létrehozásakor, hogy ez egy új típus lesz. Amint egy adatszerkezetet deklaráltunk a struct segítségével, a program minden további részében lehet használni, akár a régi (megszokott) típusokat. Például egy kis zöldség-üzlet adatbázisában elképzelhető a következő:

struct termek {

  int suly;

  float ar;

} ;

termek alma;

termek banan, dinnye;

 

Persze ezt sem kell két külön lépésben megoldanunk, mivel bátran lehet egyszerre is összehozni. Például:

struct termek {

  int suly;

  float ar;

} alma, banan, dinnye;

 

Fontos belátni, hogy a rövidítés után ugyanott állunk, ahol az eredeti kódokkal álltunk volna! Lássuk mindezt egy gyakorlati példában - teljes kóddal:

 

---------------------------------------------

// Adat-struktúrák

#include <iostream>

#include <cstdlib>

using namespace std;

 

  struct termek

      {

      string nev;

      int suly;

      float ar;

      } ;

 

void kiir(termek mit)

{

     cout <<"\nTermek neve: " <<mit.nev;

     cout <<"\nSu'lya:      " <<mit.suly;

     cout <<"\nA'ra:        " <<mit.ar;

}

 

int main ()

{

  termek alma;

  termek banan, dinnye;

  system("cls");

  alma.nev="Alma";

  alma.suly=20;

  alma.ar=147;

  banan.nev="Bana'n";

  banan.suly=14;

  banan.ar=350; 

  dinnye.nev="Dinnye";

  dinnye.suly=4;

  dinnye.ar=99.9;

  kiir(alma);

  kiir(banan);

  kiir(dinnye);

  return 0;

}

---------------------------------------------

 

Vegyük észre, hogy a "termek" nevű adatstruktúrát előre kellett létrehozni, majd rögtön utána egy külső eljárásban már könnyedén lehet is rá hivatkozni. A fenti elvvel létre tudunk hozni beágyazott adatstruktúrákat is.

 

Kicsit fontosabb lesz a továbbiakban, hogy a C++ megengedi létező adattípusokon alapuló saját adatstruktúrák létrehozását is. Erre a typedef szolgál, például:

typedef létező_típus új_típus_név ;

Itt a létező típust már előre el kell készíteni (vagy eleve adottat kell alkalmazni), így létre lehet hozni az új adattípust. Lássunk erre példákat!

typedef char C;             // A C char típusú lesz

typedef unsigned int WORD;  // A WORD előjel nélküli egész
typedef char * pChar;       // A pChar egy char típusú mutató

typedef char field [50];    // Ez pedig egy 50 darabos tömb

 

Mint látható, a typedef nem képes alapvetően új típusokat definiálni, csak a meglévőkhöz rendelhetünk velük azonosan használható szinonimákat.

 

Az unió egy más téma. A "union" ugyanis megengedi, hogy a memória egy adott részéhez más és más adattípusokat rendeljünk hozzá. Az adatok deklarációja és használata hasonló az eddig megismertekhez, de a funkció teljesen más! Lássunk egyből egy gyakorlati példát:

union mytypes_t {

  char c;

  int i;

  float f;

  } mytypes;

 

Ez a deklaráció három elemet hoz létre:

mytypes.c

mytypes.i

mytyes.f

Mindegyik különféle adattípusú. Ám mind a három ugyanarra a memóriahelyre mutat, így ha az egyiket módosítjuk, akkor az összes többi is egyből módosul.

 

A C++ újabb csavart adott bele az uniókba, ugyanis lehetőség van névtelen (anonymous) uniók létrehozására. Ha az uniót név nélkül deklaráljuk, akkor az unió anonymous lesz és képesek leszünk az egyes tagokat közvetlenül a nevükön elérni. Például nézzük meg a következő két deklarációt:

Hagyományos módon deklarált unió:

struct {

  char cim[50];

  char szerzo[50];

  union {

    float dollar;

    int yen;

  } ar;

} konyv;

 

Új típusú (anonim módon) deklarált unió:

struct {

  char cim[50];

  char szerzo[50];

  union {

    float dollar;

    int yen;

  };

} konyv;

 

Az egyetlen különbség az ár nevének megadásában van. Itt a lényeges különbséget a hozzáférés módja adja. A hagyományos módon deklarált uniónál a hozzáférés a következő: konyv.ar.dollar, illetve konyv.ar.yen. Ám az új, anonim módnál ez egyszerűsödik: konyv.dollar, illetve konyv.yen.

 

Felsorolási típus (enum):

Ez a lehetőség új adattípusokat hoz létre, ami nincs korlátozva az értékek alaptípusaira. Lássuk a következő példát:

enum szinek {black, blue, green, cyan, red, purple, yellow, white};

 

Figyeljük meg, hogy a deklaráció semmilyen alaptípust nem tartalmaz. Nagyjából azt kell mondanunk, hogy a semmiből hoztunk létre egy teljesen új adattípust. Ezen új típus lehetséges értékeit tartalmazza a zárójel. Gyakorlati használata például a következő:

szinek nadrag;

nadrag = blue;

if (nadrag == green) nadrag = red;

Szeretném megjegyezni, hogy ilyen esetben a szinek típus legelső eleme (black) nem az 1-es számú lesz, hanem a 0-ás!

 

A felsorolási adattípus kompatibilis a számozott változókkal (felsorolható egészekkel), így az egyes elemekhez könnyedén hozzárendelhetünk egész számokat. Lássunk erre is egy gyakorlati példát:

enum honapok { januar=1, februar, marcius, aprilis,

                majus, junius, julius, augusztus,

                szeptember, oktober, november, december} ev;

Ebben az esetben az "ev" változónak 12 lehetséges értéke lesz, melyek követik egymást. Ilyen deklaráció esetén a vektorokkal ellentétben az első érték (januar) 1 lesz, nem pedig 0.

16. fejezet: File-műveletek

Sok-sok elmélet után jöjjön egy olyan anyagrész, amit már kezdők is sokan és sokszor használnak: az írás/olvasás file-műveletekkel. Fontos, hogy ez az anyagrész minden évben visszatér az emelt szintű informatika érettségin!

A következő osztályok képesek a file-műveletek elvégzésére:

    ofstream: adatfolyam írása file-ba

    ifstream: adatfolyam olvasása file-ból

    fstream: az előző két lehetőség egyszerre.

Íme egy minta:

---------------------------------------------

// Alapvető file-műveletek

#include <iostream>

#include <fstream>

using namespace std;

int main () {

  ofstream myfile;                          // Hozzárendeli írásra a myfile-t

  myfile.open ("proba011.txt");             // Ez nyitja meg a file-t írásra

  myfile << "Valamit irjunk bele.\n";       // Kiírunk valamit.

  myfile.close();                           // Hozzárendelés megszüntetése a file lezárásával.

  return 0;

}

---------------------------------------------

 

Lefuttatása után a forráskóddal (és a végrehajtható állománnyal) azonos könyvtárban lesz egy sima "proba011.txt" nevű állomány, amely a kívánt szöveget tartalmazza. A kiíratás igen hasonló a "cout"-hoz, így nem okozhat nehézséget! Miután láttuk, hogy a gyakorlat aránylag könnyű, lássuk mindezt lépésenként!

 

File megnyitása:

A legelső file-művelet általában egy fizikailag is létező file hozzárendelése egy valamilyen változóhoz. Ezen eljárás megnyit egy file-t. Ez a nyitott file lehetőséget ad arra, hogy a programon belüli adatfolyam képes legyen kommunikációt folytatni egy külső adatforrással. Pontos használata: open (filenév, mód);

Változók értelmezése:

"filenév": egy \0 végződésű karaktersorozat.

"mód": egy opcionális paraméter a következők közül:

 

ios::in              Megnyitás bevitelre.

ios::out Megnyitás kivitelre.

ios::binary       Bináris módú megnyitás

ios::ate           A kezdeti pozíciót állítsd a file legvégére. Ha ennek a jelzőnek nincsen értéke, akkor az aktuális pozíció a file legeleje lesz.

ios::app           Minden kimeneti művelet a file végén történik, mellékelve a feldolgozott file tartalmát. Ez a jelző csak a kimeneti műveletek esetén használható.

ios::trunc         Ha a file meg van nyitva a kimeneti műveletek számára mielőtt létezne, a jelenlegi tartalmát törölni kell és helyette ki kell írni az újat.

 

Az összes jelzőt egy OR (|) operátorral lehet felsorolni. Minta:

ofstream enfileom;

enfileom.open ("minta.bin", ios::out | ios::app | ios::binary);

 

Minden egyes open() művelettel megnyitott adatfolyam számára vannak alapértelmezett jelzők, melyek logikusak:

ofstream          ios::out

ifstream           ios::in

fstream     ios::in | ios::out

 

Annak ellenőrzésére, hogy a file megnyitása sikeres volt-e, megoldható az is_open() függvénnyel. Ennek bemutatására szolgál a következő példa:

---------------------------------------------

// fstream::is_open

#include <iostream>

#include <fstream>

using namespace std;

int main () {

  fstream azenfileom;

  azenfileom.open ("proba011a.txt");

  if (azenfileom.is_open())

  {

    azenfileom << "Sikeresen megnyitva.";   // Ezt a file-ba írja

    cout << "Sikeresen megnyitva.";         // Ezt pedig a képernyőre

    azenfileom.close();                     // Nyilvánvaló, hogy ez lesz a lezárás.

  }

  else

  {

    cout << "File megnyitasa sikertelen.";

  }

  return 0;

}

---------------------------------------------

A konkrét teszteléshez javaslom, hogy előző futtatásban létrehozott proba011.txt fájl segítségével  meg először futtatni a programot. Ekkor ugye a „File megnyitasa sikertelen.” üzenetet kapjuk, majd változtassuk át a fájl nevét proba011a.txt-re, amivel máris a „Sikeresen megnyitva.” üzenetet kapjuk. Így mindkét futási ágat nyugodtan tudjuk ellenőrizni!

 

Szövegfile-ok:

Összefoglalóan mindazon file-okat hívjuk így, amelyek megnyitásakor nem alkalmazzuk az ios::binary jelzőt. Ezen file-okat minden szöveges és egyéb input/output érték tárolására fejlesztették ki, így időnként szükség lehet egy-egy adatsor megfelelő konverziójára.

Fontos észrevenni, hogy a kiírás megfelel a jó öreg "cout"-nak!

---------------------------------------------

// Irás szövegfile-ba

#include <iostream>

#include <fstream>

using namespace std;

int main () {

  ofstream myfile ("proba011b.txt");

  if (myfile.is_open())

  {

    myfile << "Ez egy sor.\n";        //Ez a file-ba ír

    myfile << "Ez meg egy ujabb.\n";  //Ez is a file-ba kerül

    myfile.close();                       //Fájl lezárása

    cout <<"A file megirasa befejezodott!";  //Ez már a képernyőre megy.

  }

  else cout << "A file megnyitasa sikertelen volt.";

  return 0;

}

---------------------------------------------

 

Próbáljuk meg a most kiírt file-t kiolvasni! Ehhez nem árt egy szövegfájlt, például az előzőleg megírt fájlt ebbe a futtatási könyvtárba másolni!

---------------------------------------------

// Olvasás szövegfile-ból

#include <iostream>

#include <fstream>

#include <string>        //szövegfeldolgozáshoz kell!

using namespace std;

int main () {

  string sor;            //szövegfeldolgozáshoz szükséges változó

  ifstream myfile ("proba011b.txt");

  if (myfile.is_open())

  {

    while ( myfile.good() )                    //Egyszerű elöltesztelő ciklus, de érdemes megnézni a feltételt!

    {

      getline (myfile,sor);                    //Konkrét kiolvasás a file-ból

      cout << sor << endl;                     //Kiírás a képernyőre

    }

    myfile.close();                            //File lezárása

  }

  else cout << "A file megnyitasa sikertelen volt.";

  return 0;

}

---------------------------------------------

 

Itt is nem lehet nem észrevenni, hogy a beolvasás teljesen megfelel a megszokott "cin" struktúrájának. Továbbá nagyon fontos észrevenni, hogy a belső ciklus feltételeként használt good() eljárás igen hasznos segítőnk lesz!

 

Persze nem ez az egyetlen állapotjelző. Íme a választék (valamennyi bool típusú):

- bad(): Igaz értékkel tér vissza, ha az olvasás vagy az írás sikertelen. Például ha egy olyan file-ba próbálunk meg írni, amelyet előtte nem írásra nyitottunk meg, vagy ha az írásra használt médiánk nincs engedélyezve az írás.

- fail(): Igazzal tér vissza mindazon esetekben, mint a bad(), de akkor is, amikor formátumhibát vétünk. Például betű jött a bemenetre, holott egész számot vártunk.

- eof(): Igazzal tér vissza, ha az olvasott file elért a legvégére.

- good(): A leggyakrabban használt jelző. Hamissal tér vissza mindazon esetekben, amikor az előző jelzők igazzal térnek vissza.

- check(): Ez nem ad visszatérési értéket, csak visszatér az alapállapotra. Főleg tesztelésre érdemes használni!

 

További mutatók és eljárások:

tellg() és tellp(): ezek a paraméter nélküli függvények visszaadják a pos_type mutatóban álló adatblokk méretét, amely egy egész típusú adat. Get esetén a tellg()-t kell használni, míg put esetén a tellp()-t.

 

seekg() és seekp(): ezek a függvények megengedik, hogy a get és a put eljárásoknál megváltoztassuk a mutató helyét a függvényen belül. Prototípusai:

seekg(eltolás, irány);   //get esetén

seegp(eltolás, irány);   //put esetén

Az irány alapértelmezett értékei: ios::beg - az eltolást a file elejétől kell számolni.

ios::end - az eltolást a file végétől kell számolni.

ios::cur - az eltolást az akutális file-mutatótól kell számolni.

 

Bináris file-ok esetén kicsit másképpen kell olvasni. Itt csak két alaptípus van. Íme:

write ( memória_blokk, méret );       // Írásra

read ( memória_blokk, méret );        // Olvasásra

---------------------------------------------

// Bináris fájl olvasása

#include <iostream>

#include <fstream>

using namespace std;

 

ifstream::pos_type size;

char * memblock;        //Ezt érdemes alkalmazni, így nem lesz gondunk a file hossza miatt.

 

int main () {

  ifstream file ("proba013d.bin", ios::in|ios::binary|ios::ate);

  if (file.is_open())                       // Megnyitjuk olvasásra

  {

    size = file.tellg();                    // Megadja a következő memóriablokk méretét

    memblock = new char [size];             // Lefoglal egy memóriablokk-ot

    file.seekg (0, ios::beg);               // Beállítja a következő adatmodult olvasásra

    file.read (memblock, size);             // A memblock változóba beolvassa az adott méretű adatmodult

    file.close();                           // File lezárása

 

    cout << "A teljes file a memoriaban van.";

 

    delete[] memblock;

  }

  else cout << "A file megnyitasa sikertelen volt.";

  return 0;

}

---------------------------------------------