[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).
Megjegyzés: ebben a tananyagrészben nem lesz szó arról, hogy a ciklusoknak mit adhatunk meg feltételnek, ez már az elágazásoknál szerepelt.
Továbbá a for ciklushoz fontos ismerni az inkrementálás, dekrementálás szabályait.

itk_programming_array_example.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 built-in arrayknek, esetleg 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 (pl. push, pop. shift, unshift)
  • 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 tudunk tömbszerűen kezelhető adatterületet lefoglalni a számítógép memóriájában, amit dinamikusan 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, az új mérettel rendelkező dinamikusan allokált tömbbe át kell másolnunk a már meglévő elemeket, a régi tömböt felszabadítani delete[] operátorral, majd az új tömböt értékül adni a pointernek. Ebben könnyű hibát ejteni.
  • 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

Esetleg ide lehetne még sorolni az asszociatív tömböt, C++-ban ez az std::map adatszerkezet, melyet az STL tananyagrészben tárgyalunk:

  • STL - std::map

Tömbszerű adatszerkezetek más programozási nyelvekben

Ha valaki tanult már más programozási nyelvet, annak a számára talán segíthet, ha itt szót ejtünk arról, hogy a fentiekből melyik hasonlít más programozási nyelvekben lévő tömbökhöz.

C

A hagyományos tömb, és a dinamikusan allokált tömb használható benne (ezeket egyébként pont a C-ből vette át a C++). Alapvetően nincs benne std::array vagy std::vector, de ahhoz hasonlót struct és függvénypointerek segítségével létrehozhatunk, illetve esetleg kereshetünk mások által létrehozott libraryket, amikben ilyesmi szerepel.
C-ben nyelvi szinten nincsenek osztályok, objektumok. Helyettük lehet struct-ot és függvénypointereket használni, ami persze sokkal nehezebb

C#, Java

A hagyományos tömbök hasonlóak a C++ hagyományos tömbjéhez, de rendelkeznek length adattaggal, amivel a méretüket lehet lekérdezni.
Az std::vectorhoz hasonló adatszerkezetek C#-ban a List, Javaban pedig az ArrayList.

Javascript, Phyton

Ezekben a nyelvekben lévő tömb a C++ std::vectorához hasonlít a legjobban.

PHP

A PHP-ban lévő tömb a C++ std::map adatszerkezetéhez hasonlít.

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 elmaradhat, 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 (az ilyen tömböket VLA-nak nevezzük), 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, vagyis hol kell megállítani a végiglépkedést.

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, 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.
Ez a módszer akkor is hasznos, hogyha később esetleg módosítjuk az elemek számát.
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), a ciklus minden lépésében eggyel lesz megnövelve, és egészen a tömb méretéig halad kisebb számig halad, ami azt jelenti, hogy i < arr_size ciklusfeltétel esetén lesz olyan eset, amikor az i változó értéke arr_size lesz, viszont ekkor már a ciklusmag (a kapcsoszárójelek közötti rész) nem fut le.

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

Egyéb a ranged for ciklussal kapcsolatban:

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()+3);

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

For ciklusban ritkán előfordulhat, hogy nem tudjuk előre, hogy hányszor fut le

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
}

Azért használunk for ciklust while ciklus helyett, mert a ciklusváltozóra ebben az esetben is szükségünk van.

Mért használunk ciklusokat?

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.

Nem csak tömbök bejárására használjuk

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 << ' ';
}

A for ciklus paramétereinek elmagyarázása

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ő:

Vesszővel, szóközzel elválasztott elemek

Ha vesszővel vagy szóközzel elválasztva íratjuk ki az elemeket, akkor alapesetben az utolsó elem után is vessző vagy szóköz fog szerepelni. Ez persze nem a leghatalmasabb szépséghiba, de ennek elkerülése nem bonyolult.

//for loop, comma separated, default
#include <iostream>

int main() {
  int arr_example[] = {1,2,3,4};
  size_t arr_size = sizeof(arr_example)/sizeof(arr_example[0]);

  for (size_t i = 0; i < arr_size; ++i) {
  std::cout << arr_example[i] << ", ";
  }
}
1. eset

Ha biztosak vagyunk benne, hogy a for ciklus eljut az utolsó elemig, akkor azt a megoldást alkalmazhatjuk, hogy kiíratjuk vesszővel elválasztva az elemeket az utolsó előtti elemig, az utolsó elemet pedig a ciklus után egy külön utasításban íratjuk ki, vessző helyett mondjuk ponttal vagy újsor karakterrel (vagy akár mindkettővel) a végén.

Ügyeljünk arra, hogy amíg az arr_example[arr_size-1] a tömb utolsó elemét jelenti, addig az i < arr_size-1 az utolsó előtti elemet jelenti (ha < helyett <= lenne, akkor persze az utolsó elemet jelentené).

//for loop, comma separated, without last comma
#include <iostream>

int main() {
  int arr_example[] = {1,2,3,4};
  size_t arr_size = sizeof(arr_example)/sizeof(arr_example[0]);

  for (size_t i = 0; i < arr_size-1; ++i) {
    std::cout << arr_example[i] << ", ";
  }
  std::cout << arr_example[arr_size-1] << '.';
}
2. eset

