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

C++ programozás kezdőknek - tömb, std::array, std::vector, for ciklus

[2017. szeptember 11.] [ christo161 ]

Ebben a tananyagrészben arról lesz szó, hogyan tudunk sok azonos típusú értéket kezelni (tárolni, kiíratni, módosítani).

(EZ A TANANYAGRÉSZ JELENLEG ÁTDOLGOZÁS ALATT ÁLL.)

cpp_ciklusok_gondoltam_egy_szamra.png

Előző tananyagrész: switch-case, ternáris operátor
Következő tananyagrész: while ciklus, fájlkezelés

Tartalom

Alapvető tudnivalók

Arról már volt szó egy korábbi tananyagrészben, hogy egy változóban a program futásának egy adott pillanatában egyszerre csak egy értéket tárolhatunk.
Egy tömbben (angolul array) a program futásának egy adott pillanatában egyszerre több érték is tárolható (a C++ nyelvben ezeknek az értékeknek azonos típusúnak kell lennie, pl. Javascript és PHP nyelvekben ez nem így van). Egy tömb hasonló célt szolgál, mint több változó együttvéve, de egy tömbnek csak egy neve van (a több változóval ellentétben), és egy számmal (úgynevezett indexszel) jelöljük, hogy hanyadik elemhez szeretnénk éppen hozzáférni, ami sokszor sokkal egyszerűbb lehet, mintha sok változót külön-külön el kellene nevezni.
A tömbök fontos jellemzője még, hogy a bennük tárolt adatok a számítógép memóriájában egymás után tárolódnak (pl. egy láncolt lista esetén ez nem így van).

Egy 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.

(Különböző típusú, de logikailag összetartozó értékek tárolásához C++ nyelvben osztályokat, objektumokat használunk.)

A C++ nyelvben alapvetően 4 féle módszer létezik az adatok tömbszerű tárolására:

Tömb

Egyes tananyagokban szokták C stílusú tömbnek vagy statikus tömbnek is nevezni. A statikus tömb elnevezés megtévesztő lehet, mert esetleg összekeverhető a static tárolási osztályú (angolul storage class) tömbbel (mivel a static tárolási osztálynak a függvényeknél vagy az osztályoknál van szerepe, ezért azokban a tananyagrészekben tárgyaljuk).

  • A méretét a programozónak a forráskódban meg kell adni, nem szabványos a kód ha a felhasználótól kérjük be a tömb méretét
  • A mérete nem változhat meg, vagyis például új elem nem fűzhető hozzá
  • Nem tudja magáról a méretét, vagyis a méretét egy külön változóban kell tárolnunk, és a tömbbel együtt kezelnünk
  • Nincsenek segédfüggvényei, mint például a Javascript nyelvben lévő tömböknek
  • Könnyebb hibákat (pl. túlindexelés) elkövetni a használata esetén
  • Függvényeknek történő átadáskor pointerré alakul (ebből adódhatnak hibák)
  • Gyorsabb, mint a többi megoldás

Dinamikusan memória allokálás

Dinamikus memória allokálással (new operátorral) tudunk tömbszerűen kezelhető adatterületet lefoglalni a számítógép memóriájában, amit dinamikusal allokált tömbnek szoktak nevezni. Valójában egy pointeren keresztül érjük el a lefoglalt területet, a pointer pedig nem tömb, csak bizonyos esetekben hasonlóan használható, ezért ezt a megoldást nem mindenki szokta tömbnek nevezni.

  • A méretét akár a felhasználótól is bekérhetjük
  • A mérete megváltozhat, de akkor egy másik, ideiglenes dinamikusan allokált tömbbe át kell másolnunk az addigi elemeit, majd az átméretezett tömbbe visszamásolni, amely során könnyű hibákat elkövetni
  • Nem tudja magáról a méretét, vagyis a méretét egy külön változóban kell tárolnunk, és a tömbbel együtt kezelnünk
  • Nincsenek segédfüggvényei
  • Könnyebb hibákat (pl. túlindexelés) elkövetni a használata esetén
  • Gyorsabb lehet, mint az std::vector

std::array

  • A mérete nem változhat meg
  • A méretét fordítási időben ismerni kell (nem kérhető be a felhasználótól), különben fordítási hibát kapunk
  • Vannak segédfüggvényei (pl. lekérdezhető a mérete)
  • Hibabiztos
  • Függvényeknek történő átadáskor nem alakul át pointerré
  • Lassabb, mint a hagyományos tömb

std::vector

  • A mérete bekérhető a felhasználótól
  • A mérete megváltozhat, új elemek hozzáfűzhetők
  • Vannak segédfüggvényei (pl. lekérdezhető a mérete)
  • Hibabiztos
  • Függvényeknek történő átadáskor nem alakul át pointerré
  • Lassabb, mint a dinamikus tömb

Melyiket érdemes használni?

Függvények blokkjában (beleértve a main függvény blokkját is) lehetőség szerint std::array-t és std::vector-t használjunk, esetleg egyszerű feladatokhoz hagyományos tömböt.

Olyan feladatok esetén, ahol függvények egymásnak adogatnak át adatokat, lehetőleg ne használjunk hagyományos tömböket, csak std::array-t és std::vector-t. Az eddigi tananyagrészekben nem volt ilyen feladat, erről a témáról később lesz szó, de ennyit már most érdemes megjegyezni.

Dinamikus memória allokálást lehetőleg csak osztályokon/objektumokon belül használjunk, nem osztályokhoz/objektumokhoz tartozó függvényeken belül ne, mert a memória felszabadítása könnyen elfelejtődhet, főleg akkor, ha egymásnak adogatják a függvények a lefoglalt területre mutató pointert (ekkor nehéz eldönteni, hogy melyik függvényben szabadítsuk fel a lefoglalt memóriát).

