Típusok, operátorok és kifejezések
1. FEJEZET Tartalom 3. FEJEZET

2. FEJEZET:

Típusok, operátorok és kifejezések

A változók és az állandók alkotják a programban feldolgozott alapvető adatobjektumokat. A program deklarációi felsorolják a felhasznált változókat, megadják a típusukat és néha még a kezdeti értéküket is. Az operátorok azt határozzák meg, hogy mit kell csinálni az adatokkal. A kifejezések a változókat és állandókat egy új érték előállítása érdekében kombinálják. Az objektum típusa meghatározza, hogy az objektum milyen értékeket vehet fel és milyen operátorok alkalmazhatók rá. Ebben a fejezetben ezekkel az alapelemekkel foglalkozunk.

Az ANSI szabvány csak kis változásokat hozott az alapvető adattípusok és kifejezések terén, és inkább csak bővítette a lehetőségeket. Újdonság az összes egész típusú mennyiségre bevezetett signed és unsigned forma, valamint az előjel nélküli (unsigned) állandó és a hexadecimális karakteres állandó bevezetése. Az új szabvány szerint a lebegőpontos műveleteket egyszeres pontossággal végzi a gép, és a nagyobb pontosság eléréséhez bevezették a long double adattípust. A karakterlánc-állandók a fordítás során összekapcsolhatók. A felsorolt változók a nyelv részévé váltak a rég bevált tulajdonságok formalizálásával. Deklarálhatóvá vált a const típusú objektum, ami ezek változatlanságát jelzi. Az aritmetikai adattípusok közti automatikus konverziós kényszerek bővítik az adattípusok választékát.

2.1. Változónevek

Bár az 1. fejezetben nem említettük, de van néhány megszorítás a változók és szimbolikus állandók neveit illetően. A nevek betűkből és számjegyekből állhatnak és az első karakterüknek betűnek kell lenni. Az aláhúzás-karakter ( _ ) betűnek számít, és alkalmazásával sokszor javítható a hosszú változónevek olvashatósága. Változónevet ne kezdjünk aláhúzás-karakterrel, mivel a könyvtári eljárások gyakran használnak ilyen neveket. A nagy- és kisbetűk különböznek egymástól, így az x és X két különböző nevet jelent. A hagyományos C programozói gyakorlatban a változóneveket kisbetűvel írjuk és a szimbolikus állandókat csupa nagybetűvel.

A belső neveknek legalább az első 31 karaktere szignifikáns. A függvények és külső változók neveinek hossza 31-nél kisebb, mert ezek külső nevek, amelyeket az assemblerek és loaderek használnak a nyelvtől függetlenül (ezekre nem vonatkoznak a C nyelv szabályai). A szabvány külső nevek esetén csak 6 karakterig garantálja a megkülönböztethetőséget. A nyelv kulcsszavai (pl. if, else, int, float stb.) fenntartott szavak és nem lehetnek változónevek. A kulcsszavakat kisbetűvel kell írni.

Célszerű a programban olyan neveket választani, amelyek jelentenek valamit („beszélő nevek”) és írásmódjuk nem zavaró. Érdemes a helyi változókhoz (különösen a ciklusváltozóhoz) rövid, a külső változókhoz hosszabb neveket választani.

2.2. Adattípusok és méretek

A C nyelv viszonylag kevés alapvető adattípust használ:


char egyetlen bájt, a gépi karakterkészlet egy elemét tárolja
int egész szám, mérete általában a befogadó számítógép egészek ábrázolásához használt mérete
float egyszeres pontosságú lebegőpontos szám
double kétszeres pontosságú lebegőpontos szám


Ezekhez az alapvető adattípusokhoz még néhány minősíthető specifikáció járulhat. Ilyen az egész típusokhoz használható short és long minősítő, pl.
short int sh;
long int counter;
Az ilyen deklarációkból az int típusjelzés elhagyható, és általában el is hagyják.

A short és long minősítők bevezetésével az volt a cél, hogy ahol szükséges, két különböző hosszúságú egész álljon a programozó rendelkezésére. Szokásos módon az int mérete megegyezik a használt számítógép alap szóméretével. A short típus általában 16 bites, a long pedig 32 bites, az int típus 16 vagy 32 bites. Minden fordítóprogram a saját hardverének megfelelően választja meg az adatok méretét, és csak annyi megszorítás van, hogy a short és int adattípus legalább 16, a long adattípus legalább 32 bites kell legyen, valamint, hogy a short nem lehet hosszabb az int-nél és az nem lehet hosszabb a long-nál.

A signed és unsigned minősítők a char és bármely egész adattípus esetén használhatók. Egy unsigned típusú szám mindig pozitív vagy nulla, és a modulo 2n aritmetika szabályai szerint használható, ahol n az adott típus ábrázolásához rendelt bitek száma, így pl. ha a char típust a gép 8 biten ábrázolja, akkor az unsigned char típus értéke 0 és 255 között, amíg a signed char típus értéke -128 és +127 között lehet (kettes komplemens kódú ábrázolás esetén). Az, hogy a char típusú adat signed vagy unsigned-e a géptől függ, de a nyomtatható karakterek mindig pozitívak.

A long double típus növelt pontosságú lebegőpontos számot jelöl. Az egészekhez hasonlóan a lebegőpontos számok mérete is gépfüggő. A float, double és long double típus egyszeres, kétszeres és háromszoros méretet jelenthet.

A <limits.h> és <float.h> szabványos header állományok szimbolikus állandói között mindezen méretűek megtalálhatók, és ezek leírása a géptől, ill. a fordítóprogramtól függő tulajdonságokkal együtt a B. Függelékben található.

2.1. gyakorlat. Írjunk programot, ami meghatározza a signed és unsigned minősítőjű char, short, int és long típusú változók nagyságát a szabványos header állományokból vett megfelelő értékek kiírásával és közvetlen számítással! A feladat nehezebb, ha kiszámítjuk a nagyságokat és tovább nehezíthető, ha a lebegőpontos számok nagyságát is meg akarjuk határozni.

2.3. Állandók

Az 1234 formában leírt egész állandó minden külön jelzés nélkül int típusú. Egy long típusú egész állandó leírásánál viszont a számot l (el) vagy L betűvel kell zárni, pl. 123456789L. Ez a szám túl nagy ahhoz, hogy int típusú legyen, ezért long lesz. Az előjel nélküli (unsigned) számokat az utánuk írt u vagy U betűvel jelöljük, és az ul vagy UL toldalék unsigned long típust ír elő.

A lebegőpontos állandók tizedespontot (pl. 123.4) vagy kitevőt (pl. 1e-2) vagy mindkettőt tartalmaznak, és alapértelmezésben double típusúak. A lebegőpontos állandó után írt f vagy F float, l vagy L long double típust jelöl.

