Mutatók. Felsorolt típus. Állapotgép

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!


Admin portál:
NHF 1-2-3-4.

Beadandók (elektronikusan)

  1. Feladat választása: 7. hétig
  2. Pontosított specifikáció: 8. hétig
  3. Félkész program: 10. hétig
  4. Végleges változat: 13. hétig; laboron bemutatva!
    • Kód + felhasználói, programozói, tesztelési dokumentáció

Mutatók (pointerek)

4 Mutatók és indirekció

Mutató (pointer)


Indirekció (indirection)

double x;
double *ptr;

ptr = &x;    // cím képzése (address of)

*ptr = 3.14; // mutatott értékre hivatkozás
Az indirekció működése

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.

A 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:

Egy függvény így
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:

ptr=NULL:
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:


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ő

Ismerős?!
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

"Sztringek"

13 A sztringek létrehozása

Sztringek

A sztringek nullával ('\0' vagy 0) lezárt karaktertömbök.

hello\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.

[0] h
[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

A tömb méretét
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. :)

012345
Hello\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.


Túlindexelés veszélye: a függvény nem tudja, mekkora a cél tömb!

17 Sztring másolása – a klasszikus megoldás

A K&R könyv

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!

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 a printf(), de a sztringbe ír, nem a szabványos kimenetre.
  • sscanf(str, formátum, ...) – ugyanaz, mint a scanf(), 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

Felsorolt típus

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

Tic-tac-toe játék
Közlekedési lámpa

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

22 Felsorolt típus: a hozzárendelt értékek

Számozás

enum Lampa {
   piros,
   piros_sarga=2,
   zold,
   sarga
};

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;

Közlekedési lámpa
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;
}

Állapotgépek

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ájliEzt 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 az EOF 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:


A probléma nehézsége

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)

Digit I.-ből
hasonló téma lesz:
sorrendi hálózatok

Működése: az eddig kialakult állapottól és az eseménytől függ:


Állapotok a múlt alapján

+0 négy
+1 lyuk
+2 gally

Három állapot lehetséges:

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.


Az „ly” számláló állapotgép
lyegyéb
alap→l_volt--
l_volt→ll_voltsz+=1, →alap→alap
ll_volt-sz+=2, →alap→alap
k u l c s l y u k
Számláló: plusz1

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ént y é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án m betű jött).

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:

Ly számláló állapotátmeneti gráfja

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;
a teljes
forráskód
letölthető:
lyszaml.c
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!

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];
Letölthető a
teljes program:
szmajli.c és
lyszaml_tabla.c
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


gyakon és laboron
még lesznek példák

Felhasználásuk

É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ó.