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

C++ programozás kezdőknek - gyakori műveletek

[2021. január 12.] [ christo161 ]

Az előző tananyagrészben csak a legalapvetőbb műveletekről volt szó (kezdőértékadás, értékadás, parancssorból történő bekérés, parancssorba való kiíratás). Ebben a tananyagrészben néhány, az eddigi ismeretekhez kapcsolódó gyakori műveletet nézünk át.

előző tananyagrész: változók, konstansok, literálok
következő tananyagrész: alaptípusok jellemzői

Tartalom

Inkrementálás, dekrementálás

Egy (általában int típusú) változó értékének a növelése (inkrementálás) vagy csökkentése (dekrementálás) eggyel. Noha elsőre azt gondolhatnánk, hogy ez egy nagyon egyszerű művelet, a C++ nyelvben viszonylag sok tudnivaló és tévhit kapcsolódik hozzá. Az inkrementálást, dekrementálást leggyakrabban for ciklusban használjuk több adatot tartalmazó adatszerkezetek (pl. tömb vagy std::vector) feldolgozásához, de a ciklusok, tömbök ismerete nem szükséges az itt leírtak megértéséhez.

Ha önálló utasításként (például i++; vagy ++i;), nem pedig egy összetett kifejezés részeként (például variable = i++ vagy array_of_sth[i++] = 0 vagy n == i++) használjuk a ++ operátort (vagy dekrementálás esetén a -- operátort), akkor az eredmény szempontjából mindegy, hogy a megnövelni kívánt változó neve elé vagy mögé rakjuk, ekkor mindkettő ugyanazt jelenti, mint i = i + 1; (vagy j esetén j = j + 1;). Ebben a példában mindkét esetben a már eggyel megnövelt érték lesz kiírva.

int i = 0;
++i;
std::cout << i << '\n';
int j = 0;
j++;
std::cout << j << '\n';

Ha viszont összetett kifejezésben használjuk, akkor...

1. ha a ++ operátor a változó neve előtt szerepel (ezt nevezzük prefix inkrementálásnak), akkor a változó értéke először megnövelődik, és az összetett kifejezésben a már eggyel megnövelt érték kerül felhasználásra.
Ebben a példában mindkét utasításban az eggyel megnövelt érték lesz kiírva.

int i = 0;
std::cout << ++i << '\n';
std::cout << i << '\n';

Ezzel teljesen egyenértékű, ha ezt írjuk:

int i = 0;
std::cout << i = i + 1 << '\n';
std::cout << i << '\n';

Hasonló példa:

int i = 0;
int a = ++i;
std::cout << "i erteke: " << i << ", a erteke: " << a << '\n';

Ekkor az a változó értéke az i változó egyel megnövelt értéke lesz, vagyis ezen példa esetén 1.

2. ha a ++ operátor a változó neve után szerepel (ezt nevezzük postfix inkrementálásnak), akkor a változó eggyel még meg nem növelt értéke lesz felhasználva az összetett kifejezésben, és a változó értéke a ++ operátort tartalmazó utasítás végrehajtása után lesz csak eggyel megnövelve.
Ebben a példában először a még meg nem növelt érték lesz kiírva, aztán pedig a megnövelt.

int j = 0;
std::cout << j++ << '\n';
std::cout << j << '\n';

Hasonló példa:

int i = 0;
int a = i++;
std::cout << "i erteke: " << i << ", a erteke: " << a << '\n';

Ekkor az a nevű változó értéke az i változó még meg nem növelt értéke lesz (ezen példa esetén 0), az i változó értéke viszont az int a = i++; utasítást követően megnövelődik (ezen példa esetén 1 lesz).

Egy összetett kifejezésben viszont már nem ugyanazt jelenti a j++ és a j = j + 1, hiszen az utóbbi a már megnövelt értéket fogja jelenteni az összetett kifejezésben.
Vagyis ez a két példa nem ugyanazt eredményezi:

