Újabb vezérlési szerkezetek. Operátorok. Struktúrák

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.

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.

Újabb vezérlési szerkezetek

3 Hátultesztelő ciklus

Hátultesztelő ciklus a folyamatábrán
Hátultesztelő ciklus struktogramon
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

 while (…) {
     …
     if (…)
┌───   break;
│    …
│}
└>
┌─> while (…) {
│     …
│     if (…)
└───     continue;
      
      …
    }

Használatuk

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.

Operátorok

8 Operátorok: precedencia, asszociativitás

Operátorok

x=5+2*3;

Precedenciák: erősségek

Precedenciák,
asszociativitás:
lásd a két
oldalas C puskát!

Asszociativitás: mik az operandusok

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:

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)

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
}

Operátoroknál

11 Operátorok: főhatás és mellékhatás II.

Összeadás, a+b


Értékadás, a=b

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=i+1;
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.
1243

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!

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

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.

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”

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;
}

Struktúrák

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
Mi tartozik
ö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ó

Ismétlés
(1. előadás)
Típus: értékkészlet és hozzá tartozó műveletek.

Egyszerű, beépített típusok:

Összetett, származtatott típusok:

22 Összetett típus: struktúrák

Struktúrák: mire jók?

számláló
────────
nevező

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

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

2

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

A teljes
megoldás:
tort.c

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);

(Többszörösen) összetett adatok

30 Struktúrák vs. tömbök

Struktúrába egy dolog összetartozó adatait tesszük.


Tömbben több egyforma dolog adatait tároljuk.


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

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];
2D tömb: mátrix

3×3 játéktér

char amoba[3][3];
2D tömb: tic-tac-toe játék
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);