Programszerkezet
fordítási egység vagy modul: A nyelvnek az az egysége, ami a fordítóprogram egyszeri lefuttatásával, a program többi részétől elkülönülten lefordítható. Több modulból álló programok esetében a fordítás és a végrehajtás között: a program összeszerkesztése.
Blokk: utasítások szekvenciája. C++, C#, Java, stb. esetén utasítások { és } jelek közé írt sorozata. Egyes nyelvek esetében (script-nyelvek mintpl. Python, perl, Ruby) egy-egy blokk-ot az utasítások tagolódása határoz meg. Kozsik-diából a blokkokra jellemző tények: 1. Egyszerűsített hatóköri/láthatósági szabályok 2. Egymásba ágyazott blokkok lokális változói 3. Elfedés: csak tagok esetében 4. Lokális változók inicializálása Hatókör: A programszövegnek azt a szakaszát, amelyen belül az adott deklaráció érvényben van, a deklaráció hatáskörének nevezzük. blokk szintű (lokális, belső): a deklarációtól az azt magába foglaló blokk végéig tart. Kiterjed természetesen az alblokkokra is. függvény szintű: a függvényen belüli egyedi azonosítóknak (ún. utasítás címkéknek) van ilyen hatásköre, melyet egy olyan végrehajtható utasítás elé kell írni kettőspontot közbeszúrva, melyre el szeretnénk ágazni. függvény prototípus szintű: ilyen hatásköre a prototípusban deklarált paraméterlista azonosítóinak van, vagyis kizárólag a prototípusra terjednek ki. Gyakorlati haszna nincs, mivel azonosítók nélkül pusztán típusokkal is ugyanannyit érünk el – akkor érdemes használni ilyen azonosítókat, ha kifejezetten utalni akarunk valamire a névvel. fájl szintű (globális, külső): minden függvény testén kívül deklarált azonosítók ilyen hatáskörrel rendelkeznek; a deklarációs ponttól a forrásfájl végéig terjed – tehát minden függvényből és blokkból elérhető. Láthatóság (visibility): a forráskód azon része egy azonosítóra vonatkozóan, ahol az azonosítóhoz kapcsolódó adatelem elérhető. A hatáskör és a láthatóság többnyire megegyezik, de beszélhetünk láthatatlan/rejtett adatelemekről is. rejtett: egy már létező azonosító, melynek hatáskörét felülírja egy másik, ugyanolyan nevű adatelem definiálása. A rejtett adatelem nem érhető el, azonosítójával nem hivatkozhatunk rá, amíg a másik hatáskörének végéig érvényes. Láthatósági szint (C++): publikus (public): bárhonnan elérhetőek adattagjai és tagfüggvényei, ahonnan maga az objektum elérhető. védett (protected): csak az adott objektum és annak leszármazottjai érheti el adattagjait és tagfüggvényeit. privát (private): csak az adott objektum érheti el adattagjait és tagfüggvény Automatikus, statikus és dinamikus élettartam, szemétgyűjtés. Tárolási osztály: a C nyelvben az azonosító egyik jellemzője, mely meghatározza egy adatelem élettartamát, hatáskörét. Automatikus: az automatikusként való deklarálás egyben definíció is, vagyis megtörténik a memóriafoglalás. Rekurzió esetén az objektumok különböző memóriaterületeken helyezkednek el minden egyes blokkpéldányra vonatkozóan (mivel a program vermében tárolódnak). A függvények paraméterei ilyen tárolási osztályúak. Alapértelmezett kezdőértékük memóriaszemét. Explicit/implicit kezdőértékadás minden egyes alkalommal megtörténik, amikor a vezérlés belép az automatikus azonosítót tartalmazó blokkba. Mondják még dinamikusnak is, mert ugyanabba a blokkba való többszöri belépéskor mindig egy új, friss készlete áll rendelkezésre, amelyekben nyoma sincs azoknak az értékeknek, amelyeket az utolsó kilépéskor bennük hagytunk. Külső deklarációk vagy definíciók számára tiltott. auto: minden blokk szintű (lokális) objektum alapértelmezett tárolási osztálya. register: gyakran használt változók esetén (pl. ciklusváltozó nagy iterációban) alkalmazott típusminősítő, mely megadja a fordító számára, hogy a változót egyből az egyik – szabad, nem használt – regiszterbe rakja bele (és ne kelljen előbb a veremből mindig kiszedni és átrakni), és onnan olvasgassa
ki mindig. A fordító nem biztos, hogy teljesíti kérésünket, lehet, hogy az adott változó nem is férne be a regiszterbe. Csak sorszámozott, vagy pointer típusú változó lehet ilyen. Tilos a címére hivatkozni, mert hibához vezet. Explicit módon ilyennek deklarált változó egyben auto is, mert ha nem fér a regiszterbe, a verembe pakolja. Statikus: statikus élettartamú objektumok tárolási osztálya, melyek értékeiket végig megőrzik a program végrehajtása alatt. Rekurzió ez által értelmetlen, mivel minden blokkra nézve az objektum állapota nem változik. Alapértelmezett kezdőértéke: 0. Explicit/implicit kezdőértékadás csak egyszer történik meg, a program indulásakor. static: egy adott modulra szűkíti a láthatóság körét, tehát a static jelzővel ellátott globális változók csak az adott programon belül láthatóak. extern: külső objektumokra – vagyis olyanokra, melyek definíciója nem az adott fordítási egységben vagy hatáskörben található – használt típusminősítő. Fájl szintű változók és bármely függvények esetén ez az alapértelmezett tárolási osztály. Élettartam (lifetime): a program végrehajtása közben az az időtartam, ameddig a változó a memóriában van (csak része a hatáskörnek, de nem mindig) VAGY az az idő, amely alatt az adott objektum a rendeltetésnek megfelelő paraméterekkel üzemel. lokális: akkor történik meg a memóriafoglalás, amikor a vezérlés belép az őket magába foglaló blokkba vagy függvénybe; amint a vezérlés elhagyja ezt, megszűnnek. Veremben vagy regiszterben tárolódnak. Mindig kell őket inicializálni, mert alapértelmezetten a veremműveletek során hátra maradt memóriaszemétből kapnának kezdőértéket. Blokkszintű változók ilyenek. Más változók auto vagy register tárolási osztállyal tehetőek ilyenné. globális: a memória hozzárendelés a program indulásakor megtörténik és kitart a futás végéig; nevezik még statikusnak is. Ilyenek a fájl szintű változók és bármely függvény. Más változók static vagy extern tárolási osztállyal tehetőek ilyenné. Alapértelmezetten minden memóriabit kezdőértéke zérus. dinamikus: a felhasználó saját maga végzi a memóriafoglalást (malloc, new) és felszabadítást (free, delete) könyvtári függvények segítségével. Igazából csak a C++ nyelvben van jelen (ahol operátorok valósítják meg a memóriakezelést), mivel a C nyelvnek nem részei a könyvtári függvénye Az objektumorientált programozás (angolul object-oriented programming, röviden OOP) egy programozási módszer. Ellentétben a korábbi programozási nyelvekkel, nem a műveletek megalkotása áll a középpontban, hanem az egymással kapcsolatban álló programegységek hierarchiájának megtervezése.
Néhány nyelvben (C#, Java) ún. „garbage collector”, „szemétgyűjtő” van, ami a program futása közben automatikusan szabadítja fel a nem használt memóriát. Konstruktor, destruktor A használt programozási nyelvtől függően, de legtöbbször a class előtétszóval definiálunk osztályokat. Egy osztály csak függvényeket (metódusokat, tagfüggvényeket) és változókat (adattagokat) tartalmazhat. Mivel az osztályokban található függvényeket az objektumon keresztül lehet elérni, ezért hívják őket tagfüggvényeknek, vagy metódusoknak. Bizonyos nyelvek megengedik, az osztály függvényeinek használatát példányosítás nélkül is (például a C++-ban a statikus függvények). Ilyen esetekben azonban a függvény meghívásához meg kell adni annak az osztálynak a nevét, amelynek a metódus tagja.
Minden osztálynak van legalább egy konstruktora. Ez egy különleges függvény, amely az osztály példányosításakor hívódik meg. Feladata az osztály kezdőértékeinek beállítása, helyfoglalás a memóriában stb. Több konstruktort is megadhatunk, a fordító a szignatúrától függően választja ki a megfelelőt. A konstruktor párja a destruktor, ami az elfoglalt memóriát szabadítja fel, eltakarít maga után. Destruktorból pontosan egy darab van az osztályban. A neve meg kell egyezzen az osztály nevével, ami elé egy ~ (tilde) jelet is kell tenni (amit nem feltétlenül kell egybe írni a névvel). Paramétere és visszaadott értéke nem lehet. Ha egy programkódban objektumokról beszélünk, először az osztályok felépítését kell megvizsgálni, ugyanis minden objektum egy előre definiált osztály példánya. 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. Egy osztályt létrehozása után példányosítani kell, hogy használhatóvá váljanak a benne összegyűjtött rutinok. Az osztály példányát nevezzük objektumnak.
Objektumok másolása, összehasonlítása Az objektumoknak kétféle másolási lehetőségét különböztetjük meg: sekély másolat (shallowcopy): csak a referencia másolódik, az objektum maga ugyanazon a memóriaterületen lesz
azaz ugyanaz történik, mint egy álnév létrehozásánál, vagy cím szerinti paraméterátadásnál
ha felülírunk valamilyen értéket a másolatban, az felülírja az eredeti értéket is
mély másolat (deepcopy): a teljes objektum másolódik egy ugyanakkora memóriaterületre
értékmódosítások a másolatban nem lesznek hatással az eredeti objektumra
érték szerinti paraméterátadásnál egyszerű változókra
Amikor egy objektumot másolunk, akkor alapértelmezetten lemásolódik minden adattagja (mint a rekordoknál)
azonban az objektumok szerkezete összetett lehet, tartalmazhat például mutatókat más objektumokra ha egy objektumban mutató található egy másik objektumra, akkor a másolás során a tartalmazott objektumnak csak sekély másolata készül el, ez nem mindig megfelelő
felül kellene definiálnunk a másolás folyamatát ahhoz, hogy ezt befolyásolni tudjuk, és a további, az objektumhoz tartozó adatokat le tudjuk másolni, a C++ erre biztosít lehetőséget
az objektumlétrehozást (konstruktor) szeretnénk túlterhelni
Az objektummásolást a másoló konstruktor(copyconstructor) valósítja meg
egy már létező objektum alapján hoz létre újat
ez egy olyan konstruktor, amely paraméterben megkapja egy ugyanolyan osztályú objektum referenciáját, és törzsében elvégzi a megfelelő másolási műveleteket
vigyázzunk, ne módosítsuk az eredeti objektumot, célszerű azt konstansnak megadni
A másoló konstruktor törzsében tud hivatkozni a másik objektum adattagjaira, beleértve a rejtetteket is
általában ezeket egyenként értékül adjuk az új objektumnak ha mutató is van az adattagok között, akkor az általa mutatott objektumot is le kell másolnunk
ha szükséges, akkor további inicializálásokat végezhetünk a másoló konstruktorban
Objektumok összehasonlítása előre definiált, az adott nyelvben megírt metódusokkal történhet. C# esetében a Comparer osztály CompareTo metódusa segít két objektum összehasonlításában. Elvégezhető vele két azonos típusú objektum karakter-érzéketlen összehasonlítása. Az objektumok összehasonlításkor, a Compare függvény végrehajtásakor egy ArgumentException kivétel keletkezik, amennyiben az objektumok nem azonos típusúak, vagy ha egyik objektum sem implementálta az IComparable interfész CompareTo metódusát (ebben gondoskodva arról, hogy az adott objektum képes legyen összehasonlítást végezni a másik objektummal).
Java nyelv esetében ilyen metódus az equals, amely == referenciát hasonlít.
Programegységek, névterek Programegység: A programegység egy részfeladatot megoldó, tehát funkcionálisan összefüggő programrész. A fordítási egység programegységek halmaza. Részei: A specifikációs rész írja le az egységnek a más egységből elérhető “kapcsolódási pontjait". A törzs tartalmazza az egység funkcióját megvalósító programot. Törzs:
A deklarációs rész (ha van külön) azonosítók deklarációit, valamint konstansok, típusok, objektumok és programegységek definícióit tartalmazhatja. Az utasítássorozat tartalmazza azokat az utasításokat, amelyek végrehajtása nyomán a programegység kifejti a hatását.
(Java: Az interfész olyan programegység, amely publikus és absztrakt metódusokat, valamint publikus és statikus konstansokat tartalmazhat) Névtér (namespace): az a hatáskör, melyen belül az azonosítónak egyedinek kell lennie. Különböző névtereken belül nyugodtan létezhet két ugyanolyan azonosító a programban, a fordító meg tudja őket különböztetni. C#: A .NET Framework osztálykönyvtárai szerény becslés szerint is legalább tízezer nevet, azonosítót tartalmaznak. Ilyen nagyságrenddel elkerülhetetlen, hogy a nevek ismétlődjenek. Ekkor egyrészt nehéz eligazodni köztük, másrészt a fordító is megzavarodhat. Ennek a problémának a kiküszöbölésére hozták létre a névterek fogalmát. Egy névtér tulajdonképpen egy virtuális doboz, amelyben a logikailag összefüggő osztályok, metódusok, stb… vannak. Nyilván könnyebb megtalálni az adatbázis kezeléshez szükséges osztályokat, ha valamilyen kifejező nevű névtérben vannak (System.Data). Névteret magunk is definiálhatunk, a namespace kulcsszóval: namespace MyNameSpace { } Ezután a névtérre vagy a program elején a using –al, vagy az azonosító elé írt teljes eléréssel hivatkozhatunk: using MyNameSpace; //vagy MyNameSpace.Valami Alprogramok, paraméterátadás A programegységek legelterjedtebb fajtája az alprogram. A nyelvek kétféle alprogramot használnak, ezek az eljárások és a függvények. Az alprogramok formális paraméterekkel rendelkeznek, amelyeknek az alprogram aktivizálásakor (hívásakor) aktuális paramétereket kell megfeleltetni. Érték szerinti paraméterátadás (call-by-value): A híváskor hely foglalódik le a memóriában a változó számára, ahova a paraméterként kapott értéket bemásolja. Ha a fv. vagy eljárás megváltoztatja ennek értékét, akkor a hívó programba visszatérve ez a megváltoztatott érték elveszik, marad a régi érték. Az eljárás vagy függvény lefutása után a paraméter számára lefoglalt memóriaterületek felszabadulnak. Előnye: konstansok is átadhatók, egyszerűbb a meghívás Hátránya: lassúbb végrehajtás, nagyobb futtatható programméret Cím szerinti paraméterátadás (call-by-reference): A híváskor a paraméter címe kerül átadásra. A cím alapján a fv. vagy eljárás megváltoztathatja ennek értékét úgy, hogy a hívó programba visszatérve ez a megváltoztatott érték megmarad. Előny: gyorsabb paraméterátadás, futás, rövidebb program Hátrány: konstansok nem adhatóak át paraméterként így
C: Csak érték szerinti paraméterátadás létezik: pointerek segítségével azonban elérhető hasonló hatás. Változó hosszúságú argumentumlista: bizonyos függvények esetén nem lehet pontosan megadni az argumentumok számát és típusát. Az ilyen függvények deklarációjában három pont zárja a paraméterlistát. Ahhoz, hogy a paramétereket tartalmazó memóriaterületen megtaláljuk az átadott argumentumok értékét, az első paramétert mindig meg kell adnunk, majd mutatók segítségével beolvashatjuk az adatokat. C++: Alapértelmezés (default) szerinti paraméter érték: - Ha egy formális paraméterhez = jellel egy kifejezést kapcsolunk, az a paraméter híváskor elmaradhat, de a kifejezésnek megfelelő kezdőértéket veszi fel. - Az alapértelmezés szerinti paraméterérték megadása formálisan is egy inicializálásnak felel meg. Szabályok: - Ha egy paraméternek van alapértelmezés szerinti értéke, akkor utána lévő értékeknek is adni kell default értékeket. - A kifejezés értékének típusa a paraméter típusával kompatibilis kell legyen. - Egy adott paraméter default értékét vagy a prototípusban, vagy a definícióban kell megadni, mindkét helyen tilos! Általában minden default értéket a prototípusban adunk meg (mert ez publikus). - A kifejezés kiértékelése a hívás során futási időben történik. - Default értékkel nem rendelkező formális paraméter helyére kötelező aktuális paramétert írni. Szintaktika: Deklaráció: tipus fgvnev(tip1 vált1, tip2 valt2=kifejezes2); Fgv.hívás: fgvnev(ertek1); vagy fgvnev(ertek1, ertek2); Hivatkozás (cím) szerinti paraméterátadás: A hivatkozás szerinti paraméternek a címe adódik át. A függvényben nem kell indirekciót alkalmazni a változó értékének elérésére. Ha a formális paraméter értékét változtatjuk, akkor az aktuális paraméter is megváltozik. Az adott formális paramétert hivatkozás típusúnak kell deklarálni. Szintaktika: tipus fgvnev(&valtozo1, &valtozo2, …);
Példa: void csere (int &a, int &b) { int i; i=a; a=b; b=i; } main()
Függvény-overloading: Azonos hatáskörben, azonos névvel, különböző paraméterszignatúrával (=formális paraméterek száma és típus-sorrendje) különböző függvényeket definiálhatunk. Hogyan zajlik le: A függvény hívásakor a fordítóprogram az aktuális paraméterek szignatúráját sorban egyezteti az összes azonos nevű definíció formális paramétereinek szignatúráival. Ennek az egyeztetésnek az alábbi eredményei lehetnek: - Pontosan egy illeszkedőt talál: ilyenkor az ennek megfelelő függvény hívását fordítja be. - Ha egyetlen illeszkedőt sem talál, vagy több egyformán illeszkedőt talál: hibajelzést ad. - Korai kötéssel (early binding) valósul meg, azaz még fordítási időben hozzárendelődik a megfelelő függvénydefiníció a híváshoz Túlterhelés (C++ példa)
Művelet túlterhelés (operator overloading): ugyanaz a művelet többféle típusú objektumon //példakód: is végrehajtható, a művelet nevéhez több struct Point { különböző implementáció tartozik attól Point(int X, int Y): x(X), y(Y){} függően, hogy milyen objektumra kívánjuk int x,y; alkalmazni. (operátor polimorfizmus -}; polymorphism) Késői kötés: a futás során kapcsolódik össze a double VectorLength(int, int); művelet neve és a megfelelő implementáció. double VectorLength(Point); A túlterhelés leveszi a programozó válláról a sokféle név megjegyzésének terhét, mivel azonos int main() névvel létezhet több függvény is. { cout << VectorLength(3, 4) << endl; Akkor beszélünk túlterhelésről, ha azonos cout << VectorLength(Point(3, 4)) << endl; //feltehetőleg látókörben (scope) több azonos nevű függvény van mindkét függvény 5.0-t fog visszaadni deklarálva, különböző szignatúrával. egyébként elfedésről(ha egy külső látókörben van a másik név). Túlterhelt függvény hívásakor a fordító kiválasztja a látható függvények közül a legjobban illeszkedőt, szignatúra alapján. A feloldási szabály meglehetősen bonyolultak, így óvatosan kell túlterhelt nevet bevezetni.