int j = 0;
std::cout << j++ << '\n';
int j = 0;
std::cout << (j = j + 1) << '\n';

A felső példa esetén 0 íródik ki, az alsó esetén pedig 1. (Azt követően pedig mindkét esetben 1 lesz a j nevű változó értéke).

Esetleg talán a vessző (angolul comma) operátorral lehetne olyan utasítást csinálni, amiben nincs ++ operátor, de az eggyel meg nem növelt értéket értékeli ki az összetett kifejezés, viszont a változó értéke attól még eggyel megnövelődik. Ezen két példa végeredményben egyenértékű:

int j = 0;
std::cout << j++ << '\n';
int j = 0;
std::cout << (j = j + 1, j - 1) << '\n';

Mindkét esetben 0 íródik ki, és azt követően a j nevű változó értéke 1 lesz, viszont a köztes lépések különbözőek mert a második példa esetén először lesz elvégezve a növelés, majd az eggyel lecsökkentett érték kerül átadásra az insertion operátornak, amely a kiírást végzi el.

A vessző operátor működését valahogy úgy képzelhetjük el, mintha a helyére csak a vessző jobb oldalán lévő kifejezést írtuk volna, de a bal oldalán lévő kifejezés a háttérben mégis kiértékelődik/végrehajtódik még a vessző jobb oldalán lévő kifejezés kiértékelése előtt.

