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

C++ programozás kezdőknek - elágazások, logikai változók

[2016. szeptember 06.] [ christo161 ]

Ebben a részben arról lesz szó, hogyan tudjuk bizonyos utasítások végrehajtását feltételhez kötni, valamint szó lesz a logikai változókról is, melyeket logikai kifejezések eredményeinek tárolására használhatunk.
Előző rész: változók, konstansok, literálok
Következő rész: tömbök, ciklusok

Nyilvánvalóan nem lenne olyan sokmindenre jó a programozás, ha a programjainkban csak értékeket bekérni, módosítani és kiíratni lehetne (erről szólt az előző fejezet). A programozás egyik leglényegesebb lehetősége, hogy egy programban attól függően történhet valami, hogy mit csinál a felhasználó (pl. milyen értékeket ad meg, hova kattint, stb), vagy hogy mit tartalmaz egy fájl, illetve a programban szereplő változók.

cpp_cli_calculator.png

Az struktúrált programozási nyelvekben három alapvető vezérlési szerkezet segítségével (és ezek kombinációjával) alakíthatjuk az utasítások sorrendjét: rákövetkezés (szekvencia), elágazás (szelekció), ciklus (ismétléses vezérlés). Az eddigi programjaink csak szekvenciát tartalmaztak, vagyis a forráskód minden utasítása végrehajtódott, olyan sorrendben, ahogy a forráskódban szerepeltek.
Kiegészítő információ: a párhuzamos programozást támogató programnyelvekben szerepelhetnek egyéb vezérlési szerkezetek is, illetve a nem imperatív programozási nyelvekben (amikben nem használunk változókat) a fentiektől eltérőek a vezérlési szerkezetek (pl. a Haskell nyelvben), de ezekről ebben a tananyagban nem lesz szó.

Elágazások

Utasítások feltételhez kötésének legalapvetőbb módja az elágazás (if-else), melynek segítségével megadhatjuk, hogy milyen utasítások hajtódjak végre akkor, ha egy általunk (a program forráskódjában) meghatározott feltétel (logikai kifejezés) teljesül, illetve akkor, ha a feltétel nem teljesül (ez utóbbi opcionális).