Ebben a tananyagrészben egy ideig szerepelt a dinamikusan allokált tömbök használata. Végül azért döntöttem az eltávolítása mellett, mert a tananyagnak ezen a pontján csak olyan példákkal lehet bemutatni, amik pont hogy kerülendőek.

Hogyan használjuk a tömböket, és tömbszerű adatszerkezeteket?

Definiálás (létrehozás)

A C++ nyelvben, csak úgy mint a változókat, a tömböket és a tömbszerű adatszerkezeteket is létre kell hozni (definiálni kell) az első használatuk előtt.

Tömböket és tömbszerű adatszerkezeteket is létrehozhatunk const típusminősítővel, ekkor az elemei nem lesznek módosíthatóak. Ha egy tömb vagy tömbszerű adatszerkezet elemeit nem akarjuk módosítani, akkor célszerű const típusminősítővel létrehoznunk azt.

Tömb definiálása

Például egy 10 darab int típusú elem tárolására alkalmas tömb létrehozása:

int array_example[10];

vagy

const int arr_size = 10;
int array_example[arr_size];

vagy az elemek felsorolásával:

int array_example[] = {1,2,3,4,5,6,7,8,9,10};

gcc és clang fordítóval működik, ha a felhasználótól kérjük be a tömb méretét, de nem szabványos, kerülendő kód:

//nonstandard, should be avoided
int arr_size{};
std::cout << "Please enter the number of elements:\n";
std::cin >> arr_size;
int array_example[arr_size];

Ehelyett a megoldás helyett std::vectort használjunk.

Egy tömb nem adható értékül egy másik tömbnek. (Ehelyett for ciklussal egyenként tudjuk átmásolni egy tömb elemeit egy másikba.)

//error
int arr_example1[] = {1,2,3,4,5};
int arr_example2[] = arr_example1;

std::array definiálása

Például egy 10 elem tárolására alkalmas std::array létrehozása:

#include <array>
//...
const int arr_size = 10;
std::array<int, arr_size> stdarr_example;

Fordítási hibát okoz, ha a méretét a felhasználótól kérjük be:

//error
#include <array>
//...
int arr_size{};
std::cout << "Please enter the number of elements\n";
std::cin >> size;
std::array<int, arr_size> stdarr_example;

Egy std::array-t értékül adhatunk egy ugyanakkora méretű másik std::arraynek. Ekkor az egyik std::array elemei átmásolódnak a másikba. Onnantól kezdve ha az egyik std::array-t módosítjuk, nem módosul a másik is.

std::array<int, 3> stdarr_example1 = {1,2,4};
std::array<int, 3> stdarr_example2 = stdarr_example1;

std::vector definiálása

Nem kötelező előre megadni a méretét, hiszen egyenként hozzá lehet adni elemeket.

#include <vector>
//...
std::vector<int> stdvector_example;

Gyorsabbá teheti a programot, ha előre megadjuk az std::vector méretét, hogy ne kelljen állandóan növelgetni.

#include <vector>
//...
std::vector<int> stdvector_example;
stdvector_example.resize(10);

A felhasználótól is bekérhetjük a méretét:

#include <vector>
//...
int vec_size{};
std::cout << "Please enter the number of elements\n";
std::cin >> vec_size;
std::vector<int> stdvector_example;
stdvector_example.resize(vec_size);

Egy std::vector-t értékül adhatunk egy másik std::vector-nak, amely akár eltérő méretű is lehet. Ekkor az egyik std::vector elemei átmásolódnak a másikba. Onnantól kezdve ha az egyik std::vector-t módosítjuk, nem módosul a másik is.

std::vector<int> stdvec_example1 = {1,2,4};
std::vector<int> stdvec_example2 = stdvec_example1;

Méret lekérdezése

A hagyományos tömb és a dinamikusan allokált tömb esetén a méretet egy erre a célra fenntartott változóban kell nyilvántartanunk, amíg a tömbbel dolgozunk. Nem csak a definiáláskor (létrehozáskor) van szükség egy tömb méretére, hanem például akkor is, amikor egyenként végiglépkedünk az elemein és valamilyen műveletet végzünk az elemekkel, hiszen a tömb méretéből derül ki, hogy melyik az utolsó eleme.

Ha a tömb kezdőértékét az elemek felsorolásával adtuk meg, akkor nem szükséges megszámolni, hogy hány elemet adtunk meg, és azt az értéket literálként vagy egy változó értékeként használni a továbbiakban (főleg, ha esetleg új elemeket tennénk hozzá a tömb kezdőértékéhez), hanem a sizeof operátorral kideríthetjük a tömb méretét. Mivel a sizeof operátor bájtban adja meg a tömb méretét, ezért az eredményt el kell osztanunk a tömb egy elemének a méretével ahhoz, hogy megkapjuk az elemszámot.
Fontos: ha a tömböt átadjuk egy másik függvénynek, azon belül a függvényen belül nem működik ez a módszer, továbbá dinamikusan allokált tömbök esetén sem működik.

//determine size of array
//this doesn't work in a function we passed the array!
#include <iostream>

int main() {
  double arr_example[] = {2.3, -1.9, 10.01, 23.67, -42.1, 78.33, -12.11, 91.2, 33.6, 1.0};

  std::cout << "size of arr_example: " << sizeof(arr_example)/sizeof(arr_example[0]) << '\n';
}

C++17 vagy újabb szabvány szerint fordított kódban használhatjuk az std::size függvényt a tömbök méretének kiderítéséhez, mely szintén nem működik egy olyan függvényben, aminek átadtuk a tömböt.

//determine size of array (C++17)
//this doesn't work in a function we passed the array!
#include <iostream>
#include <iterator>

