KURZUS: Programozás alapjai
MODUL: III. modul
9. lecke: Mutatók 1/2
Ebben a leckében elkezdünk megismerkedni a mutatókkal, azok tulajdonságaival, alkalmazási szabályaikkal és lehetséges felhasználási körükkel. A C nyelv erősen épít a mutatókra: sok feladat ugyan megoldható nélkülük is, de bizonyos esetekben hatékonyabb, egyszerűbb vagy kifejezőbb megoldás adható egy problémára, de olyan esetet is mutatni fogunk, ami egyáltalán nem lenne kezelhető mutatók nélkül. A mutatókkal hatékony eszközt kap kezébe a programozó, de egyben veszélyeset is: sok nehezen felfedezhető hiba a mutatók helytelen alkalmazására vezethető vissza. | ||
A lecke végére a hallgatónak tisztában kell lennie | ||
| ||
A lecke kulcsfogalmai: mutató, adatmutató, kódmutató, NULL és void mutató, konstans és konstanst címző mutató, címképzés, indirekció, cím szerinti paraméterátadás, index operátor, nem teljes típusú tömb, tárillesztés, mutatótömb. | ||
A mutatók fő típusai | ||
A nyelvben a mutatóknak két fajtája van: | ||
| ||
Bármilyenek is legyenek, a mutatók memória címek tárolására szolgálnak. Előbb az adatmutatókkal fogunk foglalkozni. A mutató mindaddig, míg erről külön nem szólunk, jelentsen adatmutatót! | ||
A típus típusú mutató típus típusú objektum címét tartalmazhatja. A mutatók unsigned egészek, de saját szabályaik és korlátozásaik vannak a hozzárendelésre, a konverzióra és az aritmetikára. | ||
Miután a mutató is skalár objektum, létezik a mutatót címző mutató is. A mutatók azonban többnyire más skalár vagy void objektumokra (változókra), ill. aggregátumokra (tömbökre, struktúrákra és uniókra) mutatnak. | ||
Mutatódeklarációk | ||
A mutatódeklaráció elnevezi a mutató változót, és rögzíti annak az objektumnak a típusát, melyre ez a változó mutathat. | ||
Tehát csak egy bizonyos típusra (előredefiniáltra - beleértve a void-ot is - vagy felhasználó definiáltára) mutató mutató deklarálható | ||
típus *azonosító; | ||
módon, amikor is az azonosító nevű mutató típus típusú objektum címét veheti fel. | ||
Vegyük észre, hogy típus * az azonosító típusa! Például az | ||
int *imut; | ||
deklarációk alapján: imut int típusú objektumot címző mutató, fv int típusú címet visszaadó, és egy char objektumra mutató paramétert fogadó függvény. | ||
Ha a definiált mutató statikus élettartamú, akkor tiszta zérus kezdőértéket kap implicit módon. A fordító tiszta zérus címen nem helyez el sem objektumot, sem függvényt, s így a zérus cím (az ún. NULL mutató) speciális felhasználású. Magát a NULL mutatót a szabványos fejfájlokban (például az stdio.h- ban) definiálja a nyelv. | ||
Bármilyen típusú mutató összehasonlítható a NULL értékkel, ill. bármilyen típusú mutatóhoz hozzárendelhetjük a NULL mutatót. | ||
Ha lokális a definiált mutató, akkor a definíció hatására a fordító akkora memóriaterületet foglal, hogy abban egy cím elférjen, de a lefoglalt bájtok "szemetet" tartalmaznak. Az ilyen mutató tehát nem használható mindaddig "értelmesen" semmire, míg valamilyen módon érvényes címet nem teszünk bele. Hogy lehet elérni valamilyen objektum címét? | ||
Cím operátor (&) | ||
Egyoperandusos, magas prioritású művelet, mely definíció szerint: | ||
& előtag-kifejezés | ||
alakú. Az előtag-kifejezés operandusnak vagy függvényt kell kijelölnie, vagy olyan objektumot elérő balértéknek kell lennie, mely nem register tárolási osztályú, és nem bitmező. Lehet tehát például változó, vagy tömbelem. (A bitmezőkkel a struktúrák kapcsán később foglalkozunk!) | ||
Ha az operandus típusa típus, akkor az eredmény mutató a típus típusra. Cím csak mutatónak adható át. A lehetséges módszerek szerint vagy hozzárendeljük, vagy inicializátort alkalmazunk a deklarációban. Például: | ||
típus val1, val2, *ptr = &val1; /* A ptr mutatónak van | ||
Ha | ||
T1 *ptr1; T2 *ptr2; | ||
különböző típusú objektumokat címző mutatók, akkor a | ||
ptr1 = ptr2; | ||
vagy a | ||
ptr2 = ptr1; | ||
hozzárendelés figyelmeztető vagy hibaüzenetet okoz. Explicit típusmódosító szerkezetet alkalmazva viszont "gond nélkül" mehet a dolog: | ||
ptr1 = (T1 *) ptr2; | ||
Teljesen illegális dolog azonban a függvény és az adatmutatók összerendelése! | ||
Ha a mutató már érvényes címet tartalmaz, az | ||
Indirekció operátor (*) | ||
segítségével elérhetjük a mutatott értéket. Az indirekció operátor ugyancsak egyoperandusos, magas prioritású művelet, melynek definíciója: | ||
* előtag-kifejezés | ||
Az előtag-kifejezés operandusnak típus típust címző mutatónak kell lennie, ahol a típus bármilyen lehet. Az indirekció eredménye az előtag-kifejezés mutatta címen levő, típus típusú érték. Az indirekció eredménye egyben balérték is. Például: | ||
típus t1, t2; | ||
Ne kíséreljük meg azonban a cím operátorral kifejezés vagy konstans címét előállítani, vagy indirekció operátort nem címmé kiértékelhető operandus elé odaírni, | ||
ptr = &(t1 + 6); /* HIBÁS */ | ||
mert hibaüzenethez jutunk! | ||
Meghatározatlan az indirekció eredménye akkor is, ha a mutató: | ||
| ||
A mutatókkal végzett munka során "ököl-szabályunk" szerint: ahol objektum lehet egy kifejezésben, ott kerek zárójelbe téve az indirekció műveletét követően ilyen típusú objektumot címző mutató is állhat. Ha: | ||
típus val, *ptr = &val; | ||
akkor ahol val szerepelhet egy kifejezésben, ott állhat (*ptr) is. | ||
Például: | ||
int y, val=0, *ptr = &val; | ||
Fedezzük fel, hogy a cím, vagy mutatótartalom kijelzéséhez használatos típuskarakter a p a formátumspecifikációban, és hogy a printf paraméterlistájában cím értékű kifejezés is állhat, persze akár indirekcióval is! | ||
Vigyázzunk nagyon! Ha egy változónak nem adunk kezdőértéket, és mondjuk, hozzáadogatjuk egy tömb elemeit, akkor a végeredmény "zöldség", s a dolog szarvashiba. Ha mutatónak nem adunk kezdőértéket, és a benne levő "szemétre", mint címre, írunk ki értéket indirekcióval, akkor az duplán szarvashiba. A "szeméttel", mint címmel valahol "pancsolunk" a memóriában, és felülírjuk valami egészen más objektum értékét, s a hiba is egészen más helyen jelentkezik, mint ahol elkövettük. Korszerű operációs rendszerek megvédik ugyan a többi program adat- és kódterületét a hibásan működő programtól oly módon, hogy azonnal leállítják a rossz programot, de a program saját adatterületének nem kívánt módosításától semmiféle operációs rendszer és mechanizmus nem véd meg. | ||
void mutató | ||
Külön kell említenünk a | ||
void *vptr; | ||
mutató típust, ami nem semmire, hanem meghatározatlan típust címző mutató. Explicit típusmódosító szerkezet alkalmazása nélkül bármilyen típusú mutató vagy cím hozzárendelhető a void mutatóhoz. A dolog megfordítva is igaz, azaz bármilyen típusú mutatóhoz hozzárendelhetünk void mutatót. Például: | ||
típus ertek, *ptr=&ertek; | ||
void mutatóval egyetlen művelet nem végezhető csak: az indirekció, hisz meghatározatlan a típus, és a fordító nem tudja, hogy hány bájtot és milyen értelmezésben kell elérnie. | ||
ertek=*vptr; /* HIBÁS */ | ||
Statikus és lokális címek | ||
Miután a statikus élettartamú objektum minden bitjét zérusra inicializálja alapértelmezés szerint a fordító, de címe nem változik, kezdőértéke lehet akár statikus mutatónak is. Az auto változók címe viszont nem lehet statikus inicializátor, hisz a cím más-más lehet a blokk különböző végrehajtásakor. | ||
int GLOBALIS; | ||
Mutatódeklarátorok | ||
deklarátor: | ||
direkt-deklarátor: | ||
mutató: | ||
típusmódosító-lista: | ||
típusmódosító: (a következők egyike!) | ||
A deklarátorok szerkezetileg az indirekcióhoz, a függvényhez és a tömbkifejezéshez hasonlítanak, s csoportosításuk is azonos. A deklarátort követheti egyenlőségjel után inicializátor, de mindig deklaráció-specifikátorok előzik meg. A deklaráció-specifikátorok tárolási-osztály-specifikátor és típusspecifikátor sorozat tulajdonképp, és igazából nem csak egyetlen deklarátorra vonatkoznak, hanem ezek listájára. | ||
A direkt-deklarátor definíció első lehetősége szerint a deklarátor egy egyedi azonosítót deklarál, melyre a tárolási osztály egy az egyben vonatkozik, de a típus értelmezése kicsit függhet a deklarátor alakjától is. A deklarátor tehát egyedi azonosítót határoz meg, s mikor az azonosító feltűnik egy tőle típusban nem eltérő kifejezésben, akkor a vele elnevezett objektum értékét eredményezi. | ||
Összesítve, és csak a lényeget tekintve a deklaráció | ||
típus deklarátor | ||
alakú. Ezt nem változtatja meg az sem, ha a direkt-deklarátor második alternatíváját tekintjük, mert a zárójelezés nem módosítja a típust, csak összetettebb deklarátorok kötésére lehet hatással. (A függvénydeklarátorokat már tárgyaltuk, s a tömbdeklarátorokra még ebben a leckében visszatérünk!) | ||
A mutatódeklaráció így módosul: | ||
típus * <típusmódosító-lista> deklarátor | ||
ahol a * <típusmódosító-listá>-val változtatott típus a deklarátor típusa. A * operátor után álló típusmódosító magára a mutatóra, és nem a vele megcímezhető objektumra vonatkozik. | ||
Foglalkozzunk például a const módosítóval! | ||
Konstans mutató | ||
Mind a mutató, mind a mutatott objektum deklarálható const-nak. Bármely const-nak deklarált "valami" ugyebár nem változtathatja meg az értékét. Az sem mehet persze, hogy olyan mutatót kreáljunk, mellyel megsérthetnénk a const objektum érték megváltoztathatatlanságát. | ||
int i; | ||
A következő hozzárendelések legálisak: | ||
i = ci; /* const int hozzárendelése int-hez. */ | ||
A következő hozzárendelések illegálisak: | ||
ci = 0; /* Értékhozzárendelési kísérlet const int-hez.*/ | ||
Ahhoz, hogy a "kígyó megharapja a farkát" kell, hogy const objektumot címző mutatót ne lehessen hozzárendelni nem const-ra mutató mutatóhoz. Ha ez menne, akkor ugyan "kerülő úton", de a mutatott const érték megváltoztatható lenne. | ||
Mutatók és függvényparaméterek | ||
Eddig kiemelten csak az ún. érték szerinti paraméter átadással foglalkoztunk a függvényhívás kapcsán. Ezt úgy interpretálhatjuk, hogy a fordító függvényhíváskor az aktuális paraméterek (esetleg típuskonverzión átesett) értékét helyezi el, például a veremben, s a meghívott függvény nem az aktuális paraméterek értékéhez, hanem annak csak egy másolatához fér hozzá. | ||
Rövidsége és találósága miatt átvesszük [5] vonatkozó mintapéldáját! Tegyük fel, hogy a programozó azt a feladatot kapta, hogy írjon olyan függvényt, mely megcseréli két, int paramétere értékét! | ||
csere(paraméter1, paraméter2); | ||
módon hívható első kísérlete a következő volt: | ||
void csere(int x, int y){ | ||
Barátunk próbálkozása "professzionális" olyan értelemben, hogy gondolt arra, hogy a csere végrehajtásához szüksége van segédváltozóra, de a függvényt hívó programjában "meglepetten" tapasztalta, hogy semmiféle értékcsere nem történt. | ||
A csere tulajdonképpen lezajlott az aktuális paraméterek másolatain a veremben, de ennek semmilyen hatása sincs az aktuális paraméterek hívó programbeli értékeire. A függvény visszatérése miatt a veremmutató is visszaállt a hívás előtti értékére, s így a verembeli, felcserélt értékmásolatok is elérhetetlenné váltak. | ||
A megoldás a cím szerinti paraméter átadásban rejlik, azaz a csere függvényt | ||
csere(¶méter1, ¶méter2); | ||
módon kell meghívni, s a függvénydefiníció pedig így módosul: | ||
void csere(int *x, int *y){ | ||
Valójában tehát nincsen szintaktikai lehetőség az "igazi" cím szerinti paraméter átadásra a nyelvben, de a címek másolatainak érték szerinti átadásával, majd a függvényen belüli indirekcióval meg lehet kerülni a problémát. Így a hívott rutin képes módosítani a hívó függvényben látható változókat, hiszen azok címeinek másolata is ugyanazt a változót jelöli ki a tárban. | ||
Tömbök és mutatók | ||
Tömb létesíthető aritmetikai típusokból, de definiálható | ||
| ||
Bármilyen típusból is hozzuk azonban létre a(z egydimenziós) tömböt, a típusnak teljesnek kell lennie. Nem lehet félig kész, nem teljesen definiált, felhasználói típusból tömböt kreálni. | ||
A tömbök és a mutatók között nagyon szoros kapcsolat van. Ha kifejezés, vagy annak része típus tömbje, akkor a (rész)kifejezés értékét a tömb első elemét megcímző, konstans mutatóvá alakítja a fordító, és a (rész)kifejezés típusa típus * const lesz. Nem hajtja végre a fordító ezt a konverziót, ha a (rész)kifejezés cím operátor (&), ++, --, vagy a pont (.) szelekciós operátor, vagy a sizeof operandusa, vagy hozzárendelési művelet bal oldalán áll. (A . operátorral majd a struktúráknál foglalkozunk.) | ||
Elemezzük az előző bekezdésben mondottakat egy példa tükrében! Legyen a következő tömb és mutató! | ||
#define MERET 20 /* Tömbméret. */ | ||
A tömböt a fordító, mondjuk, a 100-as címtől kezdve helyezte el a memóriában. Egy tömbelem helyfoglalása sizeof(tomb[0]) ? sizeof(float) ? 4, általánosságban sizeof(típus). Az elemek tomb[0], tomb[1], ..., tomb[MERET - 1] sorrendben, növekvő címeken helyezkednek el a tárban. Tehát a tomb[0] a 100-as, a tomb[1] a 104-es, a tomb[2] a 108-as és így tovább címen van. A 180-as memóriacím már nem tartozik a tömbhöz. | ||
Ha valamilyen kifejezésben meglátja a fordító a tomb azonosítót, akkor rögtön helyettesíteni fogja a float * const 100-as címmel. Tehát, ha a pt-t fel kívánjuk tölteni tomb kezdőcímével, akkor nem kell ilyen | ||
pt = &tomb[0]; | ||
hosszadalmasan kódolni, tökéletesen elég a | ||
pt = tomb; | ||
Index operátor | ||
A Műveletek és kifejezések lecke elején definiált utótag-kifejezés második alternatívája az | ||
utótag-kifejezés[kifejezés] | ||
az indexelő operátor. Nevezik ezt indexes változónak is, vagyis mindenképpen hivatkozás ez a tömb egy meghatározott elemére. | ||
A szabályok szerint az utótag-kifejezés és a kifejezés közül az egyiknek mutatónak, a másiknak egész típusúnak kell lennie, és hatásukra a fordító a | ||
*((utótag-kifejezés) + (kifejezés)) | ||
műveletet valósítja meg. A fordító alkalmazza a következő fejezetben tárgyalt konverziós szabályokat a + műveletre és a tömbre. Ha az utótag-kifejezés a tömb és a kifejezés az egész típusú, akkor a konstrukció a tömb kifejezésedik elemére hivatkozik. | ||
Mi van a típussal? A külső zárójel párban még mutató típus (típus *) van, s ezen hajtja végre a fordító az indirekciót. Tehát az eredmény típusát a mutató dönti el. | ||
Vegyük a tomb[6] indexes változót! A mondottak szerint ebből | ||
*((tomb) + (6)) | ||
lesz, ami nem 6, hanem 6-nak, mint indexnek, a hozzáadását jelenti a tomb kezdőcíméhez, azaz: | ||
*((float *)100 + 6*sizeof(float)) | ||
Az összeadás elvégzése után | ||
*((float *)124) | ||
ami az indirekció végrehajtása után a tomb 6-os indexű elemét eredményezi. | ||
A + kommutatív művelet, így az indexelés is az. Egydimenziós tömbökre a következő négy kifejezés teljesen ekvivalens feltéve, hogy p mutató és i egész: | ||
p[i] | ||
Folytassuk az index operátor ismertetésének megkezdése előtt elkezdett gondolatmenetet, azaz a pt mutató legyen tomb értékű! | ||
pt = tomb; | ||
Ilyenkor: | ||
*pt | ||
és általában | ||
*(pt + i) | ||
Vegyük észre, hogy a pt-re szükség sincs a tömbelemek és címeik előállításához, hisz: | ||
tomb[i] | ||
és | ||
&tomb[i] | ||
Fontos különbség van azonban a tomb és a pt között. A pt mutató változó. A tomb pedig mutató konstans. Ebből következőleg nem megengedettek a következők: | ||
tomb = pt; /* Mintha 3=i-t írtunk volna fel. */ | ||
A mutató változóra természetesen megengedettek ezek a műveletek: | ||
pt = tomb; | ||
Ha pt mutató, akkor azt kifejezés indexelheti a tanultak szerint, azaz | ||
pt[i] | ||
Meg kell említeni még, hogy amikor egy függvényt tömbazonosító aktuális paraméterrel hívtunk meg, akkor is cím szerinti paraméter átadás történt, azaz a függvény a tömb kezdőcím konstans másolatát kapta meg, például, a veremben. Ez a címmásolat aztán persze a függvényben már nem konstans, el is lehet rontani stb. | ||
Vegyük észre, hogy a cím szerinti paraméter átadást szinte minden példánkban, már a kezdetek óta használjuk! Eddig a függvényparaméter tömböt mindig | ||
típus azonosító[] | ||
alakban adtuk meg, de legújabb ismereteink szerint a | ||
típus * azonosító | ||
forma használandó, hisz ez egyértelműen mutatja, hogy a paraméter mutató. A függvény testén belül ettől függetlenül szabadon dönthet a programozó, hogy | ||
| ||
kezeli az ilyen paramétert. | ||
Írjuk át ennek szellemében a pelda21.c datume függvényét! | ||
int datume(const char *s) { | ||
Lássuk be, hogy az ilyen fajta függvény átírásnak, amikor s[i]-ből *(s+i)-t csinálgatunk, semmi értelme sincs, hisz ezt a fordító magától is megteszi. Az átalakítás egyetlen előnye, hogy a formális paraméter jobban szemlélteti, hogy const karakterláncra mutat, ill. az észre vehető, hogy az atoi függvényt nem csak a karaktertömb kezdetével szabad meghívni. | ||
Foglalkozzunk még egy kicsit a tömbdeklarátorokkal! | ||
Tömbdeklarátor és nem teljes típusú tömb | ||
A Mutatódeklarátorok fejezetben a direkt-deklarátor definíció harmadik változata | ||
direkt-deklarátor [<konstans-kifejezés>] | ||
a tömbdeklarátor. A tömbdeklaráció tehát | ||
típus deklarátor [<konstans-kifejezés>]<={inicializátorlista<,>}> | ||
ahol az elhagyható konstans-kifejezésnek egész típusúnak és zérusnál nagyobb értékűnek kell lennie, s ez a tömb mérete. Ez a Típusok és konstansok lecke Deklaráció fejezetében írottakon túl további korlátozásokat ró a konstans kifejezésre, hogy egész típusúnak kell lennie. Operandusai ebben az esetben csak egész, felsorolás, karakteres és lebegőpontos állandók lehetnek, de a lebegőpontos konstanst explicit típuskonverzióval egésszé kell alakítani. Operandus lehet még a sizeof operátor is, aminek operandusára természetesen nincsenek ilyen korlátozások. | ||
Tudjuk, hogyha elmarad a tömbméret, akkor a fordító az inicializátorlista elemszámának megállapításával rögzíti azt, s a típus így válik teljessé. Megadott méretű tömb esetében a lista inicializátorainak száma nem haladhatja meg a tömb elemeinek számát. Ha az inicializátorok kevesebben vannak a tömbméretnél, akkor a magasabb indexű, fennmaradó tömbelemek zérus kezdőértéket kapnak. Tudjuk azt is, hogy tömb inicializátorai csak állandó kifejezések lehetnek. | ||
Ne felejtkezzünk meg róla, hogy ugyan a karaktertömb inicializátorlistája karakterlánc konstans, de az előbb felsorolt korlátozások ugyanúgy vonatkoznak rá is! Vagyis rögzített méretű tömb esetén a karakterlánc hossza sem haladhatja meg a méretet. Ha a lánchossz és a tömbméret egyezik, nem lesz lánczáró zérus a karaktertömb végén. | ||
Ha a tömbdeklarációból hiányzik a tömbméret és inicializátorlista sincs, akkor a deklaráció nem teljes típusú tömböt határoz meg. Lássuk a lehetséges eseteket! | ||
| ||
Mutatóaritmetika és konverzió | ||
A mutató vagy címaritmetika az inkrementálásra, a dekrementálásra, az összeadásra, a kivonásra és az összehasonlításra szorítkozik. A típus típusú objektumot megcímző mutatón végrehajtott aritmetikai műveletek automatikusan figyelembe veszik a típus méretét, azaz az objektum tárolására elhasznált bájtok számát. A mutatóaritmetika ezen kívül feltételezi, hogy a mutató a típus típusú objektumok tömbjére mutat, azaz például: | ||
int i = 6; | ||
esetén | ||
fptr += i; | ||
hatására az fptr-beli cím | ||
sizeof(float)*i | ||
-vel (általánosságban sizeof(típus)*egész-szel) nő, azaz a példa szerint ftomb[6]-ra mutat. | ||
Ha ptr1 a típus típusú tömb második és ptr2 a tizedik elemére mutat, akkor a két mutató különbsége | ||
ptr2 - ptr1 -> 8. | ||
Figyeljük meg, hogy a mutatókhoz indexértéket adunk hozzá vagy vonunk ki belőle és a mutatók különbsége is indexérték! Igazából a két mutató különbsége ptrdiff_t típusú egész indexkülönbség. A ptrdiff_t az stddef.h fejfájlban definiált, s többnyire signed int. | ||
Összeadás, kivonás, inkrementálás és dekrementálás | ||
| ||
Ha az operandus mutató, akkor az eggyel növelésben (++) vagy csökkentésben (--) a címaritmetika szabályai érvényesek, azaz az eredmény mutató a következő, vagy a megelőző elemre fog mutatni. | ||
Ha p mutató a tömb utolsó elemére mutat, akkor a ++p még legális érték: a tömb utolsó utáni elemének címe, de minden ezután következő mutatónövelés definiálatlan eredményre vezet. Hasonló probléma van akkor is, ha p a tömb kezdetére mutat. Ilyenkor a mutatócsökkentés - már a --p is - definiálatlan eredményt okoz. | ||
Minden nem tömbre alkalmazott címaritmetikai művelet eredménye definiálatlan. Ugyanez mondható el akkor is, ha a tömbre alkalmazott mutatóaritmetikai művelet eredménye a tömb legelső eleme elé, vagy a legutolsó utánin túlra mutat. | ||
Relációk | ||
A következő operandus típuskombinációk használhatók relációkban: | ||
| ||
Feltételes kifejezés | ||
Ha a K1 ? K2 : K3-ban K2, K3 egyike-másika mutató, akkor a K2 vagy K3 operandusok típusától függő konstrukció eredményének típusa a következő: | ||
| ||
Mutatók típusának összehasonlításakor a const vagy volatile típusmódosítók nem szignifikánsak, de az eredmény típusa megörökli mindkét oldal módosítóit. | ||
Írjuk át a címaritmetika alkalmazásával a pelda20.c- ben használt dverem.c- t! | ||
/* DVEREMUT.C: double verem push, pop és clear | ||
A vmut most valóban veremmutató a double veremben (és nem a processzoréban). v kezdőértékkel indul, és mindig a következő szabad helyre irányul. Ha kisebb, mint a veremhez már nem tartozó cím (v+MERET), akkor a push kiteszi rá a paraméterét, és mellékhatásként előbbre is lépteti eggyel a veremmutatót a következő szabad helyre. A pop csak akkor olvas a veremből, ha van benne valami (vmut>v). Kiszedéskor előbb vissza kell állítani a veremmutatót (az előtag --), s csak aztán érhető el indirekcióval a legutoljára kitett érték, s most ez lesz a következő szabad hely is egyben a következő push számára. | ||
Konverzió | ||
A fordító által automatikusan elvégzett, implicit konverziókat már megismertük a címaritmetika műveleteinél. | ||
Az explicit típuskonverziós | ||
szerkezetben a (típusnév) többnyire (típus *) alakú lesz, és ilyen típusú objektumra mutató mutatóvá konvertálja az előtag-kifejezés értékét. Például: | ||
char *lanc; | ||
Nullaértékű konstans, egész kifejezés, vagy ilyen (void *)-gal típusmódosítva konvertálható explicit típusmódosítással, hozzárendeléssel vagy összehasonlítással akármilyen típusú mutatóvá. Ez NULL mutatót eredményez, mely megegyezik az ugyanilyen típusú NULL mutatóval, de eltér bármely más objektumra vagy függvényre mutató mutatótól. | ||
Egy bizonyos típusú mutató konvertálható más típusú mutatóvá. Az eredmény azonban címzés hibás lehet, ha nem megfelelő tárillesztésű objektumot érne el. Csak azonos, vagy kisebb szigorúságú tárillesztési feltételekkel bíró adattípus mutatójává konvertálható az adott mutató, és onnét vissza. | ||
A tárillesztés hardver sajátosság, s azt jelenti, hogy a processzor bizonyos típusú adatokat csak bizonyos határon levő címeken helyezhet el. A legkevésbé megszorító a char típus szokott lenni. A short csak szóhatáron (2-vel maradék nélkül osztható címen) kezdődhet, a long viszont dupla szóhatáron (4-gyel maradék nélkül osztható címen) helyezkedhet el, s így tovább. | ||
void mutató készíthető akármilyen típusú mutatóból, és megfordítva korlátozás és információvesztés nélkül. Ha az eredményt visszakonvertáljuk az eredeti típusra, akkor az eredeti mutatót állítjuk újra elő. | ||
Ha ugyanolyan, de más, const vagy volatile módosítójú típusra konvertálunk, akkor az eredmény ugyanaz a mutató a módosító által előidézett megszorításokkal. Ha a módosítót aztán elhagyjuk, akkor a további műveletek során az eredetileg az objektum deklarációjában szereplő const vagy volatile módosítók maradnak érvényben. | ||
A mutató mindig konvertálható a tárolásához elegendően nagy, egész típussá. A mutató mérete, és az átalakító függvény persze nem gépfüggetlen. Leírunk egy, több fejlesztő rendszerben is használatos mutató-egész és egész-mutató konverziót. | ||
A mutató-egész konverzió módszere függ a mutató és az egész típus méretétől, valamint a következő szabályoktól: | ||
| ||
Az egész-mutató átalakítás sem portábilis, de a következő szabályok szerint mehet például: | ||
| ||
Karaktermutatók | ||
Tudjuk, hogy a karakterlánc konstans karaktertömb típusú, s ebből következőleg mögötte ugyancsak egy cím konstans van, hisz például a | ||
printf("Ez egy karakterlánc konstans."); | ||
kitűnően működik, holott a printf függvény első paramétere const char * típusú. Ez a konstans mutató azonban a tömbtől eltérően nem rendelkezik azonosítóval sem, tehát később nincs módunk hivatkozni rá. Ennek elkerülésére, azaz a cím konstans értékének megőrzésére, a következő módszerek ajánlhatók: | ||
char *uzenet; | ||
vagy | ||
const char *uzenet = "Kész a kávé!\n"; | ||
Karakterlánc kezelő függvények | ||
A rutinok prototípusai a szabványos string.h fejfájlban helyezkednek el. Egyik részüknek str-rel, másik csoportjuknak mem-mel kezdődik a neve. Az str kezdetűek karakterláncokkal (char *), míg a mem nevűek memóriaterületekkel bájtonként haladva (void * és nincs feltétlenül lánczáró zérus a bájtsorozat végén) foglalkoznak. A char *, vagy void * visszaadott értékű függvények mindig az eredmény lánc kezdőcímét szolgáltatják. | ||
A memmove-tól eltekintve, a többi rutin viselkedése definiálatlan, ha egymást a memóriában átfedő karaktertömbökre használják őket. Néhányat - teljességre való törekvés nélkül - felsorolunk közülük! | ||
char *strcat(char *cel, const char *forras); | ||
A függvények a cel karakterlánchoz fűzik a forras-t (strcat), vagy a forras legfeljebb első, n karakterét (strncat), és visszatérnek az egyesített cel karakterlánc címével. Nincs hibát jelző visszaadott érték! Nincs túlcsordulás vizsgálat a karakterláncok másolásakor és hozzáfűzésekor. | ||
A size_t többnyire az unsigned int típusneve. | ||
Írjuk csak meg a saját strncat függvényünket! | ||
char *strncat(char *cel, const char *forras, size_t n){ | ||
char *strchr(const char *string, int c); | ||
A rutinok a c karaktert keresik string-ben, ill. string első n bájtjában, és az első előfordulás címével térnek vissza, ill. NULL mutatóval, ha nincs is c a string-ben, vagy az első n bájtjában. A lánczáró zérus is lehet c paraméter. Az | ||
char *strrchr(const char *string, int c); | ||
ugyanazt teszi, mint az strchr, csak cstring-beli utolsó előfordulásának címével tér vissza, ill. NULL mutatóval, ha nincs is c a string-ben. | ||
intstrcmp(const char *string1, const char *string2); | ||
A függvények unsigned char típusú tömbökként összehasonlítják string1 és string2 karakterláncokat, és negatív értéket szolgáltatnak, ha string1 < string2. Pozitív érték jön, ha | ||
Az strncmp és a memcmp a hasonlítást legföljebb az első n karakterig, ill. bájtig végzik. | ||
A legtöbb fejlesztő rendszerben nem szabványos stricmp és strnicmp is szokott lenni, melyek nem kis-nagybetű érzékenyen hasonlítják össze a karakterláncokat. | ||
A saját strcmp: | ||
int strcmp(const char *s1, const char *s2 ){ | ||
char *strcpy(char *cel, const char *forras); | ||
Az strcpy a forras karakterláncot másolja lánczáró karakterével együtt a cel karaktertömbbe, és visszatér a cel címmel. Nincs hibát jelző visszatérési érték. Nincs túlcsordulás ellenőrzés a karakterláncok másolásánál. | ||
Az strncpy a forras legfeljebb első n karakterét másolja. Ha a forrasn karakternél rövidebb, akkor cel végét '\0'-ázza n hosszig a rutin. Ha az n nem kisebb, mint a forras mérete, akkor nincs zérus a másolt karakterlánc végén. | ||
A memcpy és a memmove mindenképpen n bájtot másolnak. Egyetlenként a karakterlánc kezelő függvények közül, a memmove akkor is biztosítja az átlapoló memóriaterületeken levő, eredeti forras bájtok felülírás előtti átmásolását, ha a forras és a cel átfedné egymást. | ||
A saját strcpy: | ||
char *strcpy(char *cel, const char *forras ){ | ||
size_t strlen(const char *string); | ||
A rutin a string karakterlánc karaktereinek számával tér vissza a lánczárót nem beszámítva. Nincs hibát jelző visszaadott érték! | ||
char *strpbrk(const char *string, const char *strCharSet); | ||
A függvény az strCharSet-beli karakterek valamelyikét keresik a string-ben, és visszaadják az első előfordulás címét, ill. NULL mutatót, ha a két paraméternek közös karaktere sincs. A keresésbe nem értendők bele a lánczáró zérusok. | ||
A példában számokat keresünk az s karakterláncban. | ||
#include <string.h> | ||
A kimenet a következő: | ||
1: 3 férfi és 2 fiu 5 disznót ettek. | ||
char *strset(char *string, int c); | ||
A memset feltölti string első n bájtját c karakterrel, és visszaadja string kezdőcímét. | ||
A legtöbb fejlesztő rendszerben létező, nem ANSI szabványos strset és strnset a string minden karakterét - a lánczáró zérus kivételével - c karakterre állítják át, és visszaadják a string kezdőcímét. Nincs hibát jelző visszatérési érték. Az strnset azonban a string legfeljebb első n karakterét inicializálja c-re. | ||
size_t strspn(const char *string, const char *strCharSet); | ||
Az strspn annak a string elején levő, maximális alkarakterláncnak a méretét szolgáltatja, mely teljes egészében csak az strCharSet-beli karakterekből áll. Ha a string nem strCharSet-beli karakterrel kezdődik, akkor zérust kapunk. Nincs hibát jelző visszatérési érték. A keresésbe nem értendők bele a lánczáró zérus karakterek. | ||
Az strcspn viszont annak a string elején levő, maximális alkarakterláncnak a méretét szolgáltatja, mely egyetlen karaktert sem tartalmaz az strCharSet-ben megadott karakterekből. Ha a string strCharSet-beli karakterrel kezdődik, akkor zérust kapunk. Például a: | ||
#include <string.h> | ||
eredménye: | ||
xyzabc láncban az első a, b vagy c indexe 3 | ||
char *strstr(const char *string1, const char *string2); | ||
A függvény string2 karakterlánc első előfordulásának címét szolgáltatják string1-ben, ill. NULL mutatót kapunk, ha string2 nincs meg string1-ben. Ha string2 üres karakterlánc, akkor a rutin string1-gyel tér vissza. A keresésbe nem értendők bele a lánczáró zérus karakterek. | ||
char *strtok(char *strToken, const char *strDelimit); | ||
A függvény a következőleg megtalált, strToken-beli szimbólum címével tér vissza, ill. NULL mutatóval, ha nincs már további szimbólum az strToken karakterláncban. Mindenegyes hívás módosítja az strToken karakterláncot, úgy, hogy '\0' karaktert tesz a bekövetkezett elválasztójel helyére. Az strDelimit karakterlánc az strToken-beli szimbólumok lehetséges elválasztó karaktereit tartalmazza. | ||
Az első strtok hívás átlépi a vezető elválasztójeleket, visszatér az strToken-beli első szimbólum címével, és ezelőtt a szimbólumot '\0' karakterrel zárja. Az strToken maradék része további szimbólumokra bontható újabb strtok hívásokkal. Az strToken következő szimbólumát az strToken paraméter helyén NULL mutatós strtok hívással lehet elérni. A NULL mutató első paraméter hatására az strtok megkeresi a következő szimbólumot a módosított strToken-ben. A lehetséges elválasztójeleket tartalmazó strDelimit paraméter, s így maguk az elválasztó karakterek is, változhatnak hívásról-hívásra. | ||
A példaprogramban ciklusban hívjuk az strtok függvényt, hogy megjelentethessük az s karakterlánc összes szóközzel, vesszővel stb. elválasztott szimbólumát: | ||
#include <string.h> | ||
A kimenet a következő: | ||
Szimbólumok karakterlánca | ||
Mutatótömbök | ||
Mutatótömb a | ||
típus *azonosító[<konstans-kifejezés>] <={inicializátorlista}>; | ||
deklarációval hozható létre. Az azonosító a mutatótömb neve: konstans mutató, melynek típusa (típus **), vagyis típus típusú objektumra mutató mutatót címző mutató. Tehát olyan konstans mutató, mely (típus *) mutatóra mutat. (Rekurzívan használtuk a * operátort.) A tömb egy eleme viszont (típus *) típusú változó mutató. | ||
Kezdjünk el egy példát magyar kártyával! | ||
A színeket kódoljuk a következőképp: makk (0), piros (1), tök (2) és zöld (3). A kártyákat egy színen belül jelöljük így: hetes (0), nyolcas (1), kilences (2), tízes (3), alsó (4), felső (5), király (6) és ász (7). A konkrét kártya kódja úgy keletkezik, hogy a színkód után leírjuk a kártya kódját. Ilyen alapon a 17 például piros ász. | ||
Készítsünk char *KartyaNev(int kod) függvényt, mely visszaadja a paraméter kodú kártya megnevezését! | ||
/* KARTYA.H fejfajl. */ | ||
Vigyázzunk nagyon a típusokkal! A szin és a kartya statikus élettartamú, belső kapcsolódású, fájl hatáskörű, karakterláncokra mutatók tömbjeinek kezdeteire mutató mutató (char **) konstansok nevei. A szin[1] a piros karakterlánc kezdetének címét tartalmazó, karakteres mutatótömb elem (char *). Igazak például a következők: | ||
*szin | ||
A szoveg statikus élettartamú karaktertömb, így nem szűnik meg a memóriafoglalása a függvényből való visszatéréskor, csak lokális hatáskörű azonosítójával nem érhető el! | ||
Írjunk rövid, kipróbáló programot! | ||
/* PELDA23.C: Kartya megnevezesek kiiratasa. */ |
Feladatok | |||||||||||||||||||||||||||||||
1. Írja át a pelda18.c-ben bemutatott egesze függvényt, hogy a paraméter karakterlánc formai helyességének eldöntésén kívül még a konvertált értéket is letárolja egy második paraméterben adott címen! | |||||||||||||||||||||||||||||||
/* PELDA18Y.C: Egesz szamok atlaga, minimuma, maximuma es | |||||||||||||||||||||||||||||||
2. Az egyszeri programozó megpróbálta áítírni a pelda20.c-beli lebege függvényt úgy, hogy a paraméter karakterlánc formai helyességének eldöntésén kívül még a konvertált értéket is letárolja egy második paraméterben adott címen. Sajnos nem sikerült neki. | |||||||||||||||||||||||||||||||
/*01*/#include <stdlib.h> | |||||||||||||||||||||||||||||||
Jelölje meg azokat a sorokat, amelyekkel a program helyes működésre bírható!
![]() | |||||||||||||||||||||||||||||||
3. Készítse el a getline függvénynek azt a változatát, ami tömbindexelés helyett mindenütt karakteres mutatókkal dolgozik! | |||||||||||||||||||||||||||||||
int getline(char *s, int n) { /* Sor beolvasása s-be. */ | |||||||||||||||||||||||||||||||
4. Programozónk megpróbálta elkészíteni azt a függvényt, amely kitörli az s karakterláncból a c karaktereket. Sajnos nem sikerült neki tökéletesen. | |||||||||||||||||||||||||||||||
/*1*/char *squeeze(char *s, int c) { | |||||||||||||||||||||||||||||||
Jelölje meg azokat a sorokat, amelyekkel a program működőképessé tehető!
![]() | |||||||||||||||||||||||||||||||
4. A kezdő programozó megpróbált készíteni egy olyan függvényt az strtok mintájára, ami minden hívás alkalmával a t karakterlánc következő előfordulásának helyét adja vissza s-en belül. Ha már nincs több előfordulás, azt a NULL visszatérési értékkel jelzi. A függvény azonban nem lett tökéletes. | |||||||||||||||||||||||||||||||
/*1*/char *strstrnext(const char *s,const char *t) { | |||||||||||||||||||||||||||||||
Jelölje meg azokat a sorokat, amelyekkel helyes működésre bírható!
![]() |