3. fejezet - Konstansok és változók


Ebben a fejezetben ismertetek néhány alapfogalmat, úgy mint literál, konstans, változó, deklaráció, definíció. Kezdjük is az elsővel, tehát a literállal. Ezek olyan szimbólumok, amelyek értékeiket egy meghatározott halmazból vehetik fel, ilyenek például a változónevek és a konstansok. A különbség, hogy a változó értéke megváltozhat, a konstansé viszont nem. Konstans például egy számjegy, vagy egy string literál.



Tekintsük az alábbi hihetetlenül nehéz és bonyolult C++ kódot. Innentől kezdve ahol nem írom ki külön a main függvényt, gondolatban mindig képzeljétek a kód köré.

CODE
int i = 2;

Ebben a kódrészletben a 2 egy konstans, nevezetesen a kettő. Emberi nyelven úgy mondhatnánk ezt a kódot, hogy "csinálj nekem egy i nevű változót, és legyen az értéke kettő".
A változókat képzeljük el úgy, mint memóriaterületeket amik értéket vehetnek fel. A matematikai változókkal ellentétben az értékük megváltozhat. A változók egyik legfontosabb tulajdonsága, hogy típusuk van, az viszont nem változhat meg. A fontosabb beépített típusok (x86 architektúrán):

Típus Más nyelvekben Méret általában (byte) Leírás
char character, byte 1 8 bites előjeles egész, -128 és +127 között vehet fel értéket. Minden karakternek megfelel egy 0 és 127 közti szám.
short short integer, word 2 16 bites előjeles egész, gépi szónak is szokták hívni.
int integer 2-4 Legalább 16 bites előjeles egész, általában 32 bit.
long long integer, dword 4-8 Legalább 32 bites előjeles egész, x86 processzorokon többnyire ez is 32 bit.
long long quad word 8 64 bites rendszereken 64 bites egész. Máshol fordítófüggö.
float float 4 Egyszeres pontosságú lebegőpontos szám. A tizedesvesszőt ponttal kell megadni, a literál végére egy f betűt kell írni. Például 0.01f vagy 1e-2f
double real 8 Kétszeres pontosságú lebegőpontos szám, itt nem kell az f betűt kiírni.
bool boolean, logic 1-4 Logikai érték, ahol a nulla hamis-t jelöl, a nem nulla igazat. Két előre definiált értéke van, a false és a true

A típusok mérete nincs explicit módon definiálva a szabványban, fordítófüggő lehet, hogy minek mennyi hely foglalódik. A táblázatbeli típusoknak létezik előjel nélküli változata is, kivéve a lebegőpontosakat. Ezeket úgy kapjuk, hogy a típus elé írjuk az unsigned kulcsszót. Hasonlóan létezik signed kulcsszó is, például konverzióknál használatos. Az előjel nélküli számok 0 és (2^bitek száma) - 1 között vehetnek fel értéket, vagyis például egy unsigned char 0 és 255 között. A char értelmezése viszont szintén fordítófüggő: valamelyik eleve unsigned -ként értelmezi, akkor viszont ha előjelessé akarjuk tenni ki kell írni a signed kulcsszót.
A short és a long is használhatók bizonyos esetekben módosítószavakként, például a long double egy 12 bájtos valós lesz. A jelenlegi szabványban még nem, de az újban már szerepel a long long, ami egy legalább 64 bites számot jelöl.

Azt amikor megmondjuk egy változó típusát és nevét, deklarációnak hívjuk (pl. extern int i;). Amikor a változónak "memória foglalódik" az a definíció (pl. int i;). Egy változót tetszőlegesen sokszor lehet deklarálni, de pontosan egyszer lehet csak definiálni. Nem kötelező, de adhatunk kezdőértéket is a változónak, azaz inicializáljuk. Ha nem adunk neki, akkor vagy memóriaszemét lesz benne, vagy egyes fordítók esetében 0. Ha több ugyanolyan típusú változót szeretnénk deklarálni/definiálni, akkor azok neveit vesszővel kell elválasztani. A következő példában deklaráltam és egyben definiáltam is egy valós számot, és inicializáltam azzal, hogy 3.2f + 2.1f.

CODE
float i = 3.2f + 2.1f; std::cout << i << "\n"; // output: 5.3

Egy adott nevű változót pontosan egyszer lehet csak definiálni egy adott környezetben (például egy függvény törzsében). A környezeteket másképpen scope-oknak hívják. A deklarálatlan vagy többször definiált változókra hibát ad a fordító. A C++ erősen típusos nyelv, tehát minden változó, függvény stb. típusának fordítási időben ismertnek kell lennie. Ezenfelül nem adható értékül például a kisbusz a rénszarvasnak. Amennyiben viszont létezik automatikus konverzió két típus között, akkor ezt elvégzi a fordító, de warningot fog dobni.

CODE
unsigned int a = -1; // warning: converting of negative value '-0x000000001' to 'unsigned int' int b = 0.005f; // warning: converting to 'int' from 'float' double b; // fordítási hiba, b-t már deklaráltuk // (error: conflicting declaration 'double b') c = 6; // fordítási hiba: c nincs deklarálva // (error: 'c' was not declared in this scope)