int main() {
  double arr_example[] = {2.3, -1.9, 10.01, 23.67, -42.1, 78.33, -12.11, 91.2, 33.6, 1.0};

  std::cout << "size of arr_example: " << std::size(arr_example) << '\n';
}

Az std::array és az std::vector mérete lekérdezhető a size() tagfüggvénnyel:

//std::vector get size example
#include <iostream>
#include <vector>

int main() {
  std::vector<int> stdvec_example = {1,2,4};

  std::cout << "the size of stdvec_example: " << stdvec_example.size() << '\n';
}

Ha esetleg egy változóba szeretnénk elmenteni egy std::array vagy egy std::vector méretét, akkor ne int típust használjunk, hanem size_t típust.

Hozzáférés egy adott elemhez

Fontos: egy tömb, vagy tömbszerű adatszerkezet elemeinek a számozása 0-tól kezdődik, másképp fogalmazva egy tömb, vagy tömbszerű adatszerkezet első elemének indexe 0.

A szögletes zárójel operátorral (angolul subscript operator) mind a 4 tömbszerű adatszerkezet esetében hozzáférhetünk az elemekhez.

Ebben a példában kiíratjuk a tömb első elemét, aztán módosítjuk, majd megint kiíratjuk.

//subscript operator example
#include <iostream>
#include <string>

int main() {
  std::string arr_example[] = {"Budapest", "Vienna", "Bratislava", "Warsaw"};

  std::cout << "the first element of arr_example: " << arr_example[0] << '\n';

  //modify the value of the first element
  arr_example[0] = "London";

  std::cout << "the first element of arr_example: " << arr_example[0] << '\n';
}

Könnyen kilogikázható, ha az első elem indexe 0, akkor az utolsó elemé az elemszámnál eggyel kevesebb. Például az utolsó elemhez való hozzáférés:

arr_example[arr_size-1] = 128;

A fenti példa esetén a tömb méretét nem mentettük el külön változóba, viszont mivel ismerjük a kezdőértékeit, ezért ismerjük a méretét is, így tudjuk, hogy a negyedik eleme az utolsó, amelynek indexe 3, vagyis az arr_example[3] kifejezéssel hivatkozhatunk rá.

Fontos: std:array és std::vector esetén használjuk az at(), front() vagy back() tagfüggvényeket az elemek eléréséhez, mert ekkor a túlindexelés esetén fordítási hibát kapunk.
A hagyományos tömböknél, ha túlindexeljük a tömböt akkor undefined behaviourt okozunk, vagyis olyan memóriaterületről olvasunk, vagy olyan memóriaterületre írunk, ami nem a tömbhöz tartozik.

std::array és std::vector esetén az első elem eléréséhez használhatjuk a front() tagfüggvényt:

std::cout << stdvector_example.front() << '\n';

További elemek eléréséhez pedig az at() tagfüggvényt, például az stdvector_example nevű std::vector negyedik elemét így érjük el:

std::cout << stdvector_example.at(3) << '\n';

Az utolsó elemet std::array és std::vector esetén a back() tagfüggvénnyel is elérhetjük:

std::cout << stdvector_example.back() << '\n';
stdvector_example.back() = 50;

Fontos: ha a feldolgozni kívánt index fordítási időben nem ismert, akkor kivételkezelést alkalmazhatunk. Például ha az elem sorszámát a felhasználótól kérjük be, a felhasználó megadhat olyan sorszámot, amely túlindexelést eredményez (pl. egy 10 elemű tömb esetén a 11. elem, vagy a -1. elem).

Ebben a példaprogramban egy 10 elemű tömb egyik elemét tudja kiíratni a felhasználó, ha egy 1 és 10 közé eső számot ad meg. Ebből a tömb indexelésénél le kell vonni 1-et, mivel a tömb indexelése nem 1-től, hanem 0-tól kezdődik.
Az std::array at() tagfüggvénye kivételt dob, ha túlindexelés történt, és a catch ágban mondhatjuk meg, hogy mit tegyünk ezesetben (jelen példában hibaüzenetet írunk ki), ha a catch-nek paraméterül adjuk az std::out_of_range-et, amely a túlindexelést jelenti.

//outofbounds exception example
#include <iostream>
#include <array>
//#include <exception>
#include <stdexcept>

int main() {
  std::array<int, 10> stdarr_example = {2,-5,6,8,10,23,44,-129,17,82};

  std::cout << "Which element would you like to print (1-10)?\n" ;
  int input_index{};
  std::cin >> input_index;

  try {
    std::cout << "The value of element number " << input_index << ":\n" << stdarr_example.at(input_index-1);
  } catch (const std::out_of_range& e) {
    std::cout << "Invalid element number.\n";
  }
}

Ha az index ismert fordítási időben, akkor nem érdemes kivételkezelést alkalmazni. Ha esetleg a programozó indexeli túl a tömböt, vagy tömbszerű adatszerkezetet, akkor ha az std::array és std::vector at() tagfüggvényét használjuk, fordítási hibát kapunk, és a hibát, ami a túlindexelést okozza, ki kell javítani.

A hagyományos tömbök esetén sokkal nehezebb lehet a hiba felderítése, az is lehet, hogy egyes esetekben működik a program, egyes esetekben viszont lefagy, esetleg leállítja az operációs rendszer (ez az undefined behavior következménye).
Tipp: ha ebben a példában átírjuk az std::array-t hagyományos tömbre (és az at() tagfüggvényt indexelő operátorra), akkor nem fog működni a kivételkezelés, például a -1. elem esetén azt az értéket fogja kiolvasni a memóriából, ami a tömb előtt van, a 11. elem esetén pedig azt, ami a tömb mögött van.

Elemek feldolgozása, bejárása (végiglépkedés az elemeken)