Egy egész szám értéke nem csak decimálisan, hanem oktális vagy hexadecimális alakban is megadható. A szám elé nullát írva az egész típusú állandó oktális alakú és ha 0x jelzést írunk a szám elé, akkor hexadecimális alakú. Például a decimális 31 oktális alakban 037 és hexadecimális alakban 0x1f vagy 0X1F. Az oktális vagy hexadecimális állandó után írt L long és U unsigned típust jelöl: pl. 0XFUL egy decimálisán 15 értékű unsigned long típusú állandó.

A karakter állandó egy egész típusú adat, amit aposztrófok közé írt egyetlen karakterrel (pl. 'x') adunk meg. A karakterállandó értéke a karakter gépi karakterkészletbeni kódszáma. Például az ASCII karakterkészletben a '0' karakterállandó értéke 48, ami semmiféle kapcsolatban nincs a 0 számértékkel. Ha a 48 kódérték helyett '0' karakterállandót írunk, akkor a program függetlenné válik a gépi karakterkészlettől és könnyebben olvasható. A karakterállandókat leggyakrabban más karakterekkel való összehasonlításra használjuk, de ugyanúgy részt vehetnek a numerikus műveletekben is, mint bármilyen egész adat.

Bizonyos karakterek a karakterállandókban vagy karaktersorozat-állandókban escape sorozattal adhatók meg, mint pl. a \n, ami az újsor-karaktert jelöli. Ezek az escape sorozatok leírva két karakternek látszanak, de valójában egyetlen karaktert jelölnek. Mindezeken kívül tetszőleges tartalmú, egy bájt méretű bitminta adható meg a

'\ooo'
specifikációval, ahol ooo egy max. háromjegyű oktális szám számjegyeit jelöli (csak a 0...7 számjegyek megengedettek), vagy a
'\xhh'
specifikációval, ahol hh egy max. kétjegyű hexadecimális szám jegyeit jelöli (a számjegyek 0...9, a...f vagy A...F lehetnek). Így minden további nélkül írhatjuk, hogy
#define VTAB '\013' /* a VTAB ASCII kódja */
#define BELL '\007' /* a BELL ASCII kódja */
vagy hexadecimálisan
#define VTAB '\xb' /* a VTAB ASCII kódja */
#define BELL '\x7' /* a BELL ASCII kódja */
Az escape sorozatok teljes listája a következő:
\a     figyelmeztető jelzés (bell, csengő)
\b     visszalépés (backspace)
\f     lapdobás (formfeed)
\n     új sor (new line)
\r     kocsi vissza (carriage return)
\t     vízszintes tabulátor (horizontal tab, HTAB)
\v     függőleges tabulátor (vertical tab, VTAB)
\\     fordított törtvonal (backlash)
\?     kérdőjel
\'     aposztróf
\"     idézőjel
\ooo   oktális szám
\xhh   hexadecimális szám
A '\0' karakterállandó egy nulla értékű karaktert, az ún. null-karaktert jelöli. A programokban gyakran írunk '\0'-t a 0 helyett, hogy ezzel is kihangsúlyozzuk a kifejezésen belül az érték karakteres természetét, de természetesen mindkét alak számértéke nulla.

Az állandó kifejezés csak állandókat tartalmaz. Az ilyen kifejezések kiértékelése még a fordítás során megtörténik, ezért bárhol szerepelhetnek, ahol állandó használata megengedett. Ilyen állandó kifejezés pl. a

#define MAXSOR 1000
char sor[MAXSOR+1];
vagy a
#define UGRAS /* pl. ugrás szökőévben */
int napok[31+18+UGRAS+31+30+31+30+31+31+30+31+30+31];
A karaktersorozat-állandó (string-konstans) nulla vagy több karakterből áll, amelyek idézőjelek között helyezkednek el. Ilyen pl. a
"Ez egy karaktersorozat"
vagy a
"" /* ez egy üres karaktersorozat */
Az idézőjelek nem részei a karaktersorozatnak, csak határolják azt. A karaktersorozat-állandókban ugyanazok az escape sorozatok használhatók, mint a karakterállandóknál, ezért a \" egy idézőjelet jelent (mivel az idézőjel határolja a karaktersorozatot, ezért annak belsejében idézőjel csak a \" escape sorozattal íratható ki). A karaktersorozatállandók a fordítás során összekapcsolhatók (konkatenálhatók), így a
"Halló" "mindenki!"
egyenértékű a
"Halló mindenki!"
karaktersorozattal. Ez lehetőséget nyújt arra, hogy a forrásprogram túl hosszú sorait több, rövid sorra bontsuk.

Gyakorlatilag a karaktersorozat-állandó egy karakterből álló tömb. Egy karaktersorozat belső ábrázolásában a sorozatot a '\0' null-karakter zárja, így a tárolásukhoz szükséges tárolóhelyek száma csak eggyel több, mint a karakterek száma. Ez a belső ábrázolásmód azt eredményezi, hogy nincs korlátozva a karaktersorozat hossza, de így a programoknak a teljes karaktersorozatot végig kell nézni ahhoz, hogy meghatározzák a tényleges hosszt. A standard könyvtár strlen(s) függvénye visszatéréskor megadja a karaktersorozat típusú s argumentumának a hosszát (a záró '\0' null-karaktert nem számítja bele a hosszba). A függvény általunk írt változata:

/* strlen: az s karaktersorozat hosszát adja */
int strlen(char s[ ])
{
   int i;
   
   i = 0;
   while (s[i] != '\0')
      ++i; 
   return i; 
}
Az strlen és még több más karaktersorozat-kezelő függvény is a <string.h> szabványos headerben található.

Ügyeljünk arra, hogy megkülönböztessük a karakterállandót és az egyetlen karaktert tartalmazó karaktersorozatot: 'x' nem azonos az "x"-szel! Az első egy egész mennyiség, amelynek számértéke az x betű gépi karakterkészletben kódja, amíg a második egy egyetlen karaktert (az x betűt) és a lezáró '\0' null-karaktert tartalmazó tömb.

Az állandók eddigiektől eltérő fajtája a felsorolt állandó. A felsorolt állandó egész értékek listájából áll, mint pl. az

enum boolean {NO, YES};
Az enum listájában az első név értéke 0, a másodiké 1 és így tovább, kivéve ha az értékeket explicit módon specifikáljuk. Ha a listában nem minden értéket specifikáltunk, akkor a nem specifikált elemek az utolsó specifikált elemtől kezdve folyamatosan a következő értéket kapják, mint ez az alábbik közül a második példában látható:
enum espaces { BELL = '\a', BACKSPACE = '\b', TAB = '\t',
               NEWKUBE = '\n', VTAB = '\v',
               RETURN = '\r' };
