C++ programozás 5. – Operátorok

Operátorok és operandusok, avagy műveleti jelek és szenvedő alanyaik

A programozási nyelvek fontos részét képezik az operátorok. Nevezzük őket műveleti jeleknek, habár nem a szó matematikai értelmében. Programozás során sokszor úgynevezett kifejezésekkel dolgozunk, amelyek valamilyen értékek és közöttük értelmezett műveletek. Megnövelünk egy számot, összeadunk két változót, hogy egy harmadiknak megkapjuk az értékét, összehasonlítunk egy változót egy számmal, hogy egyenlőek-e, stb. Operátornak magát a műveletet nevezzük, operandusnak pedig a kifejezés azon részét, amit változtatni akarunk vagy amit felhasználunk a számításhoz. Az operandus lehet egy megadott literál (egy direkt érték), változó vagy kifejezés.

  • a = b;
  • b == 2
  • c = a + b;
  • i++;
  • a += b * 2;
  • c = a == 3 ? 0 : a;

Az előző példákban pirossal emeltem ki az operátorokat, a maradékok pedig az operandusok. Igyekeztem minél több példát felsorolni, de operátor ennél sokkal több van és ezeket többféle elv mentén csoportosíthatjuk.

Csoportosíthatjuk az operátorokat annak megfelelően, hogy hány operandust kötnek össze. Ennek megfelelően megkülönböztetünk:

  • egyoperandusú,
  • kétoperandusú,
  • többoperandusú operátort.

Logikusabb azonban aszerint csoportosítani őket, hogy milyen jellegű műveletet hajtanak végre, bár vannak olyanok, amelyek nem sorolhatók be egyértelműen művelet alapján egy csoportba sem.

  • Aritmetikai operátorok
  • Relációs operátorok
  • Értékadó operátorok
  • Logikai operátorok
  • Inkrementáló (növelő) operátorok
  • Feltételes operátor
  • Bitléptető és bitenkénti operátorok (esetleg később kifejtem)

Aritmetikai operátorok

Ezek jellemzően valamilyen matematikai műveletet hajtanak végre két számértéken. Ezek az operátorok a következők:

  • +
  • *
  • /
  • %

Az első hármat nem kell nagyon megmagyarázni, ezek matematikai alapműveletek, de az utolsó kettő már érdekesebb. Változókból, mint az 5. leckében már olvashattad léteznek egész és lebegőpontos típusok. A / jel az osztás jele, azonban ez kétféleképpen működik.

Egész osztás

Amennyiben a műveletet két egész szám között hajtjuk végre, akkor egész osztásról beszélünk. Ez azt jelenti, hogy hányszor van meg az egyik szám a másikban és a maradékkal nem foglalkozunk.

int a = 10;
int b = 3;
cout << a/b << endl;
a = 12;
b = 5;
cout << a/b << endl;

Az első kiíratás 3-at, a második 2-őt fog kiírni, és nem érdekel minket a maradék.

Valós osztás

Az / operátor használatakor másfajta eredményt kapunk akkor, ha a két szám közül legalább az egyik nem egész:

int a = 10;
double d = 3.0;
cout << a/d << endl;

Az eredmény 3.3333333…
Na de mi van akkor, ha két egész számunk van, de a teljes valós eredmény érdekel minket? Az nem elég, hogy 10/3, mert az operátor csak a két szám típusa alapján tudja eldönteni, hogy egész vagy valós osztást szeretnénk. Ehhez egy kis trükköt kell alkalmazni:

cout << 10/3.0 << endl;
// vagy
cout << 10.0/3 << endl;

Lényegtelen melyiket bővítem ki lebegőpontos számmá, a lényeg, hogy legalább az egyik az legyen. De változók használata esetén ezt nem tehetem meg, mert annak semmi értelme, hogy a.0/b. Mit tehetünk ilyenkor?

int a = 10;
int b = 3;
cout << a/(b+0.0) << endl;

Egyszerűen az egyik változó értékéhez hozzáadunk 0.0-t. Ettől az értéke nem változik meg, csak a típusa, tehát valós osztás lesz belőle. És minek a zárójel? Próbáld ki nélküle:

