A C nyelv egyik erõssége más nyelvekhez képest a kifejezésekben használható operátorok gazdag választéka. Az eddig említett operátorokat (típusmódosító operátorok, indexelõ, illetve függvényaktivizáló operátorok) csak a C-ben nevezik operátornak. A következõkben a hagyományos értelemben vett operátorokat ismertetjük. Ezek többnyire elemi típusú adatokon végeznek mûveleteket, de egyik-másik közülük származtatott, illetve bonyolultabb, összetett típusú tárolási egységekre is alkalmazható. Az egyes operátorok felhasználásának lehetõségét a C++ az objektum-orientált tulajdonságai révén még tovább szélesíti. Ezt majd az ún. operator overloading kapcsán tekintjük át részletesen a 2. fejezetben.
A következõkben tehát leírjuk a C nyelvben hagyományos értelemben vett operátorokat, ismertetjük azok alkalmazását.
Az operátoroknak különbözõ precedenciájuk lehet, a kiértékelés sorrendje ezektõl függ. A fejezet végén ezért majd összefoglalóan felsoroljuk az egyes precedencia-osztályokat, és megadjuk azt is, hogy mi a teendõ, ha azonos precedenciájú operátorok együtt fordulnak elõ. A legtöbb kétoperandusú operátor megkívánja, hogy mindkét operandusa azonos típusú legyen. Például a /-rel jelölt osztási mûvelet mást jelent egész típusú operandusokra alkalmazva (maradékos egész osztás), mint lebegõpontosakra, és kevert operandusok esetén nem dönthetõ el, milyen algoritmust kell használni. Hasonló - bár nem ennyire látványos - a probléma különbözõ méretû operandusok esetén is. Nem lenne praktikus elvárás, hogy a C fordító minden elképzelhetõ operanduspárosra adjon eljárást, de nem lenne célravezetõ a teljes uniformizálás sem (mint például egyes BASIC rendszereknél, ahol minden mûveletet lebegõpontosan hajtanak végre). Ezért a C nyelv tervezõi úgy döntöttek, hogy a gépi szónál rövidebb operandusokat gépi szó méretûre bõvítik, valamint számûzik a rövid lebegõpontos mûveleteket. Ha a mûveletben résztvevõ két operandus nem azonos típusú, akkor az egyik átalakul, hogy megegyezzenek, mindig a sorszámozott típusú alakulva lebegõpontossá, a rövidebb változat a hosszabbra, az elõjeles az elõjel nélkülire. A pontos szabályokat az 1.6-os szakasz tartalmazza, és a továbbiakban ezekre mint "szokásos aritmetikai konverziók"-ra fogunk utalni.
Az ismertetést az egyoperandusú operátorokkal kezdjük, majd folytatjuk a kétoperandusúakkal, és végül kitérünk a C egyetlen, háromoperandusú operátorára is.
Az egyoperandusú opertárok közé tartozik a [ ] indexelõ, a ( ) függvényaktivizáló operátor, és az elsõdleges kifejezést képzõ operátor, az egyszerû zárójelpár. Ezeket az elõzõ, 1.5.1. pontban már ismertettük, itt csak a teljesség kedvért említjük meg õket újra.
Az egyoperandusú * operátor, az ún. dereference operator indirekciót jelez, az operandusa mutató kell, hogy legyen, eredményül a mutató által megcímzett értéket kapjuk. Az indirekció operátor inverze az egyoperandusú & operátor, amely az operandusa címét szolgáltatja ('address of' operator). Ha az 'address of' operátor operandusának típusa T, akkor az eredmény típusa "mutató T-re" lesz. Például a korábbi deklarációkat figyelembe véve a p = &hiba[3]; értékadás után *p értéke 'a' lesz. Az 'address of' operátor nem alkalmazható register tárolási osztályú változókra.
Az egyoperandusú - (minusz) operátor az operandus 2-es komplemensét szolgáltatja, a szokásos aritmetikai konverziók elvégzése után. Elõjel nélküli egészekre alkalmazva ezt az operátort, az eredményre igaz lesz, hogy hozzáadva az eredeti operandust az összeg 2n, ahol n az operandus szélessége bitben. Például -40000u értéke 25536 lesz. A BORLAND C++ megengedi az egyoperandusú + (plusz) operátor használatát: a fordítóprogram egyszerûen figyelmen kívül hagyja azt.
A logikai tagadás operátora a !, eredménye int típusú 1 vagy 0, attól függõen, hogy az operandus 0 volt-e vagy sem. Alkalmazható lebegõpontos számokra és mutatókra is.
A bitenkénti komplementálás operátorának jele ~ (a tilde karakter), ami a - kötelezõen sorszámozott típusú - operandusán elvégzett szokásos aritmetikai konverziók után kapott bitminta 1-es komplemensét adja eredményül, például ~ 0xFFFE értéke 1 lesz (16 bites gépen).
Az elõtagként alkalmazott (prefix) egyoperandusú ++ operátor operandusának az értékét megnöveli 1-gyel, és eredményül ezt a megnövelt értéket szolgáltatja olyan típusban, mint amilyen az operandusé. Ez az operátor tehát nemcsak visszaad egy értéket, de mellékhatása is van az operandusára. Ha x értéke 3, akkor az y = ++x * 2 eredményeként az x felveszi a 4 értéket, y pedig 8 lesz. Analóg módon létezik egyoperandusú prefix dekrementáló operátor is, jele -. Ez operandusa értékét csökkenti 1-gyel, és ezt adja eredményül.
Utótagként (postfix) is alkalmazható az egyoperandusú ++ operátor. Ekkor operandusának az értékét 1-gyel megnöveli, de eredményül a növelés elõtti értékét szolgáltatja olyan típusban, mint amilyen az operandusé. Ennek az operátornak tehát szintén mellékhatása van az operandusára, de az eredményben ez nem jelentkezik. Ha x értéke 3, akkor az y = x++ * 2 eredményeként az x felveszi a 4 értéket, y pedig 6 lesz. (Most már érthetõvé válik a C++ elnevezés szimbólikus jelentése: olyan C, ami egy fokkal jobb.) Analóg módon létezik egyoperandusú postfix dekrementáló operátor is, jele -. Ez operandusa értékét csökkenti 1-gyel, és eredményül a csökkentés elõtti értéket szolgáltatja.
A típuskonverzió (type cast) operátora az adott operandus elõtt zárójelben álló típusnév (absztrakt deklarátor). Az eredmény értéke - a lehetõségek határain belül - megegyezik az operanduséval, típusa a megnevezett új típus. Például (int)3.14 értéke 3, (long)0 megegyezik 0L-val, (unsigned short)-1 értéke 65535, (long *)p pedig egy olyan mutatót eredményez, ami ugyanarra a tárterületre mutat, mint p, de a megcímzett memóriarészt a fordító hosszú egésznek tekinti. Ez utóbbi talán a legjellemzõbb a típuskonverzió operátorának alkalmazására. Igy alkíthatjuk át az általános - ismeretlen típusú változóra mutató - void* "típusú" mutatókat adott típusú mutatókká. Erre a késõbbiekben egy példát is be fogunk muatatni.
A sizeof egyoperandusú operátor az operandusaként szereplõ változó (vagy zárójelben álló típus) byte-okban megadott méretét szolgáltatja. Ennek segítségével lehet gépfüggetlen módon tárterületigényeket meghatározni. Például sizeof(int) eredménye a BORLAND C++-ban 2, és minden implementációnál igaz, hogy sizeof(char) értéke 1.
Itt jegyezzük meg, hogy mind a C, mind a C++ nyelv definicója csak annyit közöl az egyes elemi típusok méreteirõl, hogy azok minden implementációban meg kell hogy feleljenek az alábbi relációknak:
1sizeof(char)
sizeof(short)
sizeof(int)
sizeof(long),
illetve
sizeof(float)sizeof(double)
A konkrét méretek a nyelv adott implementációjától függenek. (A BORLAND C++-ra vonatkozó méret információkat a 1.2. táblázatban foglatuk össze.) Általában nem érdemes a fentieknél többet feltételezni az elemi típusok méreteirõl. Például nem mindig igaz, hogy az egész típus elég nagy ahhoz, hogy pointerek is elférjenek benne (lásd a BORLAND C++ esetében a near és a far, illetve huge pointereket). A korábbi deklarátorokat figyelembe véve sizeof(p) értéke memóriamodelltõl függõen 2 vagy 4, sizeof(hiba) értéke 11, sizeof(vektor[2])-é pedig 4. A fenti példákból is látszik, hogy az implementáció-független, portábilis C, illetve C++ programozási stílus kialakításában a sizeof operátornak kulcsszerepe van.
A kétoperandusú * operátor a szorzás mûvelete, a / operátor pedig az osztásé. A szokásos aritmetikai konverziók megtörténnek. A maradékképzés operátora a %, amelynek operandusai kötelezõen sorszámozott típusúak. Egészek osztásánál, ha bármelyik operandus negatív, a csonkítás iránya gépfüggõ, és hasonlóképpen gépfüggõ az is, hogy a maradék az osztó vagy az osztandó elõjelét örökli-e. Pozitív egészek osztásánál mindig lefelé csonkítás történik. Minden esetben fennáll azonban, hogy (a/b)*b + a%b megegyezik a-val, ha b nem nulla.
A kétoperandusú + és - operátor a szokásos összeadást, illetve kivonást végzi el, a szokásos aritmetikai konverziók után. Speciális esetként lehet mutató operandusuk is, ezt a pointerekkel foglalkozó 1.9.4. fejezetben tárgyaljuk majd részletesen.
A << és >> kétoperandusú operátorok bitenkénti eltolás ( shift) mûveletet végeznek balra, illetve jobbra. Mindkét operandusnak sorszámozott típusúnak kell lenni, és a szokásos aritmetikai konverziók után az eredmény típusa a bal oldali operanduséval egyezik meg. A kilépõ bitek elvesznek, balra léptetésnél az üres helyekre 0-ák lépnek be. Jobbra történõ eltolásnál a belépõ bitek garantáltan 0-ák, ha a bal oldali operandus elõjel nélküli értelmezésû, egyébként értékük gépfüggõ (a BORLAND C++ esetében az elõjeles jobbraléptetésnél a belépõ bitek az eredeti érték elõjelbitjével egyeznek meg). Például 2 << 3 értéke 16, 0xFFFDu >> 2 eredménye 0x3FFFu, míg -3 >> 1 értéke a BORLAND C++ esetén -1.
A relációs és egyenlõség-vizsgáló operátorok a következõk: kisebb <, nagyobb >, kisebb vagy egyenlõ <=, nagyobb vagy egyenlõ >=, egyenlõ == és nem egyenlõ !=. Értelmezésük a szokásos, eredményül int 1-et vagy 0-át adnak, attól függõen, hogy a vizsgált feltétel teljesül-e vagy sem.
A bitenkénti operátorok sorszámozott típusú operandusaikon végeznek a szokásos aritmetikai konverziók után bitmûveleteket. Ezek a bitenkénti ÉS (AND) operátor: a kétoperandusú &, a bitenkénti kizáró VAGY (XOR) operátor, a ^ és a bitenkénti VAGY (OR) operátor, a |.
A logikai ÉS operátor (&&) és a logikai VAGY operátor ( || ) viszont logikai értékû operandusokat vár (0 - hamis, nem 0 - igaz). Eredményül int 0-át vagy 1-et szolgáltatnak a megfelelõ logikai mûvelet elvégzése után. Vegyük példaként azt az esetet, hogy a értéke 2, b értéke 1. Ekkor a & b, a | b, a && b és a || b értéke rendre 0, 3, 1 és 1 lesz, mivel logikailag mindkét operandus igaz értékû.
A feltételes ( ? : ) operátor az egyetlen háromoperandusú operátor a C nyelvben, alakja kif1 ? kif2 : kif3. A kifejezés feloldása a kif1 kifejezés kiértékelésével kezdõdik. Ha az eredmény nem 0, akkor a kif2, egyébként pedig a kif3 kifejezés értékelõdik ki, és ez utóbbi érték adja a mûvelet eredményét és típusát. kif2-nek és kif3-nak a szokásos aritmetikai konverziók után azonos típusúaknak kell lenniük, kivéve, ha az egyik mutató, a másik pedig konstans 0. Ekkor az eredmény típusa a mutató típusa lesz. Garantált, hogy futás közben kif2 és kif3 közül csak az egyik kerül kiértékelésre.
A C nyelvben explicit értékadó utasítás nincs; az értékadás operátorokon keresztül valósul meg, kifejezések kiértékelésének mellékhatásaként. A leggyakrabban használt értékadó operátor az egyszerû értékadás operátora, jele =. Például: y = x. Kiértékelésre kerül a jobboldali kifejezés (szükség szerint konvertálva a baloldal típusának megfelelõen), majd beíródik a baloldalon meghatározott változóba, felülírva annak korábbi tartalmát. Az egyszerû értékadó operátor baloldalán álló kifejezést az angol terminológia alapján balértéknek (lvalue), a jobboldalán álló kifejezést pedig jobbértéknek (rvalue) nevezzük. A kifejezés eredménye és típusa az értékadás során átadott értéknek megfelelõ. Példaként tekintsük a pelda.c-ben szereplõ alábbi feltételt:
(c = getchar()) != EOFMivel a zárójelek közt álló kifejezés elsõdleges kifejezés, ezért ennek kiértékelése történik legelõször. A getchar() függvény hívása által szolgáltatott visszatérési érték beíródik a c nevû változóba, és ugyanez az érték lesz a zárójelezett kifejezés értéke is, ami végül összehasonlításra kerül az EOF szimbólummal, 0-át eredményezve, ha megegyeznek, és 1-et, ha nem. A c változó tehát a feltétel kiértékelése közben mellékhatásként kapott értéket. A további értékadó operátorok a következõk:
+=, -=,
*=, /=, %=, >>=, <<=,
&=, ^ =, |=
Látható, hogy mindegyik egy kétoperandusú
operátorból és az értékadás jelébõl
tevõdik össze. Egy kif1 op= kif2 formájú
kifejezés kiértékelését úgy tekinthetjük,
mintha a helyén kif1 = kif1 op (kif2)
alakú értékadás állna - ahol op
a fenti kétoperandusú operátorok bármelyike
lehet - fontos különbség azonban, hogy kif1 kiértékelésére
csak egyszer kerül sor. Például ha x értéke
1, akkor az x += 2 kifejezés értéke 3, és
mellékhatásként x is 3 lesz.
A vesszõoperátor (comma operator) nevét jelérõl, a , karakterrõl kapta. A vesszõoperátorral elválasztott kifejezéspár sorban egymás után kiértékelõdik, és az eredmény értéke és típusa megegyezik a második (jobb oldali) kifejezésével. Ha a vesszõ egy adott kontextusban másképp is elõfordulhat (függvény paramétereinek elválasztásánál, kezdeti értékek listájánál), akkor a vesszõoperátorral felépített kifejezést zárójelekkel kell védeni, például:
fugg(a, (t = 3, t + 2), c)A fenti függvénynek három paramétert adunk át, amelyek közül a középsõ értéke 5.
Az elõzõekben felsorolt operátorok elõfordulási sorrendje megegyezik a prioritásuk sorrendjével, az egyértelmûség kedvéért azonban álljanak itt az egyes prioritási szintek csökkenõ sorrendben a hozzájuk tartozó asszociativitási iránnyal együtt:
2. Megjegyzés: A logikai operátorok (&&, ||) garantálják a balról jobbra történõ kiértékelési sorrendet. Ezenkívül azt is biztosítják, hogy a második kifejezés kiértékelését csak akkor végzik el, ha az elsõ operandusuk alapján az eredmény nem egyértelmû. Például a && b esetében b kiértékelésére nem kerül sor, ha az a 0, hiszen ekkor az eredmény már biztosan hamis logikai érték lesz. Ennek jelentõségére igyekszik rávilágítani a következõ feltétel: b != 0 && a/b < 5. Ha nem lenne garantált a fenti kiértékelési stratégia, akkor ez a feltétel hibás lenne, mert b 0 értéke esetén bekövetkezne az elkerülni kívánt 0-val történõ osztás. Ügyeljünk arra, hogy a logikai kifejezésekben fellépõ mellékhatásokat ne használjuk ki, ugyanis egy logikai kifejezés esetleg lerövidült kiértékelése következtében a várt mellékhatások egy része elmaradhat.
3. Megjegyzés: a bitenkénti operátorok precedenciája alacsonyabb, mint az egyenlõség-vizsgáló operátoroké. Gyakori hiba a következõ alakú vizsgálat: c & 0xF == 8. Ez garantáltan mindig 0-át ad eredményül. Helyes megoldás: (c & 0xF) == 8.
4. Megjegyzés: a feltételes operátor kif1 feltételrészében, és minden egyéb helyen, ahol feltételt kell megadnunk, tetszõleges kifejezés szerepelhet, és annak 0, illetve nem 0 értéke jelenti a feltétel hamis, illetve igaz voltát. Különösen veszélyes ez akkor, ha tévedésbõl összekeverjük az egyszerû értékadó operátor = jelét az egyenlõség-vizsgáló operátor == jelével. Az a = b alakú feltétel ugyanis szintaktikailag teljesen helyes, de nem a két mennyiség egyenlõségét vizsgálja, hanem b 0 vagy nem 0 volta szerint szolgáltatja az eredõ feltételt, egyben b értékével a-t is felülírva. Szerencsére a BORLAND C++ az ilyen feltételeknél - ha engedélyezzük - figyelmeztetést ad (a Possibly incorrect assignment üzenettel). Ha viszont ténylegesen a fenti esetre van szükségünk, akkor a figyelmeztetés elkerülése - és az érthetõbb felírási forma - kedvéért írjuk azt az (a = b) != 0 alakba.