[itk] Számítástechnika kezdőknek

C++ programozás kezdőknek - tömbök, ciklusok

[2017. szeptember 11.] [ christo161 ]

Ebben a részben arról lesz szó, hogyan tudunk sok azonos típusú értéket egyszerűen kezelni (tárolni, kiíratni, módosítani), valamint utasításokat (valamilyen feltétel érvényessége esetén) ismétlődően végrehajtani.
(Ez a fejezet jelenleg átdolgozás alatt áll.)

cpp_ciklusok_gondoltam_egy_szamra.png

Előző rész: elágazások, logikai változók
Következő rész: függvények

Tömbök

A tömb (angolul array) a legegyszerűbb olyan adatszerkezet, amely sok azonos típusú érték egyidejű tárolására szolgál. A benne tárolt értékekre egyetlen névvel/azonosítóval hivatkozhatunk (a tömb nevével), így nem kell minden egyes tárolni kívánt érték számára külön-külön változókat definiálni.
A tömb sok esetben logikai csoportosítás is. Általában logikailag összetartozó elemek tárolására használunk egy-egy tömböt. Pl. ha 15 szám átlagát, és másik 20 szám összegét szeretnénk kiszámolni, akkor nem egy 35 elemű tömbben szokás elhelyezni az értékeket, hanem egy 15 elemű és egy 20 elemű tömbben.

C++-ban egy tömbben csak azonos típusú elemek szerepelhetnek (szemben például a Javascripttel). Különböző típusú értékek egyidejű tárolásához struct vagy class adatszerkezeteket szokás használni, melyekről majd egy későbbi fejezetben lesz szó.