enum honapok { JAN = 1, FEB, MAR, APR, MAJ,
               JUN, JUL, AUG, SZEP, OKT, NOV, DEC };
               /* így FEB = 2, MAR = 3 stb. */
A különböző felsorolt állandókban szereplő neveknek különbözniük kell, de egy felsoroláson belül az értékeknek nem kell különbözni.

A felsorolás egy szokásos módja annak, hogy a nevekhez állandó értéket rendeljünk, és így lényegében a #define utasítás helyettesítésére alkalmas, azzal az előnnyel, hogy a nevekhez rendelt értékek automatikusan generálhatók. Bár az enum típusú változókat deklarálhatjuk, a fordítóprogramok nem ellenőrzik, hogy az így eltárolt változóknak van-e érvényes értékük a felsorolásban. Mindazonáltal a felsorolt változók jellegéből adódik az ellenőrzés lehetősége, így gyakran jobban használhatók, mint a #define utasítással definiált változók. További előny, hogy egy megfelelő debugger program szimbolikus formában is képes kiírni a felsorolt változók értékét.

2.4. Deklarációk

A felhasználása előtt minden változót deklarálni kell, bár bizonyos deklarációk implicit módon, a programkörnyezet alapján is létrejöhetnek. A deklaráció egy típust határoz meg, és utána egy vagy több adott típusú változó felsorolása (listája) áll.

Ilyen deklaráció pl.

int also, felso, lepes;
char c, sor[1000];
A változók a deklarációk közt tetszőleges módon szétoszthatók, pl. a fenti deklarációs listákkal teljesen egyenértékű az
int also;
int felso;
int lepes;
char c;
char sor[1000];
Az utóbbi forma több helyet igényel, viszont előnye, hogy az egyes deklarációkhoz kényelmesen fűzhetők megjegyzések és maga a deklaráció is egyszerűen módosítható.

A változók a deklaráció során kezdeti értéket is kaphatnak. Ha a nevet egyenlőségjel és egy kifejezés követi, akkor a kifejezés értéke lesz a kezdeti érték, mint pl. a következő deklarációban:

char esc = '\\';
int i = 0;
int hatar = MAXSOR+1;
float eps = 1.0e-5;
Ha a kérdéses változó nem automatikus, akkor a kezdeti értékadás csak egyszer, a program végrehajtásának kezdetén történik meg és a kezdő értéket megadó kifejezésnek állandó kifejezésnek kell lenni. Az explicit módon inicializált automatikus változók esetén a kezdeti értékadás minden alkalommal létrejön, amikor a vezérlés átkerül a végrehajtandó függvényhez vagy blokkhoz, és a kezdeti értéket megadó kifejezés tetszőleges lehet. Alapfeltételezés szerint a külső és statikus változók nulla kezdeti értéket kapnak. Az automatikus változók kezdeti értéke határozatlan, hacsak explicit módon nem kapnak értéket.
Bármely változó deklarációjában alkalmazható a const minősítő, ami azt jelzi, hogy a változó értékét nem fogjuk megváltoztatni. Ilyen deklaráció pl.
const double e = 2.71828182845905;
const char uzenet[ ] = "figyelem:";
A const minősítésű deklaráció tömb argumentumok esetén is használható, jelezve hogy a függvény nem változtatja meg a tömböt. Pl:
int strlen(const char [ ]);
Amennyiben a program a const minősítésű változó értékének megváltoztatására mégis kísérletet tesz, akkor a hatás a használt számítógéptől és rendszertől függ.

2.5. Aritmetikai operátorok

A C nyelv kétoperandusú aritmetikai operátorai a +, -, * és /, valamint a % modulus operátor. Az egészek osztásakor a törtrészt a rendszer levágja, ezért van szükség a % modulus operátorra. Az
x % y
kifejezés az x/y egészosztás (egész) maradékát adja, és értéke nulla, ha x osztható y-nal. Például egy adott év szökőév, ha az évszáma osztható néggyel és nem osztható százzal, kivéve a 400-zal osztható évszámokat, amik szintén szökőévek. Ezért annak eldöntése, hogy egy adott év szökőév-e vagy sem, az alábbi programrészlettel lehetséges:
if ((ev % 4 == 0 && ev % 100 != 0) || ev % 400 == 0)
   printf("%d szökőév.\n", ev);
else
   printf("%d nem szökőév.\n", ev);
A % operátor nem alkalmazható float és double típusú adatokra. Negatív operandusok esetén az egészosztás hányadosának csonkítása, valamint a modulus előjele gépfüggő, és ugyanez igaz az esetlegesen előforduló túlcsordulásra és alácsordulásra is.

Az egyoperandusú (unáris) + és - operátorok precedenciája a legmagasabb. A kétoperandusú + és - operátorok precedenciája kisebb a *, / és % precedenciájánál. Az aritmetikai operátorok mindig balról jobbra haladva hajtódnak végre (a precedencia figyelembevételével).

A fejezet végén lévő 2.1 táblázatban összefoglaltuk az összes operátor precedenciáját és asszociativitását.

2.6. Relációs és logikai operátorok

A C nyelv relációs operátorai: >, >=, <, <=. Ez a sorrend egyben a precedenciájuk sorrendje is. Ezeknél eggyel alacsonyabb precedenciájúak az egyenlőség operátorok: ==, !=.

A relációs operátorok precedenciája kisebb, mint az aritmetikai operátoroké, így pl. egy olyan kifejezés, mint i < hatar-1 úgy értékelődik ki, mint ha i <(hatar-1) formában írtuk volna (amint ez elvárható).

Sokkal érdekesebb az && és || logikai operátorok kérdése! Az && és || operátorokkal összekapcsolt kifejezések kiértékelése balról jobbra történik, és a kiértékelés azonnal félbeszakad, ha az eredmény igaz vagy hamis volta ismertté válik. A legtöbb C nyelvű program kihasználja ezeket a tulajdonságokat. Így pl. az 1. fejezetben írt getline függvény ciklusszervezése is ennek alapján működik:

for (i = 0; i<lim-1 && (c = getchar ( )) != '\n'
	   && c != EOF; ++i) 
   s[i] = c;
Az új karakter beolvasása előtt meg kell vizsgálni, hogy van-e hely számára az s tömbben, így először az i<lim-1 ellenőrzést kell végrehajtani. Ha ennek az eredménye hamis, akkor a program már nem is megy tovább a következő karakter beolvasására. Ugyancsak nem volna szerencsés, ha a c karaktert az állomány vége feltételre vizsgálnánk a getchar hívása előtt, ezért a hívásnak és az értékadásnak meg kell előzni a c karakter vizsgálatát.

