Struktúrák

18. Emlékeztető: típusok

Típus: értékkészlet és hozzá tartozó műveletek.

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

Összetett, származtatott típusok:

19. Hogyan tároljunk törteket?

Racionális számok

Tegyük fel, hogy egy olyan programot kell készítenünk, amely racionális számokkal dolgozik. Hogy ezeket pontosan tudjuk tárolni, a lebegőpontos tárolás ötletét elvetjük: mindig külön tároljuk a számlálót és a nevezőt, két egész típusú változóban. Ahogy írjuk a programot, azonban egyre bonyolultabb kifejezéseink lesznek; egyre nehezebb lesz követni, hogy melyik törtes műveletet mely változókon kell elvégezni. Pl. az alábbi művelet:

┌ a   c ┐   ┌ e   g ┐   ad+cb   eh+gf  (ad+cb)(eh+gf)
  ─ + ─   ·   ─ + ─   = ───── · ──── = ──────────────
└ b   d ┘   └ f   h ┘     bd     fh         bdfh
Mi tartozik
össze?!

Kódban ez így nézne ki:

i = (a*d+c*b)*(e*h+g*f);
j = b*d*f*h;

Még ha be is vezetünk valami konvenciót a jelölésre (pl. asz és an az a tört számlálója és nevezője), akkor is elég reménytelennek tűnik a helyzet. Két tört számlálója és nevezője – ez se sokkal jobb:

int asz, an, bsz, bn; a és b tört, számlálók és nevezők

Mi hiányzik nekünk? Az adat absztrakciója! Az hiányzik, hogy az adattípusokból ugyanúgy tudjunk építkezni, ahogyan az algoritmusoknál a függvényekkel is tettük. Legyen olyan nyelvi elem, amely segítségével több összetartozó adatot egységként kezelhetünk, és néven is nevezhetjük az így kialakult adatcsomagot.

20. Struktúrák létrehozása

Definíció szintaxisa

struct név { definíció
   T1 mező1, mező2, …;
   T2 mező3;
   …
};           pontosvessző!

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.xtí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.

21. Használhatom, ahogy egy „sima” változót?

Értékadás

struct Pont p1, p2;
p1 = p2;

A struktúra értékadás minden mezőt másol: a fenti példában 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);
}
 
/* megadja a szakaszfelezőt */
struct Pont szakaszfelezo(struct Pont p1, struct Pont p2);
 
struct Pont a;
printf("%f", origo_tavolsag(a));

Struktúra lehet függvény paramétere és visszatérési értéke is. A paraméterátadás szabályai ugyanazok, mint az egyszerű típusoknál: ha változót adunk át, akkor a függvény csak a változó értékét fogja látni, az eredeti változót nem. Tehát nem tudja megváltoztatni azt.

22. Struktúrák kezdeti értéke

inicializálás
!=
értékadás!

Struktúrák inicializálása

struct Pont {
    double x, y;
};
 
struct Pont p1 = { 2, 5 };   inicializálás: p.x 2 és p.y 5 lesz

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!

Nagyon fontos megérteni az értékadás és az inicializálás közötti különbséget. Az inicializálás azt jelenti, hogy egy éppen definiálás alatt lévő változónak megadjuk a kezdeti értékét; az értékadás pedig az, hogy egy már létező, régebben létrejött változónak adunk valami új értéket. A kettő nem ugyanaz, csak mindkettőt szintaktikailag az egyenlőségjellel kell jelezni.

Újabb C-ben (C99)

struct Pont p2 = { .x = 2, .y = 5 };    /* inicializálás */
 
p2 = (struct Pont) { 2, 5 };            /* értékadások */
p2 = (struct Pont) { .x = 2, .y = 5 };

A C nyelv újabb (C99 szabvány utáni) változataiban ilyet is lehet írni. Sajnos ezt nem minden fordító támogatja. Sőt, C++-ban ez teljesen szabálytalan, úgyhogy ne ezt szokjuk meg; csak azért szerepel itt, ha találkoztok vele valahol, tudjátok, mit jelent. A fenti inicializálás egyébként kiváltható a .x és .y nélküli formával, ahol a definíció sorrendjét használjuk; a lentebbi értékadások pedig a mezőknek egyesével értékadással:

p2.x = 2;
p2.y = 5;

23. 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.

24. 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(void) {
    Tort t1, t2; látható
}
 
int masik_fuggveny(void) {
    Tort b;   ez is
}
/* lokálisan */
int fuggveny(void) {
    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.

25. Törtes példa: komplex feladat

2

3

Racionális számok

Feladat: a C nyelv nem tartalmaz racionális szám 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(void) {
    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(void) {
    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.