Előfordulhat, hogy más tananyagokban a tömböket vektoroknak nevezik. Ebben a tananyagban nem így járunk el, mivel a vektor elnevezés könnyen összekeverhető a C++ Standard Template Library (STL) vector adatszerkezetével (melyet az #include <vector> utasítással használhatunk).

A C++ szabvány kétfajta (hagyományos) tömb használatát teszi lehetővé: statikus és dinamikus. A statikus tömböt akkor használjuk, ha a forráskódban megadjuk egy tömb mindenkori méretét, a dinamikus tömböt pedig akkor, ha például a felhasználótól kérjük be, vagy fájlból olvassuk be a tömb méretét, mely a program futása során akár meg is változtatható.
(Szót ejtünk majd az STL std::vector adatszerkezetéről is, mely lényegében egy kényelmi funkciókkal ellátott dinamikus tömb (pl. könnyen módosítható és lekérdezhető az elemszáma)).

Statikus tömb definiálása

Egy statikus tömb mérete egész szám konstans (pl. const int, const unsigned int) segítségével adható meg:

const int elemszam = 5;
float tomb[elemszam]; //5 darab float típusú érték tárolására alkalmas tömb definiálása

Vagy ha nem adjuk meg a tömb méretét a tömb definíciójában, de megadjuk a kívánt mennyiségű elemek kezdőértékét, akkor a megadott elemek darabszámából a fordító kikövetkezteti a tömb méretét.

int tomb_2[] = {5, -2, 3, 128}; /*4 darab int típusú érték tárolására alkalmas tömb definiálása, kezdőértékekkel*/

Megjegyzés: ha egy statikus tömb méretét nem egész szám konstans segítségével adjuk meg, hanem például a felhasználótól vagy fájlból kérjük be, a kód nem lesz szabványos, ami egyrészt azzal jár, hogy nem biztos, hogy minden fordító támogatja, másrészt pedig azzal, hogy ha a fordítót -pedantic kapcsolóval paraméterezzük, akkor a kód nem szabványos részét hibaüzenetként (warningként) jelzi a fordító.

/* Nem szabványos kód: */
int elemszam;
cout << "Kerem adja meg az elemszamot: " << endl;
cin >> elemszam;
double tomb_3[elemszam];

Dinamikus tömb definiálása

Kezdőknek dinamikus tömbök helyett inkább az std::vector használatát ajánlom, de a dinamikus tömböknek is szerepelnie kell a tananyagban, hiszen aki többet foglalkozik C++-szal, biztos, hogy találkozni fog velük.

Ha egy tömb méretét a felhasználótól kérjük be, vagy fájlból olvassuk be, vagy később meg szeretnénk változtatni akkor a dinamikus tömb a szabványos megoldás, ami viszont nagyobb odafigyelést is igényel, mivel a programozó feladata, hogy a tömb számára lefoglalt memóriaterületet felszabadítsa (amely nem biztos, hogy olyan egyszerű, ha a program sok vezérlési szerkezetet tartalmaz, mivel könnyen elfelejthetjük az egyik blokkban elhelyezni az erre vonatkozó utasítást), illetve kivételkezelést is igényel (melyről későbbi fejezetben lesz szó), mivel pl. azt az esetet is kezelni kell, ha nem érhető el elegendő szabad memóriaterület.

int elemszam;
cout << "Kerem adja meg az elemszamot: " << endl;
cin >> elemszam;
double * tomb_4 = new double[elemszam]; //elemszam darab double típusú elem tárolására alkalmas tömb definiálása

Egy tömb elemszámának bekérésénél érdemes ügyelni arra, hogy a felhasználó érvénytelen (pl. nem szám típusú) értéket is megadhat. A későbbiekben erre is nézünk egy megoldást.
(Ha a felhasználó tört számot ad meg, akkor az egész számmá konvertálódik, mivel egy int típusú változóba lesz bekérve, így ebből nem adódik gond.)

Dinamikus tömböknél akkor is meg kell adnunk az elemszámot, ha a forráskódban megadjuk a tömb elemeinek kezdőértékét. Az elemszámnak értelemszerűen legalább akkorának kell lennie, mint a megadott kezdőértékek száma. Például:

int * tomb_5 = new int[3] {128, 0, -20}; //3 darab int típusú elem tárolására alkalmas tömb definiálása (kezdőértékekkel)

Ha már nincs szükségünk egy dinamikus tömb tartalmára, így szabadíthatjuk fel a számára lefoglalt memóriaterületet:

delete[] tomb_4;

Ha erről megfeledkezünk, a tömb számára lefoglalt memóriaterület a program futásának végéig nem szabadul fel.

A delete[] operátor használata esetén csak a tömb elemei törlődnek, így ugyanazzal a névvel létrehozhatunk egy eltérő méretű tömböt. Például:

tomb_4 = new int[elemszam+1];

...vagy például:

tomb_4 = new int[elemszam*2];

Nyilván ha csak módosítani szeretnénk a tömb méretét az elemei értékének a megtartásával, akkor a törlés előtt létre kell hoznunk egy vele azonos méretű tömböt, amelybe biztonsági másolatot készítünk az eredeti tömb elemeiről, majd miután azokat visszamásoltuk a módosított méretű tömbbe, törölhetjük az ideiglenesen létrehozott tömb elemeit delete[] operátorral.

Hogyan tudunk hivatkozni egy tömb elemeire?

Ha például a legelső elem értékét szeretnénk módosítani (egy t nevű, int típusú tömb esetén):

t[0] = 53;

Illetve kiíratni:

cout << t[0] << endl;

Fontos, hogy a 0 indexű (sorszámú) elem a tömb legelső eleme, tehát a t[1] már a második elem. Ennek megfelelően pedig az utolsó elem az elemszámnál egyel kisebb indexű elem, vagyis pl. egy 5 elemű (t nevű) tömb esetén a t[4] az utolsó elem. Ha túlindexeljük a tömböt (pl. t[4] helyett t[5]-öt írunk valahol a programkódban), akkor a program jó eséllyel nem fog lefordulni.

Egy 20 elemű (t nevű) tömb utolsó elemének így kérhetünk be értéket a felhasználótól:

/* Ha t egy 20 elemű tömb, akkor t[19] az utolsó eleme */
cin >> t[19];

C++-ban asszociatív tömb (melynek az indexei nem számok) például a Standard Template Library map adatszerkezetével valósítható meg, de erről ebben a fejezetben nem lesz szó.

for ciklus (elöltesztelős, számlálós ciklus)

Egy nagy elemszámú tömb esetén már hosszú lenne, ha minden egyes elem kezeléséhez külön-külön utasítást használnánk. Például ennek rövidítésére való a for ciklus.
Íme egy rövid kódrészlet, melyben definiálunk egy 20 elemű (statikus) tömböt, majd kérjük be a felhasználótól az egyes elemek értékeit:

const int elemszam = 20;
int t[elemszam];

for (int i = 0; i < elemszam; ++i) {
	cout << "Kerem adja meg a(z) " << i+1 << ". szamu elemet: " << endl;
	cin >> t[i];
}

Ennek a végeredménye ugyanaz, mintha külön-külön utasításokkal kértük volna be a tömb 20 elemének értékét. (Azzal az apró kivétellel, hogyha egyenként kérjük be őket, akkor a bekérésre vonatkozó kiírt szövegben a névelőt nem kell a(z)-nak írnunk, hanem egyenként megadhatjuk például úgy, hogy "az elso", "a masodik"...stb.
Ezt persze a for ciklus esetén is megtehetjük, mondjuk például ha egy külön erre a célra létrehozott tömbben nullákat vagy egyeseket tárolunk, a nulla jelentheti az a névelőt, az egyes pedig az az névelőt, és egy elágazással kiértékeljük ennek a tömbnek az éppen aktuális elemét.
)

A for ciklusban az i változó tartalmazza azt az értéket, amivel azt tarjuk számon, hogy hányadik lépésnél tart a ciklus (ennek az értéknek nem szükséges 0-tól egyesével haladnia egy pozitív számig).
A fenti példában az i változó képviseli például a tömb indexének aktuális értékét. Ezt úgy kell elképzelni, hogy a ciklusmag (a ciklus kapcsos zárójelei közötti utasítások sorozata) a jelen példa esetén 20-szor fut le, az első lefutásnál az i értéke 0, a másodiknál 1, a harmadiknál 2, és az utolsó lefutásnál pedig 19, illetve a ciklusmag utolsó lefutását követően az i értéke meg lesz növelve 20-ra, de ekkor a ciklusmag már nem fut le, mivel a ciklus második paraméterében megfogalmazott feltétel (i < elemszam, ezesetben 20 < 20) ekkor már nem teljesül.
Amikor pedig a bekérésre vonatkozó üzenetet íratjuk ki (cout << "kerem adja meg a(z) << i+1 << ". szamu elemet: ";), akkor az i egyel megnövelt értékét íratjuk ki, csak hogy az első elem bekérésénél ne az legyen kiírva, hogy "kerem adja meg a(z) 0. elemet", és így tovább a többi elem esetében is.
Az i változót ciklusváltozónak, vagy iterált változónak is szokták nevezni. Fontos, hogy a ciklusváltozó csak a ciklusmagban (a ciklus kapcsos zárójelei közt) érhető el. Ha a ciklus blokkján kívül hivatkozunk rá, akkor a program nem fog lefordulni.

Egy tömb elemeinek a kiíratása is a fenti példához hasonló (cin >> t[i]; helyett pl. cout << t[i] << endl; vagy pedig cout << t[i] << " "; utasítást használhatunk), illetve bármilyen módosítást is elvégezhetünk így a tömb elemein (pl. t[i] *= 2;), de természetesen nem csak tömbök feldolgozásához használhatunk for ciklust, hanem egyéb előre meghatározható lépésszámú műveletekhez. A leggyakoribb internetes példa az egész számok kiíratása mondjuk 0-tól 99-ig (cout << i << " ";).

Egy for cikluson belül természetesen egyéb utasításokat is elhelyezhetünk, akár elágazást vagy egy másik for ciklust. Ezekre majd a tananyag későbbi részében láthatunk példát.

Elnagyoltan szemléltetve így működik a for ciklus:

for (/*honnan induljon*/; /*meddig tartson*/; /*merre haladjon, milyen léptékben*/) {
	/*utasítások, melyekben felhasználhatjuk a ciklusváltozó (jellemzően i nevű változó) aktuális értékét*/
}

A for ciklus első paraméterében definiáljuk a ciklusváltozót. Nem muszáj i-nek elnevezni, de így szokás, illetve több egymásba ágyazott ciklus esetén i, j, k-nak szokták nevezni az egyes ciklusok ciklusváltozóit. Ennek a definíciónak a lényege inkább az, hogy mi legyen a ciklusváltozó kezdőértéke. Jelen példa esetén 0, de szükség esetén lehetne más is.

A for ciklus második paraméterében adjuk meg azt a feltételt, amelynek teljesülése esetén a ciklusmag még lefut. Jelen példa esetén i < elemszam, vagyis effektíve i < 20 (mivel az elemszam értéke jelen példa esetén 20). Ha az itt megadott feltétel nem teljesül, a ciklus véget ér, vagyis a ciklusmagban lévő utasítások már nem futnak le. Jelen példa esetén ez akkor lesz, ha i értéke eléri a 20-at. Amikor i értéke 20, a ciklusmagnak már nem is szabad lefutnia, különben túlindexelnénk a tömböt (ha t húsz elemű, akkor t[19] az utolsó eleme).

A for ciklus harmadik paraméterében adjuk meg, hogy mennyivel növeljük/csökkentjük a ciklusváltozót a ciklusmag minden egyes lefutását követően.
Az i++ vagy ++i esetén 1-el növeljük az i változó értékét (a ciklus szempontjából mindkét kifejezés egyenértékű), az i-- vagy --i esetén 1-el csökkentjük az i változó értékét. Illetve például az i += 2 kifejezéssel pedig kettőt adunk hozzá az i értékéhez. (Az i += 2; utasítás egyenértékű az i = i + 2; utasítással, viszont a for ciklus harmadik paraméterében szintaktikailag csak az előbbi használata helyes).

Fontos megjegyezni, hogy bármi, amit a ciklus blokkján belül (a kapcsos zárójelek között) definiálunk, nem lesz elérhető a cikluson kívül.

Az elöltesztelős ciklusok (for, foreach és while) struktogramja:

struktogram_ciklus_eloltesztelos.png

Egy std::vector definiálása

Sok azonos típusú érték tárolására használhatjuk a vector adatszerkezetet is, amely kicsit lassabb, mint a dinamikus tömbök helyes használata, de könnyebb és kényelmesebb, mert például automatikusan felszabadítja a lefoglalt memóriaterületet és egyszerűen változtatható a mérete.

Természetesen using namespace std; utasítás esetén std::vector helyett írhatunk csak simán vector-t, egyelőre azért említettem std::vector-ként, hogy egyértelműbb legyen, hogy az STL egyik adatszerkezetéről van szó.

Mindig szükséges az #include <vector> preprocesszor utasítás az std::vector használatához.

Az std::vector valójában egy class template (osztálysablon). C++-ban a sablon csupán annyit jelent, hogy a használatkor (egy osztály esetén a példányosításnál) típussal kell paramétereznünk.
Egy adatszerkezet esetén ez annyit jelent, hogy a létrehozásakor <> jelek között meg kell adnunk, hogy milyen típusú elemeket szeretnénk benne tárolni.

Például egy int típusú elemek tárolására alkalmas std::vector definiálása (méretet nem adunk meg):

std::vector<int> t_5;

Az elemeket egyenként adhatjuk hozzá a vectorhoz a push_back tagfüggvénnyel, ekkor az elemszáma értelemszerűen egyel növekszik:

t_5.push_back(-128);

Nyilvánvalóan nem kell egyenként külön-külön utasítással hozzáadogatni az elemeket a vectorhoz, a for ciklust például ennek a rövidítésére használhatjuk.

Amikor definiálunk egy vector-t, nem adjuk meg az elemszámát, hanem a size tagfüggvénnyel bármikor lekérdezhetjük. Például:

std::cout << t_5.size(); //t_5 aktuális elemszámának kiíratása

1. for ciklus példa: elemek kiíratása fordított sorrendben

Az eddigi tudnivalók alapján könnyen megírhatunk egy példaprogramot a for ciklus felhasználásával. Az egyszerűség kedvéért itt nem a felhasználótól kérjük be a tömb méretét (elemszámát), hanem egy int típusú konstansban adjuk meg a program forráskódjában. Az elemszám tárolásához használhatunk egész szám típusú (pl. short int, int) változót is, nem muszáj konstanst (const int) használni. Mindenesetre érdemesebb az elemszámot egy konstansban vagy változóban eltárolni, mert ha esetleg a későbbiek során módosítani szeretnénk az értékét, akkor csak egy helyen kell átírnunk a forráskódban, viszont ha literálokat használnánk, akkor bizony annyi helyen át kellene írni, amennyi helyen használjuk az értéket.
Az első for ciklusban a fentebbi példához hasonlóan bekérjük a tömb elemeit, majd a második for ciklus segítségével fordított sorrendben íratjuk ki őket.
Fontos, hogy a visszafele haladó for ciklusban a ciklusváltozó kezdőértéke a tömb utolsó elemének indexe legyen (ami ugyebár nem elemszam, hanem elemszam-1). Továbbá a helyes működés érdekében a feltételt úgy kell megfogalmazni, hogy amikor a ciklusváltozó értéke eléri a 0-t, a ciklusmag még lefusson (mivel a tömb első indexe 0). Ezt az i >= 0 (i nagyobb vagy egyenlő mint 0) feltétellel érhetjük el. Ügyeljünk arra, hogy i >= 0 helyett nehogy i > 0-t írjunk, mert akkor a tömb első elemét nem írja ki a program.
A for ciklus harmadik paramétere pedig egyértelműen --i (vagy i--), mivel a for ciklus minden lépésében csökkenteni kell a ciklusváltozó értékét.

#include <iostream>
using namespace std;

int main() {
  const int elemszam = 10;
  int tomb[elemszam];
  for (int i = 0; i < elemszam; ++i) {
    cout << "Kerem adja meg a(z) " << i+1 << ". elemet: ";
    cin >> tomb[i];
  }
  cout << "Az elemek fordított sorrendben: " << endl;
  for (int i = elemszam-1; i >= 0; --i) {
    cout << "Az " << i + 1 << ". elem: " << tomb[i] << endl;
  }
  return 0;
}

2. for ciklus példa: n darab szám átlaga

Ebben a példában a felhasználótól kérjük be a tömb méretét (elemszámát) az n változóba. Természetesen itt is érvényes, hogyha a felhasználó érvénytelen értéket (pl. nem számot) ad meg az elemszám bekérésekor, akkor a program nem fog jól működni. (Erre a do-while ciklus tárgyalásakor nézünk megoldást.)

Valahány szám átlagát úgy számolhatjuk ki, ha összeadjuk a számokat, és a végeredményt elosztjuk annyival, ahány számot összeadtunk (jelen példában az n változó értéke hivatott ez utóbbit tartalmazni).
A példaprogramban float típusú elemeket tárolunk a tömbben, de természetesen int típusú tömböt is definiálhattunk volna (ekkor egész számok átlagát számolná ki a program).
Az atlag változót ne a cikluson belül definiáljuk, mert akkor csak a cikluson belül lesz elérhető.
Ebben a példában nem hozunk létre külön változót az egyes elemek összegzésére, hanem az atlag változót használjuk a részeredmények tárolására is. Ahogy a ciklusban bekérünk egy értéket, azt rögtön hozzá is adjuk az atlag változó értékéhez, majd a ciklus után elosztjuk az atlag változó értékét az n változó értékével, vagyis az elemszámmal.
Ismétlésképpen megemlítjuk, hogy pl. az atlag += t[i]; utasítás egyenértékű az atlag = atlag + tomb[i]; utasítással.

#include <iostream>
using namespace std;

int main() {
  int n; //ebben a példában az n változóba kérjük be a tömb méretét
  cout << "Kerem adja meg, hany darab szam atlagat szamolja ki a program: ";
  cin >> n;
  float t[n];
  float atlag = 0;
  for (int i = 0; i < n; ++i) {
    cout << "Kerem adja meg a(z) " << i+1 << ". szamot: ";
    cin >> t[i];
    atlag += t[i];
  }
  atlag /= n;
  cout << "A szamok atlaga: " << atlag;
  return 0;
}

continue utasítás

Ha azt szeretnénk, hogy egy általunk megadott feltétel teljesülése esetén a ciklusmag hátralévő utasításait hagyja ki a ciklus annál a lépésnél, ahol éppen tart (de utána folytatódjon tovább a következő lépéssel), akkor a continue; utasítást használhatjuk.
Ez például akkor lehet hasznos, ha például egy tömb elemeinek feldolgozásánál bizonyos típusú elemeket, vagy akár egy bizonyos elemet ki szeretnénk hagyni.

Szintaktikát szemléltető példa:

for (int i = 0; i < elemszam; i++ ) {
  //mindenképp végrehajtandó utasítások
  if ( /* feltétel */ ) {
    continue;
  }
  //utasítások, melyek akkor hajtódnak végre, ha az if-ben megadott feltétel hamis
}

Ebben az esetben nem kell a feltételtől függő utasításokat else ágba foglalni, hiszen a continue utasítás rögtön a ciklus következő lépéséhez ugrik, anélkül, hogy a ciklus blokkjában lévő hátralévő utasítások végrehajtódnának.

Utasítások végrehajtása, vagy nem végrehajtása mindig a ciklus egy adott lépésében értendő. Például lehetséges, hogy a feltétel igaz a ciklus harmadik lépésében (mondjuk amikor i értéke 2), így a ciklusmagban hátralévő utasítások nem hajtódnak végre a ciklus harmadik lépésében. Viszont mondjuk a ciklus negyedik lépésében a feltétel hamis, így ekkor a ciklusmag hátralévő utasításai végrehajtódnak.

1. continue példa: nem 0 értékű számok összeszorzása

Ez a rövidke példaprogram összeszorozza egy adott tömbben található nem 0 értékű számokat. Ha a for ciklusban egy 0 értékű szám a soron következő elem, akkor a ciklus azon lépésében nem lesz elvégezve a szorzás művelet. Mivel a tizedestörtek között -0.0 is előfordulhat, így a teljesség kedvéért ez is szerepel a feltételben.
Ügyeljünk arra, hogy az eredmény változó kezdőértéke 1 és ne 0 legyen.
Ebben a példában az egyszerűség kedvéért nem a felhasználótól kérjük be a tömb elemeit, de természetesen azt is megtehetnénk.

#include <iostream>
using namespace std;

int main () {
  int elemszam = 10;
  float szamok[10] = {2, 0, 2.4, 3.6, 0, 9, 2, -2.3, 0, 1};
  float eredmeny = 1.0;
  for (int i = 0; i < elemszam; i++) {
    if (szamok[i] == 0 || szamok[i] == -0.0 ) {   
      continue;
    } //if vége
    eredmeny *= szamok[i];
  } //for vége
  cout << "A nem 0 erteku szamok szorzata: " << eredmeny << endl;
} //main vége

2. continue példa: egy bizonyos elem kihagyása (sorszám alapján)

Ha csupán ki szeretnénk hagyni egy elemet, például mondjuk egy tömb ötödik elemét, akkor a feltétel: i == 4.

for (int i = 0; i < elemszam; i++) {
  if (i == 4) {
    continue;
  }
  //a ciklusban végrehajtandó utasítások
}

break utasítás

Ha pedig azt szeretnénk, hogy a ciklus egy bizonyos feltétel teljesülése esetén ne folytatódjon tovább (tehát ne hajtódjanak végre a ciklus jelenlegi lépésében hátralévő (a break utasítást követő) utasítások, és a ciklus következő lépései sem), akkor a break utasítást használhatjuk.

1. break példa: nagyon egyszerű keresés

A példában definiálunk egy 1000 elemű tömböt, amit feltöltünk véletlenszerűen generált egész számokkal (1 és 100 között), és megkeressük benne a felhasználó által megadott szám első előfordulásának helyét. Ha van ilyen érték, akkor kiíratjuk a tömb adott elemének indexénél egyel nagyobb számot, ha nincs, akkor pedig egy erre vonatkozó üzenetet.
Ha a felhasználó nem 1 és 100 közötti számot ad meg, a program (az egyszerűség kevéért) leáll. Később arra is nézünk példát, hogyan oldhatjuk meg, hogy újra meg újra bekérjük az értéket, egészen addig, amíg a felhasználó nem 1 és 100 közötti értéket ad meg (vagy a program bezárásra nem kerül).

Véletlenszerű számok generálásához például a standard library cstdlib header fájljában található rand() függvényt használhatjuk. Ebben a tananyagban az emögött álló matematikai háttér ismertetésébe nem megyünk bele, hanem csak néhány példát nézünk át, melyek alapján intuícióval kikövetkeztethető ezen utasítás működése:
int b;
b = rand() % 100; //b értéke egy 0 és 99 közötti egész szám lesz
b = rand() % 100 + 1; //b értéke egy 1 és 100 közötti egész szám lesz
b = rand() % 31 + 1987; //b értéke egy 1987 és 2017 közötti egész szám lesz

A break utasítást ebben a példában arra használjuk, hogyha a keresett számot a program megtalálta, akkor a ciklusnak már fölösleges tovább futnia (legalábbis e példa szerint). Persze akár az összes előfordulását is kiírathatnánk a keresett számnak (annak megvalósításához nem lenne szükség break utasításra).

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

int main() {
  int szamok[1000];
  for (int i = 0; i < 1000; i++) {
    szamok[i] = rand() % 100 + 1;
  }
  int k;
  cout << "Kerem adja meg a keresendo szamot (1 es 100 kozott)" << endl;
  cin >> k;
  if (k > 100 || k < 1) {
    cout << "Hiba: nem 1 es 100 kozotti szam lett megadva" << endl;
    return 0;
  }
  bool vanolyan = false;
  for (int i = 0; i < 1000; i++) {
    if (szamok[i] == k) {
      vanolyan = true;
      cout << "A keresett szam elso elofordulasi helye: " << i+1 << endl;
      break;
    }
  }
  if (!vanolyan) {
    cout << "A keresett szam nem talalhato a random generalt szamok kozott"  << endl;
  }
}

2. break példa: ciklus befejezése egy adott elemnél

Természetesen a break utasítás esetén is megtehetjük, hogy a feltételben egy konkrét tömbindexet adunk meg, hasonlóan, mint a continue utasítás esetén.
Ebben a nagyon egyszerű példában ha a felhasználó egy 1 és 10 közötti számot ad meg, akkor a program 1-től írja ki az egész számokat a felhasználó által megadott számmal bezárólag, egyébként pedig 1-től tízig.

#include <iostream>
using namespace std;

int main() {
  int szam;
  cout << "Kerem adjon meg egy 1 es 10 kozotti szamot" << endl;
  cin >> szam;

  for (int i=0; i < 10; i++ ) {
    if ( i == szam) {
      break;
    }
    cout << i+1 << endl;
  }
  return 0;
}

Fontos: a continue és break utasítások nem csak for cikluson belül használhatóak.

Egyéb for ciklusos példák

Egy tömb minden negyedik elemének kiíratása (kezdve a 0, 3, 7, 11... satöbbi indexű elemekkel):

/*az elemszam változó értéke a tömb elemszáma, és t[] a tömb neve*/

cout << "Az 1. elem erteke << t[0] << endl;

for (int i = 3; i < elemszam; i += 4) {
  cout << "A(z) " << i + 1 << ". elem erteke: " << t[i] << endl;
}

Hibát okoz, ha az i += 3 kifejezés helyett i = i + 3-at írunk.

Az eddigi példákból kiderül, hogy a for ciklust nem csak tömbök feldolgozásához használhatjuk. Bár önmagában nincs sok gyakorlati értelme, de kiírhatunk vele számokat is. Pl. a páros számok kiíratása 1-től 100-ig:

for (int i = 0; i < 100; i++) {
  if (i % 2 == 0) {
    cout << i << " ";
  } //if vége
} //for vége

Persze ehelyett akár azt is megtehetnénk, hogy adott számokat elhelyezünk egy tömbben, és azok közül íratjuk ki a páros számokat. Az eddig tanultak alapján az iménti példát könnyen átalakíthatjuk eszerint.

foreach ciklus (C++ 11)

A foreach ciklus a for ciklus egy speciális változata arra az esetre, ha például egy tömböt vagy std::vectort szeretnénk bejárni, egyesével az első elemtől kezdve az utolsóig. A foreach ciklusban nem adjuk meg a bejárni kívánt adatszerkezet méretét.

Ehelyett tehát...

for (int i = 0; i < elemszam; i++) {
  cout << t[i] << endl;
}

...írhatjuk ezt is:

//int típusú tömb esetén
for (int aktualis_elem : t) {
  cout << aktualis_elem << endl;
}

Ekkor az aktualis_elem változó nem a tömb indexelését látja el, hanem a ciklus minden egyes lépésében tartalmazza a soron következő elemet a t nevű tömbből (a ciklusmag első lefutásakor a tömb első elemét, a második lefutáskor a másodikat... satöbbi). Így tehát a típusa nem biztos, hogy int, hanem megegyezőnek kell lennie a tömb típusával (amilyen típusú elemek szerepelnek a tömbben).

Fontos: a foreach ciklus nem helyettesíti a for ciklust, hiszen az adott elem indexére (sorszámára) nem hivatkozhatunk benne.

Ügyeljünk arra, hogy a for ciklus ezen formáját csak a C++ 11-es szabvánnyal használhatjuk. (Az első lépéseknél volt szó arról, hogy hogyan paraméterezhetjük a fordítót a C++ 11-es szabványnak megfelelően).

Mivel a C++ 11-es szabványban használhatunk típuskikövetkeztetést, így a foreach ciklusokban nem kell megadnunk az adott tömb típusát, hanem rábízhatjuk annak felismerését a fordítóra is:

for (auto aktualis_elem : t) {
  cout << aktualis_elem << endl;
}

Egyéb:
A C++ STL (Standard Template Library) tartalmaz egy for_each nevű algoritmust, melyet az STL-ben elérhető adattípusok (pl. vector, list) feldolgozásához használhatunk, iterátorokkal paraméterezve.

A for ciklus harmadik paraméterét nyilvánvalóan a ciklusmag utolsó utasításaként is el lehetne helyezni (ha esetleg így teszünk, akkor viszont vegyük ki a paraméterlistából, különben kétszer fog lefutni), a végeredmény ugyanaz, de egy nagyon sok utasítást tartalmazó for ciklusnál talán átláthatóbb, ha a paraméterlistában szerepel. Eszerint tehát a for ciklust így is megfogalmazhattuk volna:

for (int i = 0; i < elemszam;) {
	//utasítások
	++i; //vagy i++; vagy i = i + 1;
}

Kétdimenziós tömbök

A kétdimenziós tömböket tömbök tömbjének is szokták nevezni, illetve mátrixoknak, de fontos megjegyezni, hogy a mátrix valójában egy matematikai fogalom, a programozásban nem csak kétdimenziós tömbökkel lehet mátrixot megvalósítani.

Példa: 5 napon keresztül napi 3 alkalommal megmértük a kültéri hőmérsékletet. Válasszuk ki minden egyes napon ezek közül a legmagasabbat (ahány nap, annyi eredmény).

Noha könnyű kiszámolni, hogy 5 * 3 = 15, így akár egy 15 elemű tömbben is elhelyezhetnénk az értékeket, de egyrészt jobb, ha a logikailag összetartozó elemeket megfelelően csoportosítjuk, másrészt azt mindenképp nyilván kell tartanunk, hogy az egy adott naphoz tartozó adatok hol kezdődnek és hol érnek véget, és ezt sokkal könnyebb megtenni egy 5 * 3-as kétdimenziós tömb használatával.
Egy 5 * 3-as kétdimenziós tömböt például egy táblázatként képzelhetünk el, melynek 5 sora, és 3 oszlopa van, és minden értékre úgy hivatkozhatunk, ha megadjuk a sorszámát és oszlopszámát.
Például így hivatkozhatunk egy 5 * 3-as kétdimenziós tömb (nevezzük mondjuk homerseklet-nek): első elemére  homerseklet[0][0], és az utolsó elemére: homerseklet[4][2].

A tömbökhöz hasonlóan a kétdimenziós tömbök elemeit szintén for ciklussal érdemes bejárni, azzal a különbséggel, hogy a kétdimenziós tömbök esetén két egymásba ágyazott for ciklus szükséges a bejáráshoz. A külső for ciklus számolja a sorokat (és az i ciklusváltozó tartalmazza az aktuális sor számát), a belső for ciklus pedig az oszlopokat számolja (a j ciklusváltozó tartalmazza az aktuális oszlop számát).

Ebben a példában a sorok száma megegyezik a napok számával (a 0 indexű oszlop az első nap), az oszlopok száma pedig egy adott napon mért értékek számával (a homerseklet[0][0] elem az első napon mért első érték).

A kétdimenziós tömböt az egyszerűség kedvéért (0 és 40 közti) random értékekkel töltjük fel, és ahogy egy elem megkapta az értékét, azt ki is íratjuk. Célszerű persze az értékek elé kiíratni, hogy hanyadik napon mért értékekről van szó.
Az egyes értékek persze lehet, hogy egymástól nagyon eltérőek lesznek (pl. egy napon akár kijöhet 0, 40 és 0 érték), de ettől most eltekintünk.

A legnagyobb érték tárolására létrehozunk egy változót, amit mondjuk elnevezhetünk max-nak. A legnagyobb értékek megkeresése természetesen újabb két egymásba ágyazott ciklust igényel.
Az egyes napokon mért értékeket úgy dolgozzuk fel, hogy először értékül adjuk az adott napon mért első értéket a max változónak, majd ezt összehasonlítjuk a további két értékkel. Ha azok közül valamelyik nagyobb nála (a max változónál), akkor a nagyobb értéket adjuk értékül a max változónak.

A külső for ciklus (i) egyes lépései jelentik az egyes napok feldolgozását, másképp fogalmazva a külső for ciklus egy adott lépését követően feldolgozottnak tekinthetjük az adott napot (sort), így a külső for ciklus utolsó utasításaként íratjuk ki az aktuális napra vonatkozó legnagyobb értéket (akár el is tárolhatnánk, és akkor 3 db. max változó kellene).

Kétdimenziós tömbök sorainak számát szokás m-mel, oszlopainak számát pedig n-nel jelölni.

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

int main() {
  const int sor = 5; //napok szama
  const int oszlop = 3; //egy nap vegzett meresek szama
  double homerseklet[sor][oszlop];

  /*ketdimenzios tomb feltoltese random ertekekkel (0 es 40 kozott), elemek kiiratasa*/
  for (int i = 0; i < sor; i++) {
    cout << "A(z) " << i+1 << ". napon mert ertekek: ";
    for (int j = 0; j < oszlop; j++) {
      homerseklet[i][j] = rand() % 41;
      cout << homerseklet[i][j] << " ";
    }
    cout << "\n";
  }

  /*legnagyobb ertekek megkeresese (5 ertek), kiiratasa*/
  int max;
  for (int i = 0; i < sor; i++) {
    max = homerseklet[i][0];
    for (int j = 1; j < oszlop; j++) {
      if (max < homerseklet[i][j]) {
        max = homerseklet[i][j];
      }
    }
    cout << "A(z) " << i+1 << ". napon mert legnagyobb homerseklet: " << max << endl;
  }
  return 0;
}

Ebben a tananyagban nem foglalkozunk változó oszlopszámú kétdimenziós tömbökkel (angolul jagged array). Ezeket például tömbök és mutatók (pointerek) segítségével lehet megvalósítani.

Do-while ciklus (hátultesztelős ciklus)

A do-while úgynevezett hátultesztelős ciklus, mely kifejezés azt jelenti, hogy a ciklusmagban lévő utasítások egyszer biztosan lefutnak, és a ciklusfeltételben megadott logikai kifejezés értéke csak a ciklusmag további lefutását befolyásolja.
Ebből az következik, hogy a do-while ciklust általában adatbevitel ellenőrzésre, vagy pedig utasítássorozatok ismételt végrehajtásához használjuk.
(A foreach, for és while ciklusok egyébként elöltesztelős ciklusok, mivel az esetükben a ciklusfeltétel már a ciklusmag legelső lefutását is befolyásolja: ha a ciklusfeltétel hamis, a ciklusmag egyszer sem fut le).

struktogram_ciklus_hatultesztelos.png

1. do-while ciklus példa: ismételt végrehajtás

Miután egyszer már lefutott a program, vagy bizonyos utasítások sorozata, megkérdezzük a felhasználótól, hogy szeretné-e újra végrehajtani. Igény szerint akár a program összes utasítása is szerepelhet egy do-while ciklus blokkjában.

Szemléltető vázlat:

do {
  //tetszőleges utasítások
  cout << "Szeretne ujra vegrehajtani a programot?" << endl;
  char valasz;
  cin >> valasz;
} while ( valasz == 'i' || valasz == 'I' );

2. do-while ciklus példa: adatbevitel ellenőrzés (ameddig a felhasználó nem megfelelő értéket ad meg (a jelenlegi példában negatív számot, túl sok számjegyből álló számot, vagy nem szám típusú értéket), újra bekérjük az adott értéket)

Ha például azt szeretnénk, hogy a felhasználó egy bekérésnél ne adjon meg negatív értéket, akkor a bekérésnek egyszer biztos, hogy le kell futnia, függetlenül attól, hogy a felhasználó milyen értéket adott meg, és csak akkor kell megismételni a bekérést, ha a megadott érték negatív szám, túl sok számjegyből álló szám, vagy nem szám típusú. Természetesen érdemes tudatni a felhasználóval, hogy milyen kritériumoknak megfelelő érték megadását várja el tőle a program.

Mivel több a hibalehetőség, érdemes egy logikai változót bevezetni annak az információnak a tárolására, hogy volt-e hiba a bekéréskor. Abból indulunk ki, hogy nincs hiba, ezért a kezdőértéke nyilvánvalóan false, amit a vizsgált hibalehetőségek bármelyikének előfordulása esetén true-ra változtatunk. (Jelen példában nem íratunk ki különböző hibaüzeneteket a különböző hibalehetőségek esetén).

C++-ban ugyebár ha egy szám bekérésekor a felhasználó nem szám típusú értéket ad meg (hanem mondjuk szöveget), akkor onnantól kezdve az összes bekérés át lesz ugorva, kivéve akkor, ha a szóban forgó bekérés után elhelyezzük a cin.clear(); utasítást. Persze ez az utasítás csak akkor kell, hogy lefusson, ha a felhasználó szám helyett egyéb karaktereket adott meg. Ezt a cin objektum fail tagfüggvényével tudjuk ellenőrízni, mely true értéket ad vissza, ha a felhasználó egy szám bekérésekor nem szám típusú értéket adott meg, egyénként pedig false-t. A cin.fail() által visszaadott értéket tehát lementjük a hiba nevű logikai változóba: hiba = cin.fail(); és a cin.clear(); utasítást csak akkor futtatjuk, ha a hiba változóba beolvasott érték true volt: if (hiba) { cin.clear(); }

Ezt követően az input buffert tartalmát is ürítjük ( string tmp; getline(cin,tmp,'\n'); ). Ha ezt nem tennénk meg, akkor abban az esetben, ha a felhasználó nem szám típusú értéket adna meg, végtelen ciklusba kerülne a program, hiszen egy érték csak akkor törlődik az input bufferből, ha azt sikeresen értékül adjuk egy változónak, vagy ha manuálisan töröljük.

A következő utasításban további két hibalehetőségtől függően módosítjuk (true-ra ha még nem az) a hiba változó értékét:

hiba = ( hiba || tmp.size() != 0 || ertek < 0 );

Ezt az utasítást ugyebár úgy kell értelmezni, hogy a hiba változónak akkor adunk true-t értékül, ha a megadott || jellel elválasztott logikai kifejezések közül legalább az egyik értéke (eredménye) true.

Ezek közül az ertek < 0 egyértelmű: hibalehetőség, ha negatív számot ad meg a felhasználó.

A tmp.size() != 0 kifejezés szerepét elsőre talán nem olyan könnyű kitalálni. Ezzel szűrjük ki azokat az eseteket, ha a felhasználó esetleg egy olyan értéket adna meg, aminek az első valahány karaktere elfogadható helyes értéknek, a továbbiak viszont nem. Például 33a vagy 2.45w4%s6.
Egy korábbi fejezet kiegészítő információi között már volt szó arról, hogy a C++ az egy adott bekéréskor megadott értéket először az input bufferben (a számítógép memóriájának egy bekérések számára elkülönített részében) tárolja, és az egyes bekérések az input buffer tartalmát dolgozzák fel. A program az input bufferbe beolvasott kifejezést tokenizálja (részekre bontja, tagolja), és a kifejezés elejéről törli, és értékül adja az adott bekéréshez kapcsolódó változónak azt az első valahány karaktert, ami megfelel az adott bekéréshez kapcsolódó változó típusának, a további karaktereket pedig továbbadja a következő bekérésnek. Ebben az egyszerű példában persze nincs következő bekérés, de bonyolultabb programokban hasznos lehet, ha ezt a hibalehetőséget is kezeljük.
Tehát ha a felhasználó olyan értéket ad meg, aminek az első valahány karaktere érvényes érték, a továbbiak viszont nem, akkor azt az ebben a példában szereplő program nem fogja elfogadni.
Kipróbálhatjuk, hogy ha töröljük, vagy kikommenteljük a tmp.size() != 0 kifejezést, akkor a program el fogja fogadni az előbb elmített értékeket.

A || jelekkel elválasztott kifejezések között szerepel továbbá a hiba változó értéke is. Erre azért van szükség, mert ha a hiba változó értéke korábban már true volt (ami ugyebár a hiba = cin.fail(); utasítástól függ), akkor maradjon is az, akkor is, ha az ertek < 0 és a tmp.size() != 0 kifejezések értéke false.

Ezt követően ha a hiba változó értéke true, akkor kiíratunk egy hibaüzenetet.

Ne feledjük, a ciklusmag utasításai is csak akkor futnak le újra, ha a hiba változó értéke true, hiszen a do-while ciklus végén a } while ( hiba ); kifejezés szerepel.

Ez elsőre nem tűnhet egyszerű példának, hiszen például nem minden programozási nyelvben van szükség arra, hogy manuálisan ürítsük az input buffert. Ahhoz, hogy ezt a példát megértsük, gondoljuk végig, hogy a hiba változó értéke milyen esetekben lesz true, és hogy ezen esetekben miért van szükség a ciklus ismételt végrehajtására.

#include <iostream>
using namespace std;

int main() {
  double ertek;
  bool hiba = false;
  do {
    cout << "Kerem adjon meg egy nem negativ szamot: " << endl;
    cin >> ertek;
    hiba = cin.fail();
    if (hiba) {
      cin.clear();
    }
    string tmp; getline(cin,tmp,'\n');
    hiba = ( hiba || tmp.size() != 0 || ertek < 0 );
    if (hiba) {
      cout << "A megadott ertek nem megfelelo" << endl;
    }
  } while ( hiba );

  cout << "A megadott ertek: " << ertek << endl;
  return 0;
}

Természetesen egyéb módon is kezelhetjük, ha a felhasználó nem érvényes értéket ad meg egy bekéréskor. Például három próbálkozás után kiléphetünk a programból, vagy megadhatunk egy default értéket.

3. do-while ciklus példa: gondoltam egy számra

Ebben a példában a program által generált egész számot találhatja ki a felhasználó. Egy adott szám kitalálására 5 lehetőség van, ezt követően a program megkérdezi a felhasználótól, hogy szeretne-e új számot generálni. Ha ekkor a felhasználó válaszként n vagy N karaktert ad meg, a program kilép a ciklusból és eljut a return 0; utasításig, vagyis leáll.
Ha a felhasználó nem találta ki a számot, akkor a program kiírja, hogy a felhasználó tippje kisebb, vagy nagyobb a számnál. (Ez akkor is megtörténik, ha már nincs további lehetőség a tippelésre. Tetszés szerint ezt módosíthatjuk egy újabb feltétellel, de ebben a példában az egyszerűség kedvéért ez nincs kiküszöbölve.)
Egy fentebbi példában, ahol random generált számokat használtunk, észrevehettük, hogy a program többszöri futtatása esetén a számok nem változnak. Ennek kiküszöbölése érdekében használhatjuk az srand(time(0)); utasítást, ami a random generált számokat a számítógép órájától teszi függővé, így kevésbé esélyes, hogy a program többször is ugyanazt a számot generálja. Ezen utasítás használatához szükség lesz a C++ szabványkönyvtár ctime header importálására.
A programban két do-while ciklus is szerepel. Az egyik azért, mert van a programban újrakezdési lehetőség, a másik pedig azért, mert nem szeretnénk minden tippet követően a felhasználótól megkérdezni, hogy szeretne-e új számot generálni.
A találgatás tulajdonképpen megvalósítható lenne for ciklussal is, hiszen tudjuk, hogy legfeljebb 5 találat lesz (ha pedig a felhasználó 5 találatnál hamarabb kitalálja, arra ott a break utasítás).
Ügyeljünk arra, hogyha a random generált szám kisebb, vagy nagyobb a tippnél, akkor ne így fogalmazzuk meg a feltételt: if (tipp > random ) { //utasítások } else { //utasítások }, mivel ekkor az else ág az egyenlőségvizsgálatot (tipp == random) is lefedné, azt pedig egy külön esetként kezeljük.
A változatosság kedvéért a külső while ciklus feltételét a fentebb látható do-while példától eltérően fogalmaztuk meg. Ehelyett: do { } while ( ujra == 'i' || ujra == 'I' ); Ezt használjuk: do { } while (ujra != 'n' && ujra != 'N');
A jelenleg használt (utóbbi) esetben csak akkor ér véget a külső do-while ciklus, ha a felhasználó n vagy N karaktert ad meg, bármilyen más karakter megadása esetén újra lefut a ciklusmag, vagyis egy új szám generálódik, és újra lehet tippelni. Adatbevitel ellenőrzéssel persze meg lehetne oldani, hogy csak i és n karaktereket fogadjon el a bekérés, de ezzel az egyszerűség kedvéért most nem foglalkozunk.

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

int main() {
  cout << "A program general egy egesz szamot 1 es 30 kozott. 5-szor lehet probalkozni kitalalni, majd a program egy ujabb szamot general" << endl;

  int random;
  srand(time(0));
  int tipp;
  int hanyadik_tipp;
  char ujra;
  do {
    random = rand() % 30 + 1;
    hanyadik_tipp = 1;
    do {
      cout << "Kerem adjon meg egy egesz szamot" << endl;
      cin >> tipp;
      if (tipp < random) {
        cout << "A random generalt szam ennel a tippnel nagyobb" << endl;
      } else if (tipp > random) {
        cout << "A random generalt szam ennel a tippnel kisebb" << endl;
      }
      hanyadik_tipp++;
    } while ( tipp != random && hanyadik_tipp <= 5 );
  if ( tipp == random ) {
    cout << "Gratulalunk! A jelenlegi tippje (" << tipp << ") egyezik a program altal generalt szammal." << endl;
  } else {
    cout << "Sajnos az 5 probalkozas egyike sem egyezik a program altal generalt szammal: " << random << endl;
  }
  cout << "Szeretne uj szamot generalni? (i/n)" << endl;
  cin >> ujra;
  } while (ujra != 'n' && ujra != 'N');
  return 0;
}

Hasonlóan lehet megvalósítani a dobókockával végzett műveleteket is.

While ciklus (elöltesztelős ciklus)

Amíg a foreach ciklus a for ciklus egy speciális esetére alkalmazható, úgy a while ciklussal a for ciklusnál bővebb eseteket is megvalósíthatunk (másképp fogalmazva a for ciklus a while ciklus egy speciális esete).
A while ciklus tulajdonképpen olyan mint egy if, else ág nélkül, azzal a különbséggel, hogy nem csak egyszer fut le, hanem egészen addig, amíg az általunk megadott feltétel érvényben van (tehát true az értéke). Amikor a feltétel már nem teljesül, a ciklusmag nem fut le (a ciklus leáll).
Ügyeljünk arra, hogy a do-while ciklussal ellentétben a while ciklus ciklusmagja egyszer sem hajtódik végre, ha a feltétel már a ciklus első lépésében hamis.

Szintaktika:

while ( /*feltétel*/ ) {
  //tetszőleges utasítások
}

1. while ciklus példa: egy egész szám számjegyeinek száma

For ciklus helyett while ciklust használhatunk, ha egy számlálásnál nem ismerjük a ciklusmag végrehajtásának számát.

Egy int típusú szám 10-zel való osztása valójában az adott szám legkisebb helyiértékű számjegyének elhagyását jelenti (mivel egészosztás esetén az eredményt nem tizedestört formájában kapjuk, hanem egész számként). Pl. ha int típusú 128-at osztunk 10-zel, annak eredménye 12 lesz.
(Ez persze csak akkor igaz, ha a 10 is int típusú. Ha az eredményt tizedestört alakban kívánjuk megkapni, akkor használjuk a 128 / 10.0 vagy esetleg a static_cast<double> (128 / 10) kifejezéseket).
Ha egy int típusú szám minden egyes 10-zel osztását követően növelünk egy számlálót, akkor megkapjuk az adott szám számjegyeinek számát.
A számlálónak adjunk kezdőértéket, különben az eredmény nem lesz megfelelő.

#include <iostream>
using namespace std;

int main () {
  int szam;
  cout << "Kerem adjon meg egy egesz szamot" << endl;
  cin >> szam;
  cout << "A beolvasott szam: " << szam << endl;
  int szamjegyek = 1;

  while ( szam /= 10 ) {
    szamjegyek++;
  }

  cout << "A szamjegyek szama: " << szamjegyek << endl;
  return 0;
}

2. while ciklus példa: soronkénti beolvasás fájlból

While ciklus feltételeként egy függvényhívás eredményét is felhasználjuk, jobban mondva a függvény által visszaadott értéktől függ, hogy a while ciklus magjában lévő utasítások hányszor futnak le.

Például amikor egy egyszerű szöveges fájl egyes sorait olvassuk be, a beolvasásnak nyilván meg kell állnia, ha a fájl végére értünk. Erre használhatjuk az eof() tagfüggvényt, mely true értéket ad vissza, ha a programunk a fájl végére ért.

A példaprogramban az ifstream fajl1( "feladat.txt" ); utasítással definiálunk egy fajl1 nevű objektumot amin keresztül elérhetjük az elérni kívánt fájlt, mely fájl nevét természetesen a fajl1 objektumnak (idézőjelek között) paraméterül adjuk. Ez az objektum tartalmaz egy mutatót, amelyben a program azt tárolja, hogy hol tart a fájl feldolgozása. Az fstream típusú objektum előnye továbbá, hogyha a fájl neve esetleg megváltozna, akkor nem kell annyi helyen átírni a kódban, ahány helyen hivatkozunk rá.

Definiálunk egy sor nevű string típusú változót is, amelybe a fájl egyes sorait beolvassuk.

Az fstream típusú objektumok is_open() tagfüggvényével tudjuk ellenőrízni, hogy a programnak sikerült-e megnyitnia a fájlt. A program fájlból olvasás művelet esetén értelemszerűen nem tudja megnyitni a fájlt, ha például nem létezik a fájl. (Ha írni akarnánk a fájlba, a program létrehozná a fájlt akkor is, ha nem létezik).

Nyilván csak akkor érdemes elkezdeni a fájl feldolgozását, ha a fajl1 is_open() tagfüggvénye igazat ad vissza, így a feldolgozáshoz tartozó utasításokat egy if elágazás igaz ágában helyezzük el, és feltételnek a fajl1.is_open()-t adjuk meg.

A lényeg az if elágazás igaz ágában található:

while ( !fajl1.eof() ) {
  getline(fajl1,sor);
  cout << sor << endl;
}

A fajl1 objektum eof() tagfüggvénye igazat ad vissza, ha a a fajl1 objektum mutatója a fájl végére ér. Értelemszerűen a ciklusmag akkor hajtódhat végre, ha még nem értünk a fájl végére. Ezt az eof() tagfüggvény által visszaadott érték tagadásával fogalmazhatjuk meg: !fajl1.eof().
A ciklusmag minden egyes végrehajtásakor a getline függvény beolvassa a fájl soron következő sorát a sor változóba, majd azt követően ki is íratjuk.
Ha tehát a feldolgozás a fájl végére ér, a ciklus leáll.

Rögtön a fájl feldolgozása után a close() tagfüggvénnyel lezárjuk a fájlt, hogy ezzel más programok számára elérhetővé tegyük a hozzáférést. Ha ezt elfelejtenénk, a program sikeres lefutását megelőzően automatikusan megtörténne, de a hibalehetőségek csökkentése érdekében érdemes hozzászokni ahhoz, hogy rögtön a fájl feldolgozását követően mi magunk tegyük meg.

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

int main() {
  ifstream fajl1( "feladat.txt" );
  string sor;
  if ( fajl1.is_open() ) {
    while ( !fajl1.eof() ) {
      getline(fajl1,sor);
      cout << sor << endl;
    }
    fajl1.close();
  } else {
    cout << "Hiba a fajl megnyitasakor" << endl;
  }
return 0;
}

Előfordulhat, hogy könyvekben, internetes példákban ennek a kódrészletnek egy kicsit módosított változatával találkozunk:

while ( getline(fajl1,sor) ) {
  cout << sor << endl;
}

Elsőre furcsának tűnhet, hogy a while ciklus feltételeként nem egy logikai kifejezés, hanem egy utasítás van megadva. Nos, ez csak azért lehetséges, mert ez az utasítás egy függvényhívás, és a getline függvénynek is van visszatésési értéke. Abba most nem megyünk bele, hogy pontosan mi a getline visszatérési értéke, a lényeg, hogy logikai értékké konvertálva akkor lesz false, amikor a feldolgozás már a fájl végére ért. Tehát ezen kódrészlet eredménye lényegében egyezik a fentebbiével.

3. while ciklus példa: szavankénti beolvasás fájlból

A fájlból történő szavankénti beolvasás szintaktikailag egyezik a felhasználótól történő bekéréssel, az eltérés csupán annyi, hogy cin helyett az fstream típusú objektum nevét adjuk meg.
Tehát például ehelyett: cin >> n; ezt írjuk: fajl2 >> n;

Nyilvánvalóan ahhoz, hogy egy fájlt megfelelően tudjunk szavanként feldolgozni, ismernünk kell a tartalmát. Például tudnunk kell, hogy szóközzel elválasztva találhatók benne az elemek.
Legyen a feladat például az, hogy a feladat2.txt fájlban található (egymástól szóközökkel elválasztott) számok közül válasszuk ki a legkisebbet, és az eredményt írjuk a fájl egy újabb sorába. A fájl tartalma legyen mondjuk:

1.3 -23 -5.4 2 1 0 63.1 4.6 45.23

Ha egy fájlt olvasni és írni is szeretnénk, akkor fstream típusú objektumot használhatunk, és a definiálásánál paraméterül adjuk a használni kívánt fájl nevét. (Egyébként csak olvasáshoz használhatunk ifstream, csak íráshoz pedig ofstream objektumokat).

Bár a fájlba írás esetén nem okoz hibát, ha nincs létrehozva a fájl, nyilvánvalóan az elemek beolvasásához léteznie kell a fájlnak a megfelelő tartalommal, ezért a program lényegi utasításait az if ( objektum_neve.is_open() ) feltétel true blokkjába helyezzük el.
A feltételben biztonsági okokból elhelyezhetjük az objektum_neve.eof() kifejezést is, hogy üres fájl esetén ne kezdődjön meg a feldolgozás.
Ebben a példában nem végezünk adatellenőrzést. Ha a fájl nem szóközökkel elválasztott számokat tartalmaz, a programunk futás közbeni hibával le fog állni.

A legkisebb érték megkeresése:
Létrehozunk egy min nevű változót és értékül adjuk neki a fájlból beolvasott első értéket. A továbbiakban a beolvasott értékeket egy aktualis_elem nevű változóban tároljuk és minden egyes beolvasást követően ennek a változónak az értékét összehasonlítjuk a min változó értékével. Ha az aktualis_elem értéke kisebb, mint a min változóé, akkor értékül adjuk a min változónak.

A fájlba írás hasonlóan működik, mint a cout használata:
objektum_neve << valtozo << endl;

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

int main() {
    fstream fajl2( "feladat2.txt" );
    if ( fajl2.is_open() && !fajl2.eof() ) {
      double min;
      fajl2 >> min;
      double aktualis_elem;
      while ( !fajl2.eof() ) {
        fajl2 >> aktualis_elem;
        if ( aktualis_elem < min ) {
          min = aktualis_elem;
        }
      }
      cout << "A fajlban szereplo legkisebb ertek: " << min;
      fajl2.clear();
      fajl2 << min;
      fajl2.close();
  } else {
    cout << "Hiba a fajl megnyitasakor" << endl;
  }
  return 0;
}

Végtelen ciklus

Bármilyen ciklusról is legyen szó, gondoskodni kell arról, hogy a ciklusfeltétel egyszer majd a ciklus valamelyik lépésében hamis legyen. Végtelen ciklusnak nevezzük azokat a ciklusokat, amelyek esetén a ciklusfeltétel sosem lesz hamis, vagyis a ciklus sosem áll le. Általában a felhasználó a végtelen ciklusok eredményét úgy érzékeli, hogy nem kapja vissza a vezérlést, vagy esetleg a gép nagyon belassul (pl. azért mert betelik a memória). Persze ma már az ilyen egyszerű hibákat az operációs rendszerek sok esetben tudják kezelni, esetleg automatikusan le is állítják az adott programot, legrosszabb esetben a felhasználónak kell sajátkezűleg leállítani egy nem válaszoló, vagy nagyon belassult programot (pl. windowsban task maganer (feladatkezelő) segítségével, vagy például ubuntuban ctrl + alt + f1, és a top és kill parancsok segítségével).

Kiegészítő információk

Repeat-until ciklus

C++-ban konkrétan nincs repeat-until ciklus, de mivel sokszor láttam egyes fórumokon, hogy összekeverték a do-while ciklussal, így gondoltam, hogy mellékesen kitérek rá.
A do-while ciklusban ismétlési feltételt kell megadnunk, vagyis egy olyan logikai kifejezést, amelynek teljesülése esetén a ciklusmag utasításai végrehajtódnak. Azontúl, hogy a do-while ciklus esetén ugyebár a ciklusmag utasításai a feltételtől függetlenül egyszer biztosan lefutnak, a ciklus akkor ér véget, ha a ciklusfeltétel hamis, azaz false értékű.
Ezzel szemben a repeat-until ciklusban kilépési feltételt kell megadnunk, vagyis a ciklusmag utasításai azontúl, hogy egyszer biztosan lefutnak, akkor futnak le újra, ha a ciklusfeltétel hamis, azaz false értékű, és akkor áll meg a ciklus, ha a ciklusfeltétel igaz, azaz true értékű.

Repeat-until ciklussal például Pascal programozási nyelvben találkozhatunk.

A do-while-t és a repeat-until-t véleményem szerint csak akkor lehet összekeverni, ha valakinek csak halvány ismeretei vannak a programozásról, illetve esetleg akkor ha valaki egyszerre több programozási nyelvet tanul/használ.

GOTO utasítás

A GOTO utasítások úgynevezett ugróutasítások. Megegyezés szerint a struktúrális programozásban a használata kerülendő, kivéve néhány ritka esetet.

Például ha több egymásba ágyazott ciklusból szeretnénk kilépni, akkor azt gyorsabb megtenni egy GOTO segítségével mint break utasításokkal.

for (int i = 0; i < 10; i++) {
  for (int j = 0; j < 10; j++) {
    for (int k = 0; k <10; k++) {
      //tetszőleges utasítások
      if ( /*feltétel*/ ) {
        goto ciklusvege;
      }
    }
  }
}
ciklusvege:
/*ha a megadott feltétel igaz, a program végrehajtása ezen a ponton folytatódik*/

Ciklusok átalakítása

Egy kis átalakítással bármilyen ciklussal el tudjuk végezni a különböző típusú ciklusokkal elvégezhető feladatokat. Bár a gyakorlatban ritkán használjuk így az egyes ciklusokat (kivétel talán a while ciklus for ciklus helyett), egyes tanfolyamokon az alaposabb megértés érdekében ez is szokott szerepelni a tananyagban.

While ciklus for ciklusként

A ciklus előtt definiálunk egy int típusú változót (tetszőleges (pl. i) névvel, és tetszőleges (pl. 0) kezdőértékkel), melyet a ciklusmag utolsó utasításaként eggyel növelünk.

{
  int i = 0;
  while ( i < elemszam ) {
    //utasítások
    i++;
  }
}

Az i változót értelemszerűen nem lehet a while ciklus blokkján belül definiálni (hiszen azonos nevű változót azonos blokkon belül nem definiálhatunk többször).

Az i változó létrehozását tartalmazó utasítást, valamint a ciklust egy újabb blokkban helyezzük el, annak érdekében, hogy az i változó érvényessége csak a ciklus végéig tartson.

For ciklus while ciklusként

A for ciklus paraméterlistájában az első és a harmadik paramétert elhagyjuk (de a pontosvesszőket kiírjuk), csak feltételt adunk meg.

for ( ; /*feltétel*/ ; ) {
  //tetszőleges utasítások
}

While ciklus do-while ciklusként

A ciklusmag utasításait a ciklusfeltétel előtt egyszer végrehajtjuk.

//a ciklusmag utasításai
while ( /*feltétel*/ ) {
//tetszőleges utasítások
}

A pontos átalakítás érdekében fontos, hogy a while ciklus előtti utasítások ugyanazok legyenek, mint a ciklusmagban lévőek.

Do-while ciklus while ciklusként

A do

A bejegyzés trackback címe:

https://itkezdoknek.blog.hu/api/trackback/id/tr5610423780

Kommentek:

A hozzászólások a vonatkozó jogszabályok  értelmében felhasználói tartalomnak minősülnek, értük a szolgáltatás technikai  üzemeltetője semmilyen felelősséget nem vállal, azokat nem ellenőrzi. Kifogás esetén forduljon a blog szerkesztőjéhez. Részletek a  Felhasználási feltételekben és az adatvédelmi tájékoztatóban.

Nincsenek hozzászólások.