int a = 10;
int b = 3;
cout << a/b+0.0 << endl;

Mi is matematikában a műveletek sorrendje? Először elvégzi az osztást, ami egész osztás lesz, annak eredménye 3. Majd ehhez hozzáad 0.0-t, ami miatt továbbra is 3 lesz és nem 3.33333. Nagyon sokszor előfordul ez a hiba, amikor az a feladat, hogy átlagot kell számolni. Figyeljünk oda, hogy egész osztásra vagy valós osztásra van szükségünk, és ha valós osztásra van szükségünk egész számok között, akkor ne maradjon le a +0.0

Ettől azért egy elegánsabb megoldás is létezik. Anélkül, hogy az eredeti változók tartalmát megváltoztatnánk, megtehetjük, hogy az egyiket csak erre az egy múveletre átalakítjuk valós értékre, hogy az osztás ne egész osztás legyen, és kezelje a maradékot is. Ez az úgynevezett típuskényszerítés (cast) eszközével tehető meg. Ez is egyfajta operátor. Technikailag az történik, hogy a kívánt változó neve elé zárójelben odaírjuk azt a típust, amire szeretnénk kényszeríteni.

int a = 10;
int b = 3;
cout << (double)a/b << endl;

Gyakorlatilag annyi történik, hogy az a változó értéke ebben az egy műveletben valós lesz, így az osztás eredménye is ennek megfelelően 3.33333. Az mindegy, hogy melyik változó értékét cast-olod, a lényeg, hogy legalább az egyik valós legyen.

Maradékos osztás

Amikor két szám osztásakor nem a hányados, hanem a maradék érdekel minket, akkor van szükségünk a % operátorra. Használni is egyszerű:

int a = 10;
int b = 3;
cout << a%b << endl;
a = 12;
b = 5;
cout << a%b << endl;

Az első kiíratás 1, a második 2 lesz. Az osztás elvégzése után ennyi a maradék. A maradékos osztást számok osztóinak keresésekor szoktuk használni. Ha egy szám például 5-tel osztva nulla maradékot ad, akkor mit tudtunk meg a számról? Hogy osztható 5-tel. Ha a szám % 3 nullával egyenlő? Akkor 3-mal osztható. Ez később még többször előfordul, emlékezzünk rá.

Relációs operátorok

Az operátorok következő csoportja a relációs operátorok. Ezek a matematikában is ismeretes relációk, melyek a következők:

Reláció Jele
kisebb <
nagyobb >
kisebb vagy egyenlő <=
nagyobb vagy egyenlő >=
egyenlő ==
nem egyenlő !=

A relációk első 4 fajtáját nem nagyon kell kifejteni, ellenben az == már magyarázatra szorul. Ez semmiképpen nem keverendő a = jellel. Nagyon sokszor keverik a kezdő programozók ezt a két operátort. Az == a két oldalon szereplő literál, változó vagy kifejezés egyenlőségét vizsgálja. Az = pedig az értékadást jelenti, amit lejjebb ismertetek. A relációk, így az egyenlőségvizsgálat is, egy logikai értéket adnak eredményül, ami igaz, vagy hamis lehet. Megjegyzésbe odaírtam a kiíratások mellé az eredményt is. Megjegyezném, hogy a C++ nyelvben nem lehet kiírni direktben a bool típusú változó értékét true vagy false értékként.

cout << (5 >= 6) << endl; // 0
cout << (4 == 4) << endl; // 1
cout << (4 == 6) << endl; // 0
cout << (4 != 3) << endl; // 1
cout << (4 <= 5) << endl; // 1
cout << (6 < 3)  << endl; // 0

Értékadó operátorok

Ezek az operátorok valamilyen változónak adnak értéket. Az értékadás alapformája:

változó = kifejezés;