Fontos kihangsúlyozni, hogy az inkrementálás jelentése (monduk egy i nevű változó esetén nem i + 1, hanem i = i +1. Előfordulhat, hogy egy változó értékének az eggyel megnövelt értékét csak fel szeretnénk használni (pl. kiíratni), de nem szeretnénk megnövelni a változó értékét (ekkor az i + 1 kifejezést használjuk az i = i + 1 vagy ++i vagy i++ kifejezések helyett). Figyeljünk a kettő közti különbségre, mert ha összekeverjük, hibát okozhatunk.

A ++ operátor nem használható immutable adatokhoz

A ++ vagy -- operátort csak olyan dolgok növelésére, csökkentésére használhatjuk, amiknek az értéke megváltoztatható, például a változók, vagy későbbi tananyagrészben tárgyalt objektumok adattagjai. A literálok, konstansok értékét például nem változtathatjuk meg ++ vagy -- operátorokkal, különben fordítási hibát kapunk.

//error
std::cout << ++2 << '\n';
std::cout << 3++ << '\n';
//error
const int a = 128; ++a;

Önálló utasításként i++ helyett ++i-t használjunk

Teljesítménybeli különbség akkor is van, ha az inkrementálást különálló utasításként használjuk, nem egy összetett kifejezés részeként. Ha postfix inkrementálást használunk (i++), akkor a még meg nem növelt értéket ideiglenesen tárolni kell az utasítás kiértékeléséig.
Tehát ha nem egy összetett kifejezés részeként használjuk, hanem külön utasításként (például for ciklusban), érdemesebb a prefix inkrementálást (++i) használni, mert gyorsabb és kevesebb memóriát foglal.

operator++() és operator++(int)

A prefix- és postfix inkrementálás közti különbséget könnyű megjegyezni: ha a ++ előbb van, akkor a növelés is előbb van, ha a ++ a változó mögött szerepel, akkör a növelés később (az inkrementálást/dekrementálást tartalmazó kifejezés kiértékelését követően) történik meg.

Ha csak magát az inkrementáló operátort írjuk le (operandust nem), akkor abból nem derül ki, hogy a prefix- vagy postfix inkrementáló operátorra gondoltunk. A prefix inkrementáló operátort így jelöljük: operator++(), a postfix inkrementáló operátort pedig így: operator++(int).
Esetleg úgy lehet könnyen megjegyezni a kettő  közti különbséget, hogy a postfix inkrementálásnál egy ideiglenes érték tárolása szükséges, ami egy exra (általában int) értéket jelent.

Ezekkel a jelölésekkel osztályok kapcsán, jellemzően iterátorok kapcsán találkozhatunk majd:

Inkrementálás, dekrementálás és undefined behavior

Tekintsük az alábbi példákat:

int i = 0;
std::cout << i << ++i << '\n';
int j = 0;
std::cout << j++ << j << '\n';

Ekkor logikusan azt gondolhatnánk, hogy pl. a std::cout << i << ++i; utasításban először az i meg nem növelt értéke lesz kiírva, aztán pedig a megnövelt értéke, ami persze lehet, hogy egyes esetekben így is van, de ez nem biztos.

A C++17-es szabványt megelőző szabványok szerint fordított forráskód esetén undefined behaviort okoz, ha egyetlen utasításban szerepel egy változóra vonatkozó inkrementálás (vagy dekrementálás), és ugyanabban az utasításban ennek a változónak a neve mégegyszer szerepel.
Például ezek az utasítások undefined behaviourt okoznak:

std::cout << i << ++i << '\n';
std::cout << ++i + i++ << '\n';

Noha a tömbökről csak későbbi tananyagrészben lesz részletesebben szó, fontos, hogy ez az utasítás is undefined behaviourt okozhat (pl. egy for ciklus blokkjában):

array_of_sth[i] = ++i;

A C++17 és újabb szabványokat támogató C++ fordítók esetén ez többnyire meg lett oldva, de ezen tananyag írásának idején (2010-es, 2020-as évek) bőven vannak, akik C++17-es szabványnál régebbi C++ fordítókat használnak, így érdemes a legutóbbi fenti példákat és azokhoz hasonló eseteket (amikor egyetlen utasításon belül szerepel ++valtozo_neve vagy valtozo_neve++ és még egyszer vagy többször a valtozo_neve) kerülni a forráskódban.

Íme egy kis tesztpélda, amit ha különböző fordítókkal (pl. g++, clang, msvc) fordítunk, és futtatunk, akkor jó eséllyel eltérő eredményt kapunk. A kipróbáláshoz segíthenek az online fordítók, pl. a wandbox.org oldalon többféle C++ fordítóval (akár régebbi verziókkal is) kipróbálhatjuk:

//increment undefined behavior test example
#include <iostream>
int main () {
  int i = 0, j = 0;
  std::cout << i++ << ' ' << i++ << ' ' << i++ << '\n';
  std::cout << ++j << ' ' << ++j << ' ' << ++j << '\n';
}

Két változó értékének cseréje

Ideiglenes változóval

Ha két változó értékét ki akarjuk cserélni, az egyik lehetséges megoldás az, hogy létrehozunk egy harmadik, ideiglenes változót, abba elhelyezzük az egyik változó értékét, hogy az ne vesszen el, amikor annak a változónak értékül adjuk a másik változó értékét.
Ha az ideiglenes változót a későbbiek során nem akarjuk használni, akkor akár egy névtelen blokkba is elhelyezhetjük, így csak a blokkon belül lesz elérhető, ameddig a cserét lebonyolítjuk.

//swap example
#include <iostream>

int main() {
  int int_example1 = 128;
  int int_example2 = -256;

  std::cout << "output1: " << int_example1 << ", " << int_example2 << '\n';

  {
    int temp = int_example1;
    int_example1 = int_example2;
    int_example2 = temp;
  }

   std::cout << "output2: " << int_example1 << ", " << int_example2 << '\n';
}

A C++ standard library std::swap függvényével

Az std::swap függvényt is használhatjuk (nem csak alaptípusú) változók értékének cseréjéhez, melyet a C++ standard library utility header fájljának includolásával használhatunk.

//std::swap example
#include <iostream>
#include <utility>

int main() {
  double double_example1{3.14};
  double double_example2{2.71};

  std::cout << "output1: " << double_example1 << ", " << double_example2 << '\n';

  std::swap(double_example1, double_example2);

  std::cout << "output2: " << double_example1 << ", " << double_example2 << '\n';
}

Az std::string típus swap tagfüggvényével

Ha két std::string típusú változó értékét szeretnénk kicserélni, akkor használhatjuk az std::string típus swap tagfüggvényét.

#include <iostream>
#include <string>

int main() {
  std::string str1{"abc"};
  std::string str2{"def"};

  std::cout << "output1: " << str1 << ' ' << str2 << '\n';

  str1.swap(str2);

  std::cout << "output2: " << str1 << ' ' << str2 << '\n';
}

Egyéb módszerek

Az többféle módszert találhatunk arra vonatkozóan, hogy ideiglenes változó használata nélkül hogyan lehet két változó értékét kicserélni. Például int típus esetén tipikusan kivonás/összeadás, illetve kizáróvagyolással szokták megoldani, de léteznek egyéb módszerek is. Ezekre talán akkor lehet szükség, ha nagyon spórolni kell az erőforrásokkal (pl. beágyazott rendszerekben), de asztali számítógépekre készített programokban teljesen megfelelőek a fentebbi módszerek.

Véletlenszám generálás

1 és 100 között

A programjainkban alapvető igény lehet, hogy a változóinknak véletlenszerű értéket adjunk. Ebben lehet segítségünkre az alábbi példa, amiben egy véletlenszerűen generált 1 és 100 közti egész számot (az 1-et és 100-at is beleértve) adunk értékül a random_number nevű változónak.
A véletlenszám generálás matematikai háttere egyébként viszonylag bonyolult, de a C++ standard library ebben is nyújt számunkra kész megoldásokat.

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

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

  //random generation
  int random_number = int_dist(rnd_generator);

  //output
  std::cout << random_number << '\n';
}

