Újabb vezérlési szerkezetek. Operátorok. Struktúrák
Tartalom
- Emlékeztető: absztrakció és dekompozíció
- Újabb vezérlési szerkezetek
- Hátultesztelő ciklus
break
éscontinue
- Esetszétválasztás – a cél
- Esetszétválasztás – a
switch()
utasítás - Operátorok
- Operátorok: precedencia, asszociativitás
- Polimorfizmus és konverziók
- Operátorok: főhatás és mellékhatás I.
- Operátorok: főhatás és mellékhatás II.
- A
++
és--
értékadó operátorok - A ++ és -- tipikus használata
- Rövidzár és feltételes kiértékelés
- Vessző operátor
- Kiértékelési pontok
- Függvény paramétereinek kiértékelése
- A szünetre: mit ír ki?
- Struktúrák
- Absztrakció adatokon is
- Típuskonstrukció
- Összetett típus: struktúrák
- Struktúrák használata
- Struktúra, mint egység
- Struktúrák kezdeti értéke
- A
typedef
kulcsszó - Típusok láthatósága: lokális és globális
- Törtes példa: komplex feladat
- (Többszörösen) összetett adatok
- Struktúrák vs. tömbök
- Sztring, mint összetett típus
- 2D tömb
- Többszörös összetétel: definíciók
- Többszörös összetétel: geometria példa
1 Emlékeztető: absztrakció és dekompozíció
Felülről lefelé tervezés
Szám kiírása adott számrendszerben. Pl. 125tíz → 1, 2, 5.
A nehézség: balról jobbra kell kiírnunk a számjegyeket.
for (i=SZÁMJEGYEK_SZÁMA…-1; i>=0; i-=1) // ciklus visszafelé printf("%c", '0' + N_EDIK_SZÁMJEGY…);
Ha vannak ilyen függvényeink, nem is olyan nehéz!
Függvények
Főprogram, alprogram: a főprogram megáll, amíg az alprogram dolgozik.
- Az alprogramok nevet kapnak:
prim()
- Bemenettel, kimenettel rendelkeznek:
int prim(int szam);
- Építőkőként használhatóak:
if (prim(i)) …
A programnyelvek általában háromféle eszközt adnak a kezünkbe, amelyekkel a programunkat felépíthetjük:
- Primitívek – az alapvető építőkövek
- Kombináció – hogyan lehet belőlük építkezni
- Absztrakció – hogyan kezelhető az építmény építőkőként
Primitívnek számít például egy összeadás. Kombinációnak egy összetett kifejezés: 3+4+5
,
ebben két összeadást kombináltunk. Absztrakciónak egy függvény.
Primitív adatnak egy beépített típus, pl. egy egész szám. Az adatok absztrakciójáról eddig még nem nagyon
volt szó – de ezen az előadáson lesz.
3 Hátultesztelő ciklus
- A ciklusfeltétel ellenőrzése a ciklusmag után történik
- Emiatt a ciklusmag legalább egyszer végrehajtódik
- Az első végrehajtás a feltételtől függetlenül megtörténik
do utasítás; while (feltétel);
do { utasítások… } while (feltétel);
A feltétel a kódban is alul van. Ez emlékeztet arra, hogy csak a ciklusmag után ellenőrzi.
Példa: 5 darab lottószám – amikor kitalálunk egy újat, megnézzük, volt-e már. Ha igen, újra megpróbáljuk.
int szamok[5]; for (i=0; i<5; i+=1) { // 5 darab szám kell do { szamok[i]=rand()%90+1; mar_van=0; // van már ilyen? (lineáris keresés) for (j=0; j<i; j+=1) if (szamok[j]==szamok[i]) mar_van=1; } while (mar_van); // ha van, újra! }
Természetesen a fenti programot egy pillanat
alatt át lehetne írni elöltesztelő ciklusra. Ha elöl
lenne a mar_van
tesztelése, csak annyit kellene
tenni, hogy azt a ciklusba belépés előtt IGAZ-ra állítjuk;
mert akkor először biztosan bemegyünk a ciklus belsejébe:
mar_van=1; while (mar_van) { szamok[i]=rand()%90+1; mar_van=0; /* nezzuk meg, van-e ilyen */ for (j=0; j<i; j+=1) if (szamok[j]==szamok[i]) mar_van=1; }
Hogy mi a különbség a kettő között? Az, hogy itt praktikusabb
a hátultesztelő, mert egy számot biztosan kell sorsolnunk. Még egy
különbség van: az elöltesztelőnél kvázi trükközni kell, hogy először
bemenjünk a ciklusba, és emiatt tartalmaz egy furcsa kódsort. Az oda nem
illő sor a ciklus előtti mar_van=1
– ez valami olyasmit
állít, ami nem igaz. Hogy állíthatjuk azt, hogy már van olyan szám,
ha még nem is sorsoltunk?
A teljes program a lottószámok generálására letölthető innen: lotto.c.
4 break
és continue
Működésük
break
: megszakítja a ciklus futásátcontinue
: a következő iterációval folytatja a ciklust
while (…) { … if (…) ┌─── break; │ … │} └>
┌─> while (…) { │ … │ if (…) └─── continue; … }
Használatuk
- Néha áttekinthetőbb a kód
- Általában nem… Csak a ciklust kellene jobban szervezni!
- Ezért próbáljuk kerülni a használatukat!
continue
– vigyázat, a for()
ciklusnál
a következő iteráció azt jelenti, hogy a ciklus fejlécében megadott
műveletet, az utótevékenységet még végrehajtja!
Ezek az utasítások nem strukturált vezérlési
szerkezeteket eredményeznek. Vagyis nem felelnek meg sem a szekvenciának, sem
a ciklusnak, sem az elágazásnak. Csak nagyon indokolt esetben használjuk őket!
A break
és continue
használatára
egész félévben gyakorlatilag nem fogunk példát mutatni. Legyen az egész
félév összes programja példa inkább arra, hogy nagyon jól meg lehet
lenni break
és continue
nélkül is. És főleg
goto
nélkül!
5 Esetszétválasztás – a cél
Gyakori fordulat
Egy változó lehetséges értékei alapján különféle dolgokat kell csinálni:
1. Adatbevitel 2. Módosítás 3. Kimutatás … 0. Kilépés Választás:
if (valasztott==1) { … } else if (valasztott==2) { … } else { … }
A sorozatos if …; else if …
kiváltására
használható a switch()
.
6 Esetszétválasztás – a switch()
utasítás
printf("Töröljem a fájlt? (I)gen vagy (n)em? "); scanf(" %c", &valasz); switch (valasz) { case 'I': /* kis- és nagybetű is jó */ case 'i': printf("Igent válaszoltál, törlöm!\n"); break; case 'N': case 'n': printf("Nemet válaszoltál, meghagyom.\n"); break; default: printf("Érvénytelen válasz!\n"); break; }
(A fenti scanf()
-ben a %c
előtti szóköz szándékos. Ez
annyit tesz, hogy a karakter beolvasása előtt kapott összes szóköz, újsor és
tabulátor (whitespace) karaktert eldobja.)
A switch()
szerkezeten belül
az egyes értékekhez tartozó, több utasításból álló kódot
nem kell utasításblokkba tenni. Ezért kell a break
utasítás mindegyiknek a végére; azzal jelezzük, hogy vége van az
ahhoz az értékhez tartozó utasítássorozatnak. Fogalmazhatunk
így is: a case
kulcsszavakkal jelölt helyek a switch()
utasításon belüli utasításszekvenciába belépési pontok.
Amelyiknek megfelel a kifejezés értéke, oda ugrik a végrehajtás.
Ha nem teszünk break
-et az utasítások után, akkor a végrehajtás
továbbmegy a következő belépési pontnál található utasításokra, és végrehajtódnak
azok is! Ezt használjuk ki akkor, amikor több case
-t
írunk egymás után: fent a case 'I'
után nincs utasítás, de break
sincs.
Emiatt szokás a lenti módon használni a switch()
szerkezetet:
minden csoportban előbb a case
-ek segítségével felsorolt
lehetőségek, utána az utasítások, végül a break
:
switch (kifejezés) { // egész számra kiértékelődő case érték1: case érték2: // több is lehet, de konstansok case érték3: … break; case érték4: … … // több utasítás is lehet … break; default: // ha egyik sem (opcionális) … break; }
A switch()
lehetőségei korlátozottabbak annál, mint ami egy if() – else
sorozattal kifejezhető. A legfontosabb megkötés az, hogy a case
kulcsszavaknál
megadott értékek csak egész típusúak lehetnek (ide értve természetesen a karaktereket is, mert
azok is egész számok). A switch()
fejlécében használt kifejezésnek is egész számra
kell kiértékelődnie.
8 Operátorok: precedencia, asszociativitás
Operátorok
- Kifejezések építőkockái
- Pl. matematikai műveletek: +, -, *, /
- Operandusok: amiken a műveletet végzik
-x
unáris (unary),x-y
bináris (binary), azaz egy- és kétoperandusú
Precedenciák: erősségek
- Különfélék között: melyik művelet „erősebb”
- A szorzás „erősebb”, ezért 5+2*3 = 5+(2*3)
- Ha másképp szeretnénk, zárójelezünk
Asszociativitás: mik az operandusok
- Egyformák között: csoportosíthatóság
- Összeadás balra:
a+b+c
→(a+b)+c
- Értékadás jobbra:
a=b=c
→a=(b=c)
.
9 Polimorfizmus és konverziók
int a=5, b=2; double c=5, d=2; double x; x = c / d; // 2.5 printf("%g\n", x); x = a / b; // 2 printf("%g\n", x); x = (double)a / b; // 2.5 printf("%g\n", x);
Polimorfizmus (többalakúság)
Az operátorok jelentése függhet az operandusok típusától:
a/b
: osztás. Haa
ésb
isint
, egész osztás. Ez lefelé kerekít!- Ha bármelyik lebegőpontos, az eredmény is. Ha kell, a másik automatikusan konvertálódik.
Az egész osztás sok esetben hasznos: például ha az a kérdésünk, hogy 5500 Ft kifizetéséhez hány ezresre van szükségünk. 5500/1000 = 5 darab ezres, és 5500%1000 = 500 Ft a maradék, amelyet máshogy kell megoldanunk, nem ezresekkel.
Automatikus konverzió egyéb
esetekben is történik. Pl. short+int
összeadás esetén a
short
típusú operandus a nagyobb ábrázolási tartományú
int
típusúvá konvertálódik. Ugyanígy,
int+long
esetén az összeadás előtt az int
konvertálódik automatikusan, az 5+2.3
kifejezésben pedig
az 5
-ből lesz 5.0
. Mindig a nagyobb ábrázolási tartomány
felé történik az automatikus konverzió, hogy ne amiatt legyen adatvesztés
vagy túlcsordulás.
Kézi konverzió (cast)
- Ha két
int
van, de lebegőpontos osztást szeretnénk, jelezni kell - Az egyik operandust
double
-lé alakítva, elé írva:(double)
- Ez a konverziós operátor (cast)
Fontos megfigyelni, hogy a c=a/b
kifejezésben
az eredmény még így is 2
, hogy utána azt egy double
típusú
változóba másoljuk! Az értékadás egy másik operátor, amelynek az osztás eredményére
már nincsen hatása. Az osztás egész/nem egész jellege nem azon múlik, hogy az
elvégzése után mit csinálunk az eredménnyel!
A konverzió segítségével más típusúvá alakítható egy érték.
Egy lebegőpontos érték elé
(int)
-et írva egésszé alakítható az, természetesen a
törtrészt elveszítve.
Számtani műveletek esetén ritkán kell kézi konverziót alkalmazni. Más típusoknál, a mutatóknál, amelyek egy későbbi előadáson fognak szerepelni, nagyobb szerepet kapnak a konverziók. De ezekről majd később.
10 Operátorok: főhatás és mellékhatás I.
Emlékeztető: függvények fő- és mellékhatása
int osszeg(int a, int b) { printf("Összeadom: %d és %d\n", a, b); // mellékhatás return a+b; // főhatás }
- Főhatás: a függvénynek értéke van
- Mellékhatás: minden egyéb, aminek nyoma van, pl. kiír valamit
Operátoroknál
- Főhatás: a kiértékelés után a kifejezésnek értéke van
- Mellékhatás: történhet más is, pl. megváltozik egy változó értéke
11 Operátorok: főhatás és mellékhatás II.
Összeadás, a+b
- Főhatás: az összeg kiszámolódik, mellékhatás: nincs
Értékadás, a=b
- Mellékhatás:
b
értékea
-ba másolódik - Főhatás: az egész
a=b
kifejezés értéke a másolt érték - A főhatás miatt az értékadás láncolható:
a=b=1; a=(b=1); /* ugyanaz */ b=1; a=b; /* ez is */
printf("%d", a=b);
a=b; printf("%d", a);
- Balérték=jobbérték (lvalue=rvalue).
Jobbérték: ami kiértékelhető, balérték: aminek érték adható.
Vegyük észre, hogy a mellékhatással nem
rendelkező operátorokkal leírt képletek mindig ugyanazt az eredményt
adják! 5*2+4
mindig 14 lesz; míg pl.
a+=1
-ről ez nem mondható el, hiszen többször egymás
után kiértékelve mindig más eredményt kapunk. Az =
operátort a mellékhatása miatt használjuk, a főhatásával ritkán
törődünk. Az utóbbi viszont lehetővé teszi a láncolt értékadást: pl.
az a=b=c=0
kifejezés mindhárom változót nullázza. Akár
kifejezőbb is lehet, mint külön leírni mind a három nullázást.
Az értékadás főhatása miatt olyanokat is lehet
írni, mint a fenti printf
– de nem érdemes. Az ilyesmi
csak zavart okoz. Bár a fordító által készített gépi kód úgyis
teljesen ugyanaz lesz, inkább kerüljük a felesleges tömörítést! Jobb
külön, két sorba leírni a két, egymástól logikailag független teendőt
(értékadás, kiírás).
A balérték, jobbérték kifejezéseket ismerni kell,
mert a fordító hibaüzeneteiben gyakran megjelennek. Például az 5=6
kifejezésre az „lvalue required as left operand of assignment” jelzést kapjuk,
vagyis hogy az értékadás bal operandusaként egy balérték kell szerepeljen.
Ugyanígy helytelen emiatt az 5=a
kifejezés is.
A fentiek érvényesek az összes kombinált értékadó
operátorra is: +=
, -=
, >>=
stb.
12 A ++
és --
értékadó operátorok
#include <stdio.h> int main() { int a; a=5; printf("előtte: %d\n", a); printf("értéke: %d\n", ++a); printf("utána: %d\n", a); printf("\n\n"); a=5; printf("előtte: %d\n", a); printf("értéke: %d\n", a++); printf("utána: %d\n", a); return 0; }
A ++
és --
operátorokat inkremens (increment) és dekremens (decrement)
operátoroknak nevezzük, és az értékadó operátorok közé tartoznak.
Jelentésük „következő” és „előző”. Mindkét operátornak két változata van,
egy poszt (post) és egy pre forma. A kettőt az különbözteti meg, hogy az
operátort a változó neve elé vagy mögé írjuk: az a++
kifejezés a posztinkremens operátort, a
++a
pedig a preinkremenst használja.
A mellékhatása mindkét alaknak ugyanaz. A ++a
és
az a++
kifejezés is megnöveli az a
változót
eggyel. De míg a posztinkremensnél a
kiértékelés után (poszt) növelődik meg, és ezért a kifejezés értéke
még régi, növelés előtti állapotot mutatja, a
preinkremensnél a kifejezés értéke a már megnövelt értékkel
lesz egyenlő (pre, azaz kiértékelés előtt). Ezt onnan lehet
megjegyezni, hogy a posztinkremensnél: a++
az operátor
a változó neve után van, vagyis előbb vesszük az értékét, és
később növeljük, a preinkremensnél a változó neve előtt.
Ennek megfelelően, ha a kifejezések értékét (főhatását) is használjuk, a két forma eltérően írható át külön utasításokra:
Preinkremens esetén
a=5; printf("%d", ++a); // 6
a=5; ++a; printf("%d", a); // 6
Posztinkremens esetén
a=5; printf("%d", a++); // 5
a=5; printf("%d", a); // 5 ++a;
A dekremens párjaik ugyanígy működnek.
13 A ++ és -- tipikus használata
Ciklusban vagy önmagában
i+=1;
++i;
for (i=0; i<100; ++i) …
Nem használjuk a főhatást. Itt mindegy, melyik formát írjuk.
Tömb feltöltése
szamok[db++]=szam_beolvas();
0. | 1. | 2. | 3. | 4. |
---|---|---|---|---|
12 | 43 |
Posztfix: a darabszám régi értéke az index; oda beírjuk, utána növeljük.
Pl. ha db=2
, az új elem szamok[2]
helyre kerül, utána db=3
lesz.
Ami pont stimmel, hiszen az 5 elem a szamok[0]…szamok[4]
helyeken van.
Vegyük észre: kényelmes dolog, hogy 0-tól számozódnak a tömbindexek!
14 Rövidzár és feltételes kiértékelés
A logikai &&
, ||
rövidzár tulajdonsága
Ha a bal oldal alapján eldől az eredmény, a jobb ki sem értékelődik!
A&&B
: ha A=HAMIS, nem számít B, az egész biztosan HAMISA||B
: ha A=IGAZ, a kifejezés értéke biztosan IGAZ
if (b!=0 && a/b>3) // elkerüljük a 0-val osztást!
A jobb oldali rész lehet, hogy nem értékelődik ki – ez problémás lehet akkor, ha
olyan kifejezés van ott, aminek van mellékhatása.
Pl. if ((x=a/2) && (y=b/2))
kódrészlet:
ha a/2
értéke 0, y
-ba nem másolódik be b/2
.
Az ilyen program nagyon nehezen érthető. Lehetőleg kerüljük, ne írjunk ilyeneket!
Ne használjunk olyan kifejezést az &&
és a ||
operátorok
operandusában, amelynek mellékhatása van!
?:
feltételes operátor
- Az egyetlen 3 operandusú operátor (ternary operator)
- Formája:
feltétel ? igaz_kif : hamis_kif
- Ha a feltétel igaz, akkor a kifejezés értéke
igaz_kif
- Ha hamis, akkor pedig
hamis_kif
y = x<0 ? -x : x;abszolút érték
nagyobb = a>b ? a : b;a nagyobbik
Erre ugyanaz érvényes, mint a fentire. A két kifejezés közül csak az egyik fog kiértékelődni. A másiknak a mellékhatása is elmarad.
15 Vessző operátor
Több kifejezésből csinál egyet; értéke az utolsó kifejezés értéke.
- Pl.
i=0, j=1
két értékadás, balról jobbra sorrendben - Leggyakrabban
for
ciklus fejlécébenunsigned i, j; for (i=0, j=1; i<5; i++, j<<=1) /* erőltetett példa */ printf("Kettő %u. hatványa: %u\n", i, j);
Kettő 0. hatványa: 1 Kettő 1. hatványa: 2 Kettő 2. hatványa: 4 Kettő 3. hatványa: 8 Kettő 4. hatványa: 16
- Ne vigyük túlzásba a használatát!
Függvényhívás paraméterei: az nem vessző operátor!
16 Kiértékelési pontok
A mellékhatások kiértékelési pontoknál (sequence point) érvényesülnek.
i++ + i++ = ?!
- Utasítás végén:
;
vagy}
if
,while
,for
feltétele után- Néhány operátornál menet közben:
,
és?:
&&
és||
: a bal oldal után- Függvényhívás előtt az összes paraméter kiértékelődik
„A szabvány által nem definiált”
- A kiértékelési pontokon túl a mellékhatások sorrendje kötetlen!
- A függvényparaméterek kiértékelési sorrendje kötetlen
- Ha kell, több sorba töréssel, segédváltozók használatával kényeszeríthetjük a sorrendet.
Ne írjunk olyan kódot, ahol több mellékhatás van két szomszédos kiértékelési pont között!
Például, de ez nem törvényszerű, az alábbi printf()
kétszer
írhatja ki a 3-as számot:
int a=1; printf("%d %d", ++a, ++a);
3 3
A szabvány csak annyit mond, hogy a kiértékelés előtt valamikor meg kell történnie a növelésnek; ez a fordító mindkét növelést megtette a kiértékelések előtt. Másik fordító, másik gépen, másik beállításokkal esetleg más kódot generálhatna. Mégis mindkettő megfelelne a szabványnak! Ugyanezen kódrészlet kimenete lehet akár „2 1” is:
int a=1; printf("%d %d", ++a, ++a);
2 1
Itt a második ++a
-hoz tartozó
növelés történt meg előbb. Csak annyiban lehetünk biztosak, hogy a printf()
hivása
előtt már mindkettő megtörtént!
Ezért be kell tartani a következő szabályokat:
- Ne zsúfoljunk egy kifejezésbe több mellékhatással rendelkező műveletet!
- Ne keverjük a mellékhatással rendelkező és a rövidzáras operátorokat!
- Ne tegyünk az
if
,while
… utasítások feltételébe fölöslegesen mellékhatás kifejezést!
Ilyenekre úgysem lesz szükség programozás közben. Ha mégis megsértjük ezeket a szabályokat, nemcsak azt kockáztatjuk, hogy követhetetlen és olvashatatlan lesz a programunk, hanem azt is, hogy egyszerűen nem fog működni. Különböző fordítók (de még akár ugyanaz a fordító is, más beállítások mellett) másképpen fogják értelmezni a kódot! Ettől nem rossz a C. Sőt emiatt lehet gyors, és emiatt van minden elképzelhető fajta számítógépre C fordító. Csak be kell tartanunk a játékszabályokat.
17 Függvény paramétereinek kiértékelése
#include <stdio.h> int egy() { printf("egy\n"); return 1; } int ketto() { printf("ketto\n"); return 2; } int main() { printf("%d %d\n", egy(), ketto()); return 0; }
Ez a program a fentieket szemlélteti. A meglepő fordulat ez lehet a kimeneten:
ketto egy 1 2
Előbb a jobb oldali paramétert értékelte ki, ezért
íródott ki előbb a ketto
. Vegyük észre,
hogy itt is egy mellékhatással állunk szemben; a függvény
mellékhatása a képernyőre írás, a fő hatása a visszatérés
2-vel. Mivel a függvény paramétereinek kiértékelési sorrendjét
a fordító szabadon megválaszthatja, a mellékhatások esetleg
a nem várt sorrendben történhetnek meg.
A fenti példa alapján látható, miért veszélyes egy ilyen programrész:
/* beolvas egy számot a billentyűzetről */ int beolvas(void); /* kivonás */ printf("A különbség: %d", beolvas()-beolvas());
Nem tudhatjuk, hogy a bal vagy a jobb oldali beolvas()
fog először meghívódni, vagyis hogy a program a kisebbítendőt vagy
a kivonandót kéri először. Ezt úgy lehet javítani, hogy külön kiértékelési
pontokat vezetünk be:
a = beolvas(); /* kiértékelési pont! */ b = beolvas(); /* ez is! */ printf("A különbség: %d", a-b);
18 A szünetre: mit ír ki?
#include <stdio.h> int main() { double a=2, b=3, c=5; printf("%f", a --- b+c ); return 0; }
20 Absztrakció adatokon is
Racionális számok
┌ a c ┐ ┌ e g ┐ ad+cb eh+gf (ad+cb)(eh+gf) ─ + ─ · ─ + ─ = ───── · ───── = ─────────── └ b d ┘ └ f h ┘ bd fh bdfh
össze?!
i=(a*d+c*b)*(e*h+g*f); j=b*d*f*h;
Mi hiányzik nekünk? Az adat absztrakciója!
Tort a, b, c, d; saját típus Tort osszeg(Tort t1, Tort t2); saját műveletek Tort szorzat(Tort t1, Tort t2); x = szorzat(osszeg(a, b), osszeg(c, d));
21 Típuskonstrukció
(1. előadás)
Típus: értékkészlet és hozzá tartozó műveletek.
Egyszerű, beépített típusok:
- Egész számok:
int
,long int
stb. - Lebegőpontos számok:
float
,double
- Karakterek:
char
- Logikai:
int
Összetett, származtatott típusok:
- Tömb: tároló; egyforma típusú elemek sorban, sorszámozva
- Pl.
int t[10]
→t[0]
…t[9]
, 10 darab egész szám - Speciális tömb a sztring: karakterek végjeles sorozata
- Pl.
- Struktúra: összetartozó adatok
22 Összetett típus: struktúrák
Struktúrák: mire jók?
────────
nevező
- Új típus létrehozása
- Absztrakt fogalom reprezentációja egyszerűbb, meglévő típusokkal
- Összetartozó adatok egységként!
Pl. két tört számlálója és nevezője struktúra nélkül:
int asz, an, bsz, bn; // a és b tört, számlálók és nevezők
Ehelyett a törteket egységként kezelve, struktúrával:
struct Tort { int szaml, nev; // tört: számláló és nevező }; struct Tort a, b; // a és b tört
A változók és típusok elnevezésénél érdemes figyelni a következetességre,
mert az megkönnyíti a programok írását és megértését. Sok helyen
szabályokat is alkotnak erre, amelyeket egy adott cégnél vagy programozói
közösségnél szigorúan betartanak. Egyik ilyen elterjedt szokás az, hogy a saját típusok
neveit nagybetűvel kezdik, a változókat pedig kicsivel. Ezért lett a tört
struktúra neve a fenti példákban a nagybetűs Tort
, az egyes példányok neve
pedig a kisbetűs a
és b
.
23 Struktúrák használata
Definíció szintaxisa
struct név { definíció
T1 mező1, mező2, …;
T2 mező3;
…
};
Definíció és példányosítás
struct Pont { double x, y; }; struct Pont p1, p2;
Az egyes mezők deklarációjának
szintaktikája megegyezik a változók deklarációinak szintaktikájával: Típus név;
.
Csak itt nem változó lesz belőlük, hanem egy struktúra adattagjai lesznek.
T1, T2… bármilyen, már létező típusok lehetnek. A struktúra neve is bármi lehet, ami még nem foglalt.
Hasonlóan, a mezők különböző nevűek kell legyenek – azonban az megengedett, hogy különböző struktúrák
ugyanolyan mezőneveket tartalmazzanak. Pl. a Pont2D struktúra mezői lehetnek x és y, a Pont3D struktúra
mezői pedig x, y és z.
Mezőkre (adattagokra) hivatkozás
p pont: (3;6)
struct Pont p; p.x=3; // az x koordinátája legyen 3 p.y=6; printf("p pont: (%f;%f)", p.x, p.y);
A struktúra mezőkből áll, más néven: tagok vagy adattagok (member).
Adott mezőre ponttal hivatkozunk: változó.mezőnév. Pl.
p.x
jelentése: a p
pont x koordinátája.
Ebben p
típusa struct Pont
, p.x
típusa pedig double
.
Egy adattag teljesen ugyanúgy tud viselkedni, mint bármelyik másik változó: érték adható neki, kifejezésekben
szerepelhet, printf()
kiírja, scanf()
beolvassa. Sajnos ez utóbbi
függvények a struktúrát, mint egészt, nem tudják kezelni.
24 Struktúra, mint egység
Értékadás
struct Pont p1, p2; p1=p2; // minden mezőt másol: p1.x=p2.x; p1.y=p2.y;
Függvény paramétere, visszatérési értéke
/* megadja a pont origótól mért távolságát */ double origo_tavolsag(struct Pont p) { return sqrt(p.x*p.x + p.y*p.y); // pont típusú paraméterből } struct Pont a; printf("%f", origo_tavolsag(a));
Visszatérési érték is lehet.
25 Struktúrák kezdeti értéke
Struktúrák inicializálása
struct Pont { double x, y; }; struct Pont p = { 2, 5 }; // inicializálás: p.x→2 és p.y→5
Az egyes értékek a definíció sorrendje szerint meghatározott módon kerülnek a mezőkbe. Vigyázni kell, ha megváltoztatjuk a sorrendet!
Szóhasználat: értékadás != inicializálás
- Inicializálás: a változó definiálásakor a kezdeti értéket is megadjuk
- Értékadás: később új értéket adunk neki
- Ez nem ugyanaz, csak mindkettő jele az
=
jel!
26 A typedef
kulcsszó
A typedef
kulcsszóval egy típusnak adhatunk új nevet:
typedef int Egesz; // meglévő név és új név typedef char Betu; Egesz x; // x egész, vagyis int Betu b;
A typedef
kulcsszóval egy meglévő
típusnak adhatunk egy új nevet. Olyan nevet érdemes adni, amelyik
számunkra beszédesebb és jobban kifejezi az adott típus szerepét.
Itt is hasonló a szintaktika, mint a változó deklarációjánál: előbb
a típus, utána a név. Csak a névből nem változó neve lesz, hanem a
típusnak egy másik neve.
Struktúráknál gyakran használjuk:
struct Pont { double x, y; }; typedef struct Pont Pont; Pont p;
typedef struct Pont { double x, y; } Pont; Pont p;
Mindkét forma ugyanazt jelenti.
A struktúrák esetén leginkább arra használjuk, hogy spórolni
lehessen a gépeléssel: typedef struct Pont Pont
után nem kell mindig kiírni, hogy struct Pont
, elég
annyit, hogy Pont
. Lustaság, fél egészség. A jobb oldalt látható
szintaktikával a struktúra definíciója és az új név megadása
összevonható. Ilyenkor a sturktúrának nem is lenne kötelező
nevet adni, vagyis az első Pont
szó elhagyható lenne.
Ilyennel is gyakran találkozni C programokban. A struktúra
maga ilyenkor névtelen (anonymous structure):
typedef struct { double x, y; } Pont;
A struktúra neve (Pont), és a typedef
segítségével adott
név nem kötelezően egyforma. De ha nem így teszünk, csak összevisszaságot
okozunk vele, úgyhogy érdemes úgy megadni, hogy egyformák legyenek.
27 Típusok láthatósága: lokális és globális
A típusokat általában globálisan adjuk meg: mindenhol látszódjanak.
/* globálisan */ typedef struct Tort { int szaml, nev; } Tort; int fuggveny() { Tort t1, t2; // látható } int masik_fuggveny() { Tort b; // ez is }
/* lokálisan */ int fuggveny() { typedef struct Tort { int szaml, nev; } Tort; Tort t1, t2; // látható } /* Ez így HIBÁS! */ int masik_fuggveny() { Tort t; // ismeretlen! }
A saját típusainkat definiálhatjuk lokálisan és globálisan. A típusok általában azért globálisak, mert a programunk adatai azokon belül több helyen is előkerülnek. Vagyis több függvényben is. Ennek ellenére természetesen lehetséges az, hogy egy adott típus csak egy függvényen belül létezik. Ha csak ott használjuk, akkor érdemes lokálisan megadni, mert akkor követhetőbb a program mások számára.
28 Törtes példa: komplex feladat
─
3
Racionális számok
Feladat: a C nyelv nem tartalmaz tört típust. Hozzunk létre egyet! Írjuk meg az ezeket összeadni, szorozni, kiírni tudó programrészeket!
Megoldás
- Ez új típus! Saját értékkészlet és műveletek!
- Összetartozó adatok is. Ezért ez egy struktúra lesz!
- A műveletek pedig függvények.
A törtek struktúrája
typedef struct Tort { // függvényen kívül: globális int szaml, nev; } Tort; int main() { Tort t1; // a typedef miatt elég annyi, hogy Tort t1.szaml=1; // 1/2 t1.nev=2; return 0; }
Mivel a struktúrát több függvény is használja, globálisan definiáljuk.
Tört kiírása
A printf()
nem ismeri a tört típust, ezért
a kiírást nekünk kell megoldanunk. Ezt szeretnénk:
Tort t1; t1.szaml=2; t1.nev=3; tort_kiir(t1); // 2/3 jelenjen meg
A függvény nem tér vissza semmivel, csak kiírja a törtet.
/* Kiírja a törtet számláló/nevező alakban */ void tort_kiir(Tort t) { printf("%d/%d", t.szaml, t.nev); }
Tört valós értéke
Szükségünk lehet a tizedes törtre is:
Tort x={2, 3}; printf("%f\n", tort_valos(x)); // 0.666667
A függvény egy törtből csinál double
típusú lebegőpontos számot.
/* Visszatér a tört lebegőpontos értékével */ double tort_valos(Tort t) { return (double)t.szaml / t.nev; }
Vigyázni: ne egész osztást végezzünk! Különben 1/2 = 0.
Törtek összeadása
osszeg=tort_osszead(a, b);
A szorzat lehet közös nevező. Két törtet összegző függvény:
a c ad+cb ─ + ─ = ───── b d bd
/* visszatér a két tört összegével */ Tort tort_osszead(Tort t1, Tort t2) { Tort uj; uj.szaml=t1.szaml*t2.nev + t2.szaml*t1.nev; uj.nev=t1.nev*t2.nev; return uj; }
Törtek összeadása – eredmény?!
Itt tartunk most:
#include <stdio.h> typedef struct Tort { int szaml, nev; } Tort; void tort_kiir(Tort t); Tort tort_osszead(Tort t1, Tort t2); int main() { Tort x={1, 2}, y={1, 4}; tort_kiir(tort_osszead(x, y)); return 0; } void tort_kiir(Tort t) { printf("%d/%d", t.szaml, t.nev); } Tort tort_osszead(Tort t1, Tort t2) { Tort uj; uj.szaml=t1.szaml*t2.nev +t2.szaml*t1.nev; uj.nev=t1.nev*t2.nev; return uj; }
A program futási eredménye:
6/8
Ez helyes is, és nem is. Helyes, mert 6/8 az 3/4, és az összeg tényleg annyi. De lehetne jobb is, ha a program egyszerűsíteni is tudna.
Tört létrehozása – egyszerűsítve!
x=tort_letrehoz(50, 100); // 1/2
Nagyon fontos itt a függvény filozófiája. A két egész szám összerakva nem csak egyszerűen két egész szám együtt, hanem egy tört. Speciálisabb, mint egy sima számpár. Ezért amikor egy törtet „építünk”, azaz létrehozunk két egész számból, akkor el kell végeznünk egy egyszerűsítést rajta. Az egyszerűsített tört egyenértékű az összes bővített változatával. Innentől kezdve, hogy ez a függvényünk megvan, mindig ezt fogjuk használni akkor, amikor egy számlálóból és egy nevezőből létrehozunk egy törtet. Így minden törtünk egyszerűsítve lesz! Sőt aki a törtes függvényeinket használja, annak is azt javasoljuk, hogy minden törtet ezzel a függvénnyel hozzon létre, ne pedig struktúra inicializálással vagy pedig „kézi” értékadással külön a számlálónak és a nevezőnek. Így neki sem kell törődnie majd az egyszerűsítéssel.
/* Törtet hoz létre, egyszerűsítve */ Tort tort_letrehoz(int szaml, int nev) { Tort uj; int a=szaml, b=nev; while (b!=0) { // Euklidész int t=b; b=a%b; a=t; } uj.szaml=szaml/a; uj.nev=nev/a; // legnagyobb közös osztó = a return uj; }
Az euklidészi algoritmus megkeresi két szám legnagyobb közös osztóját. Ezzel osztva a számlálót és a nevezőt megkapjuk az egyszerűsített törtet.
Törtek összeadása és szorzása – most már helyesen
/* műveletek törtekkel */ osszeg=tort_osszead(a, b); szorzat=tort_szoroz(a, b);
Az összeadást és a szorzást megvalósító függvények:
/* Visszatér a törtek összegével. */ Tort tort_osszead(Tort t1, Tort t2) { return tort_letrehoz(t1.szaml*t2.nev + t2.szaml*t1.nev, t1.nev*t2.nev); }
a c ac ─·─ = ── b d bd
/* Visszatér a törtek szorzatával. */ Tort tort_szoroz(Tort t1, Tort t2) { return tort_letrehoz(t1.szaml*t2.szaml, t1.nev*t2.nev); }
Az összeadás most már elvégzi az egyszerűsítést
is, hiszen a törtet létrehozó függvény tartalmazza azt is. Egyszerűbb
lett a függvény, hiszen a lokális változóra sincsen már szükség. Amit
a tort_letrehoz()
visszaad, azt passzolja is tovább a
hívónak. A szorzás ugyanígy működik programozásilag, és a többi művelet:
kivonás, osztás sem különböző.
Tört beolvasása
Olvassunk be egy törtet a billentyűzetről:
Írd be a törtet: 6/8
Tort t; t=tort_beolvas();
/* beolvas egy törtet a billentyűzetről, és visszaadja */ Tort tort_beolvas(void) { int szam, nev; scanf("%d / %d", &szam, &nev); return tort_letrehoz(szam, nev); }
Mi történik, ha nem számot ír be? Ha 0 nevezőt ad?
Kérdés, mit csináljunk akkor, ha a billentyűzetről nem érvényes adat érkezik. Akár nincs a két szám között törtvonal, akár a felhasználó nem számot ír be, akár nullát ad meg nevezőnek – sok okból lehet helytelen az adat. Ha a függvényt a fenti formában írjuk meg, akkor mindenképpen vissza kell térnünk egy törttel (hiszen ez a függvény visszatérési értéke). Na de mi legyen ez a tört hiba esetén? 1/1? 0/0? Valamilyen módon a hibát jó lenne jelezni. 1/1 nem lehet a visszatérési érték, mert az egy helyes tört. A 0/0 talán jobb ötlet lenne.
A probléma igazából onnan gyökerezik, hogy a függvénynek nem egy, hanem két eredményt kell előállítania. Egy hibakódot (sikerült vagy nem sikerült), és magát a törtet. A fenti függvénynek pedig csak egy visszatérési értéke van.
A következő előadáson bemutatott módszerrel lehetségessé válik majd több visszatérési
érték adása egy függvényből. Tulajdonképpen a scanf()
is ezt teszi:
int siker_db; double szam; siker_db=scanf("%lf", &szam);
30 Struktúrák vs. tömbök
Struktúrába egy dolog összetartozó adatait tesszük.
- Különálló, új típus!
- Ezekhez műveletek is tartoznak
- Pl. egy könyv adatai: cím, szerző, oldalszám
Tömbben több egyforma dolog adatait tároljuk.
- Ez csak egy tároló.
- Egy tömbbe kerülő dolgoknak nem feltétlenül van közük egymáshoz!
- Pl. számok sorozata, névsor, könyvek katalógusa
Elfajulások lehetségesek. Létezhet egy elemű struktúra vagy tömb is. Alma, körte, az, a: ezek szavak, még az „a” is, hiába egy betűs!
Ne feledjük: a típus egy értékkészlet és műveletek együttese. Egy dátum struktúra létrehozásával egy új típust hozunk létre, amelyen új műveletek értelmezhetőek. Pl. ki lehet számolni két dátum között a különbséget napokban. Ez kizárólag csak a dátumokon értelmezett művelet (év, hónap, nap), nem pedig az összes háromelemű, egészekből álló tömbön!
Ha az összetartozó adatok különböző típusúak (pl. a név karaktersor, a dátum pedig egész számokból áll), akkor biztonsan struktúráról van szó. Ha egyformák a típusok, gyakran akkor is. Balgaság a tört számlálóját és nevezőjét nem struktúrával, hanem egy kételemű tömbbel megadni. Úgyszint egy év, hónap, napból álló dátum is inkább struktúra, bár mindegyik eleme egész szám. A tömb választása azt is éreztetné, hogy az év, hónap, nap felcserélhetőek, ami nem igaz. Egy névsor elemei, amelyet tömbben tárolunk, viszont igen: sorba rendezhetőek az emberek név, születési évszám, magasság stb. szerint is.
És még egy dolog, amit ne felejtsünk el: nem azért használunk tömböt
vagy struktúrát, mert sok adattal dolgozunk, hanem azért, mert
az adatoknak közük van egymáshoz! A tört számlálóját és nevezőjét
is betettük egy struktúrába, pedig csak két elemről van szó. Ugyanígy,
egy három betűből álló szó is tömb a programozás szempontjából. Sőt ha
egyszer eldöntjük, hogy a szavakat karaktertömbökben tároljuk, akkor még
az „a” névelő is egy tömb! Mondhatjuk, hogy ha valamilyen adatoknak a tömbbe
vagy struktúrába tevése által megszűnik a programkódban a „sorminta”
(pl. a1=b1; a2=b2; a3=b3;
helyett a=b
lesz
a struktúra értékadás által), akkor jó úton járunk. Ha „sorminta” van
a programunkban, akkor pedig valószínűleg rossz úton. Az összetett
típusokban az adataink közötti összefüggéseket rögzítjük, és ez kihatással
van a programkód felépítésére is: annak áttekinthetőségére, egyszerűségére
és legfőképp minőségére!
Néha van olyan eset, amikor nem teljesen egyértelmű, hogy strukturáról vagy tömbről van szó, ilyen a sok dimenziós tér esete is. Ha mindegyik komponenst kiírjuk, akkor rengeteg mezőnév keletkezik, amelyeket meg kell jegyeznünk. Ha tömböt használunk helyette, akkor az egyes komponenseket ciklussal dolgozhatjuk fel.
struct Pont10D { int x, y, z, a, b, c, d, e, f, g; // struktúra ennyi névvel? }; int koord[10]; // vagy inkább tömb?
A problémára talán a legjobb megoldás ez:
struct Pont10D { int koord[10]; // így lehet értékül és paraméterként adni };
Így ciklussal is fel lehet dolgozni a komponenseket, ugyanakkor egy Pont10D
struktúrát lehet értékül adni, és függvénynek paraméterként is.
31 Sztring, mint összetett típus
Szövegek reprezentálása
- A szöveg: sztring (string), karakterek sorozata
- C-ben nincs külön típus, hanem karakterek tömbjeként adják meg
- Röviden:
char str[100]="hello"; printf("A szöveg: [%s].", str); // A szöveg: [hello].
Mivel a sztring a C-ben karaktertömb, a méretét meg kell mondanunk előre. Az
=
értékadás operátor sem használható rajta. Következő előadáson részletesen szerepelni fog.
32 2D tömb
A kétdimenziós tömb többszörösen összetett: tömbök tömbje.
5 sor × 6 oszlop
double matrix[5][6];
3×3 játéktér
char amoba[3][3];
palya[0][2]='o';
33 Többszörös összetétel: definíciók
Többszörösen összetett adatok esetén a definíciók sorrendjére figyelni kell: csak a már definiált típusokból lehet építkezni.
struct Datum { int ev, honap, nap; }; struct Ember { char nev[100]; // struktúrában tömb char lakcim[150]; struct Datum szuletes; // struktúrában struktúra };
A typedef
itt jól használható:
typedef struct Pont { int x, y; } Pont; typedef Pont Hatszog[6]; // Pontokból álló tömb Hatszog h;
34 Többszörös összetétel: geometria példa
Típusok:
typedef struct Pont { // egy pont a síkban double x, y; } Pont; typedef struct Szakasz { Pont eleje, vege; // szakasz két pont között } Szakasz; typedef struct Kor { // középpont és sugár Pont kozeppont; double sugar; } Kor;
Művelet példa:
/* igazzal tér vissza, ha egy pontban metszik egymást */ int metszi_e(Szakasz sz1, Szakasz sz2);