Az értékadás bal oldalán mindenképpen egy változónak kell szerepelnie, jobb oldalon pedig egy literál, változó, vagy olyan kifejezés (operátorok és operandusok összessége), amely egy értéket határoz meg, amit eltárolunk az értékadás bal oldalán lévő változóban. Az értékadó kifejezésben maga a bal oldali változó is szerepelhet. A kiemelt sorban egy olyan értékadás látható, ahol a kifejezésben is megtalálható a bal oldali változó, ebben a lépésben valójában a változót 1-gyel megnöveljük. Fontos, hogy a kezdőérték megadásakor (inicializáció) ilyen nem lehetséges, mert addig nem használható fel egy változó egy kifejezésben, amíg nincs kezdőértéke! Itt a kiemelt sor előtt a kezdőérték megadása megtörtént, tehát utána már növelhetem ilyen értékadással.

int a = 0;
a = a + 1;

Értékadó operátorból azonban több is van. Ezek többsége valamilyen matematikai művelettel van összekapcsolva:

  • +=
  • -=
  • *=
  • /=
  • %=

Ezek a típusok a már tanult aritmetikai operátorokkal kapcsolja össze a műveletet. Ilyen operátor használatakor kiértékelésre kerül a jobb oldal, és a bal oldali változó értékét az értékadáshoz kapcsolt műveletnek megfelelően végzi el. Megnöveli a változó értékét a jobb oldallal, csökkenti a változó értékét a jobb oldallal, szorozza a változó értékét a jobb oldallal, osztja a változó értékét a jobb oldallal, stb. Itt is igaz az, hogy ha a két oldalon egész értékek szerepelnek akkor a /= egész osztást jelent, ha legalább az egyik lebegőpontos érték, akkor valós osztás. Az utolsó típus a bal oldali változó eredeti értékét osztja a jobb oldallal és a maradékot tárolja el a bal oldali változó új értékének. Az utolsó esetben nem azonos típusok esetén tizedesjegy csonkolások is előfordulhatnak.

Logikai operátorok

A logikai operátorok feltételeket kapcsolnak össze. Ezek a feltételek rendszerint a már előzőleg ismertetett relációkhoz kapcsolódnak. Programozásban középiskolai szinten jellemzően 3 logikai műveletet használunk:

  • negálás (tagadás)
  • logikai és
  • logikai vagy

Ezek közül az első különbözik a másik kettőtől, mert ő nem feltételeket kapcsol össze, csak egy logikai kifejezés eredményét változtatja meg az ellenkezőjére.

Negálás

A negálás (tagadás) egy logikai kifejezés értékét az ellenkezőjére változtatja. Ami igaz volt, az hamis lesz, ami hamis volt, az igaz lesz. Vagy egy eldöntendő kérdést fordíthatunk meg vele. A szám NEM páros? (tehát páratlan, mivel más lehetőség nincs)

!(szam % 2 == 0) // nem páros
!(szam > 5) // nem nagyobb, mint 5
!true // nem igaz, tehát hamis

Programozás során azonban a feltételek sokszor nem önmagukban állnak, hanem többet össze kell kapcsolni. Ezeket összetett feltételeknek nevezzük. Ha több feltételünk van, de azok együtt értendők, akkor azokat össze kell kapcsolni valamilyen logikai művelettel, erre szolgál a logikai és, valamint a logikai vagy művelet.

  1. a szám páros és pozitív?
  2. a szám nagyobb, mint 10 és páratlan?
  3. a szám nagyobb, mint 10 és kisebb, mint 30?
  4. a szám osztható 3-mal vagy 7-tel?
  5. a szám nem negatív vagy páros?

Itt láthatóan összetett feltétellel dolgozunk, de nem mindegy, hogy azokat mi kapcsolja össze. Ráadásul a feltételek száma nem csak 2 lehet, bármennyi feltételt összekapcsolhatunk.

Logikai és

A logikai és két vagy több feltételt kapcsol össze egyetlen összetett logikai kifejezéssé. Ha azt mondom, hogy a piros és gyors autókat szeretem, akkor szóba sem jöhetnek a kékek, zöldek, lassúak, stb, de egy tűzpiros Jaguar (amit gyorsnak feltételezünk) igen. A két feltételnek egyszerre kell teljesülnie. Definíció szerint ez a következőt jelenti: A logikai és művelettel összekötött részfeltételek akkor adnak együtt igaz értéket, ha a kifejezés minden részfeltétele igaz. Ebből következik, hogy ha egy részfeltétel hamis, akkor hamis az egész kifejezés. Természetesen több feltételt is megadhatok. Piros, gyors, Ferrari. Ettől kezdve az előző tűzpiros Jaguar is kiesett a kosárból, míg az előző két részfeltételes esetben még megfelelt volna. Minél több feltételt kötök össze, annál kevésbé kapok végeredményként igaz értéket. A logikai és művelet jele: &&