C++ -ban bárhol állhat deklaráció/definíció ahol utasítás, szemben a C-vel ahol az első utasítás előtt minden változót deklarálni kell (azaz a függvény elején). Ha egy változót a main -en kívül deklarálunk, az globális elérésű lesz, tehát az adott fordítási egységen (a forrásfájlon) belül bárhol látható lesz. Más fordítási egységekből az extern kulcsszóval lehet elérni globális változókat.

A fenti példákban a deklaráció és a definíció egybeesik, de vannak esetek amikor nem. Nem kell messzire menni, függvényeket is lehet előre deklarálni: megadjuk a fejlécüket, például void foo(int a);. A függvényt majd akkor fogjuk defininálni, amikor megadjuk az implementációt is {} -ek között.


A szimbólumok között megtalálhatók a műveleti jelek is, amiket operátoroknak is lehetne nevezni, de ez utóbbi egy tágabb fogalom: vannak olyan operátorok is amik nem egyetlen jelből, hanem jelsorozatból állnak, például az operator new.
Az operátorokat is több csoportba lehet sorolni, mint az aritmetikai műveletek (+, -, *, /, %, <, >, <=, >=, ==, !=), logikai műveletek (&&, ||, ^^, !) és bitenkénti műveletek (&, |, !, ^, <<, >>), értékadó operátorok (=, +=, *=, stb.) Nézzünk meg ezek közül néhányat egy példán keresztül.

CODE
int a = 1; int b = 3; std::cout << a + b << std::endl; // összeadás std::cout << a * b << std::endl; // szorzás std::cout << a % b << std::endl; // osztási maradék std::cout << (a << b) << std::endl; // biteltolás; vajon miért tettem zárójelbe? std::cout << (b << a) << std::endl; // nem kommutatív std::cout << (a | b) << std::endl; // bitenkénti vagy; na és ezt miért tettem zárójelbe? std::cout << (a ^ b) << std::endl; // bitenkénti kizáró vagy // output: 4 3 1 8 6 3 2

Gondolom feltűnt, hogy most ravasz módon az std::endl -t használtam a sorvégejel helyett, csakis azért, hogy mindenkit összezavarjak (igazából azért, hogy ne kelljen annyit színezni). Aki összezavarodva érzi magát futtassa le a kódot és vegye észre, hogy a két dolog ugyanaz (nem igaz, az std::endl üríti az output buffert).

Nyilván a fenti dolog konstansokkal is működik. A zárójelezések kiértékelése balról jobbra történik, ahogy azt papíron is csinálnánk. A * / % műveleteknek itt is nagyobb a precedenciája, mint a + - -nak, azaz a szorzás előbb értékelődik ki mint az összeadás. A többi precedenciát meg már én se tudom megjegyezni, ezért inkább kiteszem mindig a zárójeleket.
Kicsit kilóg a sorból az értékadás. Például azért mert nem szimmetrikus. Amikor azt írjuk le, hogy b = a;, akkor b-nek értékül adjuk a -t, de fordítva nem. Ezt úgy olvassuk ki, hogy "b legyen egyenlő a". Sőt, vannak nem kommutatív operátorok is, például a fenti példában a <<, azaz a biteltolás.

Szemfüles olvasóknak feltűnhet, hogy a cout objektumra is a << operátort hívtam meg. Ezt azért lehet megtenni mert C++ -ban az operátorok túlterhelhetőek, teljesen új értelmezés adható nekik. Az ostream például (ilyen típusú objektum a cout) azt az értelmezést adja neki, hogy írjon ki valamit.

Itt most jön egy fontos fogalom, ezért is írom új bekezdésbe. Mostantól mindent ami értékadás bal oldalán állhat, balértéknek fogok hívni. Azt pedig ami jobb oldalon állhat, de bal oldalon nem, jobbértéknek. Például a konstans szimbólumok jobbértékek, a változók balértékek (a konstans változók is, ld. később).

A +=, *= stb. operátorok kényelmi célokat szolgálnak, a = a + b; helyett írhatom azt, hogy a += b;. Nézzünk még egy példát, ebben már új dolgok is lesznek, például elágazás amiről a következő fejezetben lesz szó.

CODE
int a = 1; int b = 3; a += b; // ehelyett irhatnám azt is, hogy a = a + b; if( a > b ) std::cout << a << " nagyobb mint " << b << "\n"; else std::cout << b << " nagyobb mint " << a << "\n"; // output: 4 nagyobb mint 3

Egy változót módosíthatatlanná tehetünk a const kulcsszóval. Ez azt jelenti, hogy deklarációkor kaphat értéket, többször nem (nem igaz, majd látunk példát). Ellentéte a mutable (osztályoknál). Később ezek hasznosak lesznek, majd a referenciáknál és pointereknél mutatok néhány érdekes trükköt. Nem keverendők össze a fejezet elején emlegetett konstansokkal.

CODE
const int a = 1; // konstansnak deklaráltuk a = 2; // forditási hiba: 'a' konstans // (error: 'a' : you cannot assign to a variable that is const)