Az && precedenciája nagyobb, mint a || precedenciája, és mindkét operátor alacsonyabb precedenciájú, mint a relációs és egyenlőség operátorok, így az

i<lim-1 && (c = getchar( )) != '\n' && c != EOF
kifejezés nem tartalmaz felesleges zárójeleket. De mivel a != precedenciája nagyobb, mint az értékadásé, ezért zárójelezés szükséges:
(c = getchar( )) != '\n'
Ezzel elérjük, hogy először az értékadás történjen meg, és csak ezután hasonlítsuk össze a c értékét az '\n' karakterrel.

Egy relációs vagy logikai kifejezés számértéke definíció szerint 0, ha a kifejezés hamis, és 1, ha igaz.

A ! unáris (egyoperandusú) negáló operátor a nem nulla (igaz) operandust 0 értékűvé (hamissá), a 0 értékű (hamis) operandust 1 értékűvé (igazzá) alakítja. A ! operátort gyakran használjuk olyan szerkezetekben, mint pl. az

if (!igaz)
az
if (igaz == 0)
kifejezés helyett. Nehéz általános esetben megmondani, hogy melyik változat a jobb. A !igaz szerkezet általában jól olvasható („nem igaz”), de bonyolultabb esetben nehezen érthető.

2.2. gyakorlat. Írjunk az előző for ciklussal egyenértékű ciklust, ami nem használja az && vagy || operátorokat!

2.7. Típuskonverziók

Ha egy operátor operandusai különböző típusúak, akkor a művelet végrehajtása előtt azokat egy közös típusra kell hozni. Az átalakításra néhány szabály vonatkozik. Általában az automatikus konverzió csak akkor jön létre, ha egy „keskenyebb" operandust egy „szélesebb” operandussá kell alakítani, mivel így biztosan nem vész el információ. Ilyen konverzió jön létre pl. az f+1 alakú kifejezésekben, ahol az i egész lebegőpontossá alakul az f lebegőpontos szám miatt. Az értelmetlen kifejezések, mint pl. float típusú adat indexként való használata, nem megengedettek. Ha egy hosszabb egész típust egy rövidebbhez, vagy egy lebegőpontos típust egy egészhez rendelünk, akkor információ veszhet el, ezért figyelmeztető jelzést kapunk, de maga a művelet nem tilos.

A char típusú adatok kis egész számok, ezért az összes aritmetikai műveletben szabadon használhatók, ami nagymértékben egyszerűsíti és rugalmassá teszi a karakterkezelést. Erre jó példa az atoi függvény, amely a számjegyekből álló karaktersorozatot a megfelelő értékű számmá alakítja. A függvény egyszerűsített változata:

/* atoi: az s karaktersorozat egész számmá alakitása */
int atoi(char s[ ])
{
   int i, n;

   n = 0;
   for (i = 0; s[i] >= '0' && s[i] <= '9'; ++i)
      n = 10 * n + (s[i] - '0'); 
return n; 
}
Amint azt az 1. fejezetben már elmondtuk, az s[i]-'0' kifejezés megadja az s[i]-ben tárolt karakter számértékét, mivel a '0', '1' stb. folyamatosan növekvő sorozatot alkot.

A char típus int típussá alakítására egy másik példa a lower függvény, amely egy (kizárólag ASCII karakterkészletbeli) karaktert alakít kisbetűvé. Ha a karakter nem nagybetű, akkor a függvény változatlan formában adja vissza.

/* lower: a c ASCII karakter kisbetűssé alakitása */
int lower(int c)
{
   if (c >= 'A' && c <= 'Z')
      return c + 'a' - 'A';
   else
      return c; 
}
A függvény csak ASCII karakterkészlet esetén használható, mivel kihasználja, hogy a nagy- és kisbetűk számértéke (kódja) rögzített távolságra van egymástól, és a nagy-, ill. kisbetűs ábécék folyamatosak (azaz A és Z, ill. a és z között a betűkön kívül nincs más karakter). Ez az utóbbi kitétel nem igaz az EBCDIC karakterkészletre, így a lower nem működne helyesen (nem csak betűket alakítana át).

A B. függelékben leírt <ctype.h> standard header állományban számos függvény van, amelyek a karakterkészlettől független karakter-ellenőrzésre és -átalakításra használhatók. Például a tolower(c) függvény a c karakter kisbetűs értékével tér vissza, ha az nagybetűs karakter volt, így általánosabb formában helyettesítheti a lower függvényünket.

Az atoi függvényben használt

c >= '0' && c <= '9'
vizsgálat is helyettesíthető az
isdigit(c)
könyvtári függvénnyel. A továbbiakban a <ctype.h> standard header függvényeit használni fogjuk az egyes programokban.

A karakterek egésszé alakításával kapcsolatban szólnunk kell még a C nyelv egy további finomságáról. A nyelv nem határozza meg, hogy a char típusú változó előjeles vagy előjel nélküli mennyiség-e. Vajon char típusú adat int típusúvá alakításakor létrejöhet-e negatív szám? A válasz gépről gépre változik, a felépítéstől függően. Néhány számítógépnél azok a karakterek, amelyek bal szélső (legnagyobb helyiértékű) bitje 1, negatív egész számmá konvertálódnak (előjel-kiterjesztés!). Más gépeken az átalakítás során keletkező int típusú adat balról nullákkal töltődik fel, ezért mindig pozitív marad.

A C nyelv definíciója garantálja, hogy a gép szabványos karakterkészletében lévő egyetlen nyomtatható karakter sem lesz negatív, így ezek a karakterek egy kifejezésben mindig pozitív mennyiségként szerpelnek. Ez természetesen nem igaz a karakteres változóban tárolt tetszőleges bitmintákra, amelyek néhány gépen negatív, más gépeken pozitív értékűek lehetnek. Az egyértelműség és hordozhatóság érdekében célszerű a signed vagy unsigned minősítőt használni, ha nem karakteres adatot tárolunk char típusú változóban.

Az && és || logikai műveletekkel összekapcsolt i > j alakú relációs kifejezések és logikai kifejezések eredményének értékét úgy definiáljuk, hogy az 1, ha az eredmény igaz, és 0, ha hamis. Így a

d = c >= '0' && c <= '9'
értékadás a d változóhoz 1 értéket rendel, ha c számjegy, és 0 értéket, ha nem. Másrészről az isdigithez hasonló függvények igaz esetben tetszőleges nem nulla értékkel térnek vissza. Az eltérő értelmezések okozta hibák kiküszöbölése érdekében az if, while, for stb. utasítások vizsgáló részében az „igaz” ugyanazt jelenti, mint a „nem nulla”, ezért a kétféle értékadás hatása között nincs különbség.