szam > 5 && szam % 2 == 0 // a szám 5-nél nagyobb ÉS páros
szám < 0 && szam % 2 != 0 // a szám negatív ÉS páratlan
szam > 10 && szam < 20    // a szám 10 ÉS 20 között van

Logikai vagy

A logikai vagy szintén két vagy több feltételt kapcsol össze egyetlen összetett logikai kifejezéssé. Ha azt mondom, hogy a piros vagy gyors autókat szeretem, akkor ez jóval megengedőbb, mint az előző példa. Szóba jöhet a fekete Ferrari és a tűzpiros Trabant is, de természetesen a tűzpiros Ferrari is. A két feltételnek nem kell egyszerre teljesülnie ahhoz, hogy az összetett feltétel igaz legyen. A logikai vagy művelettel összekötött részfeltételek akkor adnak együtt igaz értéket, ha a kifejezés legalább egy részfeltétele igaz. Vagyis ha bármi igaz benne, akkor igaz az egész együtt is. Ha minden hamis, csak akkor hamis az egész kifejezés. A logikai vagy művelet jele: ||

szam > 0 || szam < 0 // a szám 0-nál nagyobb, VAGY 0-nál kisebb
szam > 10 || szam < 0 // a szám 10-nél nagyobb vagy negatív

Kizáró vagy

Nem említettem meg egy logikai műveletet, ami még előfordulhat a programozási feladatokban, igaz ritkán. Ez a kizáró vagy. Nem győzöm eleget hangsúlyozni:

logikai vagy != kizáró vagy

Az igazi probléma a magyar nyelvvel van. Szeretem, használom, imádom, de a programozásban használatos gondolkodásmóddal sokszor szöges ellentétben áll:

  • Moziba menjek vagy tanuljak?
  • Négyes vagy ötös lesz a dolgozatom?
  • Fej vagy írás?
  • Fej vagy gyomor?

A magyar nyelvben nagyon sokszor a kizáró vagy műveletet használjuk. Vagy moziba megyek, vagy tanulok, a kettő együtt nem igazán működik. A dolgozatom vagy négyes vagy ötös lesz, de csak az egyik (jobb esetben). A kizáró vagy akkor igaz, ha pontosan egy részfeltétele igaz. Vagyis a két vagy több feltételből nem teljesülhet több egyszerre. De hogy a magyar nyelvben melyik vagy műveletet kell érteni a vagy szócskán ezt mindig a szövegkörnyezet és a feladat típusa határozza meg. Ha a két dolog egyszerre nem fordulhat elő, akkor csak a kizáró vagy jöhet szóba. De ha a barna hajú vagy szemüveges nők tetszenek, akkor a barna hajú és szemüveges is valószínűleg megfelel. A kizáró vagy művelet jele: ^

Relációk problémája

Térjünk vissza kicsit a relációkra. A tagadás, mint már említettem, megfordít valamit. Egy logikai értéket az ellenkezőjére változtat, de a relációkra is hatással van. Ez nem igazán a logikai kifejezésekhez kapcsolódó témakör, inkább logikai-szövegértési feladat, amivel kapcsolatban az a tapasztalat, hogy nagy problémák vannak ezzel a területtel.

Vegyük például a következőt: Mit jelent az, hogy nem nagyobb? A tipikus válasz: kisebb. NEM! A nem nagyobb azt jelenti, hogy kisebb vagy egyenlő. Hiszen, ha valami nem nagyobb, attól még vele egyenlő is lehet! Hasonlóan a nem kisebb jelentése: nagyobb vagy egyenlő. Lássuk akkor, hogy melyik relációnak melyik a tagadása:

