Típus: értékkészlet és hozzá tartozó műveletek.
Egyszerű, beépített típusok:
int
, long
int
stb.float
, double
char
bool
Összetett, származtatott típusok:
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
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.
struct név { definíció T1 mező1, mező2, …; T2 mező3; … }; pontosvessző!
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.
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.
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;
.
/* 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.
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.
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;
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.
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.
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!
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.
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); }
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.
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; }
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.