Adatok, műveletek és vezérlés Számábrázolás, alaptípusok. Típuskonstrukciók. Operátorok, kifejezések kiértékelése. Utasítások, vezérlési szerkezetek, rekurzió, kivételkezelés. Adatabsztrakció. Osztály, öröklődés, statikus és dinamikus kötés, altípusos polimorfizmus. Generikusok. Számábrázolás, alaptípusok. Logikai típusok ábrázolása: programnyelvenként eltérő. Példák: C++ : 0 (8 darab 0 bit) = false, minden más true Java : külön típus (Boolean) FALSE és TRUE értékekkel (nem konvertálható int-té!) C# : külön típus (System.Boolean), amely konvertálható int-té (de nem implicit, mint C++ esetén) (Előjeles) egész számok ábrázolása: úgynevezett kettes komplemens alakban történik. A kettes komplemens alak előállítása: vesszük a szám kettes számrendszerbeli reprezentációját, eléje írunk egy 0-t. Második lépésben a számsorozatot negáljuk (egyes komplemens), majd utolsó lépésben a negált számhoz hozzáadunk 1- et. A számítógépekben (nagyon ritka kivételektől eltekintve) csak egész byte-on tudunk tárolni, ennek következtében egy byte-on előjel nélkül [0, 255] közötti egészek tárolhatóak, előjelesen pedig [-128, +127] intervallumba esők (hiszen az első bit jelzi az előjelet: 0 esetén +, 1 esetén, és ezután marad 7 bitünk a számra). Figyeljük meg, hogy az intervallumok egymásnak kölcsönösen megfeleltethetőek! Megállapíthatjuk, hogy az alábbi egyenlőségek igazak, ha n a vett byte-ok száma: előjel nélküli esetben az ábrázolt számtartomány: [0, 2 n -1] előjeles esetben az ábrázolt számtartomány: [-2 n-1, +2 n-1-1] Törtszámok ábrázolása: úgynevezett fixpontos vagy lebegőpontos alakban történik. Fixpontos esetben a szám felépítése: 1 bit előjel n bit egészrész m bit törtrész Lebegőpontos esetben: kicsit bonyolultabb a felépítés. A számot normalizált alakban tároljuk (±M 2 ±E, M = mantissza, E = exponens/kitevő). Ennek lépései: A mantisszát törtekre vagy egészekre normalizáljuk (értéke legyen (0.5, 1) közötti vagy (1, 2) közötti). Az első bit mindenképpen 1. Ezért őt elhagyhatjuk (ún. implicit bit, számolás elött őt vissza kell tennünk!) Legyen a normalizált, implicit bitet elhagyott mantissza m. Karakterisztika eltolása (offszet karakterisztika): az exponenshez hozzáadunk egy d = 2 k-1 vagy d = 2 k-1-1 alakú számot, hogy mindenképp pozitív legyen a kitevő (c:= E + d). Legyen s 0 vagy 1. 0 ha a szám pozitív, 1 ha negatív. Ekkor a lebegőpontosan ábrázolt szám: N = (-1) s m 2 c-d Alaptípusok: nyelvenként különbözőek. Példának vegyük a C++, Java, C# nyelveket. Az egész számok mögött zárójelben olvasható, hogy az adott típus hány byte-on tárolódik (C++ esetén a 4/8 azt jelenti, hogy architektúra függő a tárolás (32 vs. 64 bites architektúra)). A törtszámok esetén zárójelben olvashatóak a következők: tároló byte-ok száma intervallum tizedespontosság
Egész számot ábrázoló alaptípusok: C++ char (1), wchar_t (2), short(2), int (4/8), long (4), long long (8) Java byte (1), short (2), int (4), long (8) C# byte (1), short (2), int (4), long (8) Törtszámot ábrázoló alaptípusok: C++ float (4 [3.4E-38, 3.8E+38] 6), double (8 [1.7E-308, 1.7E+308] 15), long double (10 [3.4E-4932, 3.4E+4932] 19) Java float (4 [2-149, (2-2 -23 ) 2 127 ] 7), double (8 [2-1074, (2-2 -52 ) 2 1023 ] 16) C# float (4-3.4 10 38 to +3.4 10 38 7), double (8 ±5.0 10 324 to ±1.7 10 308 15-16), decimal (16 (-7.9 x 10 28 to 7.9 x 10 28 ) / (100 to 28), 28-29) Java és C# esetén van még egy 'char'-nak nevezett primitív típus is (karakterek ábrázolására). (Majdnem) mindegyik primitív típusból van unsigned és signed változat is. Little endian vs. Big endian: Architektúrája válogatja, hogy hogyan tárolja el a számokat. Little endian esetén a legkisebb memória címre kerül a legkisebb helyi értékű byte. Big endian esetén a legkisebb memória címre a legnagyobb helyi értékű byte kerül. (megjegyzés: a little endian fordított az írotthoz képest, amit mi 1024-nek írunk, azt egy little endian architektúra (ha tízes számrendszerben tárolna) 4201-ként tárolná) Típuskonstrukciók. Az alaptípusokon túl (de még az osztályok előtt) van lehetőségünk különböző típuskonstrukciókat használni. Ezek lehetnek iterált, direktszorzat és unió típusúak. A típuskonstrukciók összefoglalása a következő: tömb típus: Egy tömb egy olyan adatszerkezet, amely azonos típusú elemek sorozatait tartalmazza. gyakorlatilag leképezés egy diszkrét intervallum és az adott típus között (a diszkrét intervallum elemeit indexeknek hívjuk) indexek száma megadja a tömb méretét alapművelete az indexelés (az i. elemet gyorsan el tudjuk érni) léteznek programnyelvek, ahol különböző típusokat is tárolhatunk egy tömbben, de általában csak egyfélét direktszorzat típus: több különböző típust kell szinkron kezelnünk. Például: rekord típus (pl.: C++ esetén struct) komponensek a rekord ún. mezői alapművelete a komponenskiválasztás léteznek programnyelvek, ahol nincs ilyen típuskonstrukció, ezekben osztályokat kell használnunk unió típus: például több rekord közös kezelése (Férfiak, Nők külön rekordok, de a Népességet is kezelnünk kell valahogy) a modern programozási nyelvekben kiváltották feladatát az öröklődés és a polimorfizmus, így azok nem is támogatják (pl.: Java, Eiffel)
Operátorok, kifejezések kiértékelése. Az operátorok és a paraméterekből és operátorokból álló kifejezések kiértékelése programnyelvenként változó algoritmussal írható le, amely fontos sarokkő az egyes programozási nyelvek specifikációjában. Példának hasonlítsuk össze a C++ és a Java nyelveket: C++ forrás: wikipédia
Java forrás: java.sun.com Fontos az első táblázatban látható asszociativitás kérdésköre (hiszen ha például az = balasszociatív lenne, akkor értelmetlen lenne az a = b = c = 0; sor). Fontos ezen kívül a túlterhelés (C++ esetén léteznek túlterhelhető operátorok, Java-ban egyik sem az). Operátor: a különféle műveleti jelek, melyek öszekapcsolják a kifejezésekben szereplő operandusokat. Operandus: változók, konstansok, függvény- és eljáráshívások. Kifejezés: operátorok és operandusok sorozatából álló nyelvi elem. Művelet: tevékenység(sorozat), amit az operátorok előírnak. Kifejezés kiértékelése: a benne szereplő összes művelet elvégzése. Elsőbbségi (precedencia) szabályok: a műveletek során a kifejezések kiértékelési sorrendjét meghatározó szabályok. Ezáltal precedenciaszintenként csoportosíthatóak a műveletek. A sorrend zárójelezés segítségével testreszabható, mert először mindig a zárójelben lévő műveletek hajtódnak végre. Asszociativitás: az azonos precedencia szinten lévők kiértékelési sorrendje (balról-jobbra vagy jobbról-balra). Minden művelet balról-jobbra értékelődik ki, kivéve az alábbiakat, amik jobbrólbalra: Egyoperandusú műveletek Értékadással egybekötött kétoperandusú műveletek Háromoperandusú művelet Mellékhatás (side effect): bizonyos műveletek függvényhívás, többszörös értékadás, léptetés (+
+, --) feldolgozásakor jelentkező jelenség, melynek során a kifejezés értékének megjelenése mellett bizonyos változók is megváltoztathatják értékeiket. Kiértékelésük sorrendjét nem határozza meg a C szabvány, így ügyeli kell rájuk, el kell kerülni az olyan utasításokat, melyek kiértékelése függ a precedenciától. Rövidzár (short circuit, lusta kiértékelés): az a kiértékelési mód, amely során nem szükséges kiértékelni a teljes kifejezést ahhoz, hogy egyértelműen meghatározzuk az értékét. Pl. ha egy && bal oldali operandusa 0, a jobb oldalit már szükségtelen kiértékelni, a kifejezés értéke egyértelműen 0 lesz. A műveleteket operandusuk száma és típusa szerint csoportosítjuk, programozási nyelvtől függetlenül: Egyoperandusú (unáris) műveletek Kétoperandusú (bináris) műveletek Háromoperandusú (trináris) művelet Kiértékelés: Minden kifejezésből építhető ún. szintaxisfa, például: forrás: digitus.itk.ppke.hu A kifejezések kiértékelését rengeteg különféle módon el lehet végezni, egyik legismertebb a lengyelforma algoritmus. Ez először a kifejezést postfix alakra hozza, majd ezt kiértékeli. Az alábbi struktogrammokban x a kifejezés (sorként ábrázolva) s egy verem, y a postfix alak (sorként ábrázolva), v egy verem, z pedig a kifejezés értéke. Postfix alakra hozás:
Kiértékelés: struktogrammok forrása: digitus.itk.ppke.hu Utasítások, vezérlési szerkezetek, rekurzió, kivételkezelés. Utasítás: A program egy sora, a legkisebb építő eleme. Megfigyelhető a hierarchia: egy gépi kódú utasítás pontosan egy processzorművelet, az assembly ennek olvashatóbb formája, míg az egyre magasabb szintű programozási nyelvek egy utasítása általában egyre több gépi kódú utasításnak felel meg. Ezért cserébe kapjuk a jobb olvashatóságot, a kisebb hibázási lehetőséget, stb. Viszont végül mindig gépi kódú utasításokat futtatunk. Vezérlési szerkezet: Az utasításokat blokkokra tagolhatjuk. Speciális blokkok a vezérlési szerkezetek, amelyek bár programnyelvenként változhatnak a legáltalánosabbak majdnem minden nyelvben megtalálhatóak, ezek: elágazás (if-else): az utasításblokkok az úgynevezett ágak. Valamilyen kifejezés értékétől tesszük függővé, hogy az if ágba tartozó utasítok lefutnak-e (esetleg az else ágba tartozók, avagy semmi), ciklus (while): az utasításblokk az úgynevezett ciklusmag. Amíg az adott feltétel teljesül újra és újra végrehajtjuk a ciklusmagot, léptetős ciklus (for): egy ciklus, beépített változóval. Például ha valamit 10-szer akarunk végrehajtani. (C++ esetén használható teljesen másképp is például!), hátultesztelős ciklus (do-while): a ciklusmag egyszer mindenképpen végrehajtódik, és ezután ameddig a kifejezés a ciklusmag végén igaz, addig újra és újra végrehajtjuk azt, kapcsoló (switch-case): többágú elágazás, amely egy adott kifejezés adott értékei szerint ágazik szét. (Figyeljünk rá, hogy a legtöbb megvalósításban minden eset (case) után ki kell ugranunk a switch-ből (break), és általában adunk alapesetet is (default)), iterációs ciklus (foreach, enchanced for, stb.): olyan ciklus, amely egy adott adatszerkezet minden elemére lefuttatja a ciklusmagot. Rekurzió: Előfordul, hogy egy-egy programblokkot (például függvényt) rekurzívan akarunk használni, vagyis a függvényből meghívjuk ismét a függvényt. A rekurziót általában idő- és tárpazarlónak tartják (imperatív nyelvekben), de vannak esetek, amikor gyakorlatilag kikerülhetetlen (funkcionális nyelvekben) ( A rekurzivitás jelenségét az újraírhatóság vagy láncszabály kifejezésekkel is le szokták írni. A rekurzivitás valójában olyan eljárásokat jelent, melyek nyitott végűek, és azáltal
vezetnek eredményre, hogy saját magukra alkalmazódnak. Így válnak képessé véges elemből és véges szabály által végtelen elemű rendszert létrehozni. ). Léteznek olyan függvények, amelyekre bár létezik iteratív megoldás (például faktoriális számítás, Fibonacci-sorozat n-edik elemére még explicit képlet is van), de könnyebb őket rekurzívan megadni (még ha így sokkal kevésbé hatékony is a megvalósító kód). Kivételkezelés: A kivételkezelés egy programozási mechanizmus, melynek célja a program futását szándékosan vagy nem szándékolt módon megszakító esemény (hiba) vagy utasítás kezelése. Az eseményt magát kivételnek hívjuk. A hagyományos, szekvenciális és struktúrált programozási kereteken túlmutató hibakezelésre, valamint magasabb szintű hibadetekcióra, esetleg korrigálásra használható. Régen, az alapvető utasításkészlettel ezek a hibák nem voltak kezelhetőek. A magas szintű programozási nyelvekben elterjedt egy módszer ezek kezelésére, mely általánosan így írható le: try {blokk} catch (mit) {blokk} finally {blokk} A szerkezet lényege: A try-blokkban lévő utasítások során kiváltódott hibákat a catch (mit) ágban/ágakban kaphatjuk el, és az ebben a blokkban kiadott utasítások hajtódnak végre a kivétel kiváltódása után. A finally-blokk lényege, hogy a megelőző blokkok után mindenképpen végrehajtódik, ha történt kivétel, ha nem. A kivételeket alapvetően két csoportra oszthatjuk: Operációs rendszerszintű kivételek: például memória elfogyása, rendszererőforrás problémák, fájlhibák, 0-val osztás stb. Nyelvi szintű kivételek: általános kivételek, jelzések, eseményjelzők, kódblokk megszakítások, erőforrás felszabadító szakaszok. Adatabsztrakció. Az adatabsztrakció lényege: összetett adatokkal dolgozó programjainkat úgy építjük föl, hogy az adatokat felhasználó programrészek az adatok szerkezetéről ne tételezzenek fel semmit, csak az előre definiált műveleteket használják, az adatokat definiáló programrészek az adatokat felhasználó programrészektől függetlenek legyenek. Osztály, öröklődés, statikus és dinamikus kötés, altípusos polimorfizmus. Osztály: Egy osztályban általában egy feladat elvégzéséhez szükséges kódrészleteket gyűjtik össze funkciójuk szerint, tagfüggvényekbe rendezve. Ezen kívül az osztályhoz tartozhatnak változók (osztály ill. példányszintűek), amelyeket ez esetben mezőknek (field) hívunk. Az osztály tekinthető a való világ egy részéből kreált adatabsztrakciós megvalósításnak is. Az osztály által megkapjuk az OOP legfőbb vívmányait, ezek a(z): encapsulation (egységbe zárás együtt kezeljük, ami egybe való, függvények, változók); information hiding (információ elrejtés a külvilág nem tudja, az objektum mit is csinál belül, nem ismeri a függvénytörzset); code reuse (kód újrafelhasználás példányosítás, osztály továbbörökítése, stb.).
Objektum: Az osztály egy konkrét példányát nevezzük objektumnak. Öröklődés: Egy osztály definiálása után előfordulhat, hogy az osztályban szereplő kódokat más osztályokban is használni szeretnénk. Ehhez csak arra van szükség, hogy egy adott osztályt amiből a másik osztály tagfüggvényeit el akarjuk érni, abból származtassunk. Ezt nevezzük öröklődésnek. Öröklődés során csak a public és protected nyilvánossági szintű mezők és függvények lesznek elérhetőek az utódban. Az öröklődések mentén az osztályokat gráfba rendezhetjük, ezt nevezzük öröklődési gráfnak. Ez a gráf mindenképp körmentes kell, hogy legyen, a többi tulajdonsága programnyelvenként eltérő lehet (például C++ esetén megengedett a többszörös öröklődés, míg Java esetén a gráf mindenképpen fa kell, hogy legyen). Ugyanígy programnyelvenként eltérőek az utódban lévő felüldefiniálás (override) és túlterhelés (overload) szabályai. Ezek mind konzekvensen kell, hogy kövessék a nyelv specifikációját (lásd C+ + csak virtual esetén enged felüldefiniálni, Java minden esetben, kivéve, ha final a függvény). Interface, absztrakt osztály: Bizonyos programnyelvekben (pl.: Java) megjelenik ez a két speciális osztály, amely bár más nyelvekben is megvalósítható tulajdonságokkal bír, mégsem kaptak külön jelzőt nyelvi szinten. Az interface az ún. teljesen absztrakt osztály (csak függvényszignatúrákat és osztály szintű mezőket tartalmazhat), míg az absztrakt osztály tartalmazhat konkrét megvalósításokat is. Azonban példányosítani közvetlenül egyiket sem lehet, csak egy olyan utódon keresztül, ami megvalósítja az összes meg nem valósított függvényt. Statikus és dinamikus kötés: Statikus kötés esetén a változón értelmezett műveleteket objektum-változó esetében a metódusokat a változó típusa határozza meg. Ez történik a hagyományos programnyelvekben. A fordítóprogram a változó típusának ismeretében ellenőrzi, hogy a kijelölt művelet az adott változón végrehajtható-e, és ha igen, generálja a megfelelő kódot. Dinamikus kötés esetén a műveletet meghatározó tényező a változó által hordozott érték. Az objektum-változók esetén ez azt jelenti, hogy értékadáskor a műveleteket végrehajtó metódusok is cserélődnek. A változóval végzendő művelethez tehát most, futási időben rendelődik hozzá a műveletet végrehajtó eljárás (metódus) kódja. Ha korrekt módon akarunk eljárni, akkor bármiféle, a változóra kijelölt művelet végrehajtása előtt meg kell kérdeznünk a változót, hogy éppen milyen értéket tárol. Hiszen az sem biztos, hogy a végrehajtani kívánt művelet egyáltalán értelmezett a változóban éppen tárolt értékre (megérti-e a változóban tárolt objektum az üzenetet). Így érthető, hogy a dinamikus kötés implementálása lényegesen bonyolultabb a statikusnál. Polimorfizmus: A polimorfizmus és a dinamikus kötés különböző fogalmak! Polimorfizmusnak (többalakúságnak) azt a jelenséget hívjuk, hogy egy változó nem csak egyfajta típusú objektumra hivatkozhat. Altípusos polimorfizmus: Altípusos polimorfizmus esetén a dinamikus típus mindig a statikus típus vagy annak valamely leszármazottja (Ha B altípusa A-nak, akkor egy A típusú referenciának A vagy B típusú értéket adhatunk). Generikusok. Parametrikus polimorfizmus: Egy adott osztályt egy adott típussal tudunk paraméterezni. Szinte minden modern nyelvben fellelhető lehetőség (funkcionális nyelvekben is, C++, Java, stb.).
Generikus programozás (generic, template): Algoritmusok és adatszerkezetek általános, típusfüggetlen programozása. Bizonyos nyelvekben (pl.: Ada, C++) ez egy sablon, csak példányosításra használható. Más nyelvekben (pl.: Java, funkcionális nyelvek) ez egy kód, amely végrehajtódhat a megfelelő típusokkal. Generatív programozás: Program készítése programmal. A generikus programozás azért lehet generatív, mert egy bizonyos kód alapján a program elkészít egy másik kódot, amely már nem sablon, hanem igazi, futtatható programrészlet ( generic -ek mellett ilyen még a C++ esetén alkalmazott template metaprogramming ). Lusta példányosítás: (pl.: C++ esetén) csak a valóban használt programrészek gyártódnak le. Sablonszerződés: A sablon specifikációja megmondja, hogyan lehet a sablont használni. Ha ezt a szerződést a példányosítás betartja, akkor a generált kód helyes lesz. Ehhez: a sablon törzse nem használhat mást, csak amit a szerződés megenged, illetve a példányosításnak biztosítani kell mindent, amit a sablon specifikációja megkövetel.