Mutatók. Felsorolt típus. Állapotgép
Tartalom
- A Nagy Félreértés
- NHF kiadás
- Mutatók (pointerek)
- Mutatók és indirekció
- Cím szerinti paraméterátadás
NULL
: a sehova nem mutató pointer- Cím aritmetika (pointer arithmetic)
- Cím aritmetika – az indexelés működése
- Tömböt átvevő függvények
- Több dimenziós tömbök – röviden
- Pointerek: így már minden érthető
- "Sztringek"
- A sztringek létrehozása
- A sztringek átadása függvénynek
- A sztringek változtatása függvényben
- Sztring másolása
- Sztring másolása – a klasszikus megoldás
- Beépített sztringkezelő függvények
- Felsorolt típus
- Meghatározott értékek halmaza
- Felsorolt típus
- Felsorolt típus: a hozzárendelt értékek
- Felsorolt típus: definíció
- Praktikus párja: a
switch()
szerkezet - Állapotgépek
- Szmájlik GTalk-on, Facebookon
- Előtte még: karakterek beolvasása, kiírása
- Klasszikus állapotgép: az „ly” számláló
- Állapotgép
- Állapot- és tevékenységtábla, gráf
- Ly számláló: C kód
- Állapotgépek: szmájlik cseréje
- Állapotgép táblázattal – leképezések
- Állapotgép táblázattal: a táblázat
- Állapotgép táblázattal: a kód
- Állapotgépek általában
1 A Nagy Félreértés
A tipikus hiba
Feladat: milyen számjegynek (0…9) felel meg a beolvasott karakter?
char c; if (c>='0' && c<='9') ertek = c-'0'; else ertek = -1; /* nem szám */
switch()
tanulása előtt :)
switch (c) { case '0': ertek = 0; break; case '1': ertek = 1; break; case '2': ertek = 2; break; case '3': ertek = 3; break; ...
switch()
tanulása után :(
A programozásban…
Nem azokkal a nyelvi elemekkel kell megoldani a feladatot, amik a legutóbbi órán szerepeltek, hanem azokkal, amik valók hozzá!
2 NHF kiadás
A házi szabadon választott, egyénileg elkészített!
- Ötletek a honlapon – hasonló nehézségű saját is lehet
- A választást a laborvezető mindenkinél esetben jóvá kell hagyja!
- Kb. 500 soros C program, kötelező elemek:
- strukturált felépítés (függvények, típusok, több forrásfájl)
- fájlkezelés, dinamikus memóriakezelés
NHF 1-2-3-4.
Beadandók (elektronikusan)
- Feladat választása: 7. hétig
- Pontosított specifikáció: 8. hétig
- Félkész program: 10. hétig
- Végleges változat: 13. hétig; laboron bemutatva!
- Kód + felhasználói, programozói, tesztelési dokumentáció
4 Mutatók és indirekció
Mutató (pointer)
- A mutató egy meghatározott típusú változó memóriabeli helye, más néven: címe
- A cím képzése a
&
címképző operátorral történik:&valtozo
- A mutatott változó elérhető a
*
indirekció operátorral:*ptr
- Ez a cím eltárolható egy változóban
Indirekció (indirection)
double x; double *ptr; ptr = &x; // cím képzése (address of) *ptr = 3.14; // mutatott értékre hivatkozás
Az &x
(address of x) kifejezéssel képezzük az x
változó címét.
Azt a címet kapjuk meg, ahova a fordító, ahova a memóriában elhelyezte
a változót. Ezt a címet bemásoljuk a ptr
nevű változóba, amely, double*
típusú lévén,
pont ilyet tud tárolni.
*ptr
azt jelenti, hogy a pointer által mutatott érték. Mivel
ptr
éppen az x
változóra mutat, ezért ez egyenértékű
azzal, mintha x
-ről beszélnénk, azaz x
-nek adnánk értéket.
ptr
pointer később más változóra is mutathat. Mivel a típusa double*
, bármelyik
double
típusú változó címét tárolhatja. A pointer maga egyébként egy teljesen szokványos
változó: értéket kell adni neki használat előtt, átadható függvénynek paraméterként stb.
A címet gyakran referenciának (reference) is szokás nevezni. Ezért mondják a *
operátorra, hogy dereferál (dereference), azaz megszünteti a referenciát, és általa már a
mutatott, tényleges értéket látjuk.
Címek és mutatott értékek
#include <stdio.h> int main() { int a=5, b=10; int *p; p = &a; printf("p=%p \n *p=%d \n\n", p, *p); p = &b; printf("p=%p \n *p=%d \n\n", p, *p); return 0; }
Pointer értékét, amely a memóriacím maga (vagyis a fenti példában az
x
és y
helye a memóriában), a %p
konverzióval lehet kiírni. Ez akkor lehet jó, ha a programunkban hibát keresünk.
Ugyan a scanf %p
képes beolvasni egy pointert, de azzal
sokra nem megyünk. Nincs értelme megjegyezni egy pointert, pl. kiírni egy
fájlba és használni a program későbbi újrafuttatásánál, hiszen minden egyes
futtatáskor máshova kerülhetnek a memóriában a változók! (Próbáld ki,
futtasd le többször a programot!)
5 Cím szerinti paraméterátadás
A paraméterként kapott változók tartalmát megcserélő függvény:
képes módosítani
a paraméterként
kapott változót!
/* Megcseréli a két számot. */ void csere(int *pa, int *pb) { int temp; temp = *pa; // dereferálás *pa = *pb; *pb = temp; }
Használat:
int a=3, b=4; csere(&a, &b); // címképzés: hol van a változó?
Ez nagyon fontos! Így lehet megoldani azt,
hogy egy függvény tudjon módosítani egy paraméterként kapott változót.
A trükk itt az, hogy bár továbbra is érték szerinti
paraméterátadás van (mint mindenhol C-ben), az átadott érték az nem a változó értéke, hanem
a változó címe. A csere()
függvény most is mindent másolatként kap;
annak a pa
nevű pointerében van egy másolat az a
változó címéről, a pb
nevű pointerében pedig egy
az b
változó címéről. Ezek viszont a külső változókra
mutatnak, rajtuk keresztül az eredeti a
és b
változókat lehet elérni és akár megváltoztatni.
6 NULL
: a sehova nem mutató pointer
Két visszatérési értékű függvény:
sehova
nem mutat
void szamol(int a, int b, int *possz, int *pszorz) { if (possz!=NULL) *possz = a+b; if (pszorz!=NULL) *pszorz = a*b; } int sz; szamol(5, 7, NULL, &sz); // csak a szorzatot kérem
A NULL
pointer egy olyan mutatót jelent, amely nem mutat semmilyen
változóra. Bármilyen típusú pointer (int*
, double*
,
struct Pont*
stb.) lehet NULL
értékű. Egy lehetséges
használatra a fenti kód mutat példát: a függvény kiszámolja a két paraméter összegét
és szorzatát, amelyeket az ossz
és szorz
változókba tesz.
A számolás mindkét esetben csak akkor történik meg, ha nem NULL
pointert
kapott az adott változóhoz. Vagyis megtehetjük azt, hogy csak az összeg vagy csak
a szorzat kiszámolására kérjük meg a függvényt.
NULL-e? Nem NULL-e?
if (ptr!=NULL) printf("Mutat valahova.\n"); if (ptr==NULL) printf("Sehova sem.\n");
if (ptr) printf("Mutat valahova.\n"); if (!ptr) printf("Sehova sem.\n");
A pointerek a logikai kifejezésekhez hasonlóan használhatók.
Igazra értékelődnek ki, ha mutatnak valahova, és hamisra, ha nem.
Így aztán a !
tagadó operátor is működik: !ptr
igazra értékelődik ki, ha ptr
nem mutat sehova,
vagyis NULL pointer. „Ha nincs ptr
, akkor” – így meg
lehet jegyezni. Emiatt if (ptr!=NULL)
és if (ptr!=0)
és if (ptr)
mind ugyanazt jelentik. Ahogyan az if (ptr==NULL)
,
if (ptr==0)
és if (!ptr)
is tökéletesen egyenértékűek.
Gyakran szokott vita lenni abból még gyakorlott programozók között is,
hogy ugyanaz-e a 0
és a NULL
.
A C szabvány megengedi azt, hogy a NULL
, vagyis a sehova nem mutató pointert
0
-val jelöljük (ISO/IEC 9899:1999, § 6.3.2.3 (3)), ha a program szövegéből kiderül, hogy az egy pointer
kell legyen. Vagyis ez a kódsor tökéletesen helyes:
int *p=0;
Mindez kifejezetten a C nyelv sajátja, más nyelvekben nem feltétlenül van így!
7 Cím aritmetika (pointer arithmetic)
C-ben egy tömbbel egy valamit lehet csinálni: a nevét írva megkapjuk a tömb kezdőcímét, vagyis az első elemének helyét a memóriában.
int tomb[5], *p1, *p2, tav; p1=tomb; // a tömb kezdőcíme p2=&tomb[4]-1; // tomb[4-1] címe tav=p2-p1; // távolság: 3
Mivel az elemeik egymás után helyezkednek el, a többi elem címe kiszámítható!
A pointer aritmetika azt jelenti, hogy memóriacímekkel végzünk számításokat.
Ennek tömböknél van értelme, hiszen ezáltal a tömb kezdőcímének és a típusának
ismeretében meghatározható az egyes elemek címe. (Ahogyan azt is meg tudjuk
mondani, mi a szomszédos ház címe.) Ezt a kódban úgy jelöljük, hogy a pointerhez
hozzáadunk egy egész számot – azt a számot, hogy a tömbben a címtől számítva
hányadik elem címére vagyunk kíváncsiak. A hozzáadás a tömb vége felé, a kivonás a tömb
eleje felé való mozgást jelent. Két pointert akár ki is vonhatunk egymásból,
hogy megkapjuk a közöttük lévő távolságot (ugrásszámot). Sőt még a
<=
, >
stb. operátorokat is használhatjuk.
Természetesen ezeknek csak akkor van értelme, ha a két pointer ugyanazon
tömb belsejére mutat.
Emiatt is fontos a pointerek típusa. A típusból tudja a fordító,
hogy az adott változó, amire a pointer mutat, hány bájtból áll.
Ha például a pointer 8 bájtos double
típusra mutat,
a p+1
azt jelenti, hogy 8 bájtot ad hozzá a p
pointer
értékéhez. Ezzel azonban nekünk nem kell foglalkozni, a
fordító ezt automatikusan megoldja a háttérben! Nekünk csak arra kell gondolni, hogy
p+1
a következő double
, p+2
az azt követő stb. A bájtok számolgatását végző kódért a fordító felel.
Tudni kell azt, hogy egy önálló int
változóra mutató
int *p
pointer esetén is helyes szintaktikailag a p+1
kifejezés. Vagyis a program lefordítható,
csak szemantikailag helytelen. Ugyanis nem tudhatjuk, hogy milyen
változót helyezett el a fordító az adott egész után. Ilyen hibákat elkövetve
ahhoz hasonló misztikus hibákat és programlefagyásokat kelthetünk, mint amilyeneket
például tömb túlindexeléssel is.
Néhány szó a fenti változódefiníciókról. Az
int tomb[5], *p1, *p2, tav;
definíciók azt jelentik, hogy négy változót hozunk létre egyszerre, amelyek
mind egész számokkal kapcsolatosak. Nevezetesen: megfelelő módon
használva azokat, mindegyik által egész számokhoz juthatunk.
A tav
változó egy szokványos int
.
A tomb
egy
öt elemű tömb: valami, amit ha megindexelünk []
,
egy int
-et kapunk. p1
és p2
int
-re
mutató pointerek: az indirekció
*
operátorát használva rajtuk int
-eket kapunk.
int* pi, i;
Látható az elv, hogy C-ben a változókat a használat módja alapján kell
létrehozni. Ezért szokás a *
-ot a változók neve mellé tenni,
nem pedig a típus neve (itt az int
) mellé.
Hiszen ha több pointert hozunk létre, akkor mindegyik neve mellé oda kell
tennünk a csillagot. Hiába tesszük a csillagot az int
mellé, a jobb oldali példában akkor is egy pointert és egy egész számot hozunk létre.
Ezért ne használjuk így, hanem inkább a fenti formában!
A fentiek alapján a *
karakter a programkódban kétféle dolgot
jelenthet, attól függően, hogy
deklaratív helyen van (vagyis egy változó típusának megadásakor), vagy utasításban.
Deklaratív helyen: int *pi
azt jelenti, hogy a nevezett változó egy
pointer. Utasításban: *pi=5
jelentése az, hogy a pi
pointert
dereferáljuk, azaz az általa mutatott változóról beszélünk. A két jelentés persze
nincs távol egymástól: az előbbinél megmondjuk, hogy hogyan fogjuk használni, az utóbbi
pedig a konkrét használat.
8 Cím aritmetika – az indexelés működése
Tömbök és pointerek
int tomb[10]; *(tomb+2)=3; tomb[2]=3; // ugyanaz
int *p = tomb; *(p+2)=3; p[2]=3; // ugyanaz
A tömbök egy elemének elérésekor mindig két művelet történik.
Az egyik a kérdéses elem címének kiszámítása (ez egy pointer aritmetikai
művelet), a másik pedig az elem elérése (az indirekció a kiszámított
pointerrel.) A tömb nevének leírásával tulajdonképp mindig azt
kérjük a fordítótól, hogy adja meg, hol található a tömb a memóriában.
Ilyenkor egy pointert kapunk, amelyen a []
indexelő operátort
használva címszámítást és dereferálást is végzünk.
A C nyelv az összes tömbi műveletet így értelmezi. Ez történt a 3. előadás
óta az összes tömbös programban, csak mindezidáig hallgattunk róla. Ezt mutatja
a bal oldalon látható kódrészlet is: *(tomb+2)
ugyanazt jelenti,
mint t[2]
, egy címszámítást és egy dereferálást.
A jobb oldalon a p
pointert ugyanazon tömb elejére állítjuk (a 0. indexű elemre). Így a
p+2
kifejezés értéke egy pointer, amely a tömb 2. elemére mutat,
*(p+2)
pedig ez a pointer dereferálva, vagyis a 2. indexű elem maga.
A p
pointeren keresztül is a tomb
nevű tömb elemeit
érjük el, mivel ez a pointer a tömb elejére mutat.
Nagyon fontos megjegyezni: a tömbökön és a pointereken is használható az indexelő operátor. Mindkét esetben ugyanazt jelenti a használata, egy címszámítást és egy dereferálást.
Tömbös ciklusok
double t[100]; int i; /* i=0→99, 100 már nem */ for (i=0; i!=100; ++i) t[i] = 0.0;
double t[100]; double *p; /* p=t+0→t+99, t+100 nem */ for (p=t; p!=t+100; ++p) *p = 0.0;
A tömbökön végigmenő ciklusokat nem csak indexeléssel,
hanem pointerek használatával is megírhatjuk. A bal oldalon látható a szokásos,
indexelő operátort használó forma (annyi különbséggel, hogy i<100
helyett i!=100
szerepel, de jelen esetben ezek egyformán viselkednek).
A jobb oldalon a pointeres. A ciklus kezdetén a pointert beállítjuk a tömb elejére,
és egészen addig fut a ciklus, amíg el nem éri a pointer a tömb 100. indexű
elemét. Mivel a tömb csak 0…99-ig indexelődik, a t+100
cím
használata már túlindexelés lenne, ezért a ciklus itt megáll.
A két forma egymással teljesen egyenértékű. Mindkét
esetben egyébként balról zárt, jobbról nyílt intervallummal dolgoznak a ciklusok:
az i=0
indexű, azaz a t+0
című elemet feldolgozzák,
az i=100
indexű, azaz t+100
címűt pedig nem.
9 Tömböt átvevő függvények
Fejléc és törzs
Az alábbi függvények tömböt vesznek át paraméterként.
A kiir()
kiírja az elemeiket visszafelé (az utolsótól
az elsőit), a beolvas()
pedig feltölti a tömböt
a billentyűzetről beolvasott értékekkel.
void kiir(double *tomb, int meret) // kezdőcím és méret { int i; for (i=meret-1; i>=0; --i) printf("%g ", tomb[i]); // indexelő operátor printf("\n"); } void beolvas(double tomb[], int meret); // ugyanazt jelenti (!)
A függvénynek átadjuk a tömb elejére mutató pointert. Az még csak egy pointer, abból nem fogja tudni az elemszámát – ezért átadjuk neki a tömb méretét is. Nagy előny, hogy így a függvény bármekkora tömbön használható.
Bár a függvény pointert kap, azon belül tömbként használhatjuk, mivel a C nyelv megengedi azt, hogy pointeren használjuk az indexelő operátort. Fel sem tűnik a különbség, mivel ez ugyanúgy működik a pointeren, mintha „igazi” tömb lenne! (Ne felejtsük: ha tömbön használjuk az indexelő operátort, akkor is ugyanez történik!)
Ha tömböt adunk át egy függvénynek, akkor a függvény
formális paramétereinek listájában használható a tomb[]
jelölés is. Ez azonban ne tévesszen meg senkit: ilyenkor is csak
egy pointer adódik át. Tökéletesen ugyanazt jelenti,
mint a *tomb
forma – és az egyetlen hely,
ahol definiálatlan méretű tömb (vagyis üres []
zárójel)
használható.
Mivel a függvény a tömböt a pointerével veszi át, bele is tud írni.
Ez természetesen független attól, hogy a fejlécében *tomb
vagy tomb[]
formában hivatkozunk rá, mert a kettő egy
és ugyanaz.
Hívás (használat)
double szamok[10]; kiir(szamok, 10); // neve → kezdőcím
A híváskor a tömb nevét adjuk első paraméternek, ilyenkor a függvény
a tömb kezdőcímét kapja meg. Természetesen a tömb méretét is meg kell adni.
Fontos viszont, hogy mivel a függvény cím szerint veszi át a tömböt,
meg is tudja változtatni az elemeit! Ebből a szempontból
nagy a különbség a beépített típusok és a tömbök függvény paraméterként
történő átadása között. De, mint azt eddig láttuk, igazából semmi különbség
nincsen – ilyenkor is érték adódik át, csak az érték a tömb kezdőcíme
(ami pointerként egy beépített típus), nem pedig a teljes tartalma.
A beolvas()
függvény egyébként így képes ellátni a feladatát,
hogy a billentyűzetről számokkal töltse fel a tömböt.
10 Több dimenziós tömbök – röviden
int tomb[3][4]; // 3 sor, 4 oszlop
A két dimenziós tömbök sorfolytonosan helyezkednek el a memóriában:
- Első sor vége után a második sor eleje
- Indexelés:
tömb[sor][oszlop]
- A fordító az
y*szélesség+x
képletet használja.
Emiatt két dimenziós tömböt úgy kell átadni függvénynek, hogy a szélességét is meg kell adnunk, már a típusban:
void fuggveny(int tomb[][4], int magassag); // int (*tomb)[4]
A void fuggveny(int tomb[][])
emiatt szabálytalan!
11 Pointerek: így már minden érthető
int a; scanf("%d", &a); // cím szerinti átadás
Így már érthető! Különben nem tudná beleírni a beolvasott számot.

A többi furcsaság
- Az elmondottak miatt nincs
t1=t2
tömb értékadás - Ezért nincs sztring értékadás (azok is tömbök)
- Ezért nem lehet sztringeket
==
operátorral összehasonlítani. A címüket hasonlítja össze, nem a tartalmukat!
13 A sztringek létrehozása
Sztringek
A sztringek nullával (
'\0'
vagy0
) lezárt karaktertömbök.
h e l l o \0 ± ¤ % X § » " $ »
A NUL nevű ASCII vezérlőkódot a sztringek végének jelölésére
tartjuk fenn. A karakter kódja 0. Ezt a forráskódban '\0'
és 0
formában írhatjuk – de nem keverendő a '0'
-val, ami a nullás számjegyet
jelöli, és a kódja 48!
Sztring létrehozása és inicializálása
char szoveg1[50] = { 'h', 'e', 'l', 'l', 'o', '\0' }; char szoveg2[50] = "hello";
A karaktertömb tartalma: a karakterek és a lezáró nulla.
Ha a "Hello"
formát írjuk, akkor is hozzáteszi a fordító
a lezáró nullát. Ezért a fenti két inicializálás tökéletesen ugyanazt
jelenti, de természetesen az alsót használjuk inkább. (Az ilyesmit
szintaktikai édesítőszernek (syntactic sugar) szokás nevezni – szebb,
olvashatóbb kódot kapunk.)
A tömb ilyenkor 50 karakterből áll. Abból a szöveg 5 bájtos, de végülis a sztring 6 bájt helyet foglal el belőle, mert 1 bájt a lezáró nullának is kell!
Egy adott méretű tömbbe méret−1 hosszú, azaz egy karakterrel rövidebb szöveg fér csak! A lezáró 0-nak is kell hely!
Erre a szabályra nagyon fontos emlékezni! Az „alma” szó eltárolásához például
egy 5 (öt!) elemű karaktertömbre van szükség: char szoveg[5]="alma"
.
Négy nem elég, mert a lezáró nulla akkor már nem férne bele. A számonkéréseken
pontlevonás járhat azért, ha valaki erre nem figyel.
A fenti utasítások egyébként nem értékadások, hanem inicializálások. Az =
jel itt
azt jelenti, hogy létrehozunk egy tömböt, amelyet kezdeti értékekkel töltünk fel.
Nem pedig értékadást – hiszen tömbök közötti értékadás nincs.
[1] e
[2] l
[3] l
[4] o
[5]\0
#include <stdio.h> int main() { char str[50]="hello"; printf("1. %s\n", str); str[0]='H'; printf("2. %s\n", str); str[5]='!'; str[6]='\0'; printf("3. %s\n", str); return 0; }
A 2. lépésnél a sztring legelső karakterét, a 'h'
-t
felülírjuk egy nagybetűs 'H'
-val. Mivel tömbről van szó,
az első karaktere a 0. indexű.
A 3. lépésben egy új karaktert fűzünk a sztringhez. Az eredeti
sztringben a Hello
szöveg betűi a tömb 0–4. indexű
elemeit foglalták el; az 5. indexen volt a lezáró nulla. Azt a lezáró
nullát felülírjuk egy felkiáltójellel, és a következő üres helyre
elhelyezünk egy új lezáró nullát, hiszen annak mindig lennie kell.
Így lesz az új tartalom "Hello!"
.
A printf %s
pedig a lezáró nulla alapján tudja, hol
van vége a sztringnek, azért nem írja ki mind a húsz karaktert.
14 A sztringek átadása függvénynek
nem veszi át!
int sztring_hossza(char *sztring) { int i; for (i=0; sztring[i]!='\0'; ++i) ; /* üres */ return i; }
Az üres ciklus igazából nem üres: ++i
.
char str[20]="Hello"; printf("%d", sztring_hossza(str)); // 5
Miért nem adjuk most át a függvénynek a tömb méretét?
De tényleg, miért, mikor az előbb a tömbök/függvények témakörben azt mondtuk, hogy mindig át kell adni?! Hát azért, mert a tömb méretének nincs köze a sztring hosszához! Azért, mert a lezáró 0-ból tudni fogjuk, hol van a sztringnek vége. És persze azért, mert épp azt várjuk a függvénytől, hogy számolja meg. :)
0 | 1 | 2 | 3 | 4 | 5 |
H | e | l | l | o | \0 |
A ciklus i=5
-nél fog megállni, mivel a sztringben
az 5. indexű elem a lezáró nulla. Ez egyben pont a sztring hossza is,
vagyis a benne lévő hasznos karakterek száma (a lezáró nullán kívül).
Ez azért jön ki pont így, mivel az értékes karakterek a 0. indextől
kezdődően találhatóak a tömbben.
Konklúzió: a lezáró nulla pont ott annyiadik indexű elem a tömbben, mint amekkora a sztring.
15 A sztringek változtatása függvényben
toupper()
a→A
#include <ctype.h> void sztringet_nagybetusit(char *sztring) { int i; for (i=0; sztring[i]!='\0'; ++i) sztring[i] = toupper(sztring[i]); }
A sztringet címével látja, ezért meg is változtathatja azt!
char str[]="Hello"; sztringet_nagybetusit(str); // Hello → HELLO
Nem kell & operátor a paraméter átadásánál! A sztring egy tömb, amelynek a nevét írva már eleve pointert kapunk a tömb elejére!
16 Sztring másolása
void sztringet_masol(char *ide, char *innen) { int i; for (i=0; innen[i]!='\0'; ++i) // ugyanolyan indexűeket ide[i]=innen[i]; ide[i]='\0'; // és még a lezáró nulla! }
A ciklus átmásolja az „értékes” karaktereket.
- Utána pedig még a lezáró nullát kell, pont az
ide[i]
helyre
Túlindexelés veszélye: a függvény nem tudja, mekkora a cél tömb!
- Emiatt nem tud felelősséget vállalni ezért! Ha túl kicsi, túlírja!
- A függvény hívója felel érte, hogy elég nagy legyen!
17 Sztring másolása – a klasszikus megoldás

Alapmű: Brian Kernighan and Dennis Ritchie: The C Programming Language.
void masol(char *ide, char *innen) { while (*ide++ = *innen++) ; }
A ciklus feltételében értékadás van!
- Főhatás: a másolt karakter kódja
- Mellékhatás: mindkét pointer a következő karakterre mutat (postincrement, utólagos)
A főhatás a ciklus feltétele: ha a lezáró nulla, az logikai hamis. Ettől megáll a ciklus, de azt még átmásolta.
18 Beépített sztringkezelő függvények
#include <string.h> char str[50], *hol; strcpy(str, "alma"); strcat(str, "fa"); printf("%d", strlen(str)); hol=strstr(haystack, needle);
#include <stdio.h> gets(str); // problémás puts(str); scanf("%s", str); // ! printf("str: %s\n", str); sprintf(str, "x=%d", 19); sscanf(str, "%d", &i);
A függvények:
char* strcpy(char *ide, char *ezt)
– sztringet másol.char* strcat(char *ehhez, char *ezt)
– „ehhez” sztringhez hozzáfűzi „ezt”.size_t strlen(char *str)
– visszatér a sztring hosszával (size_t egy egész szám).char* strstr(char *haystack, char *needle)
– megkeresi a tűt (needle) a szénakazalban (haystack). Ha megtalálta, pointert ad rá, ha nem, NULL pointer.gets(char *str)
– beolvas egy egész sort a billentyűzetről.puts(char *str)
– kiírja a sztringet és új sort kezd.printf("%s", str)
– kiírja a sztringet.scanf("%s", str)
– beolvas egy szót (szóköz, enter, tabulátor karakterig).sprintf(str, formátum, ...)
– ugyanaz, mint aprintf()
, de a sztringbe ír, nem a szabványos kimenetre.sscanf(str, formátum, ...)
– ugyanaz, mint ascanf()
, de a sztringből olvas, nem a szabványos bemenetről.int strcmp(char *a, char *b)
– összehasonlít két sztringet. A visszatérési értéket lásd feljebb.
Azoknál a függvényeknél, amelyek egy sztringet írnak, a hívó felelőssége megfelelő méretű tömböt biztosítani!
Pl. az strcpy()
esetén az ide[]
tömb legalább akkora kell legyen, mint strlen(ezt)+1
.
Sok függvénynek van n
betűs párja: strncpy()
, strncat
stb., amelyek figyelembe
tudják venni a cél tömb méretét is. Azonban ezek nem pontosan úgy működnek, ahogy várnánk: nem biztos,
hogy lezárják nullával a cél tömböket!
Ez a probléma különösen a gets()
-nél jelentkezik, mivel ott a sor hossza a felhasználótól
függ. Emiatt azt veszélyes függvénynek
szokták tartani, hiszen sokszor használták már ki ezt a dolgot számítógépek feltöréséhez (crack),
jogosulatlan hozzáférés megszerzéséhez. Ajánlott az fgets()
függvényt használni
helyette.
Van még sok másik, amelyekkel kapcsolatban a puskát érdemes böngészni. További fontos függvények:
char *strchr(char *str, int c);
– c karakter első előfordulásának címe. NULL, ha nincs.char *strrchr(char *str, int c);
– az utolsó előfordulás címe vagy NULL.char *strstr(char *szenakazal, char *tu);
– a tű sztring keresése a szénakazalban. Első előfordulás címe vagy NULL.
Fontos, hogy a sztring paramétereknél sehol nem kell &
címképző operátor, még a scanf()
-nél sem!
Emlékezzünk arra, hogy a sztringek C-ben tömbök, amelyeknek a neve önmagában pointert jelent.
Összehasonlítás (compare)
int strcmp(char *a, char *b); if (strcmp(s1, s2)==0) printf("Egyformák.\n");
A visszaadott egész szám:
- 0, ha egyformák
- negatív, ha
a<b
- pozitív, ha
a>b
20 Meghatározott értékek halmaza
Kártya
♠ ♣ ♥ ♦
Napok
Hétfő, kedd, szerda… vasárnap.
Tic-tac-toe
üres, kör, iksz
Közlekedési lámpa
piros, piros+sárga, zöld, sárga

21 Felsorolt típus
Olyan típus, amelynek értékkészlete egy névvel megadott értékhalmaz.
Definíciója
enum Lampa { piros, piros_sarga, zold, sarga };
Használata
enum Lampa l; l=piros; if (l==zold) printf("Mehet!\n");
A színfalak mögött
- Minden névhez egy egész számot rendel a fordító
- A lefordított programban azok a számok szerepelnek
- Így a program gyors, nekünk pedig érthető a kód!
22 Felsorolt típus: a hozzárendelt értékek
Számozás
enum Lampa { piros, piros_sarga=2, zold, sarga };
- Ha mást nem adunk meg, 0-tól 1-esével
- Ezt módosíthatjuk:
piros=0, piros_sárga=2, zöld=3, sárga=4 - De ha ilyet teszünk, gondoljuk meg: ilyenkor általában helytelen választás az enum
Trükkös konstans
enum { MERET=100 }; int i, tomb[MERET]; // fordítási idejű konstans! for (i=0; i<MERET; ++i) scanf("%d", &tomb[i]);
Persze így csak egész szám konstanst lehet létrehozni. Ha a felsorolt típus neveihez egész számértéket rendelünk, akkor még akár azt is megtehetjük, hogy az egyes nevekhez ugyanazt. Ilyenkor azok egymással ekvivalens nevek lesznek.
23 Felsorolt típus: definíció
enum Cella { ures, kor, iksz }; typedef enum Cella Cella;
/* egyben a typedeffel */ typedef enum Cella { ures, kor, iksz } Cella;
Ez a leggyakrabban használt forma.
/* rögtön két változó is */ enum Cella { ures, kor, iksz } c1=ures, c2=iksz;
Ezzel definiáljuk az enum Cella
típust, és
egyből létre is hozunk ilyen típussal két változót: c1
-et
és c2
-t. Még inicializáljuk is őket.
/* névtelenül - ritkán */ enum { ures, kor, iksz } palya[3][3];
Egy névtelen felsorolt típus, amelyből 3×3-as tömböt építünk.
Ugyanazok a szintaktikák használhatóak, mint a struct
esetén. Névtelen típust azonban nem ajánlott létrehozni –
ha más miatt nem, azért, mert a fordító a hibaüzeneteiben nem tud
rá hivatkozni.
24 Praktikus párja: a switch()
szerkezet
enum Lampa { piros, piros_sarga, sarga, zold };
/* a lámpa állapota */ enum Lampa lampa1; /* az egyes lencsék */ int p, s, z;

switch (lampa1) { case piros: p=1; s=0; z=0; break; case piros_sarga: p=1; s=1; z=0; break; case zold: p=0; s=0; z=1; break; case sarga: p=0; s=1; z=0; break; }
26 Szmájlik GTalk-on, Facebookon
Feladat: megkeresni a beírt szövegben a szmájlikat, és utána úgy kiírni a szöveget, hogy képek szerepelnek benne helyettük.
Szmájli | Ezt kell beírni |
---|---|
![]() | :) :-) |
![]() | :D :-D |
![]() | 8-) |
![]() | :(|) |
![]() | <3 |
27 Előtte még: karakterek beolvasása, kiírása
Karakterek olvasása: a printf
, scanf
%c
-n kívül használhatóak:
int getchar(); // beolvas
int putchar(int); // kiír
Nagyon fontos: a getchar()
visszatérési típusa int
!
int c; c=getchar(); if (c == EOF) printf("Bemenet vége!"); else printf("Karaktert olvastam, kódja: %d", c);
A getchar()
függvénynek a visszatérési értéke a fájl vége jelet
is tudja jelezni. Ezt úgy oldották meg a C-ben, hogy az stdio.h
tartalmaz egy EOF
nevű konstanst, amely ezt jelzi.
Ennek a konstansnak az értéke szándékosan kívül esik a karakter
típus ábrázolási tartományán, hiszen minden olyan szám, ami azon belül
van, az egy bájt, ami szerepelhet a program bemenetén. Ezért a
getchar()
függvény visszatérési típusa int
!
A visszatérési értéke EOF
, ha vége van a bemenetnek, és
nem sikerült már beolvasni egy karaktert sem; és egy karakterkód, ha
sikerült.
A getchar()
függvény visszatérési értékét char
típusban tárolni hiba, akkor az elvesző bitek miatt
(emlékezzünk: a char
kisebb, mint az int
)
lesz olyan karakter, amelyiket összekeveri a program a fájl vége jellel.
Természetesen miután meggyőződtünk róla, hogy nem EOF
, már bemásolhatjuk
vagy castolhatjuk karakter típusúra. A legtöbb karaktert kezelő függvény, pl.
putchar()
, toupper()
, isdigit()
stb. int
típusú paraméterrel rendelkezik, de ez legtöbbször nem lényeges a használatuk közben.
A getchar()
-nál viszont nagyon is!
Ez a ploglam minden 'r' betűt 'l' betűle cselél:
#include <stdio.h> int main() { int c; /* kalaktel */ while ((c=getchar()) != EOF) // éltékadás is egyben! if (c=='r') putchar('l'); else if (c=='R') putchar('L'); else putchar(c); return 0; // letuln zéló }
A (c=getchar())!=EOF
kifejezés működése
a következő:
- Kiértékelődik a
getchar()
kifejezés. Erre beolvasódik egy karakter vagy azEOF
fájl vége jel. - Ez bemásolódik a
c
változóba az értékadás miatt. - Az egész zárójelezett kifejezés az értékadás. Ennek értéke a másolt érték, vagyis maga a karakter.
- Ezt hasonlítjuk össze az
EOF
konstanssal. - Ha nem egyenlő vele, akkor karaktert olvastunk, és mehetünk be a ciklusba.
- Ha egyenlő, fájl vége jelet, akkor pedig kiléphetünk a ciklusból.
Fontos a zárójel. Ha az nem lenne ott, akkor az =
értékadás
és !=
egyenlőségvizsgálat operátorok precedenciája miatt
a getchar()!=EOF
összehasonlítás eredménye kerülne a c
változóba, nem a karakter! (A legkülső zárójelnek természetesen nincs köze a kifejezéshez, mert az
a while
-hoz tartozik.)
28 Klasszikus állapotgép: az „ly” számláló
Feladat: olvassunk be egy szöveget.
Számoljuk meg benne az ly
betűket:
- Az
ly
1-nek számít:lyuk
, - az
lly
2-nek:gally
, - nagybetűkkel most ne foglalkozzunk.
A probléma nehézsége
- Önmagában egyik karakternél sem egyértelmű a teendő!
l
-nél: nem tudjuk, mit kell majd csinálni:alma
,lyuk
,gally
y
-nál: a teendő attól függ, előbb mi történt:négy
,lyuk
Azt azért sejtjük, hogy a végleges döntés az y
karakternél
fog megszületni. Az l
-nél nem lehet, hiszen a jövőbe nem látunk. Úgyhogy
az utóbbi gondolatmenet a járható út. Eltárolni a teljes szöveget viszont felesleges,
hiszen elég mindig csak egy kis részletet látni belőle.
A kidolgozatlan ötlet ezek alapján:
sz=0; while ((c=getchar()) != EOF) { if (c=='y') // ha ez egy y switch (… ELŐZMÉNYEK …) { case ………: sz+=1; // … és l volt előtte break; case ………: sz+=2; // … és ll volt előbb break; default: break; // egyéb esetben semmi teendő } } printf("%d darab ly szerepelt.\n", sz);
29 Állapotgép
Állapotgép (véges automata, finite-state machine)
hasonló téma lesz:
sorrendi hálózatok
Működése: az eddig kialakult állapottól és az eseménytől függ:
- Az elvégzendő tevékenység
- A következő állapot
Állapotok a múlt alapján
+0 négy +1 lyuk +2 gally
Három állapot lehetséges:
- Előbb
l
volt - Előbb
ll
volt - Valami más karakter volt előbb
Ez eltárolható egy változóban → felsorolt típus (enum).
30 Állapot- és tevékenységtábla, gráf
Tervezzük meg! Melyik állapotban, milyen esemény hatására, mi a teendő?
Az állapottábla felépítése a következő. A táblázat sorai az egyes állapotokat mutatják (amelyekbe valamely régebbi események alapján került az automata). A táblázat oszlopai pedig az eseményeket (ezek most a beérkező karakterek). Minden eseménynél, vagyis minden karakter olvasásánál az aktuális állapottól és az éppen beolvasott karaktertől függően dől el az, hogy mit kell csinálni (tevékenység) és hova kell ugrani (állapot). Gyakran ezt két külön táblázatban adják meg – itt most az állapot- és tevékenységtábla egy táblázatba összegyúrva szerepel.
l | y | egyéb | |
---|---|---|---|
alap | →l_volt | - | - |
l_volt | →ll_volt | sz+=1, →alap | →alap |
ll_volt | - | sz+=2, →alap | →alap |
k | u | l | c | s | l | y | u | k |

Minden beérkező karakternél a tevékenység és a következő állapot az függ
a beérkező karaktertől és az állapottól. Más például a teendő egy beérkező y
karakternél akkor, ha előzőleg l
betűt láttunk. Ezért minden
karakter feldolgozásánál a táblázat egy cellájából kell kiolvasnunk a teendőket.
Az ly számláló állapottáblája a következőket jelenti:
- alap állapot: semmi, amire figyelni kellene. Ez egyben a kiindulási állapot is.
- Ha jön egy
l
betű, átmegyünk l_volt állapotba- Ha ilyenkor jön egy
y
, akkor a számlálót növelni kell (és → alap!) - Ha viszont még egy
l
, akkor meg ll_volt állapotba. Azért, mert ha harmadikkénty
érkezik, akkor nem +1, hanem +2 kell a számlálóba. - Ha bármi más, akkor viszont vissza alap állapotba (pl. almafa, az
l
utánm
betű jött).
- Ha ilyenkor jön egy
Az állapottábla mechanikusan előállítható. Először felvesszük egy táblázat
oszlopaiba a számunkra érdekes karaktereket (jelen esetben ezek az l
,
az y
és az összes többi). Utána az első sorba az alapállapotot,
ahonnan indul az automata. Végiggondoljuk, hogy ebben az állapotban mely
karakterre minek kell történnie. Ha kell, új állapotokat veszünk föl;
és addig folytatjuk, amíg van kitöltetlen sora a táblázatnak.
Jelen esetben: alap állapotban egy l
hatására még nem történik semmi,
de tudjuk, hogy a következő karakternél figyelni kell, mert az esetleg egy y
lehet. Ezért felvesszük az l_volt
állapotot. Alap állapotban a másik két
karaktertípus hatására semminek nem kell történnie. Ezzel kész az első sor. A második
sorban, az l_volt
állapotnál y
esetén növeljük a számlálót,
és visszaugrunk alap állapotba (hiszen a következő karakternél már nem lesz igaz, hogy
az ahhoz képest előző l
betű volt). Az ll_volt
állapotnál viszont
egy harmadik l
betű esetén maradunk ugyanabban az állapotban, mert a következő
karakternél igaz lesz az, hogy az előző kettő l
volt. (Más kérdés, hogy
van-e ilyen magyar szó egyáltalán.)
Az állapotgép működését gráffal is megadhatjuk:
Ez a megadás teljesen ekvivalens a táblázattal. Minden állapotra (a gráf csúcsai) megadja, hogy az egyes eseményeknél (élekre írt karakterek) mi a teendő. A nyíl az új állapot felé mutat, illetve az elvégzendő tevékenység is az él mellé írva szerepel.
31 Ly számláló: C kód
typedef enum Allapot { alap, l_volt, ll_volt } Allapot;
Allapot all=alap; /* kezdeti */ while ((c=getchar()) != EOF) { switch (all) { case alap: // alap állapot if (c=='l') all=l_volt; break; case l_volt: // már volt egy 'l' switch (c) { case 'l': all=ll_volt; break; case 'y': szaml+=1; all=alap; break; default: all=alap; break; } break; case ll_volt:
32 Állapotgépek: szmájlik cseréje
normál | : | ) | < | 3 | |
---|---|---|---|---|---|
alap | →alap c | →kettőspont (semmi) | →alap c | →kisebb (semmi) | →alap c |
kettőspont | →alap :, c | →kettőspont : | →alap![]() | →kisebb : | →alap :, c |
kisebb | →alap <, c | →kettőspont < | →alap <, c | →kisebb < | →alap![]() |
Itt is az állapotgép táblázatában minden cella tartalmaz egy következő
állapotot (fent) és egy tevékenységet (lent). A tevékenység minden esetben
valamilyen karakter vagy karakterek kiírását jelenti. c
-vel jelöltem
az épp beolvasott karakter képernyőre másolását; a többi kiírásnál pedig a megadott jelet
kell majd a programnak kiírnia (pl. kisebb jel vagy kettőspont).
Tábla → táblázat → csináljunk 2D tömböt a kódban!
- Minden cellában egy tevékenység és egy állapot. Ez azt jelenti, hogy struktúrák 2D tömbje lesz.
- A sorokat az állapot szerint indexeljük:
tabla[all]
- Az oszlopot a karakter szerint indexeljük:
tabla[all][kar_o]
- A tevékenységek: jelöljük meg
enum
-mal
A tevékenység az ly számláló példájában könnyen leképezhető akár egy egész számra.
Összetettebb esetben ún. függvénymutatókat szokás ehhez használni, ez későbbi előadáson
szerepel majd. Egész számokra a tevékenység azonban nem csak így képezhető le;
el is nevezhetjük a tevékenységeket egy másik felsorolt típussal, és a programban aszerint
switch()
-elhetünk a tevékenység végrehajtásakor. Lásd lentebb.
33 Állapotgép táblázattal – leképezések
typedef enum Allapot { // állapot → egész szám Aalap, Akettospont, Akisebb } Allapot; int karakterosztaly(char c) { // char fajtája → egész szám switch (c) { default: return 0; /* sorminta, de csak kicsit */ case ':': return 1; case ')': return 2; case '<': return 3; case '3': return 4; } } typedef enum Tevekenyseg { // mit írunk ki? Tmosolyog, Tsziv, Takt, Tkpont, Tkpont_akt, Tkisebb, Tkisebb_akt, Tsemmi } Tevekenyseg;
A fenti switch ()
szerkezetben a break
utasítások elhagyhatóak, hiszen a return
utáni részek
úgysem hajtódnak végre a függvényből. Kicsit „sormintás”, de ha nem így lenne,
akkor meg egy 256 elemű tömbre lenne szükségünk, amiben szinte minden érték nulla,
csak néhány másik van – úgyhogy most jó lesz ez a megoldás is.
Itt legegyszerűbb, ha hagyjuk a fordítónak, a felsorolt típus egyes értékeihez az alapértelmezetteket társítsa. Azok 0, 1 és 2 lesznek. Mivel 0-tól számozódik, pont jó lesz számunkra tömbindexnek is.
34 Állapotgép táblázattal: a táblázat
struct TablaCella { // tevékenység és köv. állapot Allapot all; Tevekenyseg tev; } allapotgep[3][5] = { { {…}, {…}, {…}, {…}, {…} }, { {…}, {…}, {…}, {…}, {…} }, // egy sor { {Aalap, Tkisebb_akt}, {Akpont, Tkisebb}, // egy cella { } {Aalap, Tkisebb_akt}, {Akisebb, Tkisebb}, {Aalap, Tsziv} }, };
A fenti kódrészlet mutatja a struktúra definícióját, és a táblázat egy sorának kifejtését a kódban. A kifejtett sor az alábbi.
normál | : | ) | < | 3 | |
---|---|---|---|---|---|
kisebb | alap <, c | kettőspont < | alap <, c | kisebb < | alap ♥ |
35 Állapotgép táblázattal: a kód
struct TablaCella { Tevekenyseg tev; Allapot all; } allapotgep[3][5];
void csinal(Tevekenyseg t, char akt); … Allapot all; int c; all=Aalap; while ((c=getchar())!=EOF) { int kar=karakterosztaly(c); csinal(allapotgep[all][kar].tev, c); all=allapotgep[all][kar].all; }
A táblázatból kiolvassuk a tevékenységet és a következő állapotot.
36 Állapotgépek általában
Előnyök
- Tervezésnél: a tervezés eszköze!
- A felesleges állapotok kiszűrhetők
- Kódolásnál: mechanikusan kódolható
- Áttekinthetőbb, érthetőbb a kód, mint egy ad-hoc megoldás
még lesznek példák
Felhasználásuk
- Szűrőprogramok (fájlok feldolgozása); fordítóprogramok, nyelvi elemzők
(pl.
/* kommentek */
kiszűrése) - Alkalmazások vezérlése (pl. egér kattintások, mozdulatok)
- Internetes alkalmazások kommunikációja (protokollok)
- Hardver: a processzor egy nagy állapotgép!
Érdekesség: hardver oldalról is fontos az állapotgép. A számítógép belseje is tele van ilyenekkel. A processzor működését is egy állapotgép vezérli: utasítás beolvasása, beolvasott utasítás dekódolása, utána további operandusok beolvasása (már a dekódolt utasítás jelentése alapján) stb. Erről Digitből lesz szó.