Mind a 4 tömbszerű adatszerkezet feldolgozására van 2 lehetőségünk. Ha szükségünk van az indexekre, akkor for ciklus, ha nincs szükségünk az indexekre, akkor ranged for ciklus.
Az std::array és az std::vector esetén van még egy lehetőség, az algorithm header fájl includeolásával használhatjuk a for_each függvényt.

Mind a három esetben (for ciklus, ranged for ciklus, for_each függvény) a tömb vagy tömbszerű adatszerkezet elemein lépkedünk végig, és valamilyen műveletet alkalmazunk rájuk (pl. kiíratjuk őket vagy módosítjuk az értéküket (pl. megnöveljük 1-el őket), vagy pl. az egyes elemek értékeit bekérjük a felhasználótól, vagy akár fájlból olvassuk be az egyes elemek értékeit).

Az egyes elemek felhasználótól való bekérésének nem vagyok a híve, mivel időigényes lehet pl. 10-20 elemet a program minden egyes futtatásakor megadni, ezért ezekben a példákban inkább kezdőértékként adjuk meg a tömb elemeinek értékét.

Az elemek bejárása for ciklussal

A for ciklus ciklusváltozója a tömb indexein lépked végig. A kezdőértéke általában 0 (az első elem indexe), és egészen a tömb méreténél 1-el kisebb számig halad (ami az utolsó elem indexe), a ciklus minden lépésében eggyel lesz megnövelve.

A for ciklusban kiírathatjuk azt is, hogy hanyadik elemről van szó éppen, nem csak az elemek értékét tudjuk felhasználni. Ekkor persze érdemes a ciklusváltozóhoz 1-et hozzáadni, hogy nulladik elem helyett első elem íródjon ki, satöbbi.

//for loop default example: std::cout
#include <iostream>

int main() {
  double arr_example[] = {2.3, -1.9, 10.01, 23.67, -42.1, 78.33, -12.11, 91.2, 33.6, 1.0};

  const size_t arr_size = sizeof(arr_example)/sizeof(arr_example[0]);

  for (size_t i = 0; i < arr_size; ++i) {
    std::cout << "Element " << i+1 << ": " << arr_example[i] << '\n';
  }
}

Ha for ciklusban módosítjuk az elemek értékét (ebben a példában hozzáfűzünk az elemek értékéhez egy alsóvonalat és az adott elem sorszámát), azt nem szoktuk ugyanabba a for ciklusba tenni, amelyikben kiíratjuk az elemeket.

//for loop example: modify and std::cout
#include <iostream>
#include <string>

int main() {
  std::string arr_example[] = {"Budapest", "London", "Prague", "Vienna", "Warsaw"};

  const size_t arr_size = sizeof(arr_example)/sizeof(arr_example[0]);

  //modify the elements
  for (size_t i = 0; i < arr_size; ++i) {
    arr_example[i].push_back('_');
    arr_example[i].push_back('1' + i);
  }

  //print the elements
  for (size_t i = 0; i < arr_size; ++i) {
    std::cout << "Element " << i+1 << ": " << arr_example[i] << '\n';
  }
}

for ciklussal akár azt is megoldhatjuk, hogy ne minden elemet járjunk be, hanem pl. minden másodikat:

//for loop example (i+=2)
#include <iostream>
#include <vector>

int main() {
  std::vector<double> stdvec_example = {2.3, -1.9, 10.01, 23.67, -42.1, 78.33, -12.11, 91.2, 33.6, 1.0};

  for (size_t i = 0; i < stdvec_example.size(); i+=2) {
    std::cout << "Element " << i+1 << ": " << stdvec_example.at(i) << '\n';
  }
}

Ha egy tömbszerű adatszerkezet size() tagfüggvényével kérdezzük le a méretét, akkor ne int típusú ciklusváltozót használjunk, hanem size_t típusút.

A for ciklusnak nem muszáj pontosan az első elemmel kezdődnie, és az utolsó elemnél véget érnie, a tömb vagy tömbszerű adatszerkezet egy belső intervallumát feldolgozhatjuk, ebben a példában a 4. elemtől a 8. elemig.

//for loop example (4-8)
#include <iostream>
#include <array>

int main() {
  std::array<char, 10> stdarr_example = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'};

  for (size_t i = 3; i < stdarr_example.size()-2; ++i) {
    std::cout << "Element " << i+1 << ": " << stdarr_example.at(i) << '\n';
  }
}

for ciklussal akár fordított sorrendben (az utolsótól kezdve az elsőig) is bejárhatjuk az elemeket:

//for loop example reverse
#include <iostream>

int main() {
  double arr_example[] = {2.3, -1.9, 10.01, 23.67, -42.1, 78.33, -12.11, 91.2, 33.6, 1.0};

  const int arr_size = sizeof(arr_example)/sizeof(arr_example[0]);

  for (int i = arr_size-1; i >= 0; --i) {
    std::cout << "Element " << i+1 << ": " << arr_example[i] << '\n';
  }
}

Ez a példa size_t típussal nem biztos, hogy működik, mivel a size_t unsigned típus.

Ha csak bizonyos feltételnek megfelelő elemeket szeretnénk feldolgozni a for ciklusban (ebben a példában a páros számokat szeretnénk kiíratni), akkor azt egy, a for ciklusba ágyazott iffel tudjuk megoldani.

//for loop default example: filter
#include <iostream>

int main() {
  int arr_example[] = {2, 7, 8, 1, 4, 6, 3, 9, 0, 10};

  const size_t arr_size = sizeof(arr_example)/sizeof(arr_example[0]);

  for (size_t i = 0; i < arr_size; ++i) {
    if (arr_example[i] % 2 == 0) {
      std::cout << "Element " << i+1 << ": " << arr_example[i] << '\n';
    }
  }
}

Az elemek bejárása ranged for ciklussal

A ranged for ciklust más programozási nyelvekben for each ciklusnak nevezik (Javascriptben pedig for ofnak).