Íme egy kódrészlet, amivel az if-else alapvető működését szeretném szemléltetni. Amikor ilyen kódrészletet helyezünk el a programkódunkban, a /*feltétel*/ komment helyére egy logikai kifejezést írhatunk (pl. a>-1, c<=d, e==f, g!=h), vagy egy logikai változót (melyeket általában logikai kifejezések eredményének tárolására használunk), de ha bármely más (nem logikai) kifejezést írunk (pl. a+b vagy getline(fajl, valtozonev) vagy esetleg cout << "Hello"), akkor a kifejezés logikai értékké konvertálódik (mégpedig olyan módon, hogy ha a kifejezés értéke 0-val egyenértékű, akkor hamis (false) értékké konvertálódik (ekkor az else ágban lévő utasítások fognak végrehajtódni), ha pedig 0-val nem egyenértékű a /*feltétel*/ helyére írt kifejezés (ekkor pedig true értékké konvertálódik, és az első blokkban lévő utasítások fognak végrehajtódni).

if ( /*feltétel*/ ) {
	//az ide írt utasítások hajtódnak végre, ha a feltétel igaz
} else {
	//az ide írt utasítások hajtódnak végre, ha a feltétel hamis
}

Egy elágazás végrehajtásra kerülő utasításblokkja csak egyszer fog lefutni. Adott utasítás vagy utasításblokk többszöri végrehajtása ciklusokkal (for, while, do-while) végezhető el, mely a következő fejezet témája.

Kiegészítő információ: elsőre szokatlannak tűnhet, hogy egy if vagy valamilyen ciklus paramétereként nem egy logikai kifejezés van megadva, hanem mondjuk egy egyszerű utasítás (mint például a getline(fajl, valtozonev)), de bizony előfordulhat, hogy ilyennel találkozunk egy forráskódban. Nyilván ezeknek is megvan a maga értelme, a getline(fajl, valtozonev) kifejezés például false értéket ad vissza, ha a feldolgozás elérte az adott fájl végét.

Logikai változók, logikai értékek

A logikai kifejezések eredménye vagy true (igaz) vagy false (hamis) lehet. A true és a false a C++ nyelvben literálnak minősülnek, hasonlóképp ahogy például az 5 egy int típusú literál, vagy a "szoveg" egy sztringliterál. A true és a false literálok típusa bool (logikai).

A logikai kifejezések eredményét eltárolhatjuk logikai (bool típusú) változókban (például annak érdekében, hogy többször hivatkozhassunk rájuk):

int a;
cout << "Kerem adjon meg egy negativ szamot" << endl;
cin >> a;

bool c = a < 0; //c értéke true, ha az a-ba beolvasott érték negatív

Egy if elágazásban így használhatunk egy logikai változót:

if(c) {
	//végrehajtódó utasitasok ha c értéke true
} else {
	//végrehajtódó utasitasok ha c értéke false
}

A feltételt persze így is megfogalmazhattuk volna: c == true, ami teljesen egyenértékű azzal, ha csak c-t írunk helyette. Noha szintaktikailag helyes, nem szokás ezt a hosszú alakot használni.

Egy logikai változó vagy literál értékének ellentettjét a ! operátorral kaphatjuk meg. Például:

bool e, f, g;

e = true;
f = !e; //f változó értéke false lesz
g = !false; //g változó értéke true lesz

A logikai változókat gyakran false értékkel inicializáljuk:

bool c = false;

Ha egy logikai változónak nem logikai, hanem egyéb típusú értéket adunk értékül, akkor az az érték false értékké konvertálódik, ha 0-val egyenértékű, és true értékké konvertálódik, ha 0-val nem egyenértékű.

bool b = false;
/* A következő utasítások végrehajtását követően b értéke szintén false lesz*/
b = 0;
b = 0.0;
b = '\0';
b = NULL;
b(nullptr);

bool c = true;
/* A következő utasítások végrehajtását követően c értéke szintén true lesz*/
c = -1;
c = '0';
c = "szoveg";
c = "";

Ha logikai értékeket konvertálunk számmá, akkor a false érték nullává, a true érték eggyé konvertálódik.

int a = false; //a értéke 0 lesz
int b = true; //b értéke 1 lesz

bool c = false;
int d = c; //d értéke 0 lesz

Különböző programozási nyelvekben eltérő lehet, hogy mely értékek konvertálódnak false, vagy true értékké. Például Javascript nyelvben ha logikai értékké konvertálunk üres sztringliterált, akkor false értékké fog konvertálódni, míg C++ nyelvben egy üres sztringliterál true értékké konvertálódik.
Javascript: !!("") == false
C++: static_cast<bool>("") == true

Változók láthatósága

Hatókör (scope): ha egy változót egy elágazás valamely blokkján belül definiálunk, akkor azt a változót nem érhetjük el az elágazás adott blokkján kívül (a másik blokkjában sem).
Tehát ügyeljünk arra, hogyha egy változót nem csak az elágazás egyik blokkjában szeretnénk használni, hanem azon kívül is, akkor a definícióját az elágazás blokkjain kívül helyezzük el.

Egyik példa:

/*Hibás kód*/
if ( /*feltétel*/ ) {
	int a;
	//tetszőleges utasítások
} else {
	cout << a; //fordítási hiba
}

Másik példa:

/*Hibás kód*/
if ( /*feltétel*/ ) {
	string b;
	//tetszőleges utasítások
} else {
	//tetszőleges utasítások
}
cout << b; //fordítási hiba

Elfedés: amennyiben az elágazás egyik blokkján belül definiálunk egy a blokkon kívül már definiált változóval azonos nevű változót, akkor az elágazás azon ágán belül a kívül definiált változót nem érhetjük el.

int a = 1;
if ( /*feltétel*/ ) {
	int a = 3;
	/*ebben a blokkban nem tudunk a külső a változóra hivatkzoni*/
	cout << a << endl; //ekkor 3 lesz kiírva
} else {
	cout << a << endl; //ekkor 1 lesz kiírva
}
cout << a << endl; //ekkor 1 lesz kiírva

Kivételt képeznek a globális változók (melyeket a függvények blokkján kívül (beleértve a main függvényt is) definiálunk). Elfedés esetén a :: operátorral elérhetjük a globális változókat.

char c = 'y'; /*ez egy globális változó, melyet bármely blokkból elérhetünk*/

int main () {
	char c = 'n'; /*ez egy lokális változó, melyet a saját blokkjában mindig elérhetünk, a saját blokkjában belüli blokkokban pedig csak akkor, ha nincs elfedve azonos nevű változó által*/
	cout << c << endl; //ekkor n lesz kiírva
	cout << ::c << endl; //ekkor y lesz kiírva
}

Elrejtés: ha azt szeretnénk, hogy egy változót csak az elágazás két ágában lehessen használni, de az elágazáson kívül ne, akkor létrehozhatunk egy önálló, az elágazástól független blokkot.
Ez például azért hasznos, mert így biztosan elkerülhetjük, hogy egy változó a kód további részeiben definiált változókkal névütközést okozzon, illetve azt is elkerülhetjük, hogy esetleg véletlenül hivatkozzunk rá a kód további részeiben (hiszen egy hosszú, szövevényes kódban nem könnyű eligazodni).

{
	int a;

	//itt használhatjuk az a változót

	if ( /*feltétel*/ ) {
		//itt is használhatjuk az a változót
	} else {
		//itt is használhatjuk az a változót
	}

	//itt is használhatjuk az a változót

}
//ezen a ponton megszűnik az a változó érvényessége

Fontos: a fentebb leírtak nem csak az elágazások blokkjaira vonatkoznak, hanem a következő fejezetben tárgyalt ciklusok blokkjaira is.

Struktogramok

A struktogramok vezérlési szerkezetek egyszerűsített ábrái (az úgynevezett algoritmusleíró eszközök egy fajtája), melyeket bonyolultabb programok vagy programrészek könnyebben átlátható, vázlatos formában történő leírásához használhatunk. A folyamatábrák talán jobban szemléltetik az egyes vezérlési szerkezetek működését, viszont bonyolultabb programok szerkezetét struktogramokkal sokkal tömörebben lehet ábrázolni, mint folyamatábrákkal.
(A struktogramokban szereplő jelölések programnyelvtől függetlenek. Például az értékadás := a struktogramokban, míg C++-ban sima =, az egyenlőségvizsgálat pedig a struktogramokban sima =, míg C++-ban ==).

A szekvencia struktogramja:

struktogram_szekvencia.png

Ez természetesen a szekvencia legegyszerűbb példája, de az utasítás1 és utasítás2 feliratú téglalap kicserélhető egyéb vezérlési szerkezetek struktogramjára vagy több utasításra.

Az elágazás struktogramja:

struktogram_elagazas.png

Egy elágazás úgy is megvalósítható, hogy csak a megadott feltétel teljesülése esetén hajtódnak végre utasítások, és ha a feltétel nem teljesül, ne történjen semmi (a program lépjen tovább az elágazást követő utasításra, ha van). Ez esetben nem írunk else ágat, vagy nem írunk utasításokat az else ág blokkjába, esetleg kikommenteljük az else ágat.

struktogram_elagazas_skip.png

A struktogramokban a SKIP kifejezést használjuk egy elágazás valamely ágának kihagyására.

1. példa: egy szám abszolútértéke

Az első példában egy, a felhasználó által megadott szám abszolútértékét íratjuk ki. Az abszolútérték ugyebár egy szám 0-tól mért távolsága, mely minden esetben egy pozitív érték.
Ha a megadott szám negatív (vagyis kisebb, mint 0), akkor a -1-el megszorzott értékét íratjuk ki (így csinálunk belőle pozitívat), ha pedig a szám 0, vagy 0-nál nagyobb, akkor egyszerűen csak kiíratjuk, módosítás nélkül.

//ez a peldaprogram beker a felhasznalotol egy szamot, es kiirja annak az abszolut erteket

#include <iostream>
using namespace std;

int main () {
	float b;
	cout << "Kerem adjon meg egy (nem tul sok szamjegybol allo) szamot" << endl;
	cin >> b;
	cout << "A megadott szam abszolut erteke: ";
	if (b < 0) {
		cout << -b << endl; //ha a bekert szam negativ, akkor megszorozzuk -1-el
	} else {
		cout << b << endl; //egyebkent (ha a bekert szam pozitiv, vagy 0) modositas nelkul kiiratjuk
	}
	return 0;
}

Természetesen másképp is megírhattuk volna a programot.
Például a b változó módosított értékét el is tárolhattuk volna kiíratás előtt. Ez esetben nincs szükség else (egyébként) ágra, hiszen csak akkor kell módosítani a számot, ha az negatív.

if (b < 0) {
	b = -b; //vagy esetleg b *= -1;
}
cout << b << endl;

Továbbá a feltételt is megfogalmazhattuk volna másképp. A b < 0 logikai kifejezés ellentettjét (b >= 0) is megadhattuk volna feltételnek. Ezt bármilyen logikai feltétel esetén megtehetjük, de ügyeljünk arra, hogy helyesen adjuk meg az adott logikai kifejezés ellentettjét, és hogy ekkor az elágazás egyes ágai felcserélődnek.

if (b >= 0) {
	cout << b << endl;
} else {
	cout << -b << endl;
}

2. példa: egy szám paritása

A következő példában a felhasználó által megadott szám paritását (páros vagy páratlan) íratjuk ki. Egy egész szám páros, ha kettővel elosztva 0 maradékot ad, és páratlan, ha 1-et, vagy -1-et (lényegtelen, hogy hányszor van meg benne a 2, csak a maradék a lényeg). (Nem egész szám nem lehet se páros, se páratlan, mivel, ha el is osztanánk őket maradékosan, a maradék se 0, se 1 nem lenne).
Egész számot kérünk be a felhasználótól, de ha mégis racionális számot ad meg (pl. -4.33), akkor azt a program átkonvertálja egész számmá (mivel egy int típusú változóba kérjük be az értéket a felhasználótól). Ezt akár le is ellenőrízhetjük, ha kiíratjuk a bekért értéket is.

A maradék megállapításához a % operátort használhatjuk. Például a 7 % 3 kifejezés eredménye 1, mivel a 7-ben 2-szer van meg a 3, és 1 a maradék. Másképp fogalmazva 3*2 + 1 = 7.
A % operátor operandusai csak egész szám típusú (int, vagy ahhoz hasonló: pl. short int, unsigned long int... stb.) értékek lehetnek. Ha lebegőpontos szám típusú (float, double, long double) változókkal próbáljuk használni, a program nem fog lefordulni.

Két érték egyenlőségének a vizsgálatára az == operátort használhatjuk (nem csak számok esetén). Az így kapott logikai kifejezés igaz lesz, ha az == operátor bal, és jobb oldalán szereplő kifejezések eredménye/értéke megegyezik.
Pl. b == 0 logikai kifejezés jelentése: b változó értéke egyenlő-e 0-val.

Ügyeljünk arra, hogyha esetleg két különböző típusú, de egyenértékű dolgot hasonlítunk össze, akkor az összehasonlítás eredménye true (igaz) érték lesz. Pl. 0 == '\0' vagy 0 == NULL vagy 0 == nullptr kifejezések értéke true.

#include <iostream>
using namespace std;

int main () {

	int b;
	cout << "Kerem adjon meg egy (nem tul sok szamjegybol allo) egesz szamot" << endl;
	cin >> b;

	cout << "A megadott szam (" << b << ") ";

	if (b % 2 == 0) { //b valtozo erteket kettovel elosztva 0 maradekot kapunk-e
		cout << "paros" << endl;
	} else {
		cout << "paratlan" << endl;
	}
	
    return 0;
}

Ha esetleg a b % 2 == 0 logikai kifejezés ellentétét szeretnénk megfogalmazni, ne felejtsük el, hogy az nem a b % 2 == 1, hiszen a negatív páratlan számok 2-vel osztva -1-et adnak maradékul. Ekkor tehát azt kell vizsgálnunk, hogy a b % 2 == 1, illetve a b % 2 == -1 logikai kifejezések közül igaz lesz-e az egyik. Ezt az || (or) operátorral tehetjük meg. Tehát a b % 2 == 0 logikai kifejezés ellentettje: b % 2 == 1 || b % 2 == -1
Az || operátor true (igaz) értéket ad eredményül, ha a bal és jobb oldalán álló logikai kifejezések közül legalább az egyik értéke true.
Jelen kifejezés esetén azért egyenértékű a két operátor, mivel biztos, hogy b értéke nem lehet egyszerre 1 és -1 is, így a két logikai kifejezés közül biztos, hogy legfeljebb csak az egyik lesz igaz.

(Ha az || operátor bal oldalán lévő logikai kifejezés értéke igaz, akkor a program nem fogja megvizsgálni a másik kifejezés értékét, hiszen (ha legalább az egyik logikai kifejezés értéke igaz, akkor) már tudható, hogy az || operátorral képezett logikai kifejezés is igaz lesz. Ezt mohó (greedy) kiértékelésnek nevezzük. Vannak olyan programnyelvek is, amik biztonsági okokból akkor is kiértékelnek minden kifejezést, ha azok nem szükségesek a végeredmény eldöntéséhez (pl. ADA)).

Egy logikai kifejezés ellentettjének magadásához nem minden esetben muszáj megfogalmazni azt. Az egyenlőségvizsgálat ellentettjét megadhatjuk a != (nem egyenlő) operátorral is. Tehát például a b % 2 == 0 logikai kifejezés elleltéte megadható így is: b % 2 != 0.

Van lehetőség továbbá az egész feltétel tagadására is, ha eléírunk egy ! (negáló/tagadó) operátort, de ha azt akarjuk, hogy a negálás legyen érvényes az egész feltételre, akkor ne felejtsük el zárójelezni az egész logikai kifejezést. Például:

if ( !(b % 2 == 0) ) {
	cout << "paratlan" << endl;
} else {
	cout << "paros" << endl;
}

Mindenesetre bármilyen módon is fogalmazunk meg feltételeket, logikai kifejezéseket, fordítsunk rá különös figyelmet, gondoljuk át, hogy egy elágazás egyes ágainak milyen eseteket kell lefednie, mert ebben könnyű hibázni.

3. példa: egy karakter "típusa"

(A karakter típusa jelen esetben természetesen nem programozásbeli értelemben értendő (mint például a változók típusa), hanem hétköznapi értelemben, vagyis: betű, szám, írásjel vagy speciális karakter).

Egy elágazásnak természetesen nem csak két ága lehet, hanem több is. Például ha egymásba ágyazzuk az egyes if-else elágazásokat (if-elseif-elseif-...-else).

Fontos megjegyezni, hogy például egy if-elseif-else és két egymás után elhelyezett if-else eltérő működést eredményez, hiszen egy if-elseif-else esetén csak egy blokk utasításai fognak lefutni, két if-else esetén pedig két blokk utasításai is. Arra is ügyelni kell persze, hogy egy if-elseif-else típusú elágazásoknál jól fogal

struktogram_elagazas_sokiranyu.png

A következő példában bekérünk a felhasználótól egy karaktert és kiíratjuk annak a típusát (betű, szám, írásjel vagy speciális karakter), valamint mellékesen az ASCII kódját is. 
Az előző rész kiegészítő információi közt már esett szó arról, hogy egy char típusú változóban valójában az adott karakter ASCII kódjának megfelelő egész számot tároljuk, és amikor cout-tal kiíratjuk az értékét, akkor az ASCII kódnak megfelelő karakter íródik ki. Az ASCII táblában megtekinthetjük, hogy milyen ASCII kódhoz milyen karakter tartozik:

ascii_table.gif

Ha egy karakter értékének bekérésekor a felhasználó például b karaktert ad meg, akkor az adott char típusú változó értéke 98 lesz, mert a b karaktert ASCII kódja 98.

char c;
cin >> c; //pl. ha a felhasználó b-t ad meg, c értéke 98 lesz

A példaprogramban azt fogjuk vizsgálni, hogy a felhasználó által megadott karakterérték ASCII kódja milyen tartományba esik. Ha megnézzük az ASCII táblát, akkor láthatjuk, hogy például a betűk 65 és 90, valamint 97 és 122 között találhatók, a számok pedig 48 és 57 között.

Azért van szükség többágú elágazásra ebben a példában, mert amikor a bekért karakter típusát határozzuk meg, nem csak két eset van, hanem több (betű, szám, írásjel vagy speciális karakter). Bár az írásjeleket és a speciális karaktereket is megkülönböztethetnénk egymástól, kizárólag a program egyszerűsítése céljából nem döntöttem emellett, hiszen az írásjelek kicsit szétszórtabban helyezkednek el az ASCII táblában.

Fontos: amikor azt vizsgáljuk, hogy egy valamilyen szám típusú változó értéke két érték közé esik-e, akkor a logikai kifejezést a programkódban NEM így kell megfogalmazni: 47 < b < 58.
(Ekkor ugyanis az történne, hogy a program először kiértékelné a 47 < b részkifejezést, ennek az eredménye true (igaz), vagy false (hamis) lenne , a következő részkifejezésnél pedig az adott logikai érték számmá konvertálódna (a true 1-é, a false pedig 0-vá), és ezen számértékek egyikét hasonlítaná össze a program az 58-al.
Tehát ha például b változó értéke 58, vagy annál nagyobb szám lenne, a 47 < b < 58 logikai kifejezés akkor is igaz lenne, hiszen a program először azt vizsgálná, hogy a b változó értéke nagyobb-e mint 47, ez igazat adna eredményül, ez az igaz érték átkonvertálódna 1-é, majd az 1 lenne összehasonlítva az 58-al, vagyis a program valójában ezt vizsgálná: 1 < 58.
Hasonlóképp, ha b változó értéke kisebb lenne, mint 47, akkor a 47 < b < 58 logikai kifejezés szintén igazat adna eredményül. Aki szeretné, végiggondolhatja, hogy miért, de a lényeg az, hogyC++ programkódban ezt a logikai kifejezést máshogy kell megfogalmazni, hiszen láthattuk, hogy így megfogalmazva a programunk hibás működést fog eredményezni
).

Ebben a kifejezésben (47 < b < 58) valójában két dolgot vizsgálunk, mely két dolognak egyaránt igaznak kell lennie ahhoz, hogy a kifejezés végeredménye igaz legyen. Erre az esetre az && (and) operátort használhatjuk. A helyes logikai kifejezés így néz ki: 47 < b && b < 58
(Hasonlóan mint az || (or) operátornál, az && operátor esetén a program csak az első részkifejezést értékeli ki ha annak az eredménye hamis, hiszen abból már tudható, hogy a végeredmény is hamis lesz (mivel ahhoz, hogy a végeredmény igaz legyen, minden egyes && operátorral összekapcsolt részkifejezésnek igaznak kell lennie).

Amikor a többágú elágazás egyes ágaira vonatkozó logikai kifejezéseket fogalmazzuk meg, arra is lesz példa, hogy két darab && operátoros logikai kifejezést kell összekapcsolni egy || operátorral. Ez konkrétan a betűk esetén fog előfordulni, hiszen a betűk két különböző tartományban is lehetnek, és ugyebár az egyes tartományok vizsgálatához egy && operátorral képzett logikai kifejezést használunk (pl. az egyik tartomány vizsgálatához használható logikai kifejezés: 64 < b && b <91), azt pedig, hogy a vizsgált érték (itt most a b változó értéke) vagy az egyik, vagy pedig a másik tartományba tartozik-e, úgy tudjuk vizsgálni, ha az egyes tartományok vizsgálatához használható logikai kifejezéseket egy || operátorral kapcsoljuk össze.
Az || logikai operátorról már esett szó korábban. A segítségével képezett logikai kifejezés eredménye igaz lesz, ha az egyik, vagy másik oldalán lévő logikai kifejezések közül legalább az egyik igaz. Jelen példaprogramnál nyilván nem lesz olyan eset, amikor mindkettő igaz lesz, hiszen a betűk vagy 65-től 90-ig helyezkednek el, vagy 97-től 122-ig, de az nem lehetséges, hogy a vizsgált változó számértéke egyszerre essen 64 és 90, valamint 97 és 122 közé (ezért itt is egyenértékű az || (or) operátor használata a ^ (xor) operátor használatával).
Az így kapott kifejezés tehát:
( ( 64 < b && b < 91 ) || ( 96 < b && b < 123 ) )
Bár jelen példa esetén nem okoz hibát ha nem zárójelezzük külön-külön az && operátoros kifejezéseket, de azért legyen megemlítve, hogy logikai kifejezések esetén különösen figyeljünk a zárójelezésre.

Egy apró megjegyzés: a példaprogramban rövidítése céljából például 48 <= b helyett 47 < b-t írok, mivel egész számok esetén ez ugyanazt jelenti.

Íme a teljes példaprogram:

#include <iostream>
using namespace std;

int main () {
	char karakter;
	cout << "Kerem adjon meg egy tetszoleges karaktert: " << endl;
	cin >> karakter;
	int ascii = karakter;

	cout << "A megadott karakter (" << karakter << ") ASCII kodja: " << ascii << ", tipusa: ";

	if ( 47 < ascii && ascii < 58) {
		cout << "szam" << endl;
	} else if ( ( 64 < ascii && ascii < 91 ) || ( 96 < ascii && ascii < 123 ) ) {
		cout << "betu" << endl;
	} else {
		cout << "irasjel vagy specialis karakter" << endl;
	}
	return 0;
}

Az ékezetes karaktereket ez az egyszerű program nyilván nem ismeri fel, az előző fejezetben már említésre került, hogy char változóban ékezetes karakterek tárolása nem kezdőknek szóló téma, így itt most nem lesz róla szó.

4. példa: kisbetű esetén nagybetűsítés és fordítva

Az előző példa alapján könnyen írhatunk egy olyan programot, ami kiírja a felhasználó által megadott karaktert, de betűk esetén kisbetűsít, illetve nagybetűsít, attól függően, hogy kis- vagy nagybetűt adott meg a felhasználó.
Az ASCII táblában megfigyelhetjük, hogy ha egy kisbetűs karakter ascii kódjából levonunk 32-t, akkor megkapjuk a neki megfelelő nagybetű ascii kódját, valamint ugyanez fordítva is érvényes (ha egy nagybetű ascii kódjához 32-t hozzáadunk, megkapjuk a neki megfelelő kisbetű ascii kódját).

Tegyük fel, hogy ennél a bekérésnél a felhasználó egy kisbetűt (pl. b) ad értékül:

cin >> karakter;

Ha a cout << karakter - 32; utasítást használjuk, akkor a program nem a nagybetűsített karaktert fogja kiírtni, hanem annak az ASCII kódját. Ha magát a karaktert szeretnénk kiíratni, akkor használjuk a static_cast<char>( karakter - 32) kifejezést, vagy pedig külön utasításban végezzük el a karakter - 32 műveletet, majd a coutnak a char típusú változó nevét (jelen esetben karakter) adjuk csak paraméterül.

#include <iostream>
using namespace std;

int main () {
	char karakter;
	cout << "Kerem adjon meg egy tetszoleges karaktert: " << endl;
	cin >> karakter;

	if (64 < karakter && karakter < 91) {
		cout << "A megadott ertek kisbetusitve: " 
		<< static_cast<char>(karakter += 32);
	} else if (96 < karakter && karakter < 123) {
		cout << "A megadott ertek nagybetusitve: "
		<< static_cast<char>(karakter -= 32);
	} else {
		cout << "A megadott ertek: " << karakter;
	}

	return 0;
}

5. példa: nagyon egyszerű számológép

A többágú elágazások egy speciális változata a switch-case (esetkiválasztásos szelekció), amit csak akkor használhatunk, ha egy változó értékét vizsgáljuk, és a változó értékétől függően szeretnénk további utasításokat megadni. 
Ez tulajdonképpen helyettesíthető egymásba ágyazott if-else-if... elágazásokkal, amikben minden esetben ugyanarra a változóra vonatkozó egyenlőségvizsgálat van feltételnek megadva (pl. x == 1, x == -3, satöbbi). A switch esetén az egyenlőségvizsgálat nem helyettesíthető más logikai kifejezéssel, továbbá csak egész szám típusú változók (pl. int, char, short int) értékeit vizsgálhatjuk switch segítségével. Tehát ha például float vagy string típusú változók értékét próbáljuk meg switchhel vizsgálni, akkor a program nem fog lefordulni.

Egy szemléltető kódrészlet a switch-case típusú elágazás működéséről:

switch (vizsgalt_valtozo_neve) {
case 1:
	//az ide írt utasítások hajtódnak végre, ha a vizsgált változó értéke 1
	break;
case 2:
	//az ide írt utasítások hajtódnak végre, ha a vizsgált változó értéke 2
	break;
case 3:
	//az ide írt utasítások hajtódnak végre, ha a vizsgált változó értéke 3
	break;
default:
	/*az ide írt utasítások hajtódnak végre, ha a vizsgált változó értéke a fenti értékek közül egyikkel sem egyezik*/
}

Ha a vizsgált változó char típusú, akkor az egyes vizsgálandó értékeket aposztrófok között kell megadni (lásd példaprogram).

Fontos: ha az egyik ágban (kivéve a default ágat) elhelyezett utasítások végére nem tesszük ki a break; utasítást, akkor azon ág utasításainak végrehajtásakor az egyel alatta lévő ág utasításai is végre fognak hajtódni. Ez akár hasznos is lehet bizonyos esetekben, de erre mindig figyeljünk, ha switch-case típusú elágazást használunk.

Jelen példában bekérünk a felhasználótól két számot (egy-egy float típusú változóba), illetve egy műveleti jelet (egy char típusú változóba) a két szám bekérése között.
A felhasználó az értékeket akár egy sorban is megadhatja szóközök használatával, vagy azok nélkül (pl. 4.2 / 2.1, 8 % 3, 1 / 0), de természetesen külön sorban is megadhat minden egyes értéket.
Ennek az egyszerű példaprogramnak a lényege nyilván a char típusú változóba bekért értéken múlik. Ha például a felhasználó +-t adott meg (a char típusú változó értékének), akkor a két szám összeadásra kerül.
Érdekességképp a megadható műveleti jelek között szerepel a % (egészosztási maradék meghatározása) is, mely esetén nyilvánvalóan át kell konvertálnunk a float típusú változók értékét egész számokká, hiszen a maradékos osztás csak egész számok esetén van értelmezve.

Nullával való osztás, vagy maradékos osztás esetén kilépünk a programból a return 0; utasítás segítségével még az elágazás végrehajtása előtt. Ehelyett azt is megtehetnénk, hogy az elágazáson belül a case '/' és a case '%' ágakban egyaránt elhelyeznénk a következő kódot (még a művelet elvégzése előtt):

if (szam2 == 0) {
	cout << "hibauzenet" << endl;
	return 0;
}

Érdemes azért megjegyezni, hogy a return 0; csak a main függvényen belül eredményezi a programból való kilépést, ha ezek az utasítások más függvények blokkjában lennének, ott a return 0; utasítás csak annyit jelentene, hogy az adott függvény az őt meghívó utasításnak adjon vissza egy 0 értékű számot.
Mivel saját függvényeket még nem definiáltunk, így abba most még nem megyünk bele, hogy azok blokkján belül hogyan lehetne ezt megoldani.

A példaprogram kódja:

#include <iostream>
using namespace std;

int main () {
	float szam1, szam2;
	char muveletijel;

	cout << "Kerem adjon meg egy szamot, egy muleveleti jelet, majd egy masik szamot" << endl;
	cin >> szam1 >> muveletijel >> szam2;
	cout << "Az eredmeny: ";

	if (szam2 == 0 && ( muveletijel == '/' || muveletijel == '%' ) ) {
		cout << "a 0 nem lehet semmilyen szam osztoja, hiszen a 0-nak minden tobbszorose 0 (nincs olyan szam, amivel ha megszorozzuk a 0-t, nem 0-t kapunk)." << endl;
		return 0;
	}

	switch (muveletijel) {
	case '+':
		cout << szam1 + szam2 << endl;
		break;
	case '-':
		cout << szam1 - szam2 << endl;
		break;
	case '*':
		cout << szam1 * szam2 << endl;
		break;
	case '/':
		cout << szam1 / szam2 << endl;
		break;
	case '%':
		cout << static_cast<int> (szam1) % static_cast<int> (szam2);
		break;
	default:
		cout << "ismeretlen muveleti jel, vagy hibas adatok." << endl;
	}
	return 0;
}

Tipp: kipróbálhatjuk mi történik, ha töröljük, vagy kikommenteljük a 0-val való osztásra vonatkozó kilépést a program forráskódjából, és a program futtatásakor osztást admunk meg műveleti jelként, a második számnak pedig 0 értéket.

Ternáris operátor

Egy elágazást tömörebben leírhatunk a ? : ternáris operátor segítségével. A ternáris jelentése: három operandusú. (Két operandusú (bináris) operátor például az összeadás (+), értékadás (=), vagy az egyenlőségvizsgálat (==) hiszen ezen operátorok két oldalán két kifejezés állhat, illetve egy operandusú (unáris) például a - vagy a ++ operátor).
A ternáris operátor első operandusa a feltétel (általában logikai kifejezés, vagy logikai változó) a kérdőjel előtt, másik két operandusa pedig az elágazás két ága (az igaz és a hamis ág), a kettőspont előtt és után.

A ternáris operátor használatának szemléltetése:

/*feltétel*/ ? /*utasítás vagy kifejezés, ha a feltétel igaz*/ : /*utasítás vagy kifejezés, ha a feltétel hamis*/

Lényeges különbség az ifhez képest, hogy a ternáris operátor egyes ágaiban nem csak utasítások, hanem kifejezések is állhatnak. (Nyilván ha a ternáris operátor egyik ágában kifejezés áll, akkor a másik ágában is kifejezés kell hogy álljon, és ez ugyanígy igaz utasítás esetén is). Ha a ternáris operátor egyes ágaiba kifejezéseket írunk, akkor magát a ternáris operátoros kifejezést például értékül adhatjuk egy változónak (a változó értéke pedig a feltételtől függően az egyik ágban, vagy a másik ágban lévő kifejezés értéke lesz), vagy kiírathatjuk cout-tal (ekkor a feltételnek megfelelő ágban lévő kifejezés értékét írja ki a cout). Mindkettőre nézünk példát.

A 2. példaprogram ternáris operátorral:

#include <iostream>
using namespace std;

int main () {
	int b;
	cout << "Kerem adjon meg egy (nem tul sok szamjegybol allo) egesz szamot" << endl;
	cin >> b;
	cout << "A megadott szam (" << b << ") ";
	cout << (b % 2 == 0 ? "paros" : "paratlan") << endl;

	return 0;
}

A ternáris operátort tartalmazó utasítást persze így is megfogalmazhattuk volna:

a % 2 == 0 ? cout << "paros" << endl : cout << "paratlan" << endl;

..., de a példaprogramban szereplő módszer, miszerint a ternáris operátor egy-egy ága nem utasítást tartalmaz, hanem kifejezést, ezen példa esetében rövidebbnek bizonyult.

A ternáris operátor használata esetén nem árt odafigyelni a zárójelezésre, különben összetett kifejezések esetén könnyen adódhatnak hibák. Ha például a fenti példában elhagyjuk a ternáris operátoros kifejezést körülvevő zárójeleket, a program le sem fordul.
Az interneten gyakran előforduló példa a #define MAX(a, b) ( (a) > (b) ? (a) : (b) ) utasítás, melyben a prerocesszor segítségével definiálunk egy makrót. Ha lefordítjuk az ezt tartalmazó programot, még a fordítás előtt a preprocesszor a programunkban szereplő összes MAX(a,b) szöveget kicseréli erre: ( (a) > (b) ? (a) : (b) ). Viszont lehet, hogy a példaprogramban valahol szerepel egy összetett kifejezés, aminek a ternáris operátoros kifejezésre cserélendő kifejezés csak egy részkifejezése, például: eredmeny = MAX(a,b) * -1; Ha a definícióban nem lenne ilyen alapos a zárójelezés a ternáris operátoros kifejezésben, akkor ez esetben csak akkor lenne megszorozva -1-el az eredmény, ha a b változó lenne a nagyobb. Ez a hiba csak zárójelezéssel kerülhető el.

6. példaprogram: ternáris operátor zárójelezése

#include <iostream>
#define MAX(a, b) ( (a) > (b) ? (a) : (b) )
using namespace std;

int main () {
	int szam_1, szam_2, eredmeny;
	cout << "Kerem adjon meg ket (nem tul sok szamjegybol allo) szamot: ";
	cin >> szam_1 >> szam_2;

	eredmeny = MAX(szam_1, szam_2) * -1;
	cout << endl << "A nagyobb szam -1-szerese: " << eredmeny;

	return 0;
}

Csak ternáris operátorral lehet megoldani a konstansok értékadásának feltételhez kötését. Például eszerint az utasítás szerint ha a (korábban már definiált) fajlnev (string típusú) változó értéke fajl2.txt, akkor az elvalaszto_karakter értéke kettőspont lesz, egyéb esetben szóköz.

const char elvalaszto_karakter = ( fajlnev == "fajl2.txt" ? ':' : ' ');

Tesztelhetjük a működését, ha ezt a kódrészletet bemásoljuk egy forrásfájl main függvényének blokkjába:

/* Teszt */
std::string fajlnev;
std::cin >> fajlnev;
const char elvalaszto_karakter = ( fajlnev == "fajl2.txt" ? ':' : ' ');
std::cout << elvalaszto_karakter;

Ha fajl2.txt-t adunk meg a bekéréskor, akkor kettőspontot fog kiíni a program.

Nézzük, mindez hogyan nézne ki if segítségével:

/*Hibás kód*/
if (fajlnev == "fajl2.txt") {
	const char elvalaszto_karakter = ':';
} else {
	const char elvalaszto_karakter = ' ';
}

Egy lokális változóra nem hivatkozhatunk azon a blokkon kívül, amiben létrehoztuk.
Egy nem konstans változó esetén meg lehetne tenni, hogy az if blokkján kívül definiáljuk, de az if blokkján belül adunk neki értéket, viszont a konstansok (const típusminősítővel ellátott változók) létrehozásakor meg kell adnunk azok (kezdő)értékét is.

Hibalehetőség

Az == (egyenlőségvizsgálat) operátort ne keverjük az = (értékadás) operátorral, mert akkor a program hibásan fog működni (már ha lefordul).

Egy volt tanárom azt a tanácsot adta, hogy szoktassuk rá magunkat arra, hogy egyenlőségvizsgálat esetén az operátor bal oldalára olyan értéket írjunk, amit nem tudunk értékadással megváltoztatni (pl. const, literál, vagy aritmetikai kifejezés: b + 3), mert így ha véletlenül == helyett =-t írunk, akkor nem fog lefordulni a program (ahelyett, hogy hibás működést eredményezne). Eszerint tehát b == 0 helyett 0 == b kifejezést kéne írnunk.
Ezzel kapcsolatban amondó vagyok, hogy már az is jó, ha tudjuk, hogy van ilyen hibalehetőség, és ha nem is tartjuk be ezt a tanácsot, de ha eszünkbe jut, kevesebb az esélye, hogy hibázzunk. Mellesleg ez a tanács nyilvánvalóan nem érvényes pl. az (a == b) kifejezés esetén.

Kiegészítésképpen megjegyzendő, hogy lvalue-nek nevezzük (l = kis L) azokat a dolgokat, melyek értéke megváltoztatható értékadással (= operátorral), rvalue-nek pedig azokat a dolgokat, melyeknek nem.
Például egy változó lvalue, vagyis szerepelhet az egyenlőségjel bal oldalán, de például egy const, literál vagy aritmetikai kifejezés rvalue, azaz értékadáskor csak az egyenlőségjel jobb oldalán szerepelhet (különben nem fog lefordulni a kifejezést tartalmazó program).
Természetesen egy lvalue szerepelhet az egyenlőségjel jobb oldalán is.

Esetleg érdemes még tudni, hogy minden kifejezésnek van értéke (az értékadásoknak is). Például b = 0-nak 0 az értéke. Ha pedig ezt mondjuk egy if elágazásban a feltétel helyére írjuk, akkor logikai értékké fog konvertálódni. Ekkor ugyanaz érvényes, mint amit már fentebb leírtam. A 0 és a 0-val egyenértékű értékek hamissá konvertálódnak, minden más pedig igazzá.
Gondoljunk csak bele, ha például egy if elágazás feltételeként b == 0-t írunk, akkor ha b változó értéke 0, az igaz ágban lévő utasítások fognak végrehajtódni, ám ha azt írjuk, hogy b = 0, akkor viszont b változó értékétől függetlenül a hamis ágban lévőek (mivel a 0 logikai hamissá konvertálódik). Ugyanakkor ha mondjuk ez lenne a feltétel: b == "szoveg", csak akkor hajtódnának végre az igaz ágban lévő utasítások, ha b változó értéke: "szoveg", viszont ha ehelyett azt írnánk, hogy b = "szoveg", akkor is az igaz ágban lévő utasítások hajtódnának végre, ha b változó értéke nem "szoveg" lenne (mivel a "szoveg" érték logikai igazzá konvertálódna).

Így tehát könnyen belátható, elég egy apró elírás, és a programunk teljesen eltérően működhet, mint ahogy azt szeretnénk (ha egyáltalán lefordul).

KIEGÉSZÍTŐ INFORMÁCIÓK

Egy bool típusú változó értékének kiíratása szövegesen

Ha esetleg bool típusú értékeket szeretnénk cout-tal kiíratni, akkor alapbeállítás szerint false helyett 0, true helyett 1 fog kiíródni. Ha szövegesen szeretnénk kiíratni őket, akkor a boolalpha kifejezést adjuk paraméterül a cout-nak. 

/* Hibás kód: */
std::cout << std::boolalpha << 4.2f == 4.2;

Használjunk zárójelezést:

std::cout << std::boolalpha << (4.2f == 4.2);

Ha vissza akarjuk állítani az alapbeállítás szerinti állapotot, akkor a noboolalpha kifejezést adjuk paraméterül a cout-nak.

std::cout << std::boolalpha << (4.2f == 4.2) << " " << std::noboolalpha << (4.2f == 4.2);

A bool típus méretéről

A logikai (bool) típusú változók számára (általában!) 1 bájt foglalódik le a számítógép memóriájában. Igaz, hogy effektíve csak kétféle értéket tárolunk bennük (melyhez elég lenne 1 bit is), de 1 bájt a legkisebb címezhető egység a számítógép memóriájában.

Ha kiíratjuk a (bool típussal paraméterezett) numeric_limits sablonosztály digits adattagjának értékét, akkor azt az értéket kapjuk, ami az adott típus effektív értékének tárolásához szükséges (bitben megadva).
A sizeof operátorral viszont az adott típus számára lefoglalt tényleges méretet kapjuk meg. 

/* std::numeric_limist sablonosztályhoz: #include <limits> */

std::cout << std::numeric_limits<bool>::digits << " bit" << std::endl; //1 bit
std::cout << sizeof(bool) << " byte" << std::endl; //1 bájt

Egy char típusú változó tárolásához is 1 bájt szükséges. Ha numeric_limits sablonosztály segítségével íratjuk ki a méretét, akkor unsigned char esetén 8 (bit) íródik ki, signed char esetén pedig 7. Ez azért van, mert a signed char esetén 1 bit az előjel tárolásához szükséges, így az 1 bájtból csak 7 bit használható a tényleges érték tárolására.

std::cout << std::numeric_limits<unsigned char>::digits << " bit" << std::endl; //8 bit
std::cout << std::numeric_limits<signed char>::digits << " bit" << std::endl; //7 bit

Egy osztályon vagy egy struktúrán belül bitmezők segítségével megoldható, hogy 8 darab bool típusú érték tárolásához csak 1 bájtnyi memóriát használjunk, de erről ebben a tananyagban nem lesz szó. Aki kíváncsi rá, biztos talál rá példát mondjuk stackoverflowon.

Érdekességképpen elmondható, hogy noha az int a = true; utasítás esetén a változó értéke 1 lesz, false esetén pedig 0, ugyanakkor ha definiálunk egy (lokális) logikai változót, nem adunk neki kezdőértéket és később értéket (tehát a számára lefoglalt memóriaterületen lévő memóriaszemét lesz az értéke), majd ezt követően kiíratjuk az értékét cout-tal, akkor a kiírt érték nem csak 0 vagy 1 lehet, hanem egy 0 és 255 közötti szám.
A következő fejezet tananyagát felhasználva ebben a kis kódrészletben létrehozunk egy 100 elemű, bool típusú tömböt (ami olyan, mintha 100 bool típusú változót hoznánk létre, csak nem kell őket egyessével elnevezni) és (szóközökkel elválasztva) kiíratjuk az értéküket cout-tal, anélkül, hogy kezdőértéket, vagy értéket adtunk volna nekik. Tekintsük meg az eredményt.
Természetesen ebben az esetben is csak a 0 érték jelenti a logikai hamis értéket. Továbbá ismételten megemlítem, amit már az előző részben is megemlítettem, hogy mindig gondoskodjunk arról, hogy a programkódunkban szereplő változók kapjanak kezdőértéket, attól függetlenül, hogy ebben a példaprogramban éppen azt mutatom meg, hogy mi történik, ha nem.

bool b[100];
for (int i = 0; i < 100; i++) {
	cout << b[i] << " ";
}

Előző rész: változók, konstansok, literálok
Következő rész: tömbök, ciklusok

A bejegyzés trackback címe:

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

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.