Reláció Tagadása
kisebb nagyobb vagy egyenlő
nagyobb kisebb vagy egyenlő
kisebb vagy egyenlő nagyobb
nagyobb vagy egyenlő kisebb
egyenlő nem egyenlő
nem egyenlő egyenlő

Inkrementáló operátorok

Létezik két speciális operátor, mely egy változó értékének 1-gyel való növelésére és csökkentésére szolgál:

változó++;
változó--;

Úgy tűnik, hogy maga a növelés vagy csökkentés a következő utasítást helyettesíti teljes egészében:

változó = változó + 1;
változó = változó - 1;

Ez a két eset valójában 4 esetet jelent, a példákból egyértelmű lesz, mire gondolok. Ezek az operátorok rendkívül sokszor fordulnak elő, és a kód átláthatóságát sem rontja akkora mértékben, hogy ez gondot jelentene. Ez a 4 eset a következőképp néz ki változó növelés/csökkentés esetén:

változó++;
++változó;
változó--;
--változó;

Az alaphelyzet tehát az, hogy a ++ operátor megnöveli eggyel a változó értékét, míg a — csökkenti azt. Ezek a példák önálló utasításként működnek, ezért zártam le ezeket ; jellel.

Látható azonban, hogy mindkét operátor szerepelhet a változó előtt és után. Amikor az operátor a változó mögött szerepel, azt postfix alaknak nevezzük, ha előtte, akkor prefix alakról beszélünk. Nyilván nem csak esztétikai jelentősége van, lássuk a gyakorlati hasznát. Az első két példában mivel ebben a sorban csak annyi szerepel, hogy a változó értékét növeljük meg, ezért nincs a két megoldás között különbség. Azonban amikor a növelés vagy csökkentés egy kiíratás vagy összetettebb kifejezés része, akkor már fontos különbség adódik:

int a = 10;
cout << a++ << endl; // 10
cout << a   << endl; // 11
int a = 10;
cout << ++a << endl; // 11
cout << a   << endl; // 11

Az első példában a növelés, mint művelet, a változó után található. Ez a gyakorlatban azt jelenti, hogy a kiíratás először felhasználja a változó eredeti értékét (10), majd ha minden művelet lezajlott ebben a sorban, utána megnöveli a változó értékét (11), vagyis a következő sorban, ahol már művelet nélkül írjuk ki, már a megnövelt értékét láthatjuk.

A második példában a növelés, mint művelet, a változó előtt található. Ez azt eredményezi, hogy ebben a sorban először megnöveli a változó értékét, majd a változó már megnövelt értékét írja ki. A következő sorban is ugyanazt az értéket írja ki, mivel itt szintén az előzőleg megnövelt értéket használhatjuk. Ugyanez igaz a csökkentésre is.

Ezek összetettebb kifejezésben is így működnek. Amennyiben a változó előtt szerepelnek az inkrementáló operátorok, akkor ezeket hajtja végre, majd a megnövelt értékekkel dolgozik tovább. Ezek a műveletek keverhetők az aritmetikai operátorokkal is, tehát ennek is van értelme: a++ + ++b

A következő példákat tessék tesztelni, ezeken keresztül világos lesz ezen operátorok működése. Számold ki a tesztelés előtt, hogy melyik sorban mit kellene látnod eredményül. Ha elsőre nem sikerült, gyakorolj még egy kicsit.

int i = 4;
int j = 3;
cout << i + ++j   << endl;
cout << i++ + j++ << endl;
cout << ++i + j++ << endl;
cout << i + j     << endl;

Az ilyen operátorok gyakorlati felhasználása azonban jellemzően mégis inkább a ciklusokhoz kapcsolódik. Fontos azonban megjegyeznem, hogy használatuk sok esetben inkább önálló utasításként érdemes, mert a kód átláthatóságát nagyban rontják, ami programozáskor az egyik legfontosabb szabály!

Feltételes operátor

Ezt az operátort egyelőre nem fejteném ki részletesen. Abból a szempontból egyedi, hogy egyedül ő kapcsol össze 3 operandust. Alakja:

feltétel ? ha_igaz : ha_hamis;

