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

Ez a tananyagrész jelenleg átdolgozás alatt áll.

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

Tartalom

  • inkrementálás, dekrementálás
  • két változó értékének cseréje (swap)
  • véletlenszám generálás (random number)
  • matematikai műveletek
  • string (szöveg) műveletek

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 külön utasításként (például i++; vagy ++i;), nem pedig egy összetett kifejezésből álló utasítás részeként (például a = i++; vagy array_of_sth[i++]; 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ó 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 utasítás nem ugyanazt eredményezi:

std::cout << j++ << '\n';
std::cout << j = j + 1 << '\n';

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. Ez a két utasítás végeredményben egyenértékű:

std::cout << j++ << '\n';
std::cout << j = j + 1, j - 1 << '\n';

Viszont a j = j + 1, j - 1 kifejezésben a j nevű változó értéke először megnövelődik, aztán pedig az 1-el lecsökkentett érték kerül visszaadásra.

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.

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). Figyeljünk a kettő közti különbségre, mert ha összekeverjük, hibát okozhatunk.

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';
const int a = 128; ++a;

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, érdemesebb a prefix inkrementálást (++i) használni, mert gyorsabb és kevesebb memóriát foglal.

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 történik meg.

Ha csak magát az inkrementáló operátort írjuk le, 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, főként 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 tesztprogram, 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 rextester.com oldalon többféle C++ fordítóval 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.

#include <iostream>

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

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

  {
    int int_example3 = int_example1;
    int_example1 = int_example2;
    int_example2 = int_example3;
  }

   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.

#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::strint 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 interneten számtalan 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. int típus esetén tipikusan például 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

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() {
  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';
}

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

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.

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& intval_begin, const int& intval_end) {
   static std::mt19937 rnd_generator( std::random_device{}());
   std::uniform_int_distribution<int> int_dist(intval_begin,intval_end);
   return int_dist(rnd_generator);
}

int main() {
   for (int i = 0; i < 99999; ++i) {
     std::cout << generate_rnd_number(1, 100) << ' ';
   }
   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.

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 közül talán azt érdemes megemlíteni, hogy az osztás másképp van értelmezve egész- és valós számok esetén, de erről részletesebben az alaptípusok 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:

//squaring 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 megadott szam 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 megadott szam gyoke: " << sqrt(variable) << '\n';
}

Egyéb matematikai műveletek

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

string (szöveg) műveletek

Összefűzés (konkatenáció)

string literálok esetén:

Egymás után lévő string literálok összefűzésre kerülnek. Tehát ezen utasítások ugyanazt eredményezik:

std::cout << "abc" "123\n";

std::cout << "abc123\n";

std::cout << "abc" << "123\n";

A fenti példában akár külön sorba is írhatnánk a két string literált, hosszú string literálok esetén ez akár sortördeléshez is hasznos lehet:

std::cout << "abcdefghijklmnopqrstuvwxyz0123456789\n"
"abcdefghijklmnopqrstuvwxyz0123456789\n";

A string literálok összefűzése akkor is így működik, ha egy std::string típusú változónak adjuk értékül az összefűzött string literált:

std::string str = "abc" "123";
std::cout << str << '\n';

Egy string literált és egy karaktert ily módon összefűzni nem lehet:

//error
std::string str = "abc" '\n';

std::string típusú változók esetén:

std::string típusú változók összefűzéséhez a + operátort használhatjuk.

std::string str1 = "abc";
std::string str2 = "123";
std::string str3 = str1 + ", " + str2 + '\n';

A + operátort stringek összefűzésére csak akkor használhatjuk, ha az összefűzendő dolgok között legalább az egyik std::string típusú. A string literálok típusa nem std::string (erről szintén egy későbbi tananyagrészben lesz szó részletesebben).

//error
std::string str = "abc" + "123";

Méret, hossz

string literálok esetén:

Használhatjuk a standard library cstring header fájljában található strlen függvényt:

#include <iostream>
#include <cstring>

int main() {
std::cout << strlen("Hello World!\n") << '\n';
}

Esetleg a sizeof operátort:

#include <iostream>

int main() {
std::cout << sizeof("Hello World!\n")-1 << '\n';
}

Ez utóbbi esetén az eredmény eggyel nagyobb lesz, mint az strlen által visszaadott eredmény, mivel a sizeof a string literálok végén lévő lezárókaraktert ('\0') is beleszámolja a méretbe, így ki kell vonnunk egyet a sizeof által visszaadott eredményből, hogy megkapjuk a tényleges méretet.

std::string típusú változók esetén:

A size vagy length tagfüggvényt használhatjuk (mindkettő teljesen ugyanúgy működik):

#include <iostream>
#include <string>

int main() {
std::string str = "abc123";
std::cout << "the size of str: " << str.size() << '\n';
}

#include <iostream>
#include <string>

int main() {
std::cout << "the size of \"Hello World!\\n\": " << std::string("Hello World!\n").size() << '\n';
}

#include <iostream>
#include <string>

int main() {
std::cout << "Kerem irjon be valamilyen szoveget:\n";
std::string str;
getline(std::cin, str);
std::cout << "A megadott szoveg merete: " << str.size() << '\n';
}

Fontos: a stringek hosszát, méretét bájtban kapjuk meg, ami nem mindig a karakterek számát jelenti. Például az ékezetes karakterek értéke több bájt lehet.

//size is bigger than number of chars
#include <iostream>
#include <string>

int main() {
std::string str = "áéíóöőúüű";
std::cout << str.size() << '\n';
}

String típusú változók bizonyos szakaszait is felhasználhatjuk:

s4 = s1.substr(0,4) + ", " + s2.substr(0,4);

Például az s1.substr(0,4) kifejezésben a substr tagfüggvény visszaadja az s1-ben tárolt szöveg első karakterétől az ötödik karakteréig tartó részét (értelemszerűen a 0 paraméter jelenti az első karaktert, a 4 pedig az ötödiket). Így ezen kifejezés értéke jelen példa esetén "egyik" lesz. Ennek alapján könnyen kikövetkeztethető, hogy az s4 változó értékéül adott kifejezés értéke "egyik, masik" lesz.

Ha csak egyetlen karaktert szeretnénk felhasználni egy string értékéből, akkor például a valtozonev[0] kifejezést használhatjuk. Itt a 0 szintén a legelső karaktert jelenti, és például a 12 a tizenharmadikat.

string s5 = "tetszoleges";
cout << "s5 elso karaktere: " << s5[0] << endl;
s5[1] = '3';
s5[s5.length()-1] = '5';
cout << "s5 modositott erteke: " + s5 << endl;

Egy string értékébe be is szúrhatunk valamilyen szöveg típusú értéket, például:

string s6 = "aaa";
s6.insert( 2, "bb"); //ekkor s6 erteke erre modosul: aabba

Adott szövegrész törlésével is egybeköthetjük a beszúrást (felülírás), ekkor meg kell adni, hogy mettől meddig töröljük az aktuális érték egy részletét. A 0 szintén az első karaktert jelenti.

string s7 = "szoveg";
s7.replace( 1, 4, "qwert" ); //s7 erteke ezt kovetoen: sqwertg

Hasonlóan működik az erase tagfüggvény is. Értelemszerűen csak két paramétert vár, hogy mettől meddig töröljük az adott string típusú változó értékét.

std::string típusú változók további tagfüggvényei, és a használatukat bemutató példák például ezen az oldalon találhatóak.

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

A bejegyzés trackback címe:

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

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