Előfordulhat, hogy nem tudjuk, melyik lesz a ciklus utolsó lépése, például ha a cikluson belül szerepel egy if utasítás, és nem minden elemet íratunk ki.

(Persze, ebben a példában meg tudjuk mondani, hogy az utolsó 2 elem nem fog kiíródni, de gondoljunk arra, ha esetleg a felhasználó adja meg az elemeket, vagy random generált elemekkel dolgozunk.)

Ebben az esetben először kiíratjuk a ciklus első elemét vessző vagy elválasztójel nélkül.

std::cout << arr_example[0];

Majd a ciklusmagban először a vesszőt vagy elválasztójelet íratjuk ki, aztán pedig a soron következő elemet.

std::cout << ", " << arr_example[i];

Ügyeljünk arra, hogy ha már kiírtuk a ciklus előtt a ciklus első elemét, akkor a ciklus a második elemtől kezdve fusson le.

A ciklus után kiírathatunk valamilyen lezárókaraktert, például pontot vagy újsort vagy akár mindkettőt.

std::cout << '.';

A példaprogram:

//for loop, comma separated, without last comma, version 2
#include <iostream>

int main() {
  int arr_example[] = {1,2,3,4, -5, -6};
  size_t arr_size = sizeof(arr_example)/sizeof(arr_example[0]);

  std::cout << arr_example[0];
  for (size_t i = 1; i < arr_size; ++i) {
    if (arr_example[i]>0) {
      std::cout << ", " << arr_example[i];
    }
  }
std::cout << '.';
}

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?

Akiktől utánakérdeztem, azok szerint nincs olyan életszerű helyzet, ahol erre szükség lehetne, viszont mások kódjában esetleg mégis találkozhatunk ilyen esetekkel. Ekkor persze szükség van egy break; utasításra a cikluson belül, hogy legyen valami, ami miatt egyszer leáll a ciklus.

A második paraméternek nem kell mindig ilyesmi feltételnek lennie: i < arr_size

Például generálunk 5-ös lottó számokat, majd addig generálunk újabb 5-ös lottó számokat, amíg azok meg nem egyeznek az elsőként generált számokkal. Ez a gyakorlatban azt jelenti, hogyha ugyanazokkal a számokkal játszunk hetente az 5-ös lottón, akkor hány hétig kell játszani ahhoz, hogy kihúzzák a számainkat. Erre persze nem mindig ugyanaz a szám fog kijönni, de jellemzően pármillió és párszázmillió közti szám jön ki, ami egyrészt azt jelenti, hogy gyakorlatilag nincs értelme lottózni, másrészt pedig azt, hogy a program egy ideig futni fog.

Online fordítókkal (pl. rextester.com, wandbox.org) a program jó eséllyel nem fog lefutni, mert időtúllépés miatt az online fordító leállítja.

Az std::set hasonló adatszerkezet, mint az std::vector, csak nem lehet két azonos értékű eleme. Azért használjuk ezt, hogy az 5 random generált szám között ne legyen két megegyező.

//lottery test
#include <iostream>
#include <random>
#include <set>

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

  std::set<int> numbers1;
  for (int i = 0; numbers1.size() != 5; ++i) {
    numbers1.insert(int_dist(rnd_generator));
  }

  std::set<int> numbers2;
  int i = 0;
  for (; numbers1 != numbers2; ++i) {
    numbers2.clear();
    for (int j = 0; numbers2.size() != 5; ++j) {
      numbers2.insert(int_dist(rnd_generator));
    }
    std::cout << "the random numbers of second set:\n";
    for (const int& element : numbers2) {
      std::cout << element << ' ';
    }
    std::cout.put('\n');
  }
  std::cout << "the random numbers of first set:\n";
  for (const int& element : numbers1) {
    std::cout << element << ' ';
  }
  std::cout.put('\n');

  std::cout << "the number of randomizations: " << i << '\n';
}

Felmerülhet a kérdés, hogy ezt miért számlálós ciklussal oldjuk meg, miért nem while ciklussal, ha tulajdonképpen nem ismerjük, hogy hányszor fog lefutni a ciklus, hiszen nem tudjuk, hogy hányszor fog előfordulni, hogy azonos értékek jönnek ki a random szám generáláskor.
Ez valóban egy jó kérdés, viszont a válasz az, hogy itt ugyan nem kell számolni, hogy a ciklus hányszor fut le, azt viszont számolni kell, hogy hány random számot generálunk.

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

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

  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';
  }
}

Ennél a példánál is érvényes az, amit a lottós példánál már leírtunk. Felmerülhet a kérdés, hogy miért nem while ciklussal oldjuk meg ezt a feladatot, mert nem tudjuk előre, hogy hányszor fog lefutni a ciklus, viszont azt számon kell tartani, hogy hány darab véletlen számot generálunk.

Kapcsolódó tananyagrész:

Egyéb tananyagok:

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/tr2710423780

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