-50 és 50 között

Ezt a random szám generátort akár negatív számok generálására is használhatjuk. Például -50 és 50 közti egész számok generálására (beleértve a -50-et és 50-et):

//random integral number between -50 and 50
//including -50 and 50
#include <iostream>
#include <random>


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

  int random_number = int_dist(rnd_generator);

  std::cout << random_number << '\n';
}

0 és 1 között

Hasonló példa, amiben véletlenszerűen generált 0 és 1 közti valós számot (beleértve a 0 és 1 értékeket is) adunk értékül a random_number nevű változónak:

//random number between 0.0 and 1.0
//including 0.0 and 1.0
#include <iostream>
#include <random>

int main() {
  std::random_device rnd_device;
  std::mt19937 rnd_generator(rnd_device());
  std::uniform_real_distribution<double> real_dist(0.0,1.0);

  double random_number = real_dist(rnd_generator);

  std::cout << random_number << '\n';
}

Az intervallum elejét és a végét akár a felhasználótól is bekérhetnénk. Ekkor viszont ügyelnünk kell arra, hogy a második paraméter értéke nagyobb szám legyen, mint az első paraméteré, különben fordítási hibát kapunk.
Ha a két szám (alsó határ és felső határ) egyező, a random generálás működik, bár természetesen gyakorlatilag értelmetlen pl. 1 és 1 közti számhoz random generálást használni, mert az mindig 1-et fog visszaadni.

Ha includeoljuk a random header fájlt, többféle random generátort is használhatunk, de az ezek közti különbségre itt most nem térünk ki, mivel ezeknél az egyszerű példáknál nem lényeges, de aki szeretne, a lentebbi linkeken természertesen utánaolvashat.
Amit azért érdemes megemlíteni, hogy 64 bites processzorral rendelkező számítógépeken például az std::mt19937 helyett nyugodtan használhatunk std::mt19937_64-et.
Illetve a legegyszerűbb példákban std::mt19937 helyett esetleg std::default_random_engine-el találkozhatunk még.

Függvény

Bár függvények készítéséről a tananyag ezen részéig még nem volt szó, a jövőre való tekintettel mégis érdemes megjegyezni, hogyha a fenti kódrészletet egy olyan függvénybe pakoljuk bele, amit nagyon sokszor meghívunk, érdemes a random generátort (jelen példa esetén azt, aminek a típusa std::mt19937, a neve pedig rng) staticként definiálni, vagyis a std::mt19937 rng(random_dev()); sor helyett ezt írni: static std::mt19937 rng(random_dev()); azért, hogy ne kelljen a függvény minden egyes meghívásakor inicializálni a random generátort.

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