A ranged for ciklus sokkal hibabiztosabb a hagyományos for ciklusnál, mivel nem kell megadni, hogy melyik legyen az első és az utolsó feldolgozandó elem (amely kifejezésekben a hagyományos for ciklus esetén könnyű hibázni), ezért ha egy tömb vagy tömbszerű adatszerkezet minden elemén, elsőtől az utolsóig szeretnénk végiglépkedni, akkor a hagyományos for ciklus helyett érdemesebb inkább ranged for ciklust használni.

Fontos kihangsúlyozni, hogy ranged for ciklusnál a ciklusváltozóban nem a tömb indexei vannak tárolva (mint a for ciklusnál), hanem a tömb egyes elemeinek az értéke.

Ha a cikluson belül nem akarjuk megváltoztatni az elemek értékét, használjunk a const típusminősítőt az aktuális elem tárolására használt változóhoz.

Az & jel jelentése a típus után az, hogy az eredeti elemekkel dolgozunk. Ha nem írjuk ki az & jelet a típus után, akkor az elemekről másolat készül, és bármi módosítást végzünk rajtuk, az nem lesz hatással az eredeti elemekre.

//ranged for loop example
#include <iostream>

int main() {
  const double arr_example[] = {2.3, -1.9, 10.01, 23.67, -42.1, 78.33, -12.11, 91.2, 33.6, 1.0};

  std::cout << "The elements of the array:\n";
  for (const double& element : arr_example) {
    std::cout << element << '\n';
  }
}

Ebben a példában először minden elemhez hozzáadunk 100-at, majd egy másik ciklussal kiíratjuk az elemeket. Fontos tudni, hogy az elemek módosításához a ranged for ciklusban kell szerepelnie az aktuális elem típusa után egy & jelnek, azaz például double helyett double&-t írjunk.

//ranged for loop example
#include <iostream>

int main() {
  double arr_example[] = {2.3, -1.9, 10.01, 23.67, -42.1, 78.33, -12.11, 91.2, 33.6, 1.0};

  //modify the elements
  for (double& element : arr_example) {
    element += 100;
  }

  //print the elements
  std::cout << "The modified elements of the array:\n";
  for (const double& element : arr_example) {
    std::cout << element << '\n';
  }
}

A C++20 előtti szabványok szerint fordított kódban nincs egyszerű megoldás arra, hogy ranged for ciklussal fordított sorrendben járjuk be az elemeket.

Az elemek bejárása for_each függvénnyel:

A for_each függvény első két paramétere úgynevezett iterátorok melyek az első és az utolsó feldolgozandó elemet, illetve a bejárás irányát határozzák meg, a harmadik paraméter pedig egy úgynevezett lambda függvény, melyben az elemekkel elvégzendő műveletet adhatjuk meg.

//for_each example
#include <iostream>
#include <array>
#include <algorithm>

int main() {
  std::array<double, 10> stdarr_example = {2.3, -1.9, 10.01, 23.67, -42.1, 78.33, -12.11, 91.2, 33.6, 1.0};

  std::for_each(stdarr_example.begin(), stdarr_example.end(), [](const double& element) {std::cout << element << ' ';});
}

A tömbszerű adatszerkezet egy adott intervallumát is bejárhatjuk for_each függvénnyel. Pl. a 4. elemtől kezdve a 8. elemig.

//for_each example (4-8)
#include <iostream>
#include <array>
#include <algorithm>

int main() {
  std::array<char, 10> stdarr_example = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'};

  std::for_each(stdarr_example.begin()+3, stdarr_example.end()-2, [](const char& element) {std::cout << element << ' ';});
}

Ebben a példában először megszorozzuk 2-vel az egyes elemeket, és utána íratjuk ki fordított sorrendben. A kiíratás sorrendjét úgynevezett reverse iterátorokkal végezhetjük el.
Ha a lambda függvényben nem módosítjuk az adott elemet, akkor const típusminősítővel lássuk el az aktuális elem tárolásáért felelős változót (a példában az element nevű változó).

//for_each example modify + reverse
#include <iostream>
#include <array>
#include <algorithm>

int main() {
  std::array<double, 10> stdarr_example = {2.3, -1.9, 10.01, 23.67, -42.1, 78.33, -12.11, 91.2, 33.6, 1.0};

  //modify the elements
  std::for_each(stdarr_example.begin(), stdarr_example.end(), [](double& element) {element *= 2;}) ;

  //print the elements in reverse order
  std::for_each(stdarr_example.rbegin(), stdarr_example.rend(), [](const double& element) {std::cout << element << ' ';});
}

Új elem hozzáadása a többi elem után (push)

Az std::vector esetén a push_back() tagfüggvényt használhatjuk új elem hozzáadásához, mely természetesen az adott std::vector méretét is megnöveli.

//std::vector push_back example
#include <iostream>
#include <vector>

int main() {
  std::vector<int> stdvec_example = {1,2,3,4,5,6,7,8,9,10};

  std::cout << "elements before adding the new element:\n";
  for (const int& element : stdvec_example) {
    std::cout << element << ' ';
  }
  std::cout.put('\n');

  //add the new element
  stdvec_example.push_back(11);

  std::cout << "elements after the adding the new element:\n";
  for (const int& element : stdvec_example) {
    std::cout << element << ' ';
  }
  std::cout.put('\n');
}

Utolsó elem eltávolítása (pop)

std::vector esetén a pop_back() tagfüggvényt használhatjuk az utolsó elem eltávolításához. A pop_back() tagfüggvény nem adja vissza az eltávolított elem értékét, hanem azt a back() tagfüggvénnyel kérdezhetjük le, még a pop_back() tagfüggvény használata előtt.
A pop_back() tagfüggvény használata esetén természetesen az adott std::vector mérete lecsökken eggyel.