Az implicit aritmetikai típuskonverziók sokkal inkább a várakozásnak megfelelően működnek. Ha egy kétoperandusú operátor (pl. + vagy *) operandusai különböző típusúak, akkor az „alacsonyabb” típus „magasabb” típussá alakul („előlép”) a művelet végrehajtása előtt, és az eredmény a „magasabb” típusnak megfelelő lesz. A pontos konverziós szabályokat az A. Függelék 6. pontjában ismertetjük, addig is néhány egyszerű szabályt adunk. Ha az operandusok nem unsigned minősítésűek, akkor:

Ha valamelyik operandus long double típusú, akkor a másik is long double
      típusúvá konvertálódik;
különben, ha valamelyik operandus double típusú, akkor a másik is double
      típusúvá konvertálódik;
különben, ha valamelyik operandus float típusú, akkor a másik is float
      típusúvá konvertálódik;
különben a char és short típusú operandus int
      típusúvá konvertálódik;
végül, ha valamelyik operandus long típusú, akkor a másik is long
      típusúvá konvertálódik.
Megjegyezzük, hogy egy kifejezésben a float típusú operandusok nem automatikusan alakulnak át double típusúvá, ami a C nyelv eredeti definíciójához képest eltérést jelent. Általában a matematikai függvények (mint a <math.h> standard header függvényei is) kétszeres pontosságú lebegőpontos adatokat használnak. A float típus használatának fő oka, hogy nagy tömbök esetén tárterület takarítható meg, vagy ritkábban, hogy a végrehajtási idő csökken, mivel a kétszeres pontosságú aritmetika viszonylag lassú.

A konverziós szabályok unsigned minősítésű operandusok esetén sokkal bonyolultabbak. A problémát az előjeles és előjel nélküli számok összehasonlítása jelenti, mivel az értékek a géptől és a különböző egész típusok méretétől függenek. Például tegyük fel, hogy az int 16 bites, a long 32 bites. Ekkor -1L < 1U, mert 1U (ami egy int típus) „előlép” signed long típussá. Másrészt viszont -1L > 1UL, mert a -1L unsigned long típussá „lép elő”, amivel nagy pozitív számmá válik.

Az értékadás is típuskonverzióval jár: a jobb oldal értéke a bal oldal típusának megfelelő típusúvá alakul és ez lesz az eredmény típusa is.

A karakterek mindig egésszé alakulnak (előjel-kiterjesztéssel vagy anélkül, ahogy erről már volt szó).

A hosszabb egészek rövidebb vagy char típusúvá alakulásakor a magasabb helyiértékű bitek elvesznek. Így az

int i;
char c;

i = c;
c = i;
műveletsorban a c értéke változatlan marad. Ez mindig igaz, függetlenül attól, hogy van-e előjel-kiterjesztés vagy sem.

Az értékadás sorrendjének megfordítása információvesztéshez vezethet. Ha az x float és az i int típusú, akkor az x = i és i = x értékadásoknál mindkét esetben létrejön a típuskonverzió. A float int típussá alakulása a törtrész elvesztésével jár. Ha double típust alakítunk float típussá, akkor az új érték a rendszertől függően kerekítéssel vagy levágással keletkezik.

Mivel függvényhíváskor az argumentum kifejezés is lehet, az argumentum átadásakor is létrejöhet típuskonverzió. Függvényprototípus hiányában a char és short típusok int típussá, a float típus double típussá alakul. Ezért a függvények argumentumait int és double típusúnak deklaráltuk akkor is, ha a függvényt char vagy float típusú argumentummal hívtuk.

Végül megjegyezzük, hogy tetszőleges kifejezésben kikényszeríthetjük az explicit típuskonverziót a rögzítő (cast) unáris típusmódosító operátorral. Az így kialakított szerkezet:

(típusnév) kifejezés
alakú, és hatására a kifejezés a típusnévvel megadott típusúvá konvertálódik, a korábban elmondott szabályok szerint. A kényszerített típusmódosítás alapvetően úgy működik, mintha a kifejezés értékét az előírt típusú változóhoz rendelnénk egy értékadással, majd ezután ezt a változót használnánk a kifejezés helyett. Például a sqrt könyvtári függvény double típusú argumentumot vár, és értelmetlen eredményt ad, ha véletlenül valamilyen más típusú argumentumot kap. (Az sqrt függvény a <math.h> standard headerben található.) Így, ha az n változó egész típusú, akkor az sqrt függvény
sqrt((double) n)
formában használható, mivel ez a paraméterátadás előtt double típusúvá alakítja n értékét. Meg kell jegyeznünk, hogy bár a kényszerített típusmódosítás létrehozza n megfelelő típusú értékét, de magát az n változót nem változtatja meg. A kényszerített típusmódosítás operátora a többi unáris operátorral azonos precedenciájú, mint az a fejezet végén lévő összefoglaló táblázatból látszik.

Ha az argumentumot egy függvényprototípusban deklaráljuk (ami a szokásos megoldás), akkor a deklaráció bármilyen más, a függvény hívásakor felhasznált argumentumra rákényszeríti a megadott típust. Így pl. az sqrt prototípusát

double sqrt (double);
formában megadva megengedett a
gyok2 = sqrt(2)
alakú hívás, mivel a 2 automatikusan double típusú 2.0 értékké alakul, minden kényszerített típusmódosítás nélkül.

A standard könyvtár tartalmaz egy hordozható pszeudovéletlenszám-generátort, valamint annak kezdőértékét beállító függvényt. A két függvény egyszerűsített programja jó példa a kényszerített típusmódosításra. A program:

unsigned long int next = 1;

/* rand: egy 0 és 32767 közti pszeudo-véletlen számot */
/* ad vissza */
int rand(void)
{
   next = next * 1103515245 + 12345;
   return (unsigned int) (next/65536) % 32768;
}

/* srand: a rand kezdőértékének beállítása */
void srand(unsigned int alap)
{
   next = alap; 
}

2.3. gyakorlat. Írjunk htoi(s) néven függvényt, amely egy hexadecimális számjegyekből álló karaktersorozatot (beleértve a 0x vagy 0X karaktersorozatot is) a megfelelő egész számmá alakít! A megengedett számjegyek 0...9 és a...f vagy A...F.

2.8. Inkrementáló és dekrementáló operátorok