int generate_rnd_number(const int& min, const int& max) {
  static std::mt19937 rnd_generator( std::random_device{}());
  std::uniform_int_distribution<int> int_dist(min,max);
  return int_dist(rnd_generator);
}

int main() {
  //elements separated with space
  for (int i = 0; i < 99999; ++i) {
    std::cout << generate_rnd_number(1, 100) << ' ';
  }
  //last element without space, with newline
  std::cout << generate_rnd_number(1, 100) << '\n';
}

Továbbá szintén későbbi tananyagrész témáját érintő megjegyzés, hogy az std::random_device esetlegesen kivételt dobhat.

Régebbi fordítók

Elképzelhető, hogy régebbi fordítók esetén a fentebbi forráskódókkal a program minden indítása esetén ugyanazokat a random generált számokat kapjuk. Erre megoldást nyújthat a _GLIBCXX_USE_RANDOM_TR1 makró definiálása:

//random integral number with older compilers

#ifndef MY_NO_FIX_OF_RANDOM_DEVICE
#  ifdef __GNUC__
#    undef _GLIBCXX_USE_RANDOM_TR1
#    define _GLIBCXX_USE_RANDOM_TR1
#  endif
#endif

#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_number = int_dist(rnd_generator);
  std::cout << random_number << '\n';
}

Kapcsolódó tananyagok:

A random generált számok kapcsán esetleg még azt érdemes megjegyezni, hogy a C nyelvből átvett rand függvény használatát sokan kerülendőnek tekintik.

Alapvető matematikai műveletek

Az összeadás, kivonás, szorzás, osztás műveletek viszonylag magától értetődőek, ezek kapcsán talán azt érdemes megemlíteni, hogy az osztás másképp van értelmezve egész- és valós számok esetén (ha két egész típusú dolgot (pl. változót vagy literált) osztunk, akkor egész típusú lesz az eredmény, de ha a két dolog közül legalább az egyik valós típusú, akkor az eredmény is valós típusú lesz) de erről részletesebben az alaptípusok jellemzői tananyagrészben lesz szó.

Hatványozás, gyökvonás

A hatványozás és gyökvonás például a standard library cmath header fájljában található pow függvénnyel végezhető el.

Négyzetre emelés:

//square example
#include <iostream>
#include <cmath>

int main() {
  double variable{};
  std::cout << "Kerem adjon meg egy egesz szamot, vagy valos szamot:\n";
  std::cin >> variable;
  std::cout << "A beolvasott szamertek negyzete: " << pow(variable, 2) << '\n';
}

Hatványozás:

//power example
#include <iostream>
#include <cmath>

int main() {
  double base_number{};
  std::cout << "Kerem adja meg a hatvanyozni kivant szamot:\n";
  std::cin >> base_number;

  double exponent_number{};
  std::cout << "Kerem adja meg a kitevot:\n";
  std::cin >> exponent_number;

  std::cout << "A hatvanyozas eredmenye: " << pow(base_number, exponent_number) << '\n';
}

Gyökvonás:

//square root example
#include <iostream>
#include <cmath>

int main() {
  double variable{};
  std::cout << "Kerem adjon meg egy egesz szamot, vagy valos szamot:\n";
  std::cin >> variable;
  std::cout << "A beolvasott szamertek gyoke: " << sqrt(variable) << '\n';
}

Kerekítés

A C++ standard library cmath header fájljában található ceil függvény felfele kerekíti az adott törtszámot, ha az valamivel nagyobb, mint a hozzá legközelebb lévő, de nála kisebb egész szám (pl. a 2-t nem kerekíti 3-ra, de a 2.01-et már igen), a floor függvény hasonlóképpen lefele kerekít, a round pedig pl. 0.5 és nála nagyobb számokat 1-re kerekíti, de a 0.5-nél kisebb számokat (pl. 0.47-et) 0-ra.

//rounding example
#include <iostream>
#include <cmath>

int main() {
  double a1 = 2.7;
  double a2 = 2.1;
  double a3 = 2.5;
  double b1 = -2.7;
  double b2 = -2.1;
  double b3 = -2.5;

  std::cout << "ceil:\n"
  << a1 << " -> " << ceil(a1) << '\n'
  << a2 << " -> " << ceil(a2) << '\n'
  << b1 << " -> " << ceil(b1) << '\n'
  << b2 << " -> " << ceil(b2) << '\n';
  std::cout.put('\n');

  std::cout << "floor:\n"
  << a1 << " -> " << floor(a1) << '\n'
  << a2 << " -> " << floor(a2) << '\n'
  << b1 << " -> " << floor(b1) << '\n'
  << b2 << " -> " << floor(b2) << '\n';
  std::cout.put('\n');

  std::cout << "round:\n"
  << a1 << " -> " << round(a1) << '\n'
  << a2 << " -> " << round(a2) << '\n'
  << a3 << " -> " << round(a3) << '\n'
  << b1 << " -> " << round(b1) << '\n'
  << b2 << " -> " << round(b2) << '\n'
  << b3 << " -> " << round(b3) << '\n';
}

Egyéb matematikai műveletek

Jason Turner - legkisebb közös többszörös és legnagyobb közös osztó (videó)

Dátum

Jelenlegi dátum kiíratása (régi módszer)

//current date
#include <ctime>
#include <iostream>

int main() {
  std::time_t t = std::time(nullptr);
  std::tm* now = std::localtime(&t);
  std::cout << (now->tm_year + 1900) << '.'
    << (now->tm_mon + 1) << '.'
    << now->tm_mday
    << ".\n";
}

Jelenlegi dátum kiíratása (új módszer)

//current date
#include <chrono>
#include <ctime>
#include <iostream>

int main() {
  std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
  std::time_t tt = std::chrono::system_clock::to_time_t(now);

  std::tm utc_tm = *gmtime(&tt);
  std::tm local_tm = *localtime(&tt);

  std::cout << local_tm.tm_year + 1900 << '\n';
  std::cout << local_tm.tm_mon + 1 << '\n';
  std::cout << local_tm.tm_mday << '\n';
}

Két dátum közti napok kiszámítása

//days between 2 dates
#include <iostream>
#include <ctime>

std::tm make_tm(int year, int month, int day) {
  std::tm tm = {0};
  tm.tm_year = year - 1900;
  tm.tm_mon = month - 1;
  tm.tm_mday = day;
  return tm;
}

int main(){
  std::tm tm1 = make_tm(2012,4,2);
  std::tm tm2 = make_tm(1987,3,2);

  std::time_t time1 = std::mktime(&tm1);
  std::time_t time2 = std::mktime(&tm2);

  const int seconds_per_day = 60*60*24;
  double difference = std::difftime(time1, time2) / seconds_per_day;

  std::cout << "Days between the two dates: " << difference;
}

Milyen napra esik egy adott dátum?

#include <iostream>
#include <ctime>

int main () {
  time_t rawtime;
  struct tm * timeinfo;
  int year = 1987, month = 3 , day = 2;

  const char * weekday[] = { "Sunday", "Monday",
    "Tuesday", "Wednesday",
    "Thursday", "Friday", "Saturday"};

  time ( &rawtime );
  timeinfo = localtime ( &rawtime );
  timeinfo->tm_year = year - 1900;
  timeinfo->tm_mon = month - 1;
  timeinfo->tm_mday = day;

  mktime ( timeinfo );
  std::cout << "That day is a " << weekday[timeinfo->tm_wday] << ".\n";
}

Egyéb tananyagok

előző tananyagrész: változók, konstansok, literálok
következő tananyagrész: alaptípusok jellemzői

A bejegyzés trackback címe:

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

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