//std::vector pop_back example
#include <iostream>
#include <vector>

int main() {
  std::vector<int> stdvec_example = {1,2,3,4,5,6,7,8,9,10};

  std::cout << "elements before removing the last element:\n";
  for (const int& element : stdvec_example) {
    std::cout << element << ' ';
  }
  std::cout.put('\n');

  //remove the last element
  std::cout << "The element is about to be removed: " << stdvec_example.back() << '\n';
  stdvec_example.pop_back();
  std::cout.put('\n');

  std::cout << "elements after removing the last element:\n";
  for (const int& element : stdvec_example) {
    std::cout << element << ' ';
  }
  std::cout.put('\n');
}

Új elem hozzáadása a többi elem elé (unshift)

std::vector esetén az insert() tagfüggvényt használhatjuk, mely hasonlóan működik, mint a push_back, azzal a különbséggel, hogy az insert() tagfüggvénnyel nem csak az std::vector elejére, hanem a közepére is beilleszthetünk új elemet.
Ezzel az utasítással például a negyedik elem helyére illesztünk be egy új elemet:

stdvec_example.insert(stdvec_example.begin()+3, 0);

Ebben a példában az std::vector összes eleme elé (az első elem helyére) illesztünk be egy új elemet:

//std::vector insert example
#include <iostream>
#include <vector>

int main() {
  std::vector<int> stdvec_example = {1,2,3,4,5,6,7,8,9,10};

  std::cout << "elements before adding the new element:\n";
  for (const int& element : stdvec_example) {
    std::cout << element << ' ';
  }
  std::cout.put('\n');

  //add the new element
  stdvec_example.insert(stdvec_example.begin(), 0);

  std::cout << "elements after the adding the new element:\n";
  for (const int& element : stdvec_example) {
    std::cout << element << ' ';
  }
  std::cout.put('\n');
}

Az insert() tagfüggvénnyel akár egy teljes std::array vagy std::vector tartalmát beilleszthetjük egy másik std::vectorba:

Első elem eltávolítása (shift)

sd::vector esetén az erase() tagfüggvényt használhatjuk, mely hasonlóan működik, mint a pop_back() tagfüggvény, azzal a különbséggel, hogy egy adott std::vector közepéről is eltávolíthatunk vele egy elemet.
Például ezzel az utasítással az stdvec_example nevű std::vector negyedik elemét távolítjuk el:

stdvec_example.erase(stdvec_example.begin());

Az eltávolítás esetén az indexek is átrendeződnek, nem marad az etávolított elem helyén üres hely.

Ebben a példában az első elemet távolítjuk el. Az erase() tagfüggvény nem adja vissza az eltávolított elem értékét, hanem azt jelen esetben a front() tagfüggvénnyel kérdezhetjük le, más esetben használhatjuk az at() tagfüggvényt is.

//std::vector erase example
#include <iostream>
#include <vector>

int main() {
  std::vector<int> stdvec_example = {1,2,3,4,5,6,7,8,9,10};

  std::cout << "elements before removing the last element:\n";
  for (const int& element : stdvec_example) {
    std::cout << element << ' ';
  }
  std::cout.put('\n');

  //remove the last element
  std::cout << "The element is about to be removed: " << stdvec_example.front() << '\n';
  stdvec_example.erase(stdvec_example.begin());
  std::cout.put('\n');

  std::cout << "elements after removing the last element:\n";
  for (const int& element : stdvec_example) {
    std::cout << element << ' ';
  }
  std::cout.put('\n');
}

Üres-e?

Hagyományos tömb és std::array esetén nyilvánvalóan nincs értelme üres tömböt létrehozni, hiszen azoknak a mérete nem változhat.

std::vector esetén az empty() tagfüggvénnyel kérdezhetjük le, hogy egy adott std::vector példány üres-e. A clear() tagfüggvénnyel egy std::vector összes elemét eltávolíthatjuk.

//std::vector empty example
#include <iostream>
#include <vector>

int main() {
std::cout.setf(std::ios::boolalpha);

std::vector<int> stdvec_example = {1,2,3,4,5,6,7,8,9,10};

std::cout << "is the std::vector empty? " << stdvec_example.empty() << '\n';

//remove the elements
stdvec_example.clear();

std::cout << "is the std::vector empty? " << stdvec_example.empty() << '\n';
}

Egyéb műveletek

std::vector esetén esetleg érdemes lehet ezeket a műveleteket is ismerni:

A for ciklusról bővebben

Magyarul szokták számlálós ciklusnak nevezni. Típusát tekintve elöltesztelős ciklus (angolul pre-test loop), ami annyit jelent, hogyha a ciklusfeltétel már a ciklus első lépésénél nem teljesül, akkor a ciklus egyszer sem fut le.

for ciklust alapvetően akkor használunk, ha a programunk számára ismert az, hogy a ciklus hány alkalommal fog lefutni, szemben például a while ciklussal, aminek a lépésszáma a program számára jellemzően ismeretlen (például egy változó méretű fájl sorainak a beolvasása).
(A ranged for ciklus és a for_each függvény esetén ez ugyanúgy igaz, hiszen azok mindig egy tömbszerű adatszerkezetet járnak be, így a lépésszám is előre ismert).

Természetesen van olyan for ciklus, aminek az esetében nem ismert, hogy a ciklus hány alkalommal fog lefutni, például ha egy tömb elemein addig lépkedünk végig, amíg valamilyen feltétel teljesül vagy nem teljesül a tömb elemeire nézve (pl. amíg negatív számot nem találunk). Fontos kihangsúlyozni, hogy ennek akkor van értelme, ha le akarjuk állítani a ciklust, amint az első ilyen elemet megtaláltuk. A negatív számos példánál maradva:

for (size_t i = 0; i < arr_size && arr_example[i] >= 0; ++i) {
  //statements
}

A ciklusok egyik lényege (amellett, hogy feltételhez tudjuk kötni utasítások ismételt lefuttatását), hogy sok utasítást tudunk lefuttatni kevés kóddal. Például ha egy tömb minden elemét meg akarjuk növelni eggyel, akkor egy 10 elemű tömb esetén csak ennyit kell írnunk:

for (size_t i = 0; i < arr_size; ++i) {
  ++arr_example[i];
}

ciklus nélkül ez így nézne ki:

++arr_example[0];
++arr_example[1];
++arr_example[2];
++arr_example[3];
++arr_example[4];
++arr_example[5];
++arr_example[6];
++arr_example[7];
++arr_example[8];
++arr_example[9];

Egy száz elemű tömb esetén ez ciklus nélkül mégtöbb lenne.

Az alsó példából jól látszik, hogy a for ciklusban az i változó értéke a ciklus első lépésében 0, a második lépésében 1... az utolsó lépésben pedig arr_size-1, azaz 9.

A for ciklust nem csak tömbök bejárására lehet használni, hanem bármilyen kifejezés/utasítás kiértékeléséhez, amiben fel szeretnénk használni a ciklusváltozó (angolul loop counter) értékét. A legegyszerűbb példa, az egész számok kiíratása pl. 1-től 100-ig:

for (int i = 1; i <= 100; ++i) {
  std::cout << i << ' ';
}

Ha esetleg az utolsó szám után nem szóközt, hanem sorvége jelet szeretnénk tenni, akkor például a ciklus utolsó lépését a ciklus után külön írhatjuk.

for (int i = 1; i < 100; ++i) {
  std::cout << i << ' ';
}
std::cout << 100 << '\n';

A for ciklus alapvető működését szemléltető kód (ez csak szemléltető kód, nem használható, végtelen ciklust okoz):

for (/*initialization*/ ; /*condition*/ ; /*counter step*/) {
  //statements executed as many times as if the condition is true
}

Egy másik szemléltető kód:

for (/*from*/ ; /*to*/ ; /*step*/) {
  //statements
}

Általában a for ciklus első két paramétere szabja meg, hogy a ciklus mettől-meddig fut, a harmadik pedig azt, hogy milyen irányban és hanyassával lépkedünk.

A ciklus blokkját (a kapcsos zárójelek közti részt) magyarul ciklusmagnak szokták nevezni.

A for ciklus első paraméterében definiálhatunk (létrehozhatunk) és inicializálhatunk (kezdőértékkel láthatunk el) változókat, amelyek értékét a ciklusban felhasználhatunk. Ezek a változók csak a ciklusban léteznek, a ciklust követően megszűnnek.
Jellemzően egy i (esetleg idx) nevű változót definiálunk, amit magyarul ciklusváltozónak szoktak nevezni. A ciklusváltozó jellemzően azt az értéket tartalmazza, hogy hanyadik elem feldolgozásánál tart a ciklus. Fontos, hogy milyen kezdőértéket adunk ennek a változónak. Általában a bejárandó értékek közül az elsőt vagy az utolsót.
Például egy tömb bejárása esetén:

int i = 0;

vagy

int i = arr_size-1;

A for ciklus második paraméterében azt a feltételt adhatjuk meg, amelynek teljesülése esetén a ciklusmagban lévő utasítások lefutna. Másképp fogalmazva ha a feltétel igaz, akkor lesz következő lépése a ciklusnak. A feltételt bennmaradási feltételnek is szokták nevezni (más programozási nyelvekben előfordulhat olyan ciklus, ami esetén kilépési feltételt kell megadni, pl. a Pascalban a repeat-until ciklus).
Példák:

i < arr_size
i >= 0

A for ciklus harmadik paraméterében jellemzően a ciklusváltozó növelésére/csökkentésére vonatkozó utasítás szerepel. A C és C++ nyelvekben gyorsabb a ++i, mint az i++, de ha i++-t írunk, az sem hiba.
Példák:

++i
--i
i+=2
i+=3

Talán azt is érdemes lehet tudni, hogy a ciklus utolsó lépését követően a ciklusváltozó még növelésre kerül, csak a ciklusmag nem fut le. Ha kívül definiáljuk a ciklusváltozót (hogy a ciklus után ne szűnjön meg), és a ciklus után kiíratjuk az értékét, akkor letesztelhetjük.
Ennél a példánál a ciklust követően i értéke 11 lesz, nem pedig 10.

int i = 0;
for(; i <= 10; ++i) { /*statements*/ }
std::cout << i << '\n';

Hiszen amikor a ciklus már nem fut le, mivel a feltétel hamis lesz, akkor a feltétel így értékelődik ki: 11 <= 10

Egyébként a ciklusváltozó cikluson kívül definiálása lehetőleg kerülendő:

Speciális esetek (corner cases)

Elképzelhető, hogy találkozunk olyan for ciklusokkal, amiknek az egyik paramétere el van hagyva.

Mikor hagyjuk el az első paramétert?

Ha a ciklus előtt szeretnénk a ciklusváltozót létrehozni, hogy a ciklus után is hozzáférhessünk. Habár ez a coding standardok szerint kerülendő, időnként mégis célszerű lehet.
Például ha meg akarunk keresni egy bizonyos elemet, és annak az indexét a program további részében fel szeretnénk használni.

Mikor hagyjuk el a második paramétert?

//

Mikor hagyjuk el a harmadik paramétert?

Ha a cikluson belül dől el, hogy növeljük-e a ciklusváltozó értékét vagy nem. Például ha random számokat generálunk amik között nem lehet két azonos szám.

Olyan eset is előfordulhat, hogy a második és a harmadik paramétert összevonjuk:

