Imperatív programozás 4.5.
Utasítások.
Az utasítások és vezérlési szerkezetek az imperatív programozási nyelvek alapvető elemei. Ezek segítségével írjuk le, hogyan szeretnénk a programot végrehajtani.
Kifejezés utasítás
Egy kifejezés az azt követő pontosvesszővel (;) egy kifejezés utasítást (expression statement) képez. Például a
printf("Hello world\n")
kifejezés típusa int értéke 12 (ugyanis a printf visszatérő értéke a kiírt karakterek száma). Ha pontosvesszőt teszünk utána, akkor utasítást kapunk:
printf("Hello world\n");
Üres utasítás
Az üres utasítás (null statement) hatás nélküli (bár kaphat cimkét).
1
2
3
4
if ( x < 10 )
;
else
printf("else branch");
Összetett utasítás
Az összetett utasítás (compound statement) vagy blokk utasítás arra szolgál, hogy több utasítást összefogjon.
1
2
3
4
5
6
7
8
9
if ( x < 10 )
{
;
}
else
{
printf("compound statement");
printf("in the else branch");
}
Sok véletlen hibát elkerülhetünk, ha a vezérlési szerkezetekben mindig kirakjuk a { } kapcsos-zárójeleket, akkor is, ha csak egyetlen utasítást szeretnénk végrehajtani.
Elágazás
Az if elágazásnak két formája van.
if (expression) statement
if (expression) statement1; else statement2;
Az if kifejezés feltételét kötelező zárójelbe írni, ahogy azt a switch while és for esetében is. Az utasítások lehetőleg legyenek összetett utasítások. Az if utasítás esetében mindig érdekes kérdés, hogy hova tartoznak a lógó (dangling) else utasítások. A C-ben és sok más nyelvben az else a hozzá szintaktikusan legközelebbi if-hez tartozik.
1
2
3
4
5
if ( x < 10 )
if ( y > 5 )
printf("x < 10 and y > 5");
else
printf("x < 10 and y <= 5");
ekvivalens az alábbival:
1
2
3
4
5
6
7
if ( x < 10 )
{
if ( y > 5 )
printf("x < 10 and y > 5");
else
printf("x < 10 and y <= 5");
}
és eltér ettől:
1
2
3
4
5
6
7
if ( x < 10 )
{
if ( y > 5 )
printf("x < 10 and y > 5");
}
else
printf("x >= 10");
A Pythonban persze a tabulálás jelöli ki a struktúrát. A C-ben nincsen elseif vagy elif, de az else ág egyetlen utasításaként írhatunk egy újabb if utaítást. Ennek hatása hasonló, mintha elseif-ünk lenne, (kivéve persze, ha az egyik feltétel kiértékelésének olyan mellékhatása van, ami befolyásol egy másik feltételt, de az ilyen konstrukciókat inkább kerüljük).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if ( x < 10 && y > 5 )
{
printf("x < 10 and y > 5");
}
else if ( x < 10 && y <= 5 )
{
printf("x < 10 and y <= 5");
}
else if ( x >= 10 && y > 5 )
{
printf("x >= 10 and y > 5");
}
else if ( x >= 10 && y <= 5 )
{
printf("x >= 10 and y <= 5");
}
else
{
printf("impossible");
}
Szelekciós utasítás
A switch utasítás egy alternatív elágazái forma, ahol az elágazást egy kifejezés különböző értékei alapján hajtjuk végre. A switch formája:
switch (expression) statement
Az utasítás szinte mindig egy blokk, melyben case címkével ellátott utasítások szerepelnek. A címkék értékének fordítási időben megadottnak és egyedinek kell lennie, és azt a fordító ellenőrzi is.
1
2
3
4
5
6
7
8
9
10
11
12
13
int day_of_week;
//...
switch ( day_of_week )
{
default: printf("Undefined"); break;
case 2: printf("Monday"); break;
case 3: printf("Tuesday"); break;
case 4: printf("Wednesday"); break;
case 5: printf("Thursday"); break;
case 6: printf("Friday"); break;
case 1: /* fallthrough */
case 7: printf("Week-end"); break;
}
A cimkéket úgy tekinthetjük, mint célpontokat, ahová odaugrik a vezérlés, ha értékük megegyezik a feltételben megadott értékkel. Onnan a vezérlés a megadott utasításoknak megfelelően, szekvenciálisan folytatódik, amíg el nem érünk egy break utasításhoz. Onnan a vezérlés a switch-et követő utasítással folytatódik.
Ha nincsen break utasítás, akkor a vezérlés rácsorog a következő cimkét tartalmazó utasításra. Ez általában nem jó programozási stratégia, de esetenként ezt használjuk a cimkék csoportosítására. Ilyenkor ajánlott ezt a szándékunkat pl. kommentben jelezni.
Ha egyetlen címke sem egyezik meg a feltételben megadott értékkel, és van default címke, akkor a vezérlés oda adódik át. Ettől eltekintve a default címke viselkedése megegyezik a többi címkéjével. Ha nincsen default címke sem, akkor a vezérlés a switch utáni utasítással folytatódik. Ha egyetlen címkén sem csorgunk túl, akkor az egyes címkék és a default címke sorrendje közömbös.
While ciklus
A C nyelvben többféle módon szervezhetünk ciklust. Az egyik legalapvetőbb konstrukció a while ciklus.
while ( expression ) statement
A while ciklus először ellenőrzi a ciklusfeltétel kifejezést, és addig hajtja végre a ciklusmagot, ameddig a feltétel igaz. A while ciklusban nekünk kell gondoskodni arról, hogy a feltétel előbb vagy utóbb hamissá váljon.
1
2
3
4
5
6
7
8
9
10
11
12
struct list_type
{
int value;
list_type *next;
};
// ...pt-expr
list_type *ptr = first;
while ( NULL != ptr )
{
printf( "%d ", ptr->value);
ptr = ptr->next;
}
Do-while ciklus
A do-while ciklus ún. hátul-tesztelő ciklus. Ez azt jelenti, hogy a ciklusmagot egyszer mindenképpen végrehajtjuk, és csak utána ellenőrizzük a feltételt.
do statement while ( expression ) ;
Figyeljük meg a feltétel-kifejezés zárójelét lezáró pontosvesszőt. A do-while utasítás ekvivalens a következő konstrukcióval:
statement
while ( expression )
statement
A do-while konstrukciót néha alkalmazzák, amikor az első ciklusvégrehajtás előtti ellenőrzést ki akarják spórolni pl. hatékonysági okokból.
For ciklus
A for ciklus az egyik leggyakrabban előforduló ciklusfajta. Kétféle formája van:
for ( opt-expr-1 ; opt-expr-2 ; opt-expr-3 ) statement
for ( declaration; opt-expr-2 ; opt-expr-3 ) statement (C99 óta)
ahol
-
opt-expr-1 egy opcionális (elhagyható) kifejezés, ami a ciklusváltozó kezdeti értékbeállítására szolgál és a legelső ciklusvégrehajtás előtt hajtódik végre. A C99 verzió óta ezt a kifejezést helyettesíthetjük egy deklarációval. Az itt deklarált ciklusváltozó láthatósága nem terjed túl a cikluson.
-
opt-expr-2 egy opcionális feltétel, ami minden ciklusmag végrehajtása előtt kiértékelődik, és a ciklusmag csak akkor hajtódik végre, ha ennek a kifejezésnek értéke igaz. Ha ezt a kifejezést elhagyjuk, akkor értékét mindig igaznak tekintjük.
-
opt-expr-3 egy opcionális kifejezés, ami mindig kiértékelődik a ciklusmag után. Ez a kifejezés gyakran arra szolgál, hogy a ciklusváltozót módosítsa.
Az alábbi for ciklus
for ( e1 ; e2 ; e3 ) s;
nagyjából (de nem teljesen) azonos a következő while ciklussal:
{
e1;
while ( e2 )
{
s;
e3;
}
}
A három opcionális kifejezés bármelyikét elhagyhatjuk. A középső elmaradása olyan, mintha állandóan igaz kifejezést írnánk. A (látszólag) végtelen ciklus egy alakja:
for( ; ; ) statement
Ezt a ciklust még mindig elhagyhatjuk a return vagy a break utasítással.
A C99 óta lehetséges az inicializáló kifejezést helyettesíteni egy ciklusváltozó létrehozásával és inicializálásával.
1
2
3
4
5
for ( int i = 0; i < 10; ++i )
{
printf( "%d ");
}
// i is not visible here.
A break és a continue utasítások
A break utasítást nemcsak a switch-ben, hanem bármely cikluson belül is alkalmazhatjuk. Hatására a ciklusból azonnal kilépünk, és a következő utasítással folytatjuk a programot.
1
2
3
4
5
6
7
8
9
10
11
12
int t[10];
// ...
for ( int i = 0; i < 10; ++i )
{
if ( t[i] < 0 )
{
printf( "negative found");
break;
}
printf("do something with non-negatives");
}
// break jumps to here
A continue utasítás átugorja a ciklusmag hátralévő részét és a vezérlés a ciklusmag végére ugrik. Ezután a while és do-while ciklusban a feltétel ellenőrzése, a for ciklusban az opt-expr-3 majd a feltétel kiértékelése következik.
1
2
3
4
5
6
7
8
9
10
11
12
13
int t[10];
// ...
for ( int i = 0; i < 10; ++i )
{
if ( t[i] < 0 )
{
printf( "negative found");
continue;
}
printf("do something with non-negatives");
// ...
// continue jumps to here
}
Return utasítás
A return visszatér a kurrens függvény végrehajtásából a hívó függvénybe. A main függvény esetében a return hatására a program végrehajtása befejeződik.
return;
return expr;
Egy függvényben több return utasítás is szerepelhet. Ha a függvény visszatérő típusa nem void akkor a return argumentuma a függvény visszatérő típusára konvertálódik.
1
2
3
4
5
6
7
8
9
10
11
12
int find_first_negative( int t[], int length)
{
for ( int i = 0; i < length; ++i )
{
if ( t[i] < 0 )
{
printf( "negative found");
return t[i];
}
}
return 0;
}
Goto utasítás
Feltétel nélküli ugró utasítás. Csak az adott függvényen belülre ugorhatunk.
goto label;
/* ... */
label: statement
ahol label egy azonosító. Ne használjunk goto utasítást.
Példák az utasítások használatára
Az alábbiakban egy példasorozaton keresztül mutatjuk be az utasítások helyes ill. helytelen használatát. A feladat legyen egy olyan C program megírása, amely a standard inputot olvassa EOF-ig átmásolva a karaktereket a standard outputra, eközben ciklikusan konvertálva az a->e->i->o->u magánhangzókat. Minden más karaktert változás nélkül írunk ki.
Az if használata
Az alábbi hibás programban az if utasítást próbáljuk használni:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
* A standard input átmásolása a standard outputra, miközben
* ciklikus helyettesítést végzünk a->e, e->i, ..., u->a
* magánhangzókra, a többi karakter változatlan.
* if használatával.
*
* HIBÁS MEGOLDÁS
*/
#include <stdio.h>
char change( char ch);
int main()
{
int ch; /* int, mert a 256 karakter mellett
ábrázolni kell az EOF szimbólumot is */
while ( (ch = getchar()) != EOF )
{
putchar( change(ch) );
}
return 0;
}
char change(char ch)
{
if ( 'a' == ch )
ch = 'e';
if ( 'e' == ch )
ch = 'i';
if ( 'i' == ch )
ch = 'o';
if ( 'o' == ch )
ch = 'u';
if ( 'u' == ch )
ch = 'a';
return ch;
}
A fenti program HIBÁS hiszen pl. egy ‘e’ karakter esetén miután átírtuk ‘i’-re át fogjuk írni az ‘i’-t ‘o’-ra és így tovább… Helyesen használni kell az else ágakat, ahogy alább látszódik:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/*
* A standard input átmásolása a standard outputra, miközben
* ciklikus helyettesítést végzünk a->e, e->i, ..., u->a
* magánhangzókra, a többi karakter változatlan.
* if-else használatával.
*
* Működik.
*/
#include <stdio.h>
char change( char ch);
int main()
{
int ch; /* int, mert a 256 karakter mellett
ábrázolni kell az EOF szimbólumot is */
while ( (ch = getchar()) != EOF )
{
putchar( change(ch) );
}
return 0;
}
char change(char ch)
{
if ( 'a' == ch )
ch = 'e';
else if ( 'e' == ch )
ch = 'i';
else if ( 'i' == ch )
ch = 'o';
else if ( 'o' == ch )
ch = 'u';
else if ( 'u' == ch )
ch = 'a';
else /* do nothing */ ;
return ch;
}
A switch használata
Az if-else-ek hosszú sorozata meglehetősen olvashatatlanná teszi a kódot. Ha egy konkrét értéken szeretnék szétválasztani a tevékenységeket, akkor használhatunk switch szerkezetet.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*
* A standard input átmásolása a standard outputra, miközben
* ciklikus helyettesítést végzünk a->e, e->i, ..., u->a
* magánhangzókra, a többi karakter változatlan.
* switch használatával.
*
* HIBÁS MEGOLDÁS
*/
#include <stdio.h>
char change( char ch);
int main()
{
int ch; /* int, mert a 256 karakter mellett
ábrázolni kell az EOF szimbólumot is */
while ( (ch = getchar()) != EOF )
{
putchar( change(ch) );
}
return 0;
}
char change(char ch)
{
switch ( ch )
{
case 'a': ch = 'e';
case 'e': ch = 'i';
case 'i': ch = 'o';
case 'o': ch = 'u';
case 'u': ch = 'a';
}
return ch;
}
A fenti megoldás hibás, mert pl. ha ch értéke ‘e’, akkor miután átírtuk ‘i’-re, a vezérlés “továbbcsorog” a következő utasításra, egészen a switch aljáig. Helyesen alkalmaznunk kell a break utasítást, hogy szeparáljuk a case-eket, illetve ajánlatos használnunk a default címkét is.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*
* A standard input átmásolása a standard outputra, miközben
* ciklikus helyettesítést végzünk a->e, e->i, ..., u->a
* magánhangzókra, a többi karakter változatlan.
* switch használatával.
*
* Működik
*/
#include <stdio.h>
char change( char ch);
int main()
{
int ch; /* int, mert a 256 karakter mellett
ábrázolni kell az EOF szimbólumot is */
while ( (ch = getchar()) != EOF )
{
putchar( change(ch) );
}
return 0;
}
char change(char ch)
{
switch ( ch )
{
default : /* nop */; break;
case 'a': ch = 'e'; break;
case 'e': ch = 'i'; break;
case 'i': ch = 'o'; break;
case 'o': ch = 'u'; break;
case 'u': ch = 'a'; break;
}
return ch;
}
Ciklusok használata
Ha a konverziót “beégetjük” az utasításokba, az elég rugalmatlan megoldás. Nem tudjuk például a konvertálandó karaktereket futási időben beállítani vagy módosítani. A következő példákban a konverziót adatok segítségével definiáljuk, így akár futási időben is beállíthatnánk azt.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
* A standard input átmásolása a standard outputra, miközben
* ciklikus helyettesítést végzünk a->e, e->i, ..., u->a
* magánhangzókra, a többi karakter változatlan.
* while használatával.
*
* Működik, de nem túl hatékony és biztonságos
*/
#include <stdio.h>
char change( char ch);
int main()
{
int ch; /* int, mert a 256 karakter mellett
ábrázolni kell az EOF szimbólumot is */
while ( (ch = getchar()) != EOF )
{
putchar( change(ch) );
}
return 0;
}
char change(char ch)
{
char from[5] = {'a', 'e', 'i', 'o','u'};
char to[5] = {'e', 'i', 'o','u', 'a'};
int i = 0;
while ( i < 5 )
{
if ( from[i] == ch )
return to[i];
++i;
}
return ch;
}
A fenti megoldás használható, de se nem túl hatékony, se nem túl karbantartható. a from és to tömbök minden függvényhíváskor újra és újra létrejönnek majd megsemmisülnek. Ezen kívül nem túl praktikus a while feltételébe beégetni az 5-ös számot. Ha pl. új elemeket adunk a tömbökhöz, vagy törlünk belőlük, akkor a ciklus helytelen számban hajtódik végre.
A lenti megoldásban a for vezérlési szerkezetet használjuk, és több módosítást is végrehajtottunk. A tömbök deklarációjában nem írtuk ki a méretet, hagytuk, hogy azt a fordítóprogram kiszámolja. A tömbök elemei konstansok, hogy véletlenül se módosítsuk azokat. A tömböket static kulcsszóval deklaráltuk (lásd élettartam előadás), ezért csak egyszer, a program indulásakor kell létrehozni és a program végéig “élni” fog.
A ciklus hosszát automatikusan számoljuk ki a sizeof operátor segítségével.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
* A standard input átmásolása a standard outputra, miközben
* ciklikus helyettesítést végzünk a->e, e->i, ..., u->a
* magánhangzókra, a többi karakter változatlan.
* for használatával.
*
* Működik
*/
#include <stdio.h>
char change( char ch);
int main()
{
int ch; /* int, mert a 256 karakter mellett
ábrázolni kell az EOF szimbólumot is */
while ( (ch = getchar()) != EOF )
{
putchar( change(ch) );
}
return 0;
}
char change( char ch )
{
static const char from[] = {'a', 'e', 'i', 'o','u'};
static const char to[] = {'e', 'i', 'o','u', 'a'};
unsigned int i; /* size_t */
for ( i = 0; i < sizeof(from)/sizeof(from[0]); ++i )
{
if ( from[i] == ch )
return to[i];
}
return ch;
}
Ciklust nem csak egy adott hosszra, hanem egy adott feltétel, pl. egy speciális karakter, sentinel elem, eléréséig is szervezhetünk. A lenti példában ez a string literálokat lezáró bináris nulla (‘\0’) karakter lesz.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
* A standard input átmásolása a standard outputra, miközben
* ciklikus helyettesítést végzünk a->e, e->i, ..., u->a
* magánhangzókra, a többi karakter változatlan.
* for és sentinel használatával, .
*
* Működik
*/
#include <stdio.h>
char change( char ch);
int main()
{
int ch; /* int, mert a 256 karakter mellett
ábrázolni kell az EOF szimbólumot is */
while ( (ch = getchar()) != EOF )
{
putchar( change(ch) );
}
return 0;
}
char change( char ch )
{
static const char from[] = "aeiou"; // {'a','e','i','o','u','\0'}
static const char to[] = "eioua"; // {'e','i','o','u','a','\0'}
unsigned int i; /* size_t */
for ( i = 0; '\0' != from[i]; ++i )
{
if ( from[i] == ch )
return to[i];
}
return ch;
}
A fenti program már sokkal biztonságosabb, mint a legelső while alapú, de még itt is javíthatunk a program karbantarthatóságán. A két tömb használata azzal a veszéllyel jár, hogy esetleg elcsúsznak a konverziós párok. Talán jobb lenne, ha inkább egyetlen tömbünk lenne, amiben az egyes elemek fejeznék ki a from->to párokat. (A struct-okról majd az Összetett adatszerkezetek előadáson fogunk részletesen tanulni.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/*
* A standard input átmásolása a standard outputra, miközben
* ciklikus helyettesítést végzünk a->e, e->i, ..., u->a
* magánhangzókra, a többi karakter változatlan.
* for és struct használatával, .
*
* Működik
*/
#include <stdio.h>
char change( char ch);
int main()
{
int ch; /* int, mert a 256 karakter mellett
ábrázolni kell az EOF szimbólumot is */
while ( (ch = getchar()) != EOF )
{
putchar( change(ch) );
}
return 0;
}
struct conv_t
{
char from;
char to;
};
char change( char ch )
{
static const struct conv_t conv_table[] = {
{'a', 'e'},
{'e', 'i'},
{'i', 'o'},
{'o', 'u'},
{'u', 'a'}
};
unsigned int i; /* size_t */
for ( i = 0; i < sizeof(conv_table)/sizeof(conv_table[0]); ++i )
{
if ( conv_table[i].from == ch )
return conv_table[i].to;
}
return ch;
}
Konverziós tábla
Az eddigi programok különböző előnyökkel és hátrányokkal rendelkeztek. A következő megoldás valószínűleg a leggyorsabb. A megoldás alapja egy táblázat, amit úgy inicializáltunk, hogy a ch karakter lesz a tömb indexe, a megfelelő tömbelem pedig a kívánt visszatérő érték. Ezt a tömböt vagy statikusan töltjük fel, vagy - mint a példában - a program elején egyetlen egyszer meghívott init() függvény segítségével.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/*
* A standard input átmásolása a standard outputra, miközben
* ciklikus helyettesítést végzünk a->e, e->i, ..., u->a
* magánhangzókra, a többi karakter változatlan.
* konverziós tábla használatával, .
*
* Működik és gyors
*/
#include <stdio.h>
void init();
char change( unsigned char ch);
static char table[256];
int main()
{
int ch; /* int, mert a 256 karakter mellett
ábrázolni kell az EOF szimbólumot is */
while ( (ch = getchar()) != EOF )
{
putchar( change(ch) );
}
return 0;
}
void init()
{
int i;
for (i = 0; i < 256; ++i)
{
table[i] = i;
}
table['a'] = 'e';
table['e'] = 'i';
table['i'] = 'o';
table['o'] = 'u';
table['u'] = 'a';
}
char change( unsigned char ch )
{
return table[ch];
}
Figyeljük meg, hogy a change() függvény paraméterét unsigned char típusnak deklaráltuk. Ez amiatt szükséges, hogy a 127 feletti kódú karakterek is pozitív számként viselkedjenek. Ha sima char típust használtunk volna, akkor azokon a platformokon, ahol a char típus előjeles ezek a kódok negatív számokként viselkednének és hibás, nemlétező negatív indexű tömbelemeket próbálnánk visszaadni.