A C nyelv két szokatlan operátort használ a változók inkrementálására (eggyel való növelésére) és dekrementálására (eggyel való csökkentésére). A ++ inkrementáló operátor egyet ad az operandushoz, a -- dekrementáló operátor pedig egyet kivon belőle. A ++ operátort gyakran használjuk változók növelésére, pl. az
if (c == '\n') ++nl
programrészletben. A ++ és -- szokatlan vonatkozása, hogy prefix formában (a változó előtt elhelyezve, pl. ++n) és postfix formában (a változó után elhelyezve, pl. n++) egyaránt létezik. A kétféle változat egyaránt növeli (vagy csökkenti) a változót, de a ++n a felhasználás előtt, az n++ pedig utána növeli az n értékét (a -- operátor hasonlóan működik). Ebből következően minden olyan esetben, amikor a változó értékét is felhasználjuk (nem csak a növelésre vagy csökkentésre, azaz számlálásra van szükség), a ++n és az n++ különbözik. Ha pl. n értéke 5, akkor
x = n++;
hatására x értéke 5 lesz, amíg az
x = ++n;
hatására x értéke 6 lesz. Természetesen n értéke mindkét esetben 6 lesz. Az inkrementáló vagy dekrementáló operátorok csak változókra alkalmazhatók, az (i+j)++ formájú kifejezések tilosak.

Olyan esetekben, amikor az értékre nincs szükség, csak a növelésre, mint pl. az alábbi példában:

if (c == '\n')
   nl++;
a prefix és postfix forma egyenértékű. Van viszont olyan feladat, amikor csak az egyiket vagy a másikat választhatjuk. Példaként vizsgáljuk meg a squeeze(s, c) függvényt, amely az s karaktersorozatból eltávolítja az összes c karaktert.
/* squeeze: az s-ből törli az összes c-t */
void squeeze(char s[ ], int c)
{
   int i, j;

   for (i = j = 0; s[i] != '\0'; i++)
      if (s[i] != c)
         s[j++] = s[i];
   s[j] = '\0'; 
}
A függvény minden alkalommal, amikor c-től különböző karaktert talál, az i-edik pozícióból átmásolja azt a j-edik (aktuális) pozícióba, és csak ezután inkrementálja j értékét, hogy kijelölje a következő karakter helyét. Ez teljesen egyenértékű a következővel:
if (s[i] != c) {
   s[j] = s[i];
   j++;
}
A másik példát az 1. fejezetben ismertetett getline függvényből vettük:
if (c == '\n') {
   sor[i] = c;
   ++i; 
}
Ez a szerkezet helyettesíthető a sokkal tömörebb
if (c == '\n')
   sor[i++] = c;
szerkezettel. A harmadik példánk az strcat(s, t) függvény, amely a t karaktersorozatot az s karaktersorozat végéhez illeszti (konkatenálja). Az strcat feltételezi, hogy az s karaktersorozat elegendően hosszú az összekapcsolt karaktersorozat befogadásához. Az itt közölt példaprogramban az strcat visszatéréskor nem ad értéket, standard könyvtári változata viszont egy mutatót ad vissza, ami kijelöli az eredményül kapott karaktersorozat helyét.
/* strcat: a t karaktersorozatot a s karaktersorozat végéhez
kapcsolja, s elegendően hosszú */
void strcat(char s[], char t[ ] )
{
   int i, j;

   i = j = 0;
   while (s[i] != '\0') /* megkeresi s végét */
      i++;
   while ((s[i++] = t[j++]) != '\0')
      ; /* átmásolja t-t */
}
A program t minden egyes karakterének s-be másolása után a postfix ++ operátorral növeli i és j értékét, így azok a ciklusmag következő végrehajtásakor már a következő helyet jelölik ki.

2.4. gyakorlat. Írjuk meg a squeeze(s1, s2) olyan változatát, amely az s1 karaktersorozatból töröl minden karaktert, ami az s2 karaktersorozatban megtalálható!

2.5. gyakorlat. Írjunk any(s1, s2) néven függvényt, amely visszatérési értékként megadja az s1 karaktersorozat azon legelső helyét, ahol az s2 karaktersorozat bármelyik karaktere előfordul! A függvény visszatérési értéke legyen -1, ha s1 egyetlen s2-beli karaktert sem tartalmaz. (Az strbrk standard könyvtári függvény ugyanezt teszi, csak visszatérési értékként az adott helyet kijelölő mutatót adja.)

2.9. Bitenkénti logikai operátorok

A C nyelvben hat operátor van a bitenkénti műveletekre. Ezek az operátorok csak egész típusú adatokra, azaz char, short, int és long típusokra használhatók, akár előjeles, akár előjel nélküli változatban. Az egyes operátorok és értelmezésük a következő:
&     bitenkénti ÉS-kapcsolat
|     bitenkénti megengedő (inkluzív) VAGY-kapcsolat
^     bitenkénti kizáró (exkluzív) VAGY-kapcsolat
<<    balra léptetés
>>    jobbra léptetés
~     egyes komplemens képzés (unáris)
A bitenkénti ÉS operátort gyakran valamilyen bitminta kimaszkolására használják, pl. az
n = n & 0177;
művelet az n bináris értékében az alsó hét bit kivételével minden bitet nulláz. A bitenkénti VAGY operátort a bitek beállítására használják, pl. az
x = x | BEALL;
művelet x minden olyan bitjét 1-re állítja, amely a BEALL-ban is 1 volt (függetlenül attól, hogy korábban milyen értékű volt).

A kizáró VAGY operátor minden olyan helyen 1-et ad, ahol a két operandus bitjei különböztek, és 0-t, ha megegyeztek.

Az & és | bitenkénti, valamint az && és || logikai operátorok közti legfontosabb különbség, hogy az utóbbiak az igazságtábla szerint, balról jobbra haladva végzik a kiértékelést, míg az előzőek egy lépésben, bitenként. Például, ha x értéke 1 és y értéke 2 , akkor x & y értéke nulla, viszont x && y értéke egy.

A << és >> léptető operátorok a bal oldalukon lévő operandust a jobb oldalukon lévő pozitív értéknek megfelelő számú bittel jobbra vagy balra léptetik. Így pl. az x << 2 művelet x értékét két hellyel balra lépteti, a jobb szélen keletkező két üres bináris helyet nullával tölti fel. A művelet megfelel a néggyel való szorzásnak. Egy előjel nélküli mennyiség jobbra léptetésekor a bal szélen felszabaduló bitek mindig nullával töltődnek fel. Előjeles számok jobbra léptetésekor a felszabaduló bitek az előjel bittel (aritmetikai léptetés), ill. néhány számítógép esetén nullával töltődnek fel (logikai léptetés).

A ~ unáris (egyoperandusú) operátor egy egész egyes komplemensét állítja elő, ami azt jelenti, hogy a szám minden 1 értékű bitjét 0-ra, minden 0 értékű bitjét 1-re állítja. Például az