//--> example
#include <array>
#include <iostream>

int main() {
  std::array<double, 10> stdarr_example = {2.3, -1.9, 10.01, 23.67, -42.1, 78.33, -12.11, 91.2, 33.6, 1.0};

  for (size_t i = stdarr_example.size(); i-- > 0;) {
    std::cout << "Element " << i << ": " << stdarr_example.at(i) << '\n';
  }
}

Lásd itt a második kérdést:

Hibalehetőségek

A ranged for ciklussal és a for_each függvénnyel szemben a for ciklus kifejezett hátránya, hogy viszonylag sok gyakorlattal rendelkezve is könnyű elrontani azt, hogy a ciklus mettől-meddig fusson.

Első paraméter esetén:

Tekintsük ezt a két példát:

for (int i = arr_size-1; i >= 0; --i) {/*statements*/}
for (size_t i = stdarr_example.size(); i-- > 0;) {/*statements*/}

Bár az alsóval sokkal ritkábban találkozunk, mégis az egyik esetben a ciklusváltozónak a tömb méreténél eggyel kisebb számot adjuk értékül, a másiknak pedig a tömb méretét. Ha eltévesztjük, túlindexelhetjük a tömböt.

Második paraméter esetén:

Tekintsük ezt a két példát:

for (size_t i = 0; i < arr_size; ++i) {/*statements*/}
for (int i = arr_size-1; i >= 0; --i) {/*statements*/}

Az egyikben a relációs jelben nem szerepel = jel, a másikban pedig szerepel. Ha ezt eltévesztjük, túlindexelhetjük a tömböt.

unsigned típusok esetén:

A size_t típus valamilyen unsigned int típus aliasa, jellemzően az unsigned long inté. Ha például a ciklusváltozót csökkentve haladunk végig az elemeken, esetleg előfordulhat, hogy negatív számot kapunk, ami unsigned típusok esetén jellemzően egy nagyon nagy pozitív számmá konvertálódik át. Ekkor nyugodtan használjunk a size_t helyett int típust, ritka az az eset, amikor nem elég az int típus tartománya az elemek indexének tárolásához.

//error
for (size_t i = stdarr_example.size(); i >= 0; --i) {/*statements*/}
for (int i = stdarr_example.size(); i >= 0; --i) {/*statements*/}

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).

Példaprogramok

Valahány darab szám átlaga

Ez a példa különösebb magyarázatot nem igényel. A ciklusban összeadjuk az elemeket, majd a ciklus után elosztjuk az eredményt annyival, ahány elemet összeadtunk.

//average of some numbers
#include <iostream>
#include <vector>

int main() {
  double numbers[] = {2.3, 4.5, 1.1, 78.2, 61.3, 18.9};

  const int arr_size = sizeof(numbers)/sizeof(numbers[0]);

  double avg{};
  for (const double& element : numbers) {
    avg += element;
  }
  avg /= arr_size;
  std::cout << "Average value: " << avg << '\n';
}

Háromszög pattern

Rajzoljunk ki egy ilyen ábrát parancssorba:

*
**
***
****
*****
******
*******
********
*********

Általában sorokba és oszlopokba rendezhező adatoknál egymásbaágyazott ciklust használunk. az i ciklusváltozó fogja tárolni, hogy hanyadik sornál tartunk, a j pedig hogy hányadik * kiíratásánál tartunk. A belső ciklusban j i-ig megy, mivel a csillagok száma egy sorban egyezik az adott sor számával.
A külső ciklus egy lépése jelent egy adott sort. Ne felejtsük el kitenni egy sor végére a sorvége jelet.

//triangle pattern example
#include <iostream>

int main() {
  for (int i = 0; i < 10; ++i) {
    for (int j = 0; j < i; ++j) {
      std::cout << '*';
    }
    std::cout << '\n';
  }
}

Az interneten sok ehhez hasonló példa van.

Tömb feltöltése véletlen generált számokkal

//random integral number between 1 and 100
//10 times
//including 1 and 100
#include <iostream>
#include <random>

int main() {
  std::random_device rnd_device;
  std::mt19937 rnd_generator(rnd_device());
  std::uniform_int_distribution<int> int_dist(1,100);

  int random_numbers[10];

  for (int& element : random_numbers) {
    element = int_dist(rnd_generator);
  }

  for (const int& element : random_numbers) {
    std::cout << element << '\n';
  }
}

Tömb feltöltése egymástól különböző véletlen számokkal

Azért van szükség két egymásba ágyazott ciklusra, mert a külső ciklusban végiglépkedünk a tömb elemein, a belső ciklusban pedig megnézzük, hogy az éppen aktuális random generált szám egyezik-e valamelyik előző elem értékével (csak akkor növeljük a ciklusváltozó értékét, ha nem egyezik).

//random integral number between 1 and 100
//10 times, different ones
//including 1 and 100
#include <iostream>
#include <random>

int main() {
  std::random_device rnd_device;
  std::mt19937 rnd_generator(rnd_device());
  std::uniform_int_distribution<int> int_dist(1,100);

  const int arr_size = 10;
  int random_numbers[arr_size];

  for (int i = 0; i < arr_size;) {
    random_numbers[i] = int_dist(rnd_generator);
    bool equals = false;
    for (int j = 0; j < i; ++j) {
      if (random_numbers[j] == random_numbers[i]) {
        equals = true;
      }
    }
    if (!equals) {
      ++i;
    }
  }

  for (const int& element : random_numbers) {
    std::cout << element << '\n';
  }
}

Kapcsolódó tananyagrész:

Egyéb tananyagok:

Régi tananyag:

 

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.

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.

Előző tananyagrész: switch-case, ternáris operátor
Következő tananyagrész: while ciklus, fájlkezelés

A bejegyzés trackback címe:

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

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.
süti beállítások módosítása