Gyakorlat, 13. hét: állapotgépek
Czirkos Zoltán · 2019.02.26.
Állapotgépek tervezése.
Állapotgépek. A feladatok megoldásának lényege az, hogy az állapotátmeneti táblát és a tevékenyságtáblát megalkotjuk: az állapotgépet megtervezzük. Ha ez megvan, akkor onnantól a kódolás már mechanikus. Egy olyan feladatnál, ahol több állapotátmenet is van, nem lehet csak úgy nekiugrani a kódolásnak! Ha a táblázat megalkotása után fejből írunk valami kódot, nem a tábla alapján, annak sincs semmi értelme. Az is értelmetlen, ha a kettőt fordított sorrendben csináljuk meg…
A tervezés után az állapotgépet mindenképpen le kell tisztázni egy állapottáblával. A gráfot nézve ugyan jobban lehet mélázgatni az állapotátmeneteken, a táblázat sokkal áttekinthetőbb, és biztosítja azt is, hogy semelyik átmenetet sem hagytuk ki. A kódolás során pedig kifejezetten jobb abból dolgozni.
Felkészülés a gyakorlatra:
- Az állapotgépekről szóló előadás anyagának megértése.
A feladat segít felmérni, hogy állsz a tanulással. A szövege csak a többi megoldással együtt jelenik meg. Óra után pedig rögzítsd a pontszámod – a dolgozat nálad marad.
Feladat
Add meg az alábbi kifejezésekhez tartozó kifejezésfát, figyelembe véve az operátorok precedenciáját!
6+2*32*6-5/3a=b+ct[i+2]*35*-65-*6
Az alábbi rajzok közül az egyik az x=(double)a/b kifejezésfáját ábrázolja. Melyik az? Add meg a másik fa által ábrázolt kifejezést!
A feladat megoldására 10 perc áll rendelkezésre.
Megoldás és pontozási útmutató
Az x=(double)a/b kifejezést az első kifejezésfa ábrázolja. A másik kifejezés pedig: x=(double)(a/b).
Zárójelbe kell tenni az a/b-t, hogy a típuskonverzió az osztás eredményére vonatkozzon. Zárójel nélkül az
a változó értékére vonatkozik, mivel erősebb a konverziós operátor precedenciája, mint az osztás operátoré.
Pontozás: összesen nyolc kérdés volt: 6 kifejezésfa rajzolása, és a megrajzolt fákra vonatkozó két kérdés. Minden teljesen helyes válasz 1 pont.
Válaszoljunk az alábbi kérdésekre:
#include <stdio.h>
#include <ctype.h>
int main(void) {
int c;
while ((c = getchar()) != EOF) {
putchar(tolower(c));
}
return 0;
}
- Mit csinál a program?
- Miért
inttípusú a változó? - Miért kell a zárójel az értékadásnál? Rajzoljuk fel a kifejezésfát a ciklusfeltételhez!
- Miért visszatérési értékben kapjuk a
tolower()függvénytől a karaktert? - Működik a program számok vagy írásjelek esetén is? Miért?
- Megírhatjuk ugyanezt a programot a
scanf–printffüggvénypárral is? Hogyan?
Megoldás
- A szabványos bemeneten érkező szöveget kisbetűsítve a szabványos kimenetére írja.
- Mert a
getchar()értékeEOFis lehet, amelyik achartípus ábrázolási tartományán kívül esik. - A precedencia miatt; hogy a változóba kerülő értéket (karakterkódot) vizsgáljuk,
ne pedig az összehasonlítás eredményét tegyük a változóba.
- Két lehetőség lenne: cím szerint átvett változót módosít, vagy visszatérési értékben adja az eredményt.
Cím szerinti paraméter esetén
&clenne a paraméter. Amikor kitalálták, úgy döntöttek, legyen inkább visszatérési érték, mert kényelmesebb úgy használni. - Igen, mert a
tolower()függvény ezeket változatlanul adja vissza. - Igen, így:
Achar c; while (scanf("%c", &c) == 1) { printf("%c", tolower(c)); }scanf-es sorban!= EOFis lehet, viszont a változó mindenképpchar ckell legyen ebben az esetben.
A feladat az előző műveletet megcsinálni „visszafelé”. Vagyis olyan programot kell írni, amely a beírt, csupa kisbetűkből álló szöveget javítja ki, méghozzá úgy, hogy minden mondat elején álló első betűt nagybetűre cseréli.
Megoldás
Ehhez állapotgép kell. Mondat végét jelző írásjel után a következő betű nagybetű, de csak akkor, ha szóköz is jött. Figyelni kell, hogy az utána lévő szóköztől nem váltunk kisbetűs módra még! A gép alapállapota a nagybetűsítés, hiszen a bejövő szöveg legelső karaktere biztosan mondat elején van.
| . ! ? | szóköz, \n | egyéb | |
|---|---|---|---|
| nagybetű | ki: c | ki: c | ki: toupper(c) →kisbetű |
| kisbetű | ki: c →mondatvége? | ki: c | ki: c |
| mondatvége? | ki: c | ki: c →nagybetű | ki: c →kisbetű |
#include <stdio.h>
#include <stdbool.h>
#include <ctype.h>
bool mondatvege_jel(int c) {
return c == '!' || c == '.' || c == '?';
}
int main(void) {
enum allapot {
nagybetu,
kisbetu,
mondatvege
} all;
all = nagybetu; /* kiindulo allapot */
int c;
while ((c = getchar()) != EOF) {
switch (all) {
case nagybetu:
putchar(toupper(c));
if (!mondatvege_jel(c) && !isspace(c))
all = kisbetu;
break;
case kisbetu:
putchar(c);
if (mondatvege_jel(c))
all = mondatvege;
break;
case mondatvege:
putchar(c);
if (isspace(c))
all = nagybetu;
else if (!mondatvege_jel(c))
all = kisbetu;
break;
}
}
return 0;
}
Írj programot, amely a szabványos bemenetről olvas karaktereket fájl vége jelig! A szabványos bemeneten keresztül egy C program érkezik. A program szűrje ki a /* és */ közötti megjegyzéseket a szabványos bemeneten érkező szövegből, és a megjegyzések nélküli szöveget írja a szabványos kimenetre!
Megoldás
Az állapotátmeneteket itt is nyilak jelölik, a többi jelzés pedig a kiírandó karaktereket mutatja. A c betű az aktuálisan
beolvasott karaktert jelenti. Az állapotgép ezen változata nem vesz tudomást a sztringekről, pl. nem veszi figyelembe
azt, hogy "a/*b" nem komment.
| / | * | egyéb | |
|---|---|---|---|
| alap | →per | '*' | c |
| per | '/' | →komment ' ' | →alap '/', c |
| komment | →vége | ||
| vége | →alap | →komment |
#include <stdio.h>
int main(void) {
typedef enum Allapot { alap, per, komment, csillag } Allapot;
int c;
Allapot all = alap;
while ((c = getchar()) != EOF) {
switch (all) {
case alap:
if (c == '/') {
all = per;
} else {
putchar(c);
}
break;
case per:
if (c == '/') {
putchar('/');
} else if (c == '*') {
putchar(' ');
all = komment;
} else {
putchar('/');
putchar(c);
all = alap;
}
break;
case komment:
if (c == '*') {
all = csillag;
}
break;
case csillag:
if (c == '/') {
all = alap;
} else if (c == '*') {
all = csillag;
}
else {
all = komment;
}
break;
}
}
/* Ha véget ért a bemenet, elvileg alapállapotban kell lennünk, mert
* a forráskód nem végződhet bezáratlan kommenttel. Esetleg lehetséges,
* hogy egy perjel volt a végén, az viszont nem a komment vége, hanem
* egy osztás: írjuk ki! */
if (all == per) {
putchar('/');
}
return 0;
}
Írjunk állapotgépes programot, amelyik egy adott szövegből eltávolítja a (zárójelezett részeket). Előfordulhat, hogy a zárójelek tetszőleges mélységben egymásba vannak ágyazva (például így, mint ez a (zárójeles) szó).
Megoldás
Nem lehet megoldani véges számú állapotot tartalmazó állapotgéppel. Minden bezáró
zárójel „)” esetén tudni kellene, hogy hány be nem zárt zárójel van még; ez annyiféle „(” utáni
állapotot feltételez. A feladat szövege szerint pedig ezekből tetszőlegesen sok lehetne.
Ellenben állapotváltozónak használhatunk egy int-et, amit növelünk és csökkentünk.
Feltételezve, hogy az int nem csordul túl, így megoldható a feladat – de ez nem
véges automata, nem rajzolható fel egy véges állapotátmeneti gráffal vagy állapottáblával.
- Egészítsük ki úgy a C kommentszűrőt, hogy ismerje a sztringeket is! Például
"a/*b"nem komment, hiába van benne per-csillag. - Szintén a kommentszűrőhöz: ismerje fel az állapotgép a C++ nyelvből átvett,
//stílusú kommentet is! Ezek két perjellel kezdődnek, és a sor végéig tartanak. - A forráskód hány százaléka komment? Egészítsük ki a programot úgy, hogy meghatározza ezt!