Itt jegyzem meg, hogy amikor deklarációkor incializálunk egy változót, akkor az nem értékadás, ez is majd az osztályoknál lesz érthető, hogy miért nem.


Említettem, hogy a C++ erősen típusos nyelv és például a teherautót nem lehet értékül adni a rénszarvasnak (lehet, de akkor meg kell írni az értékadó operátort). Mondtam ilyeneket is, hogy ha létezik automatikus konverzió két változótípus között (például float és int), akkor ezt a fordító elvégzi. Ezt úgy hívjuk, hogy implicit típuskonverzió. Mi ezzel a baj? Tekintsük az alábbi már teljesen high-end és érthetetlen C++ kódot:

CODE
int i; i = 3.0f / 4.0f; // warning: '=' : conversion from 'float' to 'int', // possible loss of data std::cout << i << std::endl; // output: 0

Mi történt itt? Miért lett ebből 0? Ha megnézzük, az értékadás bal oldalán egy int áll, a jobb oldalon viszont két valós szám osztásának eredménye, ami szintén valós szám. A fordító fogja, és implicit módon átkonvertálja integerré, plusz még szól is nekünk, hogy hát itt adatvesztés lehet. Mi történne, ha nem valós osztást csinálnék, hanem egész osztást, vagyis 3 / 4 -et írnék? Persze akkor is 0-t kapnék, de akkor már nem az értékadás miatt, hanem mert egész számokat osztok. És ha az egyik valós, a másik meg egész? Akkor mi lesz? Ilyen esetekben a valós számok előnyt élveznek, tehát az osztás eredménye is valós lesz.

A második fejezetben mondtam, hogy nem szeretem a warningokat sem, biztos van valami módszer értelmes konverzióra. Van, ezt hívjuk explicit típuskonverziónak. C -ben fölöttéb egyszerű módon annyiból áll, hogy a jobb oldal elé írjuk zárójelben a bal oldal típusát. Ez a közvetlenül utána álló részkifejezésre lesz érvényes, tehát megfelelően kell zárójelezni. Meglepő módon C-style cast -nak hívják.

A C++ viszont bevezetett konverziós operátorokat is és így látunk végre példát olyan operátorra ami jelsorozatból áll. Ebből több is van, de ide most a static_cast kell. A szintaxisa meglehetősen furcsa, későbbi fejezetekben majd megtudjátok miért ilyen.

CODE
int i; //i = (int)(3.0f / 4.0f); // vigyázni kell, hogy melyik részkifejezésre vonatkozik i = static_cast<int>(3.0f / 4.0f); // ez már egy 'függvény', azért kell a zárójel std::cout << i << std::endl; // output: ugyanúgy 0, de nem kapunk warningot

Több hasonló operátor is van, amik most nem érdekesek, de amit a leginkább el kell kerülni az a const_cast. Ez ugyanis le tudja szedni a konstansságot egy változóról, de egyben veszélyes is, hiszen senki nem mondta, hogy pl. egy const int-nek foglalódik egyáltalán memóriaterület!!


Ebben a fejezetben megismerkedtünk a konstansokkal, változókkal, műveletekkel és konverziókkal. A fejezet végén nézzünk egy egyszerű kis programot, ami a standard inputról bekér két számot és kiírja az összegüket.

CODE
#include <iostream> int main() { int i, j; std::cout << "Ird be az elso szamot: "; std::cin >> i; std::cout << "Ird be a masodik szamot: "; std::cin >> j; std::cout << "A ket szam osszege: " << i + j << "\n"; return 0; }

Gondolom kitaláltátok, hogy a beolvasást a cin objektummal lehet elvégezni. Mielőtt valaki egerekre kezdene gondolni, az 'in' az az input rövidítése akar lenni. Nézzük meg mi történik, ha nem egész számot, hanem mondjuk egy valósat írunk be. Igen meglepő dolgot tapasztalunk: az első számot konvertálja integerre, a másodikat meg be se kéri (és a memóriaszemét marad benne). Ezen segíthetünk úgy, hogy a változókat float -nak deklaráljuk és akkor egy másik operator >> hívódik meg, konkrétan az ami valós számokat olvas be. Az integeres erre nincs felkészítve és marhaságot csinál.

További érdekesség, hogy ha az első számnak két számot írok be, szóközzel elválasztva, mondjuk 5 6, akkor a másodikat be se kell írnom, mégis jó eredményt kapok. Ez utóbbinak az az oka, hogy a cin az input karaktersorozatot felbontja részekre a szóköz karakterek mentén, majd ezeket eltárolja egy bufferben. Így nem kell annyit olvasnia és összességében gyorsabb lesz.


Höfö:
  • Írjál programokat változókkal és műveletekkel! Használd a standard inputot is!
  • Lehetőleg működjön :)

Irodalomjegyzék

http://en.cppreference.com/w/cpp
http://www.cplusplus.com/
http://www.parashift.com/c++-faq-lite/ (ezt különösen ajánlott elolvasni)
http://aszt.inf.elte.hu/~gsd/halado_cpp/ (ezt is)

back to homepage

Valid HTML 4.01 Transitional Valid CSS!