A ciklusmagot egyszer biztosan le kell futtatni?
Előfordul, hogy egy ciklus törzsét legalább egyszer biztosan le akarunk futtatni, vagy hogy a ciklus feltételének nincs értelme, amíg legalább egyszer a törzse le nem futott. A hátultesztelő ciklusban a ciklusfeltétel ellenőrzése a ciklusmag után történik. Emiatt a ciklusmag legalább egyszer végrehajtódik, tehát az első végrehajtás a feltételtől függetlenül megtörténik. A forráskódban is a feltétel alul van. Ez emlékeztet arra, hogy csak a ciklusmag után ellenőrzi:
do
utasítás;
while (feltétel);
do {
utasítások…
} while (feltétel);
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. Persze ilyenkor kiadódhat megint ugyanaz a szám, de előbb-utóbb a véletlenszámgenerátor dob majd egy különbözőt:
int szamok[5];
for (i = 0; i < 5; i += 1) {
do {
uj_szam = rand()%90 + 1;
van_ilyen = false; // van már ilyen?
for (j = 0; j < i; j += 1)
if (szamok[j] == uj_szam)
van_ilyen = true;
} while (van_ilyen); // ha van, újra!
szamok[i] = uj_szam;
}
Természetesen a fenti programot egy pillanat
alatt át lehetne írni elöltesztelő ciklusra. Ha elöl
lenne a van_ilyen
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:
van_ilyen = true;
while (van_ilyen) {
uj_szam = rand()%90+1;
van_ilyen = false;
for (j = 0; j < i; j += 1)
if (szamok[j] == uj_szam)
van_ilyen = true;
}
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 van_ilyen = true
– 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.
Megjegyzés a fenti „előbb-utóbb a véletlenszámgenerátor dob majd egy különbözőt” gondolathoz. Valóban, elviekben lehetséges, hogy ez a program soha nem áll le, mert lehetséges az, hogy a véletlenszámgenerátor soha nem ad olyan számot, ami jó lenne. De ez nagyon valószínűtlen – valószínűségszámítással meg is lehetne határozni, mennyire. Itt most ezzel nem foglalkozunk. Létezik a lottószámok generálására egy teljesen más elvű megoldás is, amely garantálhatóan rövid ideig fut: feltöltünk egy tömböt a számokkal 1…90-ig, aztán megkeverjük a tömböt, és vesszük az így kapott tömb első 5 elemét.
Ciklus megszakítása: break
break
while (keresés…) {
…
if (megvan)
┌─── break;
│ …
│}
└─▸
Ciklus folytatása, ciklustörzs kihagyása: continue
continue
┌─▸ while (vannak feldolgozandó elemek…) {
│ if (a mostani elem nem kell)
└─── continue;
…
}
Ezek az utasítások nem strukturált vezérlési szerkezeteket
eredményeznek. Csak nagyon indokolt esetben használjuk őket! A break
és continue
használatára egész félévben gyakorlatilag nem fogunk más
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!)
A break
és a continue
amúgy használhatóak for
ciklussal
is. Vigyázat, a continue
utasítás következő iterációra ugrása a for
ciklus esetében azt jelenti,
hogy a ciklus fejlécében megadott műveletet, az utótevékenységet még végrehajtja!
1. Adatbevitel
2. Módosítás
3. Kimutatás
…
0. Kilépés
Választás: _
if (valasz == 1) {
…
} else if (valasz == 2) {
…
} else {
…
}
A sorozatos if …; else if …
kiváltására használható a switch
.
Az if
igazságértékre alapozott választás helyett ezzel ún. többszörös választás
vezérlési szerkezetet lehet megadni a programkódban.
Például amikor megkérdezzük a felhasználót, szeretne-e törölni egy fájlt, ötféle választ adhat: kicsi és nagy I betű, kicsi és nagy N betű, vagy valami má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; // kiugrás
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. Ezekkel az apróságokkal
az előadásban nem foglalkozunk, csak külön írásban.
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. Az egész egyetlen egy,
nagy utasítás blokk, amelynek belépési és kilépési pontjait a case
-ek
és a break
-ek adják meg. 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! (Ennek az angol neve:
fall-through.) 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 az esetek 99%-ában így 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) {
case érték1: // ha érték1 vagy érték2, kezdd itt
case érték2:
…
break; // ugorj ki itt
case érték4: // ha érték 4, akkor itt kezdd
…
…
…
break; // és itt ugorj ki
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). Ráadásul
konstansoknak, azaz fordítás közben már ismert értékűeknek kell lenniük. A
switch
fejlécében használt kifejezésnek is egész számra kell
kiértékelődnie, ez a C nyelvben kötelező. A default
ág elhagyható.
A switch
szerkezettel egy egyszerűbb menürendszert is elkészíthetünk a programunkban.
Erről egy laborfeladatban lesz szó.
Számoljuk meg, hány prímszám van 2 és 1000 között!
int db = 0; int sz; for (sz = 2; sz <= 1000; sz += 1) if (… sz egy prím …) ! db += 1;Eldöntés tétele
bool van = false; int oszt = 2; while (oszt < sz && !van) { if (sz % oszt == 0) van = true; oszt += 1; }
int sz, db; db = 0; for (sz = 2; sz <= 1000; sz += 1){ bool van = false; int oszt = 2; while (oszt < sz && !van) { if (sz % oszt == 0) van = true; oszt += 1; } if (!van) db += 1; } printf("%d prím.\n", db);
A két egymástól független programrész összedolgozása által egy nehezen értelmezhető program keletkezett. Figyeljük meg: nagyban rontotta az áttekinthetőségét az, hogy az egyik algoritmust bele kellett építenünk a másikba, kettévágva azt. Megírni is nehezebb egy ilyet, hiszen egyszerre több dolgon kell gondolkozni.
Figyeljünk meg a fenti kódrészleten még egy dolgot. A vizsgált szám
prím voltát ellenőrző, kék színű programrész tulajdonképpen önállóan is megállná a helyét:
van bemenete (a vizsgált sz
szám) és kimenete (prím-e vagy nem). Ez emlékeztethet
minket a matematikai függvényekre: egy f(x) = x²
függvény is értelmes önmagában,
akár egy másik képlet részeként, és ennek is van bemenete (az x
szám) és kimenete
(annak négyzete, szorzata önmagával).
Függvények (function) a C nyelvben
Hasonlóak a matematikai függvényekhez:
y = x²
double negyzet(double x) {
return x * x;
}
int main(void) {
printf("%f", negyzet(2.3));
}
Tegyük fel, hogy van egy programrész, amely megmondja egy adott számról, hogy prím-e, vagy nem. A fenti
f(x) = x²
mintájára képzeljünk el egy prim_e(x)
függvényt! Ez a kapott számnak nem a négyzetét fogja
visszaadni (pl. negyzet(1.5)
→ 2.25
), hanem a kapott számról megadja majd, hogy prímszám-e vagy nem
(pl. prim_e(37)
→ true
és prim_e(25)
→ false
). Ha van egy ilyen
függvényünk, akkor a prímek számlálása feladat nagyon egyszerűvé válik: a kékkel jelölt „… sz egy prím …” programrész helyére csak
annyit kell írnunk, hogy prim_e(sz)
.
if (… sz egy prím …)
db += 1;
if (prim_e(sz))
db += 1;
Az egész programunk így együttműködő alprogramokból épülhet fel! Ezekhez is tartozik specifikáció: bemenet → kimenet összefüggése.
Vegyük észre: azáltal, hogy a függvénynek neve van, a programban akár több
helyen is hivatkozhatunk rá: több helyről indulva elvégezhetjük ugyanazt a
részfeladatot. Egy bonyolult részfeladat így elemi lépésként is kezelhető. Ha
megvan a prim_e()
függvényünk, onnantól kezdve ugyanolyan könnyen
tudjuk ellenőrizni egy szám prím/nem prím voltát egy if (prim_e(i))
sorral, mintha csak annyit kellene ellenőrizni, hogy nulla vagy nem nulla!
Azáltal pedig, hogy a függvényeknek paraméterei is lehetnek, ezeket a
részfeladatokat tudjuk „konfigurálni”. A printf()
függvényt is
egyszer megírta valaki, és mindenhol használhatjuk.
A részfeladatokat külön függvényben írhatjuk meg. Így egy nagyobb program áttekinthetőbb, kezelhetőbb lehet. Sőt a gyakran ismétlődő programrészeket is így csak egyszer kell majd megírnunk. A programok így kisebbek, hatékonyabbak lehetnek.
Főprogram
int main(void) {
int db = 0;
for (int s=2; s<=1000; s+=1)
if (prim_e(s))
db += 1;
printf("%d db.\n", db);
return 0;
}
Alprogram
bool prim_e(int szam) {
bool van = false;
int o = 2;
while (o < szam && !van) {
if (szam % o == 0)
van = true;
o += 1;
}
return !van;
}
Elnevezések: fejléc, függvénytörzs, paraméter, visszatérési érték, hívás, visszatérés, lokális változó, láthatóság, élettartam.
Dokumentálandó: bemenet, kimenet, feladat, hibalehetőségek.
A prim_e()
függvény olyan, mint a teljes programunk: van bemenete és kimenete is. Csak ezek
nem a képernyő és a billentyűzet, hanem a főprogrammal történő kommunikáció által valósulnak meg:
Függvények definiálása: szintaktika
visszatérési_típus függvénynév(paraméterlista) { … függvénytörzs … return visszatérési_érték; }
A fejléc (header) meghatározza a függvény nevét, a paraméterei (parameter) és a visszatérési értéke (return value) típusát. A függvénytörzs (function body) tartalmazza azt a programrészt, amely a függvény feladatát elvégzi.
Visszatérés a függvényből
A függvény törzsében elhelyezett return
utasítással
visszatérhetünk (return) a függvény hívásának (call) helyére.
Ezzel egyben megadjuk a visszatérési értéket is, amelyet
egyébként a függvény értékének (function value) is nevezünk. Fontos, hogy a return
utasítás ezt a két szerepet elválaszthatatlanul összeköti! Ami a return
után van, az már nem hajtódik végre. (Viszont egy függvényben lehet több helyen
is return
utasítás.)
Lokális változók
- A függvényen belül vannak definiálva
- Függvénybe belépéskor jönnek létre
- Végén megszűnnek → értékük elveszik
- Minden függvény csak a sajátjait látja! (láthatóság, scope)
Mi az előnyük? Például az, hogy minden függvénynek
lehetnek saját lokális változói, amelyekben olyan értékeket tárolnak, amelyekre
csak a futásuk idején van szükség. A fenti példában az osztó
változóra nincsen már szükség, amint meghatároztuk, hogy a szám prím-e.
(A van
változó is lokális, és az is megszűnik, azonban az
értéke lemásolódik, és átadódik a hívónak.) Ezek a változók csak a függvényen
belül látszanak (a láthatóságuk (scope) csak a függvényen belülre terjed ki),
és így a nevük csak azon belül értelmezett. Másik függvényeknek
lehetnek ugyanolyan nevű lokális változóik, a nevek mégsem
fognak ütközni. További előny, hogy a változó nem foglal memóriát, csak akkor,
ha az azt definiáló függvény belsejében vagyunk. Vagyis a változó élettartama
(storage duration, lifetime és extent szavak is használatosak az angol
szakirodalomban) is csak arra az időre terjed ki, amíg a
függvény végrehajtása tart.
A main()
függvény
Most már tudjuk, hogy a main()
is egy függvény. Egy egész
számmal kell visszatérjen, amelynek hibajelző szerepe van. Egyelőre mindig 0-ra
állítjuk, ami azt jelenti, hogy nincs hiba. Hogy a paraméterei mik, azt a
kérdést egyelőre hagyjuk nyitva!
Függvények dokumentációja
A függvények olyan kis programrészek, amelyek egy jól elhatárolt részfeladatot hajtanak végre. Ezért egy függvény dokumentálásakor pontosan meg kell határozni, hogy mire való, milyen feladatot hajt végre. A programokhoz hasonlóan rögzíteni kell azt is, hogy milyen bemenetet vár és milyen kimenetet állít elő a futása során. A bemenet dokumentálásához hozzá tartozik a bemeneti tartomány leírása is (pl. a négyzetgyököt vonó függvény nem kaphat negatív számot paraméterként). A működés leírásához pedig a hibalehetőségek rögzítése. Mindezeket a függvények előtt, a forráskódban, megjegyzés (comment) formájában is meg kell tenni, hogy ezáltal a kód kezelhetővé, karbantarthatóvá váljon. Ahol van hely, ezt meg fogjuk tenni (sajnos az előadás diákra nem mindenhol fér oda ez).
A lenti animáció a függvényhívás menetét és lokális változók élettartamát mutatja be.
#include <stdio.h> int faktorialis(int mie) { int szorzat = 1; for (int i = 2; i <= mie; i += 1) szorzat *= i; return szorzat; } int main(void) { int sz; int eredm; printf("sz = "); scanf("%d", &sz); eredm = faktorialis(sz); printf("%d! = %d\n", sz, eredm); return 0; }
main() sz: eredm:
faktorialis() mie: szorzat: i: (vissza):
A függvényhívás a következőképpen történik. Amikor main()
függvény meghívja a
faktorialis()
függvényt, létrejönnek az utóbbi lokális változói. Ezek közül
egyik a függvénynek a paramétere: a mie
változó kívülről inicializálva lesz,
méghozzá azzal az értékkel, amelyet a hívás helyén adunk neki (tehát amelyet a felhasználó
adott meg). Így a három lokális változó közül az egyik, a paraméter mie
inicializálva van, a másik kettő pedig – szorzat
és i
– nem. De
azok később kapnak értéket a számítás elvégzése közben.
A függvényhívás a return szorzat
utasítás hatására fejeződik be.
Ekkor a függvény lokális változói mie
, szorzat
és
i
megszűnnek – de a return
utasításnál megadott kifejezés
értéke (ami most a szorzat
-ba került szám) visszaadódik a hívónak.
Ez az int
típusú érték lesz a main()
kódrészletben
a faktorialis(sz)
részkifejezés értéke. Innen folytatódik a main()
végrehajtása.
A függvény paraméterét és visszatérési értékét a C nyelvben tetszőleges kifejezés
értékével inicializálhatjuk. Tehát a fenti függvény hívható lenne akár így is: faktorialis(5)
,
vagy így is: faktorialis(sz+6)
, amikor is az 5!
és az (sz+6)!
értékeket számolná ki. Továbbá egy függvény visszatérését kiváltó return
utasítás
után is tetszőlegesen bonyolult kifejezés állhat: egy függvény befejeződhet akár egy return x
,
akár egy return 5
, de akár egy return sin(x)*3 + 1.2
utasítással is.
(Lásd a fentebbi és a lentebbi példákat is!)
Néha egy függvény olyan rövid, hogy egy sorban leírható.
Ha csak egyszer írjuk és olvassuk a változót:
int teglalap(int a, int b) {
int t = a * b;
return t;
}
int teglalap(int a, int b) {
return a * b;
}
Ebben a példában azt vehetjük észre, hogy egy egyszer írt, egyszer olvasott változónk van. Az első, hosszabb
változatban a t
változóba előbb beírjuk a * b
értékét, aztán kiolvassuk onnan. A kiolvasott szám
mi más lenne, mint a * b
– ugyanezt akár a return
utasításhoz is írhatjuk, elhagyva a szükségtelen lokális
változót.
Ha logikai kifejezés értékével térünk vissza:
bool paros_e(int x) {
if (x % 2 == 0)
return true;
else
return false;
}
bool paros_e(int x) {
return x % 2 == 0;
}
Itt azt vehetjük észre, hogy az első változatban a vizsgálat eredménye (nulla-e a maradék vagy más: igaz vagy
hamis) éppen a visszatérési értéket adja. Amikor a maradék nulla, akkor az x % 2 == 0
kifejezés értéke igaz, és
ilyenkor a return true
-hoz visz a vezérlési szerkezet. Ha a maradék nem nulla, akkor az x % 2 == 0
értéke
hamis, és a return false
-hoz. Vagyis a kifejezés értéke pont ugyanaz mindkét esetben, mint ami a return
utasítások után van, így az esetszétválasztás felesleges.
Egyik esetben sem hiba a hosszabb megoldást használni. Egyszerűen csak felesleges és hosszabb.
double teglalap_kerulet(double a, double b) {
return 2 * (a+b);
}
printf("%f", teglalap_kerulet(2, 3.4)); // a=2, b=3.4
Formális paraméter (parameter): a neveik a függvény fejlécében.
- Szimbolikus paraméternek is nevezik (symbolic parameter)
- A függvényen belüli szerep szerint kell elnevezni
- Automatikusan inicializált lokális változók. Ugyanúgy megszűnnek!
Aktuális paraméter (argument): a hívás helyén adott érték.
- Híváskor a megadás sorrendje számít
- Nem csak változó lehet, hanem konstans is
Fontos, hogy a függvény deklarációja helyén a paraméterlista megadásánál minden egyes paraméter
elé oda kell írni annak típusát. Akkor is, ha azok egyformák.
Ezért van a fenti függvény fejlécében double a, double b
, és
nem pedig double a,b
, ami helytelen szintaktikailag.
Mivel a paraméter is lokális változó, a függvényből visszatérve megszűnik
létezni. Emiatt a paraméteren keresztül közvetlenül nem lehet visszaadni értéket! A
lenti programban is a függvény nem a szum
változót, hanem a szum
változó tartalmának másolatát kapja csak meg, vagyis 0-t. A szamol()
függvény szum
nevű lokális változóba tényleg bekerül az összeg, de megszűnik
a szamol()
-ból visszatérés után!
#include <stdio.h>
void osszeg(int a, int b, int szum) {
szum = a + b; /* ??! */
}
int main(void) {
int szum;
szum = 0;
printf("előtte: %d\n", szum);
osszeg(5, 6, szum);
printf("utána: %d\n", szum);
return 0;
}
A probléma megoldására egy későbbi előadáson fogunk visszatérni.
#include <stdio.h>
double kerulet(double a, double b); // deklaráció/prototípus
int main(void) {
printf("%f", kerulet(2, 3.4));
return 0;
}
double kerulet(double a, double b) { // definíció
return 2 * (a + b);
}
- A fordítónak a
main()
-nél tudnia kell, mit jelent akerulet
- Nevét, paraméterek számát és típusát, visszatérés típusát
- Vagyis deklarálni kell használat előtt; definiálni ráér később is
- A deklaráció kihagyása hiba!
A fenti példa azt is kiemeli, miért olyan fontos ez. A fordító minden függvényhívás helyén
elvégzi a paraméterek ellenőrzését: a függvénynek csak olyan típusú paraméterek adhatóak,
amelyeket a fejléce alapján vár. A main()
-ben itt a hívás helyén egy érdekes dolog
történik. A 2
konstans egy egész, ezért a típusa int
– viszont a
fordító tudja, hogy a kerulet()
valós paramétert vár, ezért előbb elvégzi a
2→2.0 átalakítást. Fordított esetben, pl. egészet váró függvénynek 3.14
-et adva
először egy lefelé kerekítés történne, és a függvény végülis a 3-as számot kapná meg. Az ilyesmit
általában figyelmeztető üzenettel jelzik is a fordítók, ugyanis a törtrészt
ilyenkor elveszítjük, amit lehet, hogy nem szeretnénk.
A függvényt egyébként nem szükséges a main()
függvény után definiálni, előtte
pedig csak deklarálni. Az egész definíció áthelyezhető a main()
függvény elé is.
Ilyenkor külön deklarációra nincsen szükség, mert a definíció is tartalmazza a nevet és a
típusokat. A lényeg az, hogy a függvényhívás helyén a függvény típusának már ismertnek kell
lennie:
/* kiszámítja egy a,b oldalú téglalap kerületét */
double kerulet(double a, double b) { // definíció
return 2*(a+b);
}
int main(void) {
printf("%f", kerulet(2, 3.4));
return 0;
}
Nagyobb programok esetén azonban nem ez a preferált.
int beolvas_szam(void) {
int sz;
scanf("%d", &sz);
return sz;
}
void kiir_szam(int szam) {
printf("%d", szam);
return;
}
x = beolvas_szam(); // A híváshoz ilyenkor is kell a ( )
kiir_szam(x);
- Ha a függvénynek nincs paramétere, azt a
void
szóval jelöljük - Ha nincs visszatérési értéke, azt is
- Ilyenkor a
return
önmagában áll:return;
, vagy el is hagyható
- Ilyenkor a
Történelmi okokból egy függvény definíciójánál a void
szó elmaradhat a paraméterlista zárójelei közül, pl. int main()
. Ez kicsit mást
jelent, így jobb, ha a void
szót mindig kiírjuk.
printf("%d\n", fakt(6));
printf("%d\n", fakt(6));
printf("%d\n", fakt(6));
720 720 720
Érték: a kifejezés értéke. A függvény kiértékelése: a függvény lefut, és a hívás helyén lévő kifejezésbe a visszatérési értéke behelyettesíthető.
printf("%d\n", rand());
printf("%d\n", rand());
printf("%d\n", rand());
8311623 2141262 16641798
Mellékhatás: a függvény valahol változást okoz. (Ezt mellékhatásnak nevezzük még akkor is, ha kifejezetten ez a célja a függvénynek!)
Általában igyekszünk olyan függvényeket írni, amelyeknek csak értéke vagy csak mellékhatása van. Ennek az elvnek neve is van: command-query separation. Eszerint kétféle függvény van. Az egyik fajta a parancsfüggvény (command), amelyet azért használunk, hogy hatása legyen. A másik fajtának kérdéseket teszünk fel (query), amely kiszámol valamit, de mellékhatása nincs. Ha ez a kettő keveredik, az könnyen összevisszasághoz, átláthatatlan programfelépítéshez és nehezen megtalálható hibákhoz vezet.
Ha a függvény értéke csak a paramétereitől függ, mindig ugyanaz kell legyen
az eredmény. Ha van mellékhatása, ez nem biztos! Valahol valaminek történnie kell, hogy a
rand()
mindig mást ad… Ez a függvény például kell rendelkezzen valamiféle belső állapottal.
Láthatóan a kimenete nem a bemenő paraméter függvénye, hiszen nincs is neki! Általában sem lenne
sok értelme a void
paraméterű vagy visszatérési értékű függvényeknek, ha nem lenne
mellékhatásuk. Az ilyenek matematikai értelemben véve nem függvények már, de ennek ellenére C-ben
így hívjuk őket.
Fontos: ha a specifikáció nem kéri a kiírást, akkor kifejezetten hibának számít, ha a függvény mégis ilyet tesz! Például kiírja a képernyőre az eredményt ahelyett, hogy visszatérne vele. Hadd döntse el a hívó, mit szeretne csinálni az eredménnyel!
Felülről lefelé tervezés (top-down design)
- Tervezés egyszerűsítése: „oszd meg és uralkodj”
- A programrészek közötti csatolások csökkentése
A funkcionális dekompozíció (functional decomposition) egy tervezési elv. A másik neve a felülről lefelé (top-down) elv. Lényege, hogy a problémát részfeladatokra bontjuk. Az egész rendszert, programot úgy tervezzük meg, hogy közben a részfeladatokat megoldottnak tekintjük. Az egyes részfeladatok megoldása közben így nem kell a többi részleteivel bajlódni. A részfeladatok ugyanúgy specifikálhatóak, mintha egy teljes programról lenne szó.
„Az egyik dolog, amit a programozásban meg kell tanulnunk, az az, hogyan hagyjuk figyelmen kívül a részleteket.”
– Gerald J. Sussman
Érdekesség: Gerald Jay Sussman amerikai programozó, matematikus, aki legtöbbet a mesterséges intelligenciával foglalkozik. Az ő nevéhez is fűződik a Structure and Interpretation of Computer Programs című könyv megírása is, amelyhez egy programozás alapjait bemutató tárgy is tartozott az MIT egyetemen. A fenti idézet az egyik előadásáról származik. A tárgyat egyébként a saját maguk által kifejlesztett programozási nyelvvel tanították, amelynek a neve Scheme.
A top-down tervezést „wishful thinking” néven mutatta be (kb. ábrándozó gondolkodás). Hiszen éppen ez a lényege: „Bárcsak lenne egy olyan függvény, amelyik megmondja egy számról, hogy prímszám-e… Mert ha igen, akkor milyen egyszerű lenne a feladatunk!” Sokszor ezzel a gondolkodásmóddal tudjuk szétválasztani a részfeladatokat.
A következő feladat a felülről lefelé (top-down) tervezést szemlélteti. Azt kell látni, hogy minden lépésben csak egy kicsit lépünk a megoldás felé; a következő lépést pedig mindig megoldottnak tekintjük.
Ernesto Cesàro (olasz matematikus): válasszunk ki véletlenszerűen két egész számot. Annak a valószínűsége, hogy ezek relatív prímek, 6/π².
Feladat: írjunk programot, amely megbecsüli a π értékét!
Ez a következőképpen nézhet ki. Először is, rendezzük az
egyenletet! Tegyük fel, hogy a keresett valószínűséget majd a később
megírandó cesaro_valoszinuseg()
függvény megmondja. Akkor
a főprogram csak ennyi:
int main(void) {
double pi;
pi = sqrt(6.0 / cesaro_valoszinuseg());
printf("pi = %f", pi);
return 0;
}
Hogyan írjuk meg ezt a függvényt? Kérdés, hogyan számoljuk ki a valószínűséget.
Tegyük fel, hogy adott egy cesaro_kiserlet()
függvényünk, amely elvégzi a kísérletet
(két véletlenszerűen választott…) A részleteivel ne foglalkozzunk, csak ennyit mondjunk egyelőre:
térjen ez a függvény vissza igazzal, ha a kísérlet sikerült. Végezzük el ezerszer! A sikeres
kísérletek számát 1000-rel osztva megkapjuk a becsült valószínűséget:
módszer
/* P meghatározása kísérletezéssel */
double cesaro_valoszinuseg(void) {
int db = 0;
for (int i = 1; i <= 1000; i += 1)
if (cesaro_kiserlet()) // elvégzi a kísérletet
db += 1;
return db / 1000.0; /* egész osztás elkerülése! */
}
Mi a kísérlet? Az, hogy két véletlenszám relatív prím. Gyártsunk ehhez 1 és 1000 között véletlenszámokat, és hasonlítsuk a legnagyobb közös osztójukat 1-hez. Mert ha 1, akkor ezek relatív prímek, tehát sikerült a kísérlet, és ezért igazzal kell visszatérnünk:
/* A kísérlet: a legnagyobb közös osztójuk 1? */
bool cesaro_kiserlet(void) {
return lnko(rand()%1000 + 1, rand()%1000 + 1)==1;
}
srand(time(0))
függvényhívással, mert
tudjuk, hogy a véletlenszám-generátort a program indulásakor inicializálni
kell egyszer.Már csak annyi a dolgunk, hogy két szám legnagyobb közös osztóját meghatározzuk. Ehhez Euklidész módszerét megnézhetjük a Wikipédián is. A pszeudokódot csak át kell írni C-be:
– Donald Knuth
/* visszatér a két szám legnagyobb közös osztójával. */
int lnko(int a, int b) {
while (b != 0) {
int t = b;
b = a%b;
a = t;
}
return a;
}
Érdekesség: Donald Knuth amerikai programozó. Leghíresebb műve a „Számítógépprogramozás művészete” (The Art of Computer Programming) című többkötetes könyv. Az ő nevéhez fűződik a TeX nevű szövegszedő program kifejlesztése is, amelynek különféle változatait most is használják könyvek, folyóiratok szerkesztéséhez, szedéséhez. Az elterjedt irodai programcsomagokkal készített dokumentumok külleme meg sem közelíti azt, ami a TeX segítségével elérhető!
Az eredmény
... és kész. A programot lefuttatva megkapjuk a π közelítő értékét.
3.131121
_
(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:
bool
Összetett, származtatott típusok:
- Tömb: egyforma típusú elemek sorszámozott tárolója
- Struktúra: összetartozó adatok – erről lesz most szó.
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
ö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.
A struktúrák fogalma
─────
nev
A struktúrák arra jók, hogy egy új típust hozzunk vele létre. Ez a típus egyedi lesz, saját névvel fog rendelkezni a programban. A struktúrák segítségével összetett dolgokat (pl. racionális számokat) tudunk egyszerűbb, meglévő típusokkal (két egész szám, számláló és nevező) reprezentálni. A strukturák által összetartozó adatokat tudunk egységként kezelni.
- Összetartozó adatok egységként
- Új típus létrehozása
- Absztrakt fogalom reprezentációja egyszerűbb típusokkal
A törteket egységként kezelve, struktúrával:
typedef struct Tort {
int szaml, nev; // tört: számláló és nevező
} Tort;
Tort a, b, c, d, x; // mindegyiknél számláló, nevező
x = szorzat(osszeg(a, b), osszeg(c, d)); // (a+b)*(c+d)
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
.
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 Tort {
int szaml, nev;
};
struct Tort t1, t2;
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
t1 tört: 3/4
struct Tort t1;
t1.szaml = 3; // a számlálója legyen 3
t1.nev = 4;
printf("t1 tört: %d/%d", t1.szaml, t1.nev);
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. t1.szaml
jelentése: a t1
tört számlálója.
Ebben t1
típusa struct Tort
, t1.szaml
típusa pedig int
. 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 egészében nem tudják kezelni, csak az egyes beépített
típusú adattagokat.
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. 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. Lássuk egy másik példán:
struct Pont {
double x, y;
};
typedef struct Pont Pont;
Pont p;
/* röviden, egyszerre: */
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 struktú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.
Struktúrák inicializálása
typedef struct Pont {
double x, y;
} Pont;
Pont p1 = { 2, 5 }; // 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.
Struktúra típusú literális
Pont p2 = { .x = 2, .y = 5 }; // inicializálás
p2 = (Pont) { 2, 5 }; // értékadások
p2 = (Pont) { .x = 2, .y = 5 };
A C nyelv újabb (C99 szabvány utáni) változataiban ilyet is lehet írni.
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;
Értékadás
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(Pont p) {
return sqrt(p.x*p.x + p.y*p.y);
}
/* megadja a szakaszfelezőt */
Pont szakaszfelezo(Pont p1, Pont p2);
Pont a, b, c;
a = szakaszfelezo(b, c);
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.
A típusokat általában globálisan adjuk meg: mindenhol látszódjanak.
/* globálisan */
typedef struct Pont {
double x, y;
} Pont;
int fuggveny(void) {
Pont p1, p2; // látható
}
int masik_fuggveny(void) {
Pont p; // itt is
}
/* lokálisan */
int fuggveny(void) {
typedef struct Pont {
double x, y;
} Pont;
Pont p1, p2; // látható
}
/* Ez így HIBÁS! */
int masik_fuggveny(void) {
Pont p; // 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.
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
- 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(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 2/3 = 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.
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.
Struktúrába egy dolog összetartozó adatait tesszük.
- Különálló, új típus, saját műveletekkel
- Pl. egy könyv adatai: cím, szerző, oldalszám
Tömbben több egyforma dolog adatait tároljuk.
- Ez csak egy tároló azonos szerepű dolgok számára.
- Pl. könyvek katalógusa
Elfajulások lehetségesek: létezhet egy elemű struktúra vagy tömb is. Az, alma, a, körte: ezek szavak, még az „a” névelő 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, az is egy szó! 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 struktúrá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 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.
Értékadás
typedef struct Pont {
double x, y;
} Pont;
Pont a, b;
a = b; // OK!
int t1[100];
int t2[100];
t1 = t2; // Hibás!
Függvény visszatérési értéke
Pont origo(void) {
Pont pont = {0, 0};
return pont; // OK!
}
????? nullatomb(void) {
int t[100] = {0};
return t; // Hibás!
}
Az értékadás a struktúrák között működik C-ben, a tömbök között nem. A függvényből visszatérés struktúrával működik C-ben, a tömbbel nem. Mindez azért van, mert a C nyelv szabályai szerint struktúrát lehet értékadással másolni, tömböt viszont nem.
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.
typedef struct Datum {
int ev, honap, nap;
} Datum;
typedef struct Ember {
char nev[100];
char lakcim[150];
Datum szuletesnap;
} Ember;
Ember e1, e2;
e1 = e2; /* Így jó, a tömbök is másolódnak! */
Vagy egy geometriai programban, 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);
Szövegek reprezentálása: karaktertömb
char str[100] = "hello";
printf("A szöveg: [%s].", str); // A szöveg: [hello].
A szöveg karakterek sorozata: sztring (string),
más néven karakterlánc. C-ben nincs külön típus, hanem karakterek
tömbjeként adjuk meg. Mivel tömbről van szó, 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.
Kétdimenziós tömb: tömbök tömbje
double matrix[5][6];
matrix[sor][oszlop] = 3.14;
Itt egy 5×6-os táblázatot adunk meg: 5 sor × 6 oszlop.
char tictactoe[3][3];
tictactoe[0][2] = 'o';
A tic-tac-toe játékhoz 3×3-as játéktér kell.