x = x ~ 077
művelet hatására x utolsó hat bitje nulla értékű lesz. Vegyük észre, hogy az x ~ 077 eredménye független az x gépi ábrázolásához használt szó hosszától, és így 16 bites szóhossz esetén (de csak akkor!) előnyösebb a vele egyenértékű x ~ 0177700 művelet. A hordozható forma semmiféle többletbefektetést nem igényel, mivel az ~077 egy állandó kifejezés, aminek kiértékelése a fordítás során történik.

Néhány bitenkénti operátor működésének bemutatására írjunk getbits(x, p, n) néven függvényt, amely az x p-edik hellyel kezdődő n bites részét adja vissza, jobbra igazítva. Tételezzük fel, hogy a 0. bitpozíció a jobb szélső, valamint azt, hogy n és p értelmes pozitív mennyiségek. Például a getbits(x, 4, 3) x 4., 3. és 2. pozícióján álló hárombites, jobbra igazított számmal tér vissza. A függvény:

/* getbits: a p-edik pozíciótól kezdődő n bitet adja */
unsigned getbits(unsigned x, int p, int n)
{
   return (x >> (p+1-n)) & ~(~0 << n);
}
az x >> (p+1-n) kifejezés a kiválasztott mezőt a szó jobb szélére mozgatja. A ~0 a teljes szóhosszon csupa 1 értékű bitet jelent, amit a ~0 << n művelettel n lépéssel balra tolunk. Ennek hatására a szó n számú jobb szélső bitje nullával töltődik fel, és ebből komplementálással alakul ki az n darab jobb szélső bitet kiválasztó (csupa egyenesből álló) maszk.

2.6. gyakorlat. Írjuk meg a setbits(x, p, n, y) függvényt, amely egy olyan x értékkel tér vissza, amit úgy kap, hogy az x p-edik pozíciótól jobbra eső n bitje helyébe bemásolja y jobb szélső n bitjét, a többi bitet változatlanul hagyva!

2.7. gyakorlat. Írjunk egy invert(x, p, n) függvényt, amely az x p-edik pozíciótól kezdődő n bitjét invertálja (az 1-eseket 0-ra, a 0-kat 1-esekre változtatja), a többi bitet pedig változatlanul hagyja!

2.8. gyakorlat. Írjunk egy rightrot(x, n) függvényt, ami n bittel jobbra rotálja az x egész mennyiséget! Jobbra rotálásnál a jobb szélen kilépő bitek a bal szélen visszakerülnek a szóba.

2.10. Értékadó operátorok és kifejezések

Az olyan kifejezéseket, mint
i = i + 2
amelyekben a bal oldalon lévő változó ismét megjelenik a jobb oldalon, az
i += 2
tömörebb formában is írhatjuk. Az új formában szereplő += jelkombinációt értékadó operátornak nevezik. A legtöbb kétoperandusú operátor (pl, mint a +, amelynek bal és jobb oldalán is operandus áll) szerepelhet az értékadó operátorban, amelynek általános alakja op=, ahol op a
+ - * / % << >> & ^ |
operátorok egyike lehet. Ha k1 és k2 két kifejezés, akkor a
k1 op= k2
egyenértékű a
k1 = (k1) op (k2)
alakkal. A tömörebb forma előnye, hogy a gép a kikifejezést csak egyszer számolja ki. Összetett kifejezések esetén ügyeljünk a zárójelekre. Például az
x *= y + 1
alak az
x = x * (y + 1)
kifejezésnek felel meg, és nem az
x = x * y + 1
kifejezésnek. Az elmondottak megvilágítására nézzük a bitcount függvényt, amely megszámolja az egész típusú argumentumában lévő 1 értékű biteket.
/* bitcount: x 1 értékű bitjeinek száma */
int bitcount(unsigned x)
{
   int b;

   for (b = 0; x != 0; x >>= 1)
      if (x & 01)
         b++;
   return b;
}
A függvény x argumentumát unsigned típusúnak deklaráltuk, hogy a jobbra léptetés során a felszabaduló helyekre garantáltan 0 értékű bitek lépjenek be, függetlenül a géptől, amelyen a program fut.

A tömörségen túl az értékadó operátorok alkalmazásának további előnye, hogy jobban megfelel az emberi gondolkodásnak. Általában azt mondjuk, hogy „adj kettőt i-hez” vagy „növeld i-t kettővel”, ahelyett, hogy azt mondanánk „vedd i-t, adj hozzá kettőt, majd tedd az eredményt vissza az i-be”. Emiatt az i += 2 kifejezés előnyösebb, mint az i = i+2. Bonyolult kifejezéseknél az értékadó operátor használatának még az is előnye, hogy érthetőbbé teszi a programot. Például egy

yyval[yypv[p3+p4] + yypv[p1+p2]] += 2
alakú kifejezésben nem kell nehézkes módon ellenőrizni, hogy a bal és jobb oldali kifejezések tényleg megegyeznek-e, ill. ha nem, akkor miért nem. Az értékadó operátorok a fordítóprogramot is segítik a hatékony kód előállításában.

Mint korábban már láttuk, az értékadó utasításnak értéke van és előfordulhat kifejezésben, amire a legegyszerűbb példa a

while ((c = getchar()) != EOF)
utasítássor. Ennek mintájára más értékadó operátort (+=, -= stb.) tartalmazó értékadások is szerepelhetnek kifejezésekben, bár ez nem túl gyakori.

Minden ilyen kifejezésben az értékadó kifejezés típusa megegyezik a bal oldali operandus típusával, és értéke az értékadás utáni érték.

2.9. gyakorlat. A kettes komplemens kódú aritmetikában az x &= (x-1) kifejezés törli x jobb szélső bitjét. Magyarázzuk meg, miért! Ezt kihasználva írjunk egy gyorsabb bitcount változatot!

2.11. Feltételes kifejezések

Az
if (a > b)
   z = a;
else
   z = b;
programrészlet hatására z az a és b értékek közül a nagyobbikat veszi fel. Ilyen és hasonló szerkezetek a C nyelv háromoperandusú ?: feltételes kifejezés operandusával egyszerűbben is leírhatók. Az operátor általános formája kifejezésekben:
kif1? kif2 : kif3
A szerkezet úgy működik, hogy először kiértékeli a kif1 kifejezést. Ha ennek értéke nem nulla (igaz), akkor a kif2 kiértékelése következik, egyébként pedig a kif3 kiértékelése. A program kif2 és kif3 közül csak az egyiket értékeli ki (a kif1 értékétől függően) és ez lesz a feltételes kifejezés értéke. Így z beállítása az a és b közül a nagyobbik értékének megfelelően a
z=(a>b) ? a : b; /* z = max (a, b) */
utasítással történhet.

Meg kell jegyeznünk, hogy a feltételes kifejezés egy valódi kifejezés, tehát ugyanúgy használható, mint bármilyen más kifejezés. Ha kif2 és kif3 eltérő típusúak, akkor az eredmény típusát a korábban tárgyalt konverziós szabályok szerint lehet meghatározni. Például, ha f float típusú és n int, akkor az

(n > 0) ? f : n
kifejezés float típusú lesz, függetlenül attól, hogy n pozitív-e vagy sem.

A feltételes kifejezésben az első kifejezést nem szükséges zárójelbe tenni, mivel a ?: operátor precedenciája nagyon alacsony (csak eggyel nagyobb, mint az értékadásé). Másrészt zárójelezéssel a kifejezés feltételrésze egyértelművé tehető.

A feltételes kifejezések használata tömör és világos kódot eredményez. Ezt jól mutatja az alábbi programrészlet, amely egy tömb n elemét nyomtatja ki úgy, hogy soronként tíz értéket ír, azokat egy szóközzel választja el és a sort újsor-karakterrel zárja.

for (i = 0; i<n; i++)
   printf("%6d%c", a[i], (i%10==9 || i==n-1) ?
      '\n' : ' ');
A programrész minden tizedik sor és n-edik elem után egy újsor-karaktert ír ki, minden más esetben pedig a számot szóköz követi. A példa elég trükkös, de mindenképpen sokkal tömörebb, mint az if-else szerkezettel megvalósított változata. Egy másik példa*:
printf("You have %d item%s. \n", n, n==1 ? " " : "s");

*[Az eddigiektől eltérően ezt a példát az angol és a magyar nyelvtan eltérő volta miatt nem tudtuk magyarra átírni. A példa lényege, hogy a programrész az n szám 1 vagy 1-nél nagyobb értékétől függően a mondatot „You have 1 item.” vagy „You have 5 items.” alakban írja ki (ha pl. n=5), azaz az „item” szó egyes vagy többes számát használja az angol nyelvtani szabályoknak megfelelően. (A mondat magyarul: „1 árucikked van”, ill. „5 árucikked van”.) (A fordító)]

2.10. gyakorlat. Írjuk át a nagybetűket kisbetűkké alakító lower függvényt úgy, hogy az if-else szerkezetet feltételes kifejezéssel helyettesítjük!

2.12. A precedencia és a kifejezés kiértékelési sorrendje

A 2.1. táblázatban összefoglaltuk az összes operátor (beleértve az eddig nem tárgyaltakat is) precedenciáját és asszociativitását (kiértékelési irányát). Az azonos sorban szereplő operátorok precedenciája azonos, a sorok csökkenő precedencia szerint követik egymást, és így pl. a *, / és % precedenciája azonos és nagyobb a kétoperandusú + és - precedenciájánál. A táblázatban a () operátor a függvényhívást jelöli, a -> és . operátorok pedig struktúrák elemeihez való hozzáféréshez használhatók. Ezekkel a 6. fejezetben foglalkozunk, csakúgy mint az objektum méretét megadó sizeof operátorral. A mutatókkal kapcsolatos indirekciós ( * ) operátort és az objektum címét megadó & operátort az 5. fejezetben, a , (vessző) operátort pedig a 3. fejezetben tárgyaljuk.

2.1 táblázat. Operátorok precedenciája és asszociativitása
Operátor Asszociativitás
() [] -> balról jobbra
! ~ ++ -- + - * & (típus) sizeof jobbról balra
* / % balról jobbra
+ - balról jobbra
<< >> balról jobbra
< <= > >= balról jobbra
== != balról jobbra
& balról jobbra
^ balról jobbra
| balról jobbra
&& balról jobbra
|| jobbról balra
?: jobbról balra
= += -= *= /= %= &= ^= |= <<= >>= balról jobbra
Megegyzés: az unáris (egyoperandusú) +, - és * operátorok nagyobb precedenciájúak, mint a kétoperandusú operátorok.

Megjegyezzük, hogy a bitenkénti &, ^ és | operátorok precedenciája ilyen sorrendben csökken és eleve kisebb az == és != precedenciájánál. Emiatt az olyan bitvizsgáló kifejezéseket, mint
if ((x & MASZK) == 0)
a helyes eredmény érdekében gondosan zárójelezni kell.

A C, több más nyelvhez hasonlóan, nem határozza meg az operátorok operandusainak kiértékelési sorrendjét. (Kivéve az &&, ||, ?: és , operátorokat.) Így pl. felmerül a kérdés, hogy az

x = f( ) + g( );
alakú kifejezésben az f kiértékelése a g előtt történik-e vagy sem. Ez azért érdekes, mert f vagy g megváltoztathatja a másik függvényben használt változókat, és emiatt x értéke függhet a kiértékelés sorrendjétől. Ha valami miatt fontos a kiértékelés sorrendje, akkor a közbenső eredményeket egy segédváltozóban kell eltárolni és később felhasználni. Hasonlóan a függvényargumentumok kiértékelési sorrendje sincs meghatározva, ezért a
printf("%d %d\n", ++n, power(2, n)); /* hibás */
utasítás különböző fordítóprogramokkal más-más eredményt adhat, aszerint, hogy az n inkrementálása a power függvény hívása előtt vagy után történik-e. A biztos megoldás természetesen az, ha az utasítást
++n;
printf("%d %d\n", n, power(2, n));
alakban írjuk.

A függvényhívások, egymásba ágyazott értékadó utasítások és inkrementáló, ill. dekrementáló utasítások mellékhatásokat okozhatnak, azaz egy kifejezés kiértékelése közben néhány változó értéke (nem szándékosan) megváltozhat. Bármely kifejezés kiértékelése vezethet mellékhatásokhoz, és sok függhet attól, hogy a kifejezésben aktuálisan szereplő változókat milyen sorrendben dolgozza fel a gép. Egy szerencsétlen, de sajnos gyakori esetet példáz a következő utasítás:

a[i] = i++;
A kérdés az, hogy az index a régi vagy az új i érték-e? A fordítóprogramok a kérdést különböző módon kezelik, és az eredmény a felhasznált fordítóprogramtól függ. A szabvány a legtöbb ilyen kérdést szándékosan nyitva hagyja. A kifejezések kiértékelése során a mellékhatások kialakulását végezetül is mindig a fordítóprogram dönti el, mivel az optimális kiértékelési sorrend nagymértékben függ a számítógép felépítésétől.

A kiértékelési sorrendtől függő programok írása minden nyelv esetén helytelen, ezért mindenképpen kerüljük és igyekezzünk kiküszöbölni a mellékhatásokat is. A mellékhatások elkerüléséhez jó, ha tudjuk, hogy adott helyzetben mit csinál a számítógép (vagy a fordítóprogram), viszont a tudatlanság sok esetben védelmet is jelenthet.


1. FEJEZET Tartalom 3. FEJEZET