KURZUS: Programozás alapjai
MODUL: III. modul
10. lecke: Mutatók 2/2
Ebben a leckében folytatjuk a mutatókkal kapcsolatos tudnivalók megismerését. Készítünk többdimenziós tömböket, megnézzük, hogyan adhatjuk ezeket át függvényeknek, és hogyan kezelhetjük őket mutatók segítségével. A kódmutatók használatáról is ejtünk szót. Közben olyan területekre is kitérünk, mint a véletlenszámok generálása, a dinamikus memóriakezelés, a parancssori paraméterek és környezeti változók használata. Elmélyítjük a típusdefiníciókkal kapcsolatos ismereteinket, hogy összetett, mutatókkal kapcsolatos típusokat áttekinthetőbben tudjunk definiálni. | ||||||||||||||||
A lecke végére a hallgatónak tisztában kell lennie | ||||||||||||||||
| ||||||||||||||||
A lecke kulcsfogalmai: többdimenziós tömb, véletlenszám generátor, elsődleges adatterület, verem, heap, dinamikus memóriakezelés, parancssori paraméter, környezeti változó, kilépési állapot, kódmutató. | ||||||||||||||||
Többdimenziós tömbök | ||||||||||||||||
A többdimenziós tömböket a tömb típus tömbjeiként konstruálja meg a fordító. A deklaráció | ||||||||||||||||
típus azonosító[<méret1>][méret2]. . .[méretN] <={inicializátorlista}>; | ||||||||||||||||
alakú. Az elhelyezésről egyelőre annyit, hogy sorfolytonos, azaz a jobbra álló index változik gyorsabban. Például a | ||||||||||||||||
double matrix[2][3]; | ||||||||||||||||
sorfolytonosan a következő sorrendben helyezkedik el a tárban: | ||||||||||||||||
matrix[0][0], matrix[0][1], matrix[0][2], matrix[1][0], matrix[1][1], matrix[1][2] | ||||||||||||||||
Egy elem elérése például | ||||||||||||||||
azonosító[index1][index2]. . .[indexN] | ||||||||||||||||
módon megy, ahol az indexekre igazak a következők: | ||||||||||||||||
0 <= index1 < méret1 | ||||||||||||||||
A tömbök inicializálásáról korábban mondottak most is érvényben vannak, de a többdimenziós tömb további tömbökből áll, s így az inicializálási szabályok is rekurzívan alkalmazandók. Az inicializátorlista egymásba ágyazott, egymástól vesszővel elválasztott inicializátorok és inicializátorlisták sorozata a sorfolytonos elhelyezési rend betartásával. Például a 4x3-as t tömb első sorának minden eleme 1 értékű, második sorának minden eleme 2 értékű, s így tovább. | ||||||||||||||||
int t[4][3]={{1, 1, 1}, {2, 2, 2}, {3, 3, 3}, {4, 4, 4}}; | ||||||||||||||||
Ha az inicializátorlistában nincs beágyazott inicializátorlista, akkor az ott felsorolt értékek az alaggregátumok, s azon belül az elemek sorrendjében veszik fel az aggregátum elemei. Az előző példával azonos eredményre vezetne a következő inicializálás is: | ||||||||||||||||
int t[4][3] = {1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4}; | ||||||||||||||||
Kapcsos zárójelek akár az egyes inicializátorok köré is tehetők, de ha a fordítót nem kívánjuk "becsapni", akkor célszerű őket az aggregátum szerkezetét pontosan követve használni! Az | ||||||||||||||||
int t[4][3]={{1, 1, 1}, {2, 2, 2}, {3, 3, 3}}; | ||||||||||||||||
hatására a t mátrix utolsó sorára (a t[3] tömbre) nem jut inicializátor, így a t[3][0], t[3][1] és t[3][2] elemek mind zérus kezdőértéket kapnak. Az | ||||||||||||||||
int t[4][3]={{1}, {2}, {3}}; | ||||||||||||||||
eredményeként a t[0][0] 1, a t[1][0] 2, a t[2][0] 3 lesz, és a tömb összes többi eleme zérus. | ||||||||||||||||
Tudjuk, hogyha nem adjuk meg, akkor az inicializátorlista elemszámából állapítja meg a tömbméretet a fordító. Többdimenziós tömb deklarációjában ez azonban csak az első méretre vonatkozik, a további méreteket mind kötelező előírni. | ||||||||||||||||
int a[][]={{1, 1}, {2, 2}, {3, 3}}; /* HIBÁS */ | ||||||||||||||||
Folytassuk tovább a pelda23.c- ben megkezdett magyar kártyás példánkat! Újabb programunknak legyen az a feladata, hogy megkeveri a paklit, s kioszt öt lapot! Aztán újra kioszt öt lapot, s így tovább mindaddig, míg a kártya el nem fogy. Újabb keverés következik és újabb osztások. A szoftvernek akkor van vége, ha már nem kérnek több lapot. | ||||||||||||||||
Azt, hogy milyen kártyákat osztott már ki a pakliból, statikus élettartamú, csak a kartya.c forrásfájlban elérhető, kétdimenziós, SZINDB* KTYDB-s, kty tömbben tartja nyilván a program. A megfelelő tömbelem zérus, ha még pakliban van a lap, és 1-gyé válik, ha kiosztják. A statikus, a kartya.c forrásmodulra ugyancsak lokális, ktydb változóban a pakli aktuális kártyaszámát őrzi a szoftver. | ||||||||||||||||
A következő sorokkal mindig a kartya.c bővítendő! | ||||||||||||||||
static int kty[SZINDB][KTYDB]; | ||||||||||||||||
Az UjPakli függvény ugyanezt az állapotot állítja elő. | ||||||||||||||||
void UjPakli(void){ | ||||||||||||||||
A Mennyi rutinnal lekérdezhető, hogy még hány kártya van a pakliban. | ||||||||||||||||
int Mennyi(void){ | ||||||||||||||||
Az int UjLap(void) függvény egy lapot ad a pakliból, azonban ezt véletlenszerűen teszi a következő technikával: | ||||||||||||||||
| ||||||||||||||||
Látszik, hogy szükségünk lesz véletlenszám generátorra! | ||||||||||||||||
Véletlenszám generátor | ||||||||||||||||
Használatához az stdlib.h fejfájlt kell bekapcsolni. Egész számok pszeudóvéletlen sorozatát generálja közel egyenletes eloszlással 0 és RAND_MAX között az | ||||||||||||||||
int rand(void); | ||||||||||||||||
függvény. A rutinnak nincs hibás visszatérése. A véletlenszám generálás kezdőértékét lehet beállítani a | ||||||||||||||||
void srand(unsigned int kezd); | ||||||||||||||||
függvénnyel. Az alapértelmezett induló érték 1, így ilyen értékű kezd paraméterrel mindig újrainicializáltathatjuk a véletlenszám generátort, azaz a rand hívások ugyanazt a véletlenszám sorozatot produkálják srand(1) után, mintha mindenféle srand megidézés nélkül használtuk volna a rand-ot. | ||||||||||||||||
Másképp fogalmazva, a rand nem véletlen módon, hanem nagyon is determinisztikusan ad vissza hívásonként egy-egy számot egy véges sorozat egymást követő tagjai közül. Az srand azt szabályozza, hogy melyik tagtól kezdjük el felhasználni a rendelkezésre álló számokat. Ha a sorozat tagjai elfogytak, akkor a rand elölről kezdve újra felhasználja a számokat. Nyilvánvaló, hogy a rand által visszaadott értékek csak akkor fognak véletlenszerűnek hatni, ha az srand is véletlenszerű paramétert kap, lehetőleg a program futása során egyetlen alkalommal, az első rand hívást megelőzően. | ||||||||||||||||
Az srand-ot a véletlenszerű indulást is biztosítandó rendszerint a time.h-ban helyet foglaló | ||||||||||||||||
time_t time(time_t *timer); | ||||||||||||||||
függvény kezd paraméterrel szokták meghívni. A time rutin az aktuális rendszer időt 1970. január elseje éjfél óta eltelt másodpercek számában, time_t (long) típusban szolgáltatja. Nincs hibás visszatérés. A visszatérési értéket a timer címen is elhelyezi, ha a paraméter nem NULL mutató. NULL mutató aktuális paraméter esetén viszont nem tesz ilyet. Az srand függvény szokásos hívása tehát: | ||||||||||||||||
srand(time(NULL)) | ||||||||||||||||
Véletlenszám generátorral kockadobás értéket a következőképp produkálhatunk: | ||||||||||||||||
rand()%6 + 1 | ||||||||||||||||
Ha a fejlesztő rendszerben nincs lebegőpontos véletlenszámot generáló függvény, akkor 0 és 1 közötti véletlenszámokat a következő kifejezéssel állíthatunk elő: | ||||||||||||||||
(double)rand()/RAND_MAX | ||||||||||||||||
Folytassuk az UjLap függvényt! | ||||||||||||||||
int UjLap(void){ | ||||||||||||||||
Elkészültünk a kibővített kartya.c-vel, s most írjuk meg a működtető pelda24.c programot! | ||||||||||||||||
/* PELDA24.C: Ot lap osztasa. */ | ||||||||||||||||
Dinamikus memóriakezelés | ||||||||||||||||
A C a memóriát három részre osztja. Az elsődleges adatterületen helyezi el a fordító a konstansokat és a statikus objektumokat. A lokális objektumokat és a függvényparamétereket a verembe (stack) teszi. A harmadik memóriaterületet - nevezzük heap-nek, bár más névvel is szokták illetni - futás közben éri el a C program, és változó méretű memória blokkok dinamikus allokálására való. Például fák, listák, tömbök vagy bármi más helyezhető el rajta. Az ismertetett, ANSI szabványos függvények prototípusai az stdlib.h fejfájlban találhatók. | ||||||||||||||||
void *calloc(size_t tetelek, size_t meret); | ||||||||||||||||
A calloc tetelek*meret méretű memória blokkot foglal, feltölti 0x00-val és visszaadja kezdőcímét. Tulajdonképpen tetelek elemszámú tömbnek foglal helyet, ahol egy elem mérete meret bájt. | ||||||||||||||||
Ha nincs elég hely, vagy a tetelek*meret szorzat értéke zérus, NULL mutatót kapunk vissza. | ||||||||||||||||
void *malloc(size_t meret); | ||||||||||||||||
A malloc legalább meret bájtos memória blokkot foglal, nem tölti fel semmivel és visszaadja kezdőcímét. A blokk nagyobb lehet meret bájtnál a tárillesztéshez igényelt, plusz terület és a karbantartási információ elhelyezése miatt. | ||||||||||||||||
Ha nincs elég hely a heap-en, NULL mutatót kapunk vissza a függvénytől. Ha a meret 0, a malloc zérusméretű blokkot allokál, és érvényes mutatót ad vissza erre a területre. | ||||||||||||||||
A calloc-kal, a malloc-kal lefoglalt, vagy a rögtön ismertetendő realloc-kal újrafoglalt terület tárillesztése olyan, hogy bármilyen típusú objektum elhelyezésére alkalmas. A függvényektől visszakapott cím explicit típusmódosító szerkezettel bármilyen típusúvá átalakítható. | ||||||||||||||||
Tegyük fel, hogy a program futása közben derül ki egy double tömb mérete! Ezt az értéket az int típusú, N változó tartalmazza. Hogyan lehet létrehozni, kezelni, s végül felszabadítani egy ilyen tömböt? | ||||||||||||||||
/* . . . */ | ||||||||||||||||
void *realloc(void *blokk, size_t meret); | ||||||||||||||||
A realloc meret méretűre szűkíti vagy bővíti a korábban malloc, calloc vagy realloc hívással allokált memória blokkot, melynek kezdőcímét megkapja a blokk paraméterben. Sikeres esetben visszaadja az átméretezett memória blokk kezdőcímét. Ez a cím nem feltétlenül egyezik meg a blokk paraméterben átadottal. Címeltérés esetén a függvény a korábbi memória blokk tartalmát átmozgatja az újba. Az esetleges rövidüléstől eltekintve elmondható, hogy az új blokk megőrzi a régi tartalmát. | ||||||||||||||||
Ha az újraallokálás sikertelen memória hiány miatt, ugyancsak NULL mutatót kapunk, de az eredeti blokk változatlan marad. | ||||||||||||||||
void free(void *blokk); | ||||||||||||||||
A free deallokálja (felszabadítja) a korábban malloc, calloc vagy realloc hívással allokált memóriaterületet, melynek kezdőcímét megkapja a blokk paraméterben. A felszabadított bájtok száma egyezik az allokációkor (vagy a realloc esetén) igényelttel. Ha a blokk NULL, a mutatót elhagyja a free, és rögtön visszatér. | ||||||||||||||||
Az érvénytelen mutatós (nem calloc, malloc, vagy realloc függvénnyel foglalt memória terület címének átadása) felszabadítási kísérlet befolyásolhatja a rákövetkező allokációs kéréseket, és fatális hibát is okozhat. | ||||||||||||||||
A többdimenziós tömbök belső szerkezetének megértéséhez hozzunk létre dinamikusan egy tömböt, a példában most egy mátrixot! | ||||||||||||||||
/* PELDA25.C: Ketdimenzios tomb letrehozasa dinamikusan. */ | ||||||||||||||||
Vegyük észre, hogy a main-nek van visszaadott értéke, mely zérus, ha minden rendben megy, és 1, ha memóriafoglalási probléma lép fel! Figyeljük meg azt is, hogy a memória felszabadítása foglalásával éppen ellenkező sorrendben történik, hogy a heap-en ne maradjanak foglalt "szigetek"! A heap C konstrukció, s ha a program befejeződik, akkor maga is felszabadul, megszűnik létezni. | ||||||||||||||||
Összesítve: A matrix azonosító a tömb kezdetére mutató, változó mutató. A tömb kezdete viszont m változó mutatóból álló mutatótömb. A mutatótömb egy-egy eleme n elemű, long double típusú tömb kezdetére mutat. A példa a részek memóriabeli elhelyezkedését is szemlélteti, azaz: | ||||||||||||||||
| ||||||||||||||||
32 bites címeket feltételezve, 1000-ről indulva, m=3 és n=5 esetén, a matrix a következőképpen helyezkedhet el a memóriában: | ||||||||||||||||
Háromdimenziós tömböt úgy valósíthatunk meg, hogy létrehozunk előbb egy mutatótömböt, melynek mindenegyes eleme egy, az előzőekben ismertetett szerkezetű mátrixra mutat. | ||||||||||||||||
Néhány szó még a többszörösen alkalmazott index operátorról! A kifejezés1[kifejezés2][kifejezés3]...-ban az index operátorok balról jobbra kötnek, így a fordító először a legbaloldalibb kifejezés1[kifejezés2] kifejezést értékeli ki. A született mutató értékhez aztán hozzáadva kifejezés3 értékét új mutató kifejezést képez, s ezt a folyamatot a legjobboldalibb index kifejezés összegzéséig végzi. Ha a végső mutató érték nem tömb típust címez, akkor az indirekció művelete következik. Tehát például: | ||||||||||||||||
matrix[2][3] | ||||||||||||||||
A fordító hasonló módszerrel dolgozik, de az általa létrehozott mátrixszal kapcsolatban az összes mutató - ideértve a mátrix azonosítóját is - konstans. | ||||||||||||||||
A többdimenziós tömbök többféleképpen is szemlélhetők. A fordító által létrehozott mátrix (ú.n. statikus tömb) példájánál maradva: | ||||||||||||||||
long double matrix[m][n]; | ||||||||||||||||
| ||||||||||||||||
A dolgokat más oldalról tekintve! | ||||||||||||||||
| ||||||||||||||||
Többdimenziós, statikus tömbök esetén a fordító nem generál mutatótömböket a memóriába, csak a puszta tömbelemeket helyezi el a tárban a dinamikus tömbnél tárgyalt módon és sorrendben. A kód azonban előállít konstans mutatóként minden, a dinamikus tömbnél megszokott mutatóhivatkozást. E számításokba a fordító fixen beépíti a többdimenziós tömb méreteit (az elsőtől eltekintve). Az indexelés is módosul kicsit ebben az értelemben. Például a matrix[i][j] elérése a következő: | ||||||||||||||||
*((long double *)matrix + i*n + j) | ||||||||||||||||
Leegyszerűsítve: A fordító a többdimenziós, statikus tömböt egy akkora egydimenziós tömbben helyezi el, melyben annak minden eleme elfér. A többdimenziós tömb méretei, és az indexek segítségével ez után meghatározza, hogy melyik egydimenziós tömbelemet jelenti a többdimenziós tömbhivatkozás. | ||||||||||||||||
Tömbök mint függvényparaméterek | ||||||||||||||||
Ha van egy | ||||||||||||||||
float vektor[100]; | ||||||||||||||||
tömbünk, és kezdőcímével meghívjuk az | ||||||||||||||||
Fv(vektor) | ||||||||||||||||
függvényt, akkor a függvény definíciójának | ||||||||||||||||
void Fv(float *v) { /* . . . */ } | ||||||||||||||||
vagy | ||||||||||||||||
void Fv(float v[]){ /* . . . */ } | ||||||||||||||||
módon kell kinéznie. (Az utóbbi alakról tudjuk, hogy a fordító rögtön és automatikusan átkonvertálja az előző (a mutatós) formára.) | ||||||||||||||||
Ne feledjük, hogy ugyan a vektor konstans mutató a hívó függvényben, de v (címmásolat) már változó mutató a meghívott függvényben. Vele tehát elvégezhető például a v++ művelet. A *v, a *(v+i) vagy a v[i] balérték alkalmazásával a vektor tömb bármelyik eleme módosítható a meghívott függvényben. | ||||||||||||||||
Emlékezzünk arra is, hogy a meghívott függvénynek is tudnia kell valahonnét a tömb méretét! Például úgy, hogy a méretet is átadjuk paraméterként neki. Az itt elmondottak többdimenziós tömbök vonatkozásában is igazak legalább is az első méretre, de ott már nem teszünk említést erről! | ||||||||||||||||
Ha van egy | ||||||||||||||||
float matrix[10][20]; | ||||||||||||||||
definíciónk, és az azonosítóval meghívjuk | ||||||||||||||||
Fvm(matrix) | ||||||||||||||||
módon az Fvm függvényt, akkor hogyan kell az Fvm definíciójának kinéznie? A | ||||||||||||||||
void Fvm(float **m) { /* . . . */ } | ||||||||||||||||
próbálkozás rossz, mert a formális paraméter float mutatót címző mutató. A | ||||||||||||||||
void Fvm(float *m[20]) { /* . . . */ } | ||||||||||||||||
változat sem jó, mert így a formális paraméter 20 elemű, float típusú objektumokra mutató mutatótömb. Ennél a kísérletnél az az igazi probléma, hogy a [] operátor prioritása nagyobb, mint *-é. Nekünk formális paraméterként 20 float elemű tömbre mutató mutatót kéne átadni. Tehát a helyes megoldás: | ||||||||||||||||
void Fvm(float (*m)[20]) { /* . . . */ } | ||||||||||||||||
vagy a "tradicionális" módszer szerint: | ||||||||||||||||
void Fvm(float m[][20]) { /* . . . */ } | ||||||||||||||||
amiből rögtön és automatikusan előállítja a fordító az előző (a mutatós) alakot. | ||||||||||||||||
Meg kell még említenünk, hogyha a többdimenziós tömböt dinamikusan hozzuk létre, akkor az előzőleg ajánlott megoldás nyilvánvalóan helytelen. A mátrix "horgonypontját" ebben az esetben | ||||||||||||||||
float **matrix; | ||||||||||||||||
módon definiáljuk, ami ugye float mutatóra mutató mutató. Tehát ilyenkor a | ||||||||||||||||
Fvmd(matrix) | ||||||||||||||||
módon hívott Fvmd függvény helyes formális paramétere: | ||||||||||||||||
void Fvmd(float **m) { /* . . . */ } | ||||||||||||||||
Parancssori paraméterek | ||||||||||||||||
Minden C programban kell lennie egy a programot elindító függvénynek, mely konzol bázisú alkalmazások esetében a main függvény. | ||||||||||||||||
Most és itt csak a main paramétereivel és visszatérési értékével szeretnénk foglalkozni! A paraméterekről állíthatjuk, hogy: | ||||||||||||||||
| ||||||||||||||||
A main legáltalánosabb alakja: | ||||||||||||||||
int main(int argc, char *argv[]); | ||||||||||||||||
A paraméterek azonosítói bizonyos, C nyelvet támogató környezetekben ettől el is térhetnek, de funkciójuk akkor is változatlan marad. | ||||||||||||||||
Az argc a main-nek átadott parancssori paraméterek száma, melyben az indított végrehajtandó program azonosítója is benne van, és értéke így legalább 1. | ||||||||||||||||
Az argv a paraméter karakterláncokra mutató mutatótömb, ahol az egyes elemek rendre: | ||||||||||||||||
| ||||||||||||||||
A main lehetséges alakjai a következők: | ||||||||||||||||
void main(void); | ||||||||||||||||
Vegyünk egy példát! | ||||||||||||||||
/* PELDA26.C: Parancssori parameterek. */ | ||||||||||||||||
Tételezzük fel, hogy a programot a következő parancssorral indítottuk: | ||||||||||||||||
pelda26 elso_par "sodik par" 3 4 stop! | ||||||||||||||||
Ekkor a megjelenő kimenet a következő lehet: | ||||||||||||||||
Parancssori paraméterek: | ||||||||||||||||
Beszéljünk kicsit a printf utolsó, *argv++ kifejezéséről! Az argv-t a main paraméterként kapja, tehát csak címmásolat, vagyis a main-ben akár el is rontható. Az argv típusa char **, és funkcionálisan a parancssori paraméter karakterláncok kezdőcímeit tartalmazó mutatótömb kezdetének címe. A rajta végrehajtott indirekcióval a típus char * lesz, s épp a mutatótömb első elemét (argv[0]) érjük el. Az utótag ++ operátor miatt eközben az argv már a második mutatótömb elemre (argv[1]) mutat. Elérjük ezt is, és mellékhatásként az argv megint előbbre mutat egy tömbelemmel. Tehát a ciklusban rendre végigjárjuk az összes parancssori paramétert. | ||||||||||||||||
Jusson eszünkbe, hogy a main-nek átadott parancssor maximális hosszát korlátozhatja az operációs rendszer! | ||||||||||||||||
A legtöbb operációs rendszerben léteznek | ||||||||||||||||
változó=érték | ||||||||||||||||
alakú, ún. környezeti változók, melyek definiálják a környezetet (információt szolgáltatnak) az operációs rendszer és a benne futó programok számára. Például a PATH környezeti változó szokta tartalmazni az alapértelmezett keresési utakat a végrehajtható programokhoz, a parancsinterpreter helyét írja elő a COMSPEC, és így tovább. Az operációs rendszer természetesen lehetőséget biztosít ilyen környezeti változók törlésére, megadására, és értékük módosítására. | ||||||||||||||||
A felhasználó pl. Windows 10 operációs rendszeren a Parancssor-t elindítva, a következő parancsokkal tudja manipulálni a környezeti változókat: | ||||||||||||||||
| ||||||||||||||||
Arra azonban ügyelni kell, hogy minden parancsértelmező a környezeti változóknak egy önálló készletével rendelkezik, azaz az egyik ilyen készlet változása nem befolyásolja a másikat. Magyarán, ha pl. a szoftverfejlesztő környezet indítja el a programunkat, akkor hiába gépelünk be új környezeti változókat egy parancssorban, abból a programunk valószínűleg semmit sem vesz majd észre. | ||||||||||||||||
A környezeti változók értékeit lekérdezni pl. az ANSI szabványos, nem kis-nagybetű érzékeny | ||||||||||||||||
char *getenv(const char *valtozo); | ||||||||||||||||
függvénnyel lehet, mely visszaadja az aktuális környezet alapján a valtozo nevű környezeti változó értékére mutató mutatót. Ha nincs ilyen változó az aktuális környezeti táblában, NULL mutatót kapunk. A visszakapott nem NULL mutatóval azonban nem célszerű és nem biztonságos dolog a környezeti változó értékét módosítani. Ehhez a nem szabványos putenv rutin használata ajánlott. | ||||||||||||||||
A környezeti változó nevének a végére nem kell kitenni az = jelet, azaz például a PATH környezeti változót a getenv("PATH") hívással kérdezhetjük le! | ||||||||||||||||
Ha expliciten nem deklaráljuk void-nak, akkor a main-nek int típusú státuszkóddal kell visszatérnie az őt indító programhoz (process), rendszerint az operációs rendszerhez. Konvenció szerint a zérus visszaadott érték (EXIT_SUCCESS) hibátlan futást, s a nem zérus státuszkód (EXIT_FAILURE 1) valamilyen hibát jelez. Magát a main-ből való visszatérést (mondjuk 1-es státuszkóddal) megoldhatjuk a következő módok egyikével: | ||||||||||||||||
return 1; | ||||||||||||||||
Foglalkozzunk kicsit a programbefejezéssel is! | ||||||||||||||||
Programbefejezés | ||||||||||||||||
A return 1 csak a main-ben kiadva fejezi be a program futását. Az stdlib.h bekapcsolásakor rendelkezésre álló, mindegyik operációs rendszerben használható | ||||||||||||||||
void exit(int statusz); | ||||||||||||||||
függvények mind befejezik annak a programnak a futását, amiben meghívják őket akármilyen mély rutin szintről is. A statusz paraméter értékét visszakapja a befejezettet indító (várakozó szülő) program, mint kilépési állapotot (exit status). A statusz értéket átveszi persze az operációs rendszer is, ha ő volt a befejezett program indítója. | ||||||||||||||||
Az exit függvény a program befejezése előtt meghív minden regisztrált (lásd atexit) kilépési függvényt, kiüríti a kimeneti puffereket, és lezárja a nyitott fájlokat. | ||||||||||||||||
Az abort alapértelmezés szerint befejezi az aktuális programot. Megjelenteti például az | ||||||||||||||||
Abnormal program termination | ||||||||||||||||
üzenetet az stderr-en, és aztán SIGABRT (abnormális programbefejezés) jelet generál. Ha nem írtak kezelőt (signal) a SIGABRT számára, akkor az alapértelmezett tevékenység szerint az abort 3-as státuszkóddal visszaadja a vezérlést a szülő programnak. Szóval nem üríti a puffereket, és nem hív meg semmilyen kilépési függvényt (atexit) sem. | ||||||||||||||||
Függvény (kód) mutatók | ||||||||||||||||
A mutatók függvények ún. belépési pontjának címét is tartalmazhatják, s ilyenkor függvény vagy kódmutatókról beszélünk. | ||||||||||||||||
Ha van egy | ||||||||||||||||
int fv(double, int); | ||||||||||||||||
prototípusú függvényünk, akkor erre mutató mutatót | ||||||||||||||||
int (*pfv)(double, int); | ||||||||||||||||
módon deklarálhatunk. A pfv azonosító ezek után olyan változót deklarál, melyben egy double, s egy int paramétert fogadó és int-tel visszatérő függvények címeit tarthatjuk. A pfv tehát változó kódmutató. Kódmutató konstans is létezik azonban, s ez a függvénynév (a példában az fv). | ||||||||||||||||
Vigyázzunk a deklarációban a függvénymutató körüli kerek zárójel pár el nem hagyhatóságára, mert az | ||||||||||||||||
int *pfv(double, int); | ||||||||||||||||
olyan pfv azonosítójú függvényt deklarál, mely egy double, s egy int paramétert fogad, és int típusú objektumot címző mutatóval tér vissza. A probléma az, hogy a mutatóképző operátor (*) prioritása alacsonyabb a függvényképzőénél (()). | ||||||||||||||||
Hogyan lehet értéket adni a függvénymutatónak? | ||||||||||||||||
Természetesen a szokásos módokon, azaz hozzárendeléssel: | ||||||||||||||||
pfv = fv; | ||||||||||||||||
illetve a deklarációban inicializátor alkalmazásával: | ||||||||||||||||
int fv(double, int); | ||||||||||||||||
Vigyázzunk nagyon a típussal, mert az most "bonyolódott"! Csak olyan függvény címe tehető be a pfv-be, mely a függvénymutatóval egyező típusú, azaz int-et ad vissza, egy double és egy int paramétert fogad ebben a sorrendben. A | ||||||||||||||||
void (*mfv)(); | ||||||||||||||||
szerint az mfv meg nem határozott számú és típusú paramétert fogadó olyan függvényre mutató mutató, melynek nincs visszatérési értéke. | ||||||||||||||||
Vegyük észre, hogy a kérdéses függvények definíciója előtt függvénymutatók inicializálására is használható a megadott függvény prototípus! | ||||||||||||||||
Hogyan hívhatjuk meg azt a függvényt, melynek címét a kódmutató tartalmazza? | ||||||||||||||||
Alkalmaznunk kell a mutatókra vonatkozó ökölszabályunkat, ami azt mondja ki, hogy ahol állhat azonosító a kifejezésben, ott állhat (*mutatóazonosító) is. Vegyük elő újra az előző példát! Ha | ||||||||||||||||
int a = fv(0.65, 8); | ||||||||||||||||
az fv függvény hívása, és valamilyen módon lezajlott a pfv = fv hozzárendelés is, akkor az | ||||||||||||||||
a = (*pfv)(0.65, 8); | ||||||||||||||||
ugyanaz a függvényhívás. | ||||||||||||||||
Itt a pfv-re alkalmaztuk az indirekció operátort (*), de mivel ennek prioritása alacsonyabb a függvényhívás operátorénál (()), ezért a *pfv-t külön zárójelbe kellett tenni! | ||||||||||||||||
A kódmutatóval kapcsolatos alapismeretek letárgyalása után feltétlenül ismertetni kell a C fordító függvényekkel kapcsolatos fontos viselkedését: implicit konverzióját! | ||||||||||||||||
Ha a kifejezés típussal visszatérő függvény típusú, akkor hacsak nem cím operátor (&) mögött áll, típussal visszatérő függvénymutató típusúvá konvertálja automatikusan és azonnal a fordító. | ||||||||||||||||
Ez az implicit konverzió mindenek előtt megvalósul a | ||||||||||||||||
utótag-kifejezés(<kifejezéslista>) | ||||||||||||||||
függvényhívásban, ahol az utótag-kifejezésnek kell típussal visszatérő függvénycímmé kiértékelhetőnek lennie. A típus a függvényhívás értékének típusa. A dolog praktikusan azt jelenti, hogy a függvény bármilyen függvényre mutató kifejezéssel meghívható. | ||||||||||||||||
Milyen műveletek végezhetők a kódmutatókkal? | ||||||||||||||||
| ||||||||||||||||
Kódmutatókra azonban nem alkalmazható a mutatóaritmetika az egyenlőségi reláció operátoroktól (== és !=) eltekintve. | ||||||||||||||||
Foglalkozzunk a kódmutató paraméterrel! | ||||||||||||||||
A függvényekre érvényes implicit típuskonverzió a függvényparaméterekre is vonatkozik. Ha a paraméter típussal visszatérő függvény, akkor a fordító automatikusan és rögtön típus típusú értéket szolgáltató függvényre mutató mutatóvá alakítja át. | ||||||||||||||||
A következő, kissé elvonatkoztatott példában kitűnően megszemlélhető a kódmutató paraméter függvény prototípusban, ill. függvény aktuális és formális paramétereként. | ||||||||||||||||
/* . . . */ | ||||||||||||||||
A kódmutató típusa szerint az ilyen függvény egy int paramétert fogad, és long értéket szolgáltat. | ||||||||||||||||
A szabványos függvénykönyvtár több olyan függvényt is tartalmaz, amely kódmutató paramétert vár. A korábban már általunk is megvalósított bináris keresés algoritmusa az stdlib.h fejfájl után elérhető: | ||||||||||||||||
void *bsearch(const void *kulcs, const void *bazis, size_t elemszam, size_t meret, | ||||||||||||||||
Ez a függvény megkeresi a kulcs címen tárolt adatot abban az elemszam elemből álló tömbben, amely a bazis címen helyezkedik el a tárban, és minden eleme meret méretű. A függvény utolsó paramétere egy olyan függvényt címez, mely két mutatót fogad, és int értékkel tér vissza. A mutatók a tömb két tetszőleges elemét címezhetik. A visszatérési érték pozitív, ha az első címen lévő érték nagyobb a második címen lévőnél, negatív, ha a második címen lévő érték nagyobb az első címen lévőnél, és zérus, ha a címzett helyeken lévő értékek egyformák. (Ebben a tulajdonságában tehát emlékeztet a karaktermutatóknál ismertetett strcmp függvényre.) A hasonlito függvényt nekünk kell megírnunk, mivel a bsearch-nek a const void* típusú mutatók miatt semmiféle fogalma nincs arról, hogy a tömb milyen jellegű adatokat tárol, és azokat hogyan kell értelmezni, összehasonlítani. Ez egyébként szándékos, és lehetővé teszi, hogy a függvényt típusfüggetlenül, a feladatok lehető legszélesebb körének megoldására használjuk. A bsearch visszatérési értéke a fellelt elem helye, vagy NULL mutató, ha a keresett elem nincs a tömbben. | ||||||||||||||||
Mint azt korábban megtárgyaltuk, bináris keresés csak rendezett tömbön hajtható végre. Szerencsére egy nagyon hatékony, ú.n. gyorsrendező (quick sort) algoritmus is megvalósításra került a szabvány könyvtárakban, így az sem okozhat gondot, ha egy tömb kezdetben rendezetlen. A | ||||||||||||||||
void qsort(void *bazis, size_t elemszam, size_t meret, | ||||||||||||||||
paramétereinek értelmezése pontosan megegyezik a bsearch-nél elmondottakkal. A qsort nem ad vissza értéket, a bazis címen kezdődő tömböt helyben rendezi. | ||||||||||||||||
A fentiek kipróbálására hozzunk létre egy olyan programot, ami egy egészekből álló, kezdetben rendezetlen tömbben keres meg értékeket! | ||||||||||||||||
/* PELDA27.C: rendezes es kereses */ | ||||||||||||||||
A program futtatásának eredménye: | ||||||||||||||||
Szamok rendezes elott: | ||||||||||||||||
Azt mondottuk, hogy kódmutatók tömbökben is elhelyezhetők. Visszatérve az első pfv-s példánkhoz, az | ||||||||||||||||
int (*pfvt[])(double, int) = {fv1, fv2, fv3, fv4, fv5}; | ||||||||||||||||
deklarációval létrehoztunk egy pfvt azonosítójú, olyan ötelemű tömböt, mely int-et visszaadó, egy double, és egy int paramétert fogadó függvények címeit tartalmazhatja. Feltéve, hogy fv1, fv2, fv3, fv4 és fv5 ilyen prototípusú függvények, a pfvt tömb elemeit kezdőértékkel is elláttuk ebben a deklarációban. | ||||||||||||||||
Hívjuk még meg, mondjuk, a tömb 3. elemét! | ||||||||||||||||
a = (*pfvt[2])(0.65, 8); | ||||||||||||||||
Alakítsuk át függvénymutató tömböt használóvá a kódmutató paraméternél ismertetett példát! | ||||||||||||||||
Az új megoldásunk main-en kívüli része változatlan, a main viszont: | ||||||||||||||||
void main(void){ | ||||||||||||||||
A kódmutató visszatérési értékhez elemezzük ki a következő függvény prototípust! | ||||||||||||||||
void (*signal(int jel, void (* kezelo)(int)))(int); | ||||||||||||||||
A signal első paramétere int, a második (void (* kezelo)(int)) viszont értéket vissza nem adó, egy int paramétert fogadó függvénymutató típusú. Kitűnően látszik ezek után, hogy a visszatérési érték void (*)(int), azaz értéket nem szolgáltató, egy int paramétert fogadó függvénymutató. A visszaadott érték típusa tehát a signal második paraméterének típusával egyezik. | ||||||||||||||||
A signal.h fejfájlban elhelyezett prototípusú signal függvénnyel különben a program végrehajtása során bekövetkező, váratlan eseményeket (megszakítás, kivétel, hiba stb.), ún. jeleket vagy jelzéseket lehet lekezeltetni. Többféle típusú jel létezik. A void (*)(int) típusú kezelőfüggvényt a manipulálni kívánt jelre külön meg kell írni. A signal rutinnal hozzárendeltetjük a kezelőt (2. paraméter) az első paraméter típusú jelhez, és a signal az eddigi kezelő címével tér vissza. | ||||||||||||||||
Egy bizonyos típusú függvénymutató explicit típuskonverzióval | ||||||||||||||||
(típusnév) előtag-kifejezés | ||||||||||||||||
átalakítható más típusú kódmutatóvá. Ha az e módon átkonvertált mutatóval függvényt hívunk, akkor a hatás a programfejlesztő rendszertől, a hardvertől függ. Viszont, ha visszakonvertáljuk az átalakított mutatót az eredeti típusra, akkor az eredmény azonos lesz a kiindulási függvénymutatóval. | ||||||||||||||||
Szedjük csak megint elő az | ||||||||||||||||
int fv(double, int), a; | ||||||||||||||||
példánkat, és legyen a | ||||||||||||||||
void (*vpfv)(double); | ||||||||||||||||
A | ||||||||||||||||
(*vpfv)(0.65); | ||||||||||||||||
eredményessége eléggé megkérdőjelezhető, de a | ||||||||||||||||
pfv=(int (*)(double, int))vpfv; | ||||||||||||||||
után teljesen rendben lesz a dolog: | ||||||||||||||||
a=(*pfv)(0.65, 8); | ||||||||||||||||
Emlékezzünk csak! Explicit típusmódosított kifejezés nem lehet balérték. | ||||||||||||||||
Foglalkozzunk csak újra egy kicsit a típusnevekkel! | ||||||||||||||||
Típusnév | ||||||||||||||||
Explicit típusmódosításban, függvénydeklarátorban a paramétertípus rögzítésekor, sizeof operandusaként stb. szükség lehet a típus nevének megadására. Ehhez kell a típusnév, mely szintaktikailag a kérdéses típusú objektum olyan deklarációja, melyből elhagyták az objektum azonosítóját. | ||||||||||||||||
típusnév: | ||||||||||||||||
absztrakt-deklarátor: | ||||||||||||||||
direkt-absztrakt-deklarátor: | ||||||||||||||||
Az absztrakt-deklarátorban mindig lokalizálható az a hely, ahol az azonosítónak kéne lennie, ha a konstrukció deklaráción belüli deklarátor lenne. Lássunk néhány konkrét példát! | ||||||||||||||||
int * | ||||||||||||||||
int típusú objektumra mutató mutató. | ||||||||||||||||
int ** | ||||||||||||||||
int típusú objektumot címző mutatóra mutató mutató. | ||||||||||||||||
int *[] | ||||||||||||||||
Nem meghatározott elemszámú, int típusú mutatótömb. | ||||||||||||||||
int *() | ||||||||||||||||
Ismeretlen paraméterlistájú, int-re mutató mutatóval visszatérő függvény. | ||||||||||||||||
int (*[])(int) | ||||||||||||||||
int típussal visszatérő, egy int paraméteres, meghatározatlan elemszámú függvénymutató tömb. | ||||||||||||||||
int (*(*())[])(void) | ||||||||||||||||
Ismeretlen paraméterezésű, int típussal visszatérő függvénymutatókból képzett, meghatározatlan méretű tömbre mutató mutatót szolgáltató, paraméterrel nem rendelkező függvény. | ||||||||||||||||
A problémán: a sok zárójelen, a nehezen érthetőségen typedef alkalmazásával lehet segíteni. | ||||||||||||||||
Típusdefiníció (typedef) | ||||||||||||||||
Az elemi típusdefinícióról szó volt már a Típusok és konstansok lecke végén. Az ott elmondottakat nem ismételjük meg, viszont annyit újra szeretnénk tisztázni, hogy a típusdefiníció nem vezet be új típust, csak más módon megadott típusok szinonimáit állítja elő. A typedef név, ami egy azonosító, szintaktikailag egyenértékű a típust leíró kulcsszavakkal, vagy típusnévvel. | ||||||||||||||||
A típusdefiníció "bonyolításához" először azt említjük meg, hogy a | ||||||||||||||||
typedef típus azonosító; | ||||||||||||||||
szerkezetben az azonosító a prioritás sorrendjében lehet: | ||||||||||||||||
| ||||||||||||||||
Megemlítendő még, hogy az előzőek alkalmazásával "csínján" kell bánni, hisz az így típusdefiniált azonosítóknak éppen a jellege (függvény, tömb, mutató) veszik el. | ||||||||||||||||
A típusdefiníció további komplexitása abból fakad, hogy a | ||||||||||||||||
typedef típus azonosító; | ||||||||||||||||
szerkezetbeli típus korábbi typedef típus azonosító;-ban definiált azonosító is lehet. Tehát a típusdefinícióval létrehozott típuspecifikátor típus lehet egy másik típusdefinícióban. | ||||||||||||||||
Nézzünk néhány példát! | ||||||||||||||||
typedef int *iptr; | ||||||||||||||||
A típusdefinícióval a program típusai parametrizálhatók, azaz a program portábilisabb lesz, hisz az egyetlen typedef módosításával a típus megváltoztatható. A komplex típusokra megadott typedef nevek ezen kívül javítják a program olvashatóságát is. | ||||||||||||||||
Lokális szinten megadott típusdefiníció lokális hatáskörű is. Az általánosan használt típusdefiníciókat globálisan, a feladathoz tartozó fejfájlban szokták előírni. | ||||||||||||||||
Egy utolsó kérdés: Mikor ekvivalens két típus? | ||||||||||||||||
| ||||||||||||||||
A típusekvivalencia meghatározásánál a tömbméretek és a függvényparaméter típusok is lényegesek. |
Feladatok | |||||||||||||
1. Kezdő programozó olyan programot szeretett volna írni, ami abécé sorrendbe rendezve megjeleníti a szabvány kimeneten a szavak tömbben tárolt szavakat. | |||||||||||||
/*01*/#include <stdio.h> | |||||||||||||
Jelölje meg azokat a sorokat, amelyekkel a hibás program működésre bírható!
![]() | |||||||||||||
2. Egyszeri programozó szeretett volna egy olyan függvényt írni, ami véletlenszerű, [min, max] intervallumba eső egész értékekkel tölti fel a paraméterként kapott, statikusan allokált mtx tömb első sor sorát és oszlop oszlopát. | |||||||||||||
/*1*/#include <stdlib.h> | |||||||||||||
Jelölje meg azokat a programsorokat, amelyekkel a hibás program helyesen fog működni!
![]() | |||||||||||||
3. Kezdő programozónk szándéka az volt, hogy 10 db. valós, -2 és +2 közé eső, véletlen számot jelenítsen meg a szabvány kimeneten. | |||||||||||||
/*01*/#include <stdio.h> | |||||||||||||
Jelölje meg azokat a programsorokat, amelyekkel programjának működése helyessé tehető!
![]() | |||||||||||||
4. Készítsen programot, mely a JANI, fordítási időben változtatható azonosítójú, környezeti változóról megállapítja, hogy létezik-e! Ha létezik, akkor eldönti, hogy értéke "lo", "szamar", "birka" vagy más. | |||||||||||||
/* JANI.C: A KORNYVALT kornyezeti valtozo a megadott ertekek egyike, | |||||||||||||
5. Készítsen programot két mátrix összeadására! Az összeadandó mátrixoknak ne, de az eredménymátrixnak dinamikusan foglaljon helyet a memóriában! Az összeadandó mátrixok mérete csak futás időben válik konkréttá. A programban használjon függvényeket az összeadandó mátrixok méretének és elemeinek bekéréséhez, valamint a két mátrix összeadásához! | |||||||||||||
/* MATRIXOS.C - Ket matrix osszeadasa */ | |||||||||||||
6. Készítsen programot, mely neveket olvas a szabvány bemenetről! Egy sorban egy név érkezik, s az üres sor a bemenet végét jelzi. A név nagybetűvel kezdődik, és a többi karaktere kisbetű. A feltételeket ki nem elégítő név helyett azonnal másikat kell kérni a probléma kijelzése után! A neveket rendezze névsorba, s listázza ki őket lapokra bontva! | |||||||||||||
/* NEVREND.C: Beolvasott, dinamikusan mutatotombben tarolt |