Ez a forma így önmagában nem is fordulhat elő, ez valamilyen kifejezés része, legyen az egy értékadás, vagy egy szöveg összefűzés. Ez annyit jelent, hogy egy általunk megadott feltétel ha teljesül, akkor a ? utáni ha_igaz eredményt kapja a kifejezés, ellenkező esetben a ha_hamis helyre írtat. A feltételvizsgálatkor majd látni fogod, hogy ez voltaképp egy egyszerű if-else szerkezet jóval tömörebb formája is lehet. Nem használható minden if-else szerkezet kiváltására, és nem használjuk gyakran. Általában egysoros utasítások rövidítésénél fordul elő, de bonyolultabb kifejezésekben szinte mindig érdemes kikerülni. Vannak olyan programfejlesztéssel foglalkozó csapatok, ahol kifejezetten tiltják a használatát átláthatósági problémák miatt.

Operátorprecedencia – végrehajtási sorrend

Operátorból ettől azért sokkal több van, de ezeket a legfontosabb megemlíteni ahhoz, hogy értsük a működésüket, és a programozást elkezdhessük. Fontos azonban megemlíteni azt, hogy az operátorok között is létezik egyfajta erősorrend, csakúgy, mint a matematikai műveletek között. Ezt hívjuk az operátorok precedenciájának, más néven kiértékelési sorrendjének. Az értékadó operátorok például a lista legalján vannak, ahol a “leggyengébb” műveletek helyezkednek el.

Operátor Precedencia
Hatókör feloldás ::
Postfix operátor, típuskényszerítés, tag elérés változó++ változó- – (int) . ->
Prefix operátor, előjel
operátorok, negálás
++változó – -változó +változó -változó !
Aritmetikai operátorok
(multiplikatív)
* / %
Aritmetikai operátorok (additív) + –
Relációs operátorok < > <= >=
Relációs operátorok
(egyenlőségvizsgálat)
== !=
Kizáró vagy ^
Logikai és &&
Logikai vagy ||
Feltételes operátor, értékadó operátorok ? : = += -= *= /= %=
Vessző ,

Ha jobban megnézed, akkor a precedencia nagyrészt megegyezik a matematikai műveletek sorrendjével, valamint itt is igaz az, hogy zárójelezéssel felül lehet, és sokszor felül is kell bírálni azt. Egyetlen fontos szabályt hagytunk csak ki. Mi van akkor, ha azonos szinten lévő operátorok szerepelnek a kifejezésben? Akkor mi a sorrend? Ebben az esetben mindig a balról-jobbra kiértékelési sorrend az érvényes.

Itt is van azonban egy kakukktojás, igaz, ez inkább elméleti dolog. Emlékszel, hogy az értékadó operátoroknál mi a kiértékelési sorrend? Hiszen az is két operandust köt össze. A bal oldalon egy változó, a jobb oldalon pedig literál, változó vagy kifejezés állhat. Na de a két operandus közül melyiket kell először használni? A jobb oldalit, mert annak az eredménye kerül a bal oldali változóba. Akkor jöjjön az elméleti példa:

int a, b;
a = b = 2;

Mi lesz ennek az eredménye? Minden változó 2 lesz. De az előbb említettem, hogy azonos precedencia szintű operátorok esetén a végrehajtási sorrend balról jobbra halad. Igen, kivéve az értékadásnál. Itt mindig a jobb oldal kerül először kiértékelésre. Vagyis:

  1. az a változó értéket kap: a = b = 2;
  2. a jobb oldalon megint egy értékadás szerepel: b = 2;
  3. a b változó jobb oldalán lévő érték bekerül a b változóba (tehát a b = 2 eredménye 2 lesz)
  4. a b = 2 kifejezés eredménye (2) bekerül az a változóba

Vagyis: jobbról balra haladva értékelte ki a többszörös értékadást. Ritka, de előfordulhat és működik. És természetesen, mivel az átláthatóságot rontja, kerülendő 🙂

Következő lecke: Adatbekérés

2 Replies to “C++ programozás 5. – Operátorok”

Vélemény, hozzászólás?

Az email címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük

*

Ez a weboldal az Akismet szolgáltatását használja a spam kiszűrésére. Tudjunk meg többet arról, hogyan dolgozzák fel a hozzászólásunk adatait..