KURZUS: Programozás alapjai

MODUL: II. modul

5. lecke: Műveletek és kifejezések

Ebben a leckében elsősorban a korábban már megismert operátorok és a segítségükkel megalkotott kifejezésekre vonatkozó szabályokat pontosítjuk. További operátorokat ismerünk meg (pl. feltételes és bit szintűek), és megvizsgálunk olyan speciális helyzeteket is, amikor az operátorok valamilyen tulajdonságát kihasználva egyszerűsíthetünk a kódunkon. Külön felhívjuk a figyelmet a leggyakoribb programozói hibákra, veszélyekre is.

A lecke végére a hallgatónak tisztában kell lennie:

  • Az egy, két és három operandusú operátorok szintaktikai szabályaival.
  • Az aritmetikai és multiplikatív operátorok használatának módjával.
  • Karakterláncok és szám típusok közötti konverziók módjával.
  • A szabványos függvénykönyvtár legfontosabb matematikai függvényeivel, azok használatával.
  • A relációs, logikai és bit szintű operátorokkal. Tulajdonságaik ismeretében el tudja dönteni, hogy egyes kifejezések mely operandusai kerülnek kiértékelésre, és milyen sorrendben.
  • Az implicit és a hozzárendelési konverzió, valamint az egész-előléptetés szabályaival. Tudnia kell explicit típusmódosítást végezni.
  • Objektumok méretének megállapítására vonatkozó szabályokkal.
  • Tisztában kell lennie a növelő és csökkentő operátorok pontos működésével, ezek és a hozzárendelés operátor által megvalósított mellékhatásokkal.
  • Képes a szelekciós szerkezetet feltételes kifejezéssel helyettesíteni.
  • Ismeri az összetett hozzárendelési operátorokat, és a vessző operátort.

A lecke kulcsfogalmai: operátor, operandus, kifejezés, egész előléptetés, implicit, szabványos, explicit és hozzárendelési típuskonverzió, hordozható program, precedencia, rendűség, prioritás.

A műveletek és kifejezések használatának általános szabályai

A műveleteket a nyelv operátorokkal (műveleti jelekkel) valósítja meg. A műveletek lehetnek:

  • Egyoperandusosak. Alakjuk "operátor operandus", ahol az operandus az a kifejezés, melyen az egyoperandusos műveletet el kell végezni. Például: -6, vagy a sizeof(int) stb.
  • Kétoperandusosak, azaz "operandus1 operátor operandus2" formájúak. Például: a + b.
  • Háromoperandusosak: A C-ben egyetlen ilyen művelet van, az ún. feltételes kifejezés. Például: (a > b) ? a : b értéke a, ha a > b és b máskülönben.

operátor: (a következők egyike!)
  [ ] ( ) . -> ++ -- & * + - ~ ! sizeof / % << >> < > <= >= == !=
  = ^ | && || ?: *= /= += -= %= <<= >>= &= ^= |= ,

A kifejezés operátorok, operandusok (és elválasztó-jelek) sorozata, mely az alábbi tevékenységek valamilyen kombinációját valósítja meg:

  • Értéket számít ki.
  • Objektumot vagy függvényt ér el.
  • Mellékhatást generál.

A kifejezésbeli operandusokat elsődleges kifejezésnek nevezik.

elsődleges-kifejezés:
  azonosító
  konstans
  karakterlánc
  (kifejezés)

kifejezés:
  hozzárendelés-kifejezés
  kifejezés, hozzárendelés-kifejezés

A konstansokat, a karakterláncot tárgyaltuk az előző leckében, a hozzárendelés-kifejezést definiálni fogjuk a hozzárendelés operátoroknál. Az azonosító lehet bármilyen egész vagy lebegőpontos típusú. Lehet enum, tömb, mutató, struktúra, unió, vagy függvény típusú. Lehet tehát:

  • változó azonosító beleértve az indexelő operátort is, azaz az azonosító[kifejezés]-t is, és az unió, ill. a struktúratagokat, vagy
  • függvényhívás, azaz azonosító a függvényhívás operátorral ( azonosító() ), melynek típusa mindig a függvény által visszaadott érték típusa lesz.

Összesítve: az azonosítónak balértéknek vagy függvényhívásnak kell lennie.

A kifejezés kiértékelése bizonyos

  • konverziós,
  • csoportosító,
  • asszociatív és
  • prioritási (precedencia)

szabályokat követ, mely függ

  • a használt operátoroktól,
  • a ( ) párok jelenlététől és
  • az operandusok adattípusától.

A kifejezések különfélék lehetnek:

  • elsődleges kifejezés (primary),
  • utótag kifejezés (postfix),
  • egyoperandusos kifejezés (unary),
  • előtag kifejezés (cast),
  • hozzárendelés kifejezés stb.

utótag-kifejezés:
  elsődleges-kifejezés
  utótag-kifejezés[kifejezés]
  utótag-kifejezés(<kifejezéslista>)
  utótag-kifejezés.azonosító
  utótag-kifejezés->azonosító
  utótag-kifejezés++
  utótag-kifejezés--

kifejezéslista:
  hozzárendelés-kifejezés
  kifejezéslista, hozzárendelés-kifejezés

egyoperandusos-kifejezés:
  utótag-kifejezés
  ++ egyoperandusos-kifejezés
  -- egyoperandusos-kifejezés
  egyoperandusos-operátor előtag-kifejezés
  sizeof(egyoperandusos-kifejezés)
  sizeof(típusnév)

egyoperandusos-operátor: ( a következők egyike!)
  & * + - ~ !

előtag-kifejezés:
  egyoperandusos-kifejezés
  (típusnév) előtag-kifejezés

típusnév:
  típusspecifikátor-lista<absztrakt-deklarátor>

absztrakt-deklarátor:
  mutató
  <mutató><direkt-absztrakt-deklarátor>

direkt-absztrakt-deklarátor:
  (absztrakt-deklarátor)
  <direkt-absztrakt-deklarátor>[<konstans-kifejezés>]
  <direkt-absztrakt-deklarátor>(<paraméter-típus-lista>)

A típusnév az adattípus típusneve. Szintaktikailag az adott típusú objektum olyan deklarációja, melyből hiányzik az objektum neve.

A hozzárendelés-kifejezést, melyről most csak annyit jegyzünk meg, hogy nem balérték, majd a hozzárendelési műveleteknél ismertetjük!

Aritmetikai műveletek (+, -, *, / és %)

Közülük a legmagasabb prioritási szinten az egyoperandusos, jobbról balra kötő előjel operátorok vannak. Létezik a

- előtag-kifejezés

és a szimmetria kedvéért a

+ előtag-kifejezés.

Az előjel operátort követő előtag kifejezésnek aritmetikai típusúnak kell lennie, s az eredmény az operandus értéke (+), ill. annak -1-szerese (-). A + művelet egész operandusát egész-előléptetésnek (integral promotion) veti alá a fordító, s így az eredmény típusa az egész-előléptetés végrehajtása után képzett típus. A - műveletet megelőzheti implicit típuskonverzió, és egész operandus esetén az eredmény az operandus értékének kettes komplemense.

Az egész-előléptetéssel az implicit típuskonverzió kapcsán rögtön foglalkozunk!

A többi aritmetikai operátor mind kétoperandusos, melyek közül a szorzás (*), az osztás (/) és a modulus (%) magasabb prioritási szinten van, mint az összeadás (+) és a kivonás (-). A szorzást, az osztást és a modulust multiplikatív operátoroknak, az összeadást és a kivonást additív operátoroknak is szokás nevezni.

Multiplikatív operátorok (*, / és %)

multiplikatív-kifejezés:
  előtag-kifejezés
  multiplikatív-kifejezés * előtag-kifejezés
  multiplikatív-kifejezés / előtag-kifejezés
  multiplikatív-kifejezés % előtag-kifejezés

Nézzük a műveletek pontos szabályait!

  • A multiplikatív operátorok mind balról jobbra csoportosítanak.
  • Mindhárom operátor operandusainak aritmetikai típusúaknak kell lenniük. A % operátor operandusai ráadásul csak egész típusúak lehetnek.
  • Ha az operandusok különböző aritmetikai típusúak, akkor a művelet elvégzése előtt implicit konverziót hajt végre a fordító. Az eredmény típusa ilyenkor a konvertált típus. Miután a konverziónak nincsenek túl- vagy alulcsordulási feltételei, értékvesztés következhet be, ha az eredmény nem fér el a konverzió utáni típusban.
  • A / és a % második operandusa nem lehet zérusértékű, mert ez fordítási vagy futásidejű hibához vezet.
  • Ha a / és a % mindkét operandusa egész, de a hányados nem lenne az, akkor:
    • Ha a két operandus - mondjuk op1 és op2 - értéke azonos előjelű vagy unsigned, akkor az op1/op2 hányados az a legnagyobb egész, ami kisebb, mint az igazi hányados és az op1%op2 osztási maradék op1 előjelét örökli meg:

      3 / 2 -> 1
      3 % 2 -> 1
      (-3) / (-2) -> 1
      (-3) % (-2) -> -1
    • Ha op1 és op2 ellenkező előjelű, akkor az op1/op2 hányados az a legkisebb egész, ami nagyobb az igazi hányadosnál. Az op1%op2 osztási maradék most is op1 előjelét örökli meg:

      (-3) / 2 -> -1
      (-3) % 2 -> -1
      3 / (-2) -> -1
      3 % (-2) -> 1

Készítsünk programot, ami beolvas egy négyjegyű évszámot, és eldönti róla, hogy szökőév-e, vagy sem!

A Gergely-naptár szerint szökőév minden, néggyel maradék nélkül osztható év. Nem szökőév a kerek évszázad, de a 400-zal maradék nélkül oszthatók mégis azok.

1.Olvassunk be a szabvány bemenetről egy maximálisan négy karakteres sort!
2.Ha a bejött karakterlánc hossza nem pontosan négy, akkor kérjük be újra!
3.Ellenőrizzük le, hogy a karakterlánc minden pozíciója numerikus-e! Ha nem, újra bekérendő.

Írjunk intnume(char s[]) függvényt, mely 1-et (igazat) ad vissza, ha a paraméter karakterlánc tiszta numerikus, és zérust (hamisat), ha nem!

int nume(char s[]){
  int i;
  for(i=0; s[i]; ++i) if(s[i]<'0'||s[i]>'9') return 0;
  return 1; }

1.Át kéne konvertálni a numerikus karakterláncot fixpontos belsőábrázolású egésszé (int n-né)! A módszer a következő:

n=(s[0]-'0')*1000+(s[1]-'0')*100+(s[2]-'0')*10+(s[3]-'0');
2.Ezt ugye ciklusban, ahol i és n zérustól indul, és i egyesével haladva végigjárja a numerikus karakterláncot, így kéne csinálni:

n=n*10+(s[i]-'0');

Írjunk int atoi(char s[]) függvényt, mely megvalósítja ezt a konverziót, s n lesz a visszaadott értéke! Az átalakítást végezze az első nem konvertálható karakterig! Engedjük meg, hogy a numerikus karakterlánc elején fehér karakterek és előjel is lehessen! Ha az előjelet elhagyják, akkor legyen a szám pozitív!

int atoi(char s[]){
  int i=0, n=0;
  int elojel=1; /* Alapertelmezes: pozitiv. */
  /* A karakterlanc eleji feher karakterek atlepese: */
  while(s[i]==' '||s[i]=='\n'||s[i]=='\t') ++i;
  /* Elojel: */
  if(s[i]=='+'||s[i]=='-') if(s[i++]=='-') elojel=-1;
  /* Konverzio: */
  for(;s[i]>='0'&&s[i]<='9';++i) n=10*n+s[i]-'0';
  return(elojel*n); }

Megemlítendő, hogy pontosan ilyen prototípusú, nevű és funkciójú függvény létezik a szabvány könyvtárban is, de bekapcsolandó hozzá a szabványos stdlib.h fejfájl. A könyvtárban van atol rutin, mely long-gá, és van atof, mely double-lé alakítja numerikus karakterlánc paraméterét.

Jöhet a program, de helyszűke miatt a függvények definícióit nem ismételjük meg!

/* PELDA14.C: A negyjegyu evszam szokoev-e? */
#include <stdio.h>
#define MAX 80
int getline(char s[],int n);
int nume(char s[]);
int atoi(char s[]);
void main(void){
  int ev = 0; /* Elfogadhatatlan ertekrol indul. */
  char s[MAX+1];/* Az input puffer. */
  printf("A negyjegyu evszam szokoev-e?\n");
  while(ev<1000 || ev>9999){
    printf("Adjon meg egy evszamot!\n");
    if(getline(s,4)==4&&nume(s)) ev=atoi(s);
    else printf("Formailag hibás bemenet!\n"); }
  if(ev%4==0&&ev%100!=0||ev%400==0)
    printf("%4d szökőév.\n", ev);
  else printf("%4d nem szökőév.\n", ev); }
Additív operátorok (+ és -)

additív-kifejezés:
  multiplikatív-kifejezés
  additív-kifejezés + multiplikatív-kifejezés
  additív-kifejezés - multiplikatív-kifejezés

Az additív operátorok csoportosítása is balról jobbra történik. Operandusaik az aritmetikai értékeken túl mutatók is lehetnek. (A mutatóaritmetikát majd a mutatók kapcsán ismertetjük!)

Aritmetikai operandusok esetén az eredmény a két operandus értékének összege (+), ill. különbsége (-). Egész vagy lebegőpontos operanduson a művelet implicit típuskonverziót is végezhet, ha szükséges. Ilyenkor az eredmény típusa a konvertált típus. Miután a konverziónak nincsenek túl vagy alulcsordulási feltételei, értékvesztés következhet be, ha az eredmény nem fér el a konverzió utáni típusban.

Matematikai függvények

A matematikai függvények nem részei a C nyelvnek. Nyilvánvaló viszont, hogy kifejezések képzésekor szükség lehet rájuk. A C filozófiája szerint a matematikai függvények családját is a szabvány könyvtárban kell elhelyezni, mint ahogyan a szabvány bemenet és kimenet kezelését végző rutinokat.

Az ANSI szabvány pontosan rögzíti ezeket a könyvtári funkciókat, így bármilyen szabványos C fordító és operációs rendszer számára kompatibilis formában létezniük kell. Magyarán: azok a programok, melyek az operációs rendszerrel való kapcsolatukat a szabvány könyvtáron át valósítják meg, minden változtatás nélkül átvihetők az egyik számítógépről a másikra, az egyik operációs rendszerből a másikba. Ezek az úgy nevezett portábilis programok.

A szabvány könyvtár függvényeit, típusait és makróit szabványos fejfájlokban deklarálták. Ezek közül néhánnyal már találkoztunk, másokkal meg még nem:

assert.hctype.herrno.hfloat.hiso646.h
limits.hlocale.hmath.hsetjmp.hsignal.h
stdarg.hstddef.hstdio.hstdlib.hstring.h
time.hwchar.hwctype.h

A matematikai függvények prototípusai a math.h fejfájlban helyezkednek el, így használatuk előtt ez a fejfájl bekapcsolandó!

#include <math.h>

Nem kívánjuk felsorolni és részletezni az összes fejfájlt, az összes függvényt, csak néhány fontosabbat említünk meg közülük. Az olvasótól azonban elvárjuk, hogy a programfejlesztő rendszere segítségéből a további fejfájlokról és rutinokról is tájékozódjék.

A matematikai függvények double értéket szolgáltatnak, s néhány kivételtől eltekintve, paramétereik is double típusúak. A matematikából ismeretes korlátozások természetesen érvényben maradnak rájuk. A trigonometrikus függvények paramétere, ill. inverzeik visszaadott értéke radiánban értendő.

Néhányat felsorolunk a teljesség igénye nélkül!

sin(x)x szinusza.
cos(x)x koszinusza.
tan(x)x tangense.
asin(x)-1<=x<=1 árkusz szinusza. Az értékkészlet: [ π /2, π /2].
acos(x)-1<=x<=1 árkusz koszinusza. Az értékkészlet: [0, π ].
atan(x)x árkusz tangense. Az értékkészlet: [ π /2, π /2].
exp(x)Az ex exponenciális függvény.
log(x)x>0 természetes alapú logaritmusa.
log10(x)x>0 tízes alapú logaritmusa.
pow(x, y)Az xy hatványfüggvény. Hiba, ha x=0 és y<=0, ill. ha x<0 és y értéke nem egész.
sqrt(x)x>=0 négyzetgyöke.
floor(x)Az x-nél nem nagyobb, legnagyobb egész szám.
fabs(x)Az x abszolút értéke.
fmod(x, y)y!=0 estén x/y osztás lebegőpontos maradéka, mely x-szel egyező előjelű.

A szabvány könyvtári függvények, így a matematikaiak is, a hibát úgy jelzik, hogy valamilyen speciális értéket (HUGE_VAL, zérus stb.) adnak vissza, és beállítják a UNIX-tól örökölt, globális

extern int errno;

(hibaszám) változót a hiba kódjára. A hibakódok az errno.h fejfájlban definiált, egész, nem zérusértékű szimbolikus állandók. A HUGE_VAL a legnagyobb, pozitív, még ábrázolható double érték.

A matematikai rutinok az értelmezési tartomány hibát EDOM értékű errno-val, és a fordítótól is függő függvény visszatérési értékkel jelzik. Értékkészlet probléma esetén az errno ERANGE. A függvény visszatérési érték túlcsorduláskor előjel helyes HUGE_VAL, ill. alulcsorduláskor zérus.

Az értelmezési tartomány hiba akkor fordul elő, ha a függvény aktuális paraméterének értéke nincs benn az értelmezési tartományban. Értékkészlet hiba egyértelműen az, ha az eredmény nem ábrázolható double értékként.

Például az sqrt(-1.) hatására az errno EDOM, és a visszakapott érték negatív HUGE_VAL.

Reláció operátorok ( >, >=, <, <=, == és !=)

A reláció operátorok prioritása - eltekintve az egyoperandusos műveletektől - az aritmetikai és a logikai operátorok között helyezkedik el. A reláció operátorok két prioritási szintet képeznek, ahol az "igazi" relációk (>, >=, < és <=) prioritása magasabb az egyenlőségi relációkénál (== és !=). Az összes reláció az első operandus értékét hasonlítja a másodikéhoz, és a reláció érvényességét vizsgálja. Az eredmény logikai érték (int típusú), mely 1, ha a reláció igaz és 0, ha nem. A definíciók:

relációs-kifejezés:
  eltolás-kifejezés
  relációs-kifejezés < eltolás-kifejezés
  relációs-kifejezés > eltolás-kifejezés
  relációs-kifejezés <= eltolás-kifejezés
  relációs-kifejezés >= eltolás-kifejezés

egyenlőségi-kifejezés:
  relációs-kifejezés
  egyenlőségi-kifejezés == relációs-kifejezés
  egyenlőségi-kifejezés != relációs-kifejezés

Az eltolás-kifejezést a bitenkénti eltolás operátoroknál definiáljuk!

A relációk operandusai egész, lebegőpontos, vagy mutató típusúak. Az operandusok típusa különbözhet. Az operátorok implicit típuskonverziót is végrehajthatnak aritmetikai operandusaikon a művelet elvégzése előtt.

Ne feledjük, hogy a

kifejezés != 0

reláció mindig rövidíthető

kifejezés

módon, mert a nyelvben a nem zérus érték logikai igaznak minősül. Példaként tekintsük meg újra a korábbi szakaszokban ismertetett atoi és getline függvényeket!

Logikai műveletek (!, && és ||)

Közülük a legmagasabb prioritási szinten az egyoperandusos, jobbról balra kötő, logikai nem operátor van, melynek alakja:

! előtag-kifejezés

ahol az előtag-kifejezés operandusnak egész, lebegőpontos, vagy mutató típusúnak kell lennie. Az eredmény mindenképpen int típusú, s az operandus logikai negációja. Az eredmény 0, ha az operandus értéke nem zérus, ill. 1, ha az operandus értéke zérus. Ez utóbbi mondatrész biztosítja, hogy a

kifejezés == 0

mindenkor rövidíthető

! kifejezés

módon. Például a multiplikatív operátoroknál ismertetett program részlet

if( ev%4 == 0 && ev%100 != 0 || ev%400 == 0)

utasítása így rövidíthető:

if( !(ev%4) && ev%100 || !(ev%400))

Két kétoperandusos logikai művelet van a nyelvben a logikai és (&&) és a logikai vagy (||), melyek prioritása alacsonyabb a relációkénál és a bit szintű műveleteknél. A logikai és prioritása ráadásul magasabb, mint a logikai vagyé. Mindkét művelet balról jobbra csoportosít. Egyik operátor sem hajt végre implicit típuskonverziót operandusain, ehelyett zérushoz viszonyítva értékeli ki őket. Az eredmény int típusú (1 - igaz és 0 - hamis).

logikai-és-kifejezés:
  vagy-kifejezés
  logikai-és-kifejezés && vagy-kifejezés

logikai-vagy-kifejezés:
  logikai-és-kifejezés
  logikai-vagy-kifejezés || logikai-és-kifejezés

A vagy-kifejezés definícióját a bit szintű műveleteknél találjuk meg!

A K1&&K2 kifejezés eredménye igaz (1), ha K1 és K2 egyike sem zérus. A K1||K2 kifejezés igaz (1), ha K1 és K2 valamelyike is nem zérus. Máskülönben K1&&K2 és K1||K2 eredménye hamis (0).

Mindkét operátor esetében garantált a balról jobbra történő végrehajtás. Először K1-et értékeli ki a fordító az esetleges összes mellékhatásával együtt, de:

  • K1&&K2 esetén, ha K1 zérus, az eredmény hamis (0), és K2 kiértékelése nem történik meg.
  • K1||K2 kifejezésnél, ha K1 nem zérus, az eredmény igaz (1) lesz, és K2 kiértékelése itt sem zajlik le.

Ha valami előbbre való, vagy mindenképp szeretnénk, hogy megtörténjen, akkor azt a bal oldali operandusba kell beépíteni. Például a pelda10.c-ben megírt getline for ciklusának feltétele nem véletlenül

i<n && (c=getchar())!=EOF && c!='\n'

sorrendű, hisz először azt kell biztosítani, hogy a paraméter karaktertömböt ne írhassa túl a függvény. Ez nem kerülhet hátrébb a kifejezésben. Aztán a következő karaktert előbb be kell olvasni a bemenetről, de mindennek vége van fájlvég esetén. Itt sincs értelme a felcserélésnek, mert felesleges vizsgálgatni a fájlvéget, hogy soremelés-e. A relációk közti és műveletek miatt látszik, hogy balról jobbra történik az operandusok kiértékelése, és ha eközben az egyik hamis lesz, teljesen felesleges lenne továbbfolytatni a kiértékelést.

Implicit típuskonverzió és egész-előléptetés

Ha kétoperandusos (például aritmetikai) műveleteknél különbözik a két operandus típusa, akkor a művelet elvégzése előtt a fordító belső konverziót (átalakítást) hajt végre. Általában a pontosabb operandus típusára konvertálja a másikat. A kétoperandusos művelet eredményének típusa a konvertált típus lesz. Ezt a fajta konverziót szabványosnak, szokásosnak is nevezik. A szabályok nem prioritási sorrendben a következők:

1.Ha az egyik operandus típusa long double, akkor a másikat is long double típusúvá konvertálja a fordító.
2.Ha az előző pont nem teljesedik, s az egyik operandus double, akkor a másik is az lesz.
3.Ha az előző két feltétel egyike sem valósul meg, és az egyik operandus float, akkor a másikat is azzá konvertálja a fordító.
4.Ha az előző három feltétel egyike sem teljesül (egyik operandus sem lebegőpontos!), akkor egész-előléptetést hajt végre a fordító az operandusok értékén, ha szükséges, és aztán:
 a)Ha az egyik operandus unsigned long, akkor a másik is azzá alakul.
 b)Ha az előző pont nem teljesül, és az egyik operandus long, a másik pedig unsigned int, akkor mindkét operandus értékét long, vagy unsigned long típusúvá konvertálja a fordító. Ha az unsigned int teljes értéktartománya ábrázolható long-ként, akkor a választás long, máskülönben pedig unsigned long.
 c)Ha az előző pontok nem teljesülnek, és az egyik operandus long, akkor a másik is az lesz.
 d)Ha nem teljesülnek az előző pontok, és az egyik operandus unsigned int, akkor a másik is azzá alakul.
 e)Ha az előző pontok nem teljesülnek, akkor minkét operandus int az érvénybe lépett egész-előléptetés (integral promotion) miatt. A signed vagy unsigned char, short int, vagy bitmező objektumok, ill. az enum típusúak használhatók kifejezésben ott, ahol bennük egész állhat. Ha az eredeti típus minden lehetséges értékét int képes reprezentálni, akkor az értéket int típusúvá konvertálja a fordító, máskülönben unsigned int-té. Az egész-előléptetési folyamat garantálja, hogy a konverzió előtti és utáni érték ugyanaz marad. A konvertált érték unsigned eredeti típusból 0X00 feltöltéssel, signed eredeti típusból viszont előjel kiterjesztéssel készül a felső bájtokba.
TípusKonvertálvaMódszer
charintAz alapértelmezett char típustól függően előjel kiterjesztés van (signed) vagy 0X00 kerül a magasabb helyiértékű bájt(ok)ba (unsigned).
 
unsigned charintA felső bájt(ok) 0X00 feltöltésűek.
signed charintElőjel kiterjesztés van a felső bájt(ok)-ba.
short intintUgyanaz az érték előjel kiterjesztéssel.
unsigned shortunsigned intUgyanaz az érték 0X00 feltöltéssel.
enumintUgyanaz az érték.

Ne feledjük azonban el, hogy a konverzió mindig függvényhívást jelent (gépidő!), azaz szükségtelenül ne alkalmazzuk! Csak "értelmes" típuskonverziókat valósít meg a fordító. Például az f + i összeadás végrehajtása előtt - feltéve, hogy f float és i int típusú - i értéke (és nem i maga!) float-tá alakul.

Az "értelmetlen" lebegőpontos kifejezés indexben még csak megvalósul úgy, hogy a kifejezés értéke tört részét levágja a fordító

#include <stdio.h>
void main(void){
int t[] = {2,3,4,5,6,7};
float f=1.75;
printf("%d\n",t[f]); }

azaz 3 lesz az eredmény.

Numerikus karakterlánc azonban sohasem alakul automatikusan aritmetikai értékké. Ehhez valamilyen konverziós függvényt kell használni. Az stdlib.h-beli atoi-ról, atol-ról és atof-ról volt már szó!

Típusmódosító szerkezet

Az implicit (szokásos, szabványos stb.) konverziókon túl magunk is kikényszeríthetünk (explicit) típuskonverziót a

(típusnév) előtag-kifejezés

alakú, a Alapismeretek 1/2 leckében megismert típusmódosító szerkezet alkalmazásával. Látjuk, hogy a típusmódosító szerkezet egyoperandusos, s ez által magas prioritású művelet. A definícióban a típusnév a céltípus, és az előtag-kifejezés értékét erre a típusra kell konvertálni. Az előtag-kifejezést úgy konvertálja a fordító, mintha az értéket egy típusnév típusú változó venné fel. Az explicit típuskonverzió tehát a hozzárendelési konverzió szabályait követi. A legális típusmódosítások:

CéltípusPotenciális források
egészbármilyen egész vagy lebegőpontos típus, vagy mutató
lebegőpontosbármilyen aritmetikai típus
voidbármilyen típus

Példaként vegyük a matematikai függvények közül a négyzetgyököt, azaz:

#include <math.h>
double sqrt(double x);

Programunkban van egy int típusú n változó, akkor az n+26 pozitív gyökét az

sqrt(double(n+26))

függvényhívással kaphatjuk meg.

Bármilyen azonosító, vagy kifejezés típusa módosítható void-dá. A típusmódosításnak alávetett azonosító, vagy kifejezés nem lehet azonban void. A void függvény hívását például hiába típusmódosítjuk int-re, a semmiből nem lehet egészet csinálni.

A void-dá módosított kifejezés értéke nem képezheti hozzárendelés tárgyát. Hasonlóan az explicit típusmódosítás eredménye nem fogadható el balértékként hozzárendelésben.

Kifejezést csak olyan helyen módosíthatunk void-dá, ahol az értékére nincs szükség. Például nincs szükség a bejövő gombnyomásra:

printf("A folytatáshoz üssön Entert-t! "); (void)getchar();
sizeof operátor

Az Alapismeretek 2/2 leckéből ismert sizeof egyoperandusos, jobbról balra kötő, magas prioritású művelet, mely mindig az operandusa tárolásához szükséges memória mennyiségét szolgáltatja bájtban. Az eredmény size_t típusú egész érték. Az stddef.h fejfájlban megtekintve a típust többnyire azt látjuk, hogy unsigned int.

Két különböző alakja van az operátornak:

sizeof(egyoperandusos-kifejezés)
sizeof(típusnév)

sizeof(egyoperandusos-kifejezés) esetén az egyoperandusos kifejezés típusát a fordító a kifejezés kiértékelése nélkül határozza meg, azaz ha az operandus tömbazonosító, az egész tömb bájtokban mért helyfoglalásához jutunk. Például a tomb tömb elemszáma a következő konstrukcióval állapítható meg:

sizeof(tomb) / sizeof(tomb[0])

A sizeof nem használható függvényre, vagy nem teljes típusú kifejezésre, ilyen típusok zárójelbe tett nevére, vagy olyan balértékre, mely bitmező objektumot jelöl ki. Bátran alkalmazható előfeldolgozó direktívákban is!

#define MERET sizeof(int)*3
Inkrementálás (++), dekrementálás (--) és mellékhatás

Ezek az operátorok mind egyoperandusosak, s ezért magas prioritásúak. Mindkét operátor létezik utótag

utótag-kifejezés++
utótag-kifejezés--

és előtag műveletként:

++ egyoperandusos-kifejezés
-- egyoperandusos-kifejezés

Az inkrementáló és dekrementáló kifejezésben az utótag- vagy az egyoperandusos-kifejezésnek skalár (aritmetikai vagy mutató) típusúnak és módosítható balértéknek kell lenniük, de az eredmény nem balérték.

Az inkrementálásnál (++) a balérték eggyel nagyobb, dekrementálásnál (--) viszont eggyel kisebb lesz. Előtag operátornál a "konstrukció" értéke egyezik az új balértékkel, míg utótag operátornál a "konstrukció" értéke az inkrementálás vagy dekrementálás végrehajtása előtti érték. Az eredmény típusát az operandus típusa határozza meg. Például:

int x, i = 3, j = 4, n = 5;
x = n++; /* x == 5 és n == 6 */
x = ++n; /* x == 7 és n == 7 */
x = --(n - j + i + 6); /* x == 11 */

A kifejezés produkálhat

  • balértéket,
  • jobbértéket vagy
  • nem szolgáltat értéket.

A kifejezésnek ezen kívül lehet mellékhatása is. Például a Típusok és konstansok leckében megírt strcopy záró sorában

while(cél[i++]=forrás[i])

a hozzárendelés mellékhatásaként az i végrehajtás utáni értéke is eggyel nagyobb lesz. A mellékhatást a kifejezés kiértékelése okozza, s akkor következik be, ha megváltozik egy változó értéke. Minden hozzárendelés operátornak van mellékhatása. Láttuk, hogy a balértékre alkalmazott inkrementálási és dekrementálási műveletnek is van. Függvényhívásnak is lehet azonban mellékhatása, ha globális hatáskörű objektum értékét változtatja meg.

Írjuk meg a voidchdel(char s[], int c)-t, mely saját helyen törli a benne előforduló c karaktereket az s karakterláncból!

Itt is másolni kell a forrásból a célba bájtról-bájtra haladva, de a c értékű karaktereket ki kell ebből hagyni. Két indexre van szükség. Az egyik az i, mely végigjárja a forrást. A másik a j, mely a célban mindig a következő szabad helyet éri el. A nem c értékű karaktert a következő szabad helyre kell másolni, s a célbeli indexnek az ezután következő szabad helyre kell mutatnia.

void chdel(char s[], int c){
  int i, j;
  for(i=j=0; s[i]; ++i) if(s[i] != c) s[j++] = s[i];
  s[j] = 0; }
Bit szintű operátorok ( ~, <<, >>, &, ^ és |)

A bit szintű operátorok csak signed és unsigned egész típusú adatokra: char, short, int és long használhatók. Legmagasabb prioritási szinten az egyoperandusos, jobbról balra kötő egyes komplemens operátor (~) van, melynek definíciója:

~ előtag-kifejezés

Az operátor előbb végrehajtja az egész-előléptetést, ha szükséges. Az eredmény típusa az operandus konverzió utáni típusa. Az eredmény maga a bit szintű egyes komplemens, azaz ahol az operandus bit 1 volt, ott az eredmény bit 0 lesz, és ahol az operandus bit 0 volt, ott az eredmény bit 1 lesz. Feltéve, hogy az egész-előléptetés 16 bites, és hogy:

unsigned short x = 0XF872,    /* 1111100001110010 */
              maszk = 0XF0F0;/* 1111000011110000 */

akkor a ~x 00000111100011012, és a ~maszk 00001111000011112.

A balról jobbra csoportosító eltolás operátorok (<< és >>) prioritása alacsonyabb az aritmetikai műveletekénél, de magasabb, mint a reláció operátoroké. Az eltolás operátorok első operandusuk értékét balra (<<) vagy jobbra (>>) tolják annyi bitpozícióval, mint amennyit a második operandus meghatároz. A definíció a következő:

eltolás-kifejezés:
  additív-kifejezés
  eltolás-kifejezés << additív-kifejezés
  eltolás-kifejezés >> additív-kifejezés

A K1<<K2 és a K1>>K2 kifejezések esetében minkét operandus egész típusú kell, legyen. Az operátorok egész-előléptetést is megvalósíthatnak. Az eredmény típusát K1 konvertált típusa határozza meg. Ha K2 negatív vagy értéke nem kisebb K1 bitszélességénél, akkor az eltolási művelet eredménye határozatlan (azaz fordítóprogramonként eltérő lehet).

Miután a C-ben nincs egész alul vagy túlcsordulás, a műveletek értékvesztést is okozhatnak, ha az eltolt eredmény nem fér el az első operandus konvertált típusában.

A K1<<K2 balra tolja K1 értékét K2 bitpozícióval úgy, hogy jobbról 0 bitek jönnek be. K1 túlcsordulás nélküli balra tolása ekvivalens K1*2K2-vel. Ilyen értelemben aztán az eltolás aritmetikai műveletnek is tekinthető. Ez a gondolatsor igaz persze az összes többi bit szintű műveletre is! Ha K1 valamilyen signed típus, akkor az eltolás eredménye csak "gonddal" szemlélhető az előjel bit esetleges kitolása miatt.

A K1>>K2 művelet K1 értékét K2 bitpozícióval tolja jobbra. Ha K1 valamilyen unsigned típusú, akkor balról 0 bitek jönnek be. Ha K1 signed, akkor az operátor az előjel bitet sokszorozza. unsigned, nem negatív K1 esetén a jobbra tolás K1/2K2 hányados egész részeként is interpretálható.

Folytatva az egyes komplemensképzésnél megkezdett példát, az x<<2 11100001110010002, ill. a maszk>>5 00000111100001112.

A bit szintű logikai operátorok prioritásuk csökkenő sorrendjében az és (&), a kizáró vagy (^), valamint a vagy (|). A többi műveletre való tekintettel prioritásuk magasabb a kétoperandusos logikai operátorokénál, de alacsonyabb a relációkénál. Lássuk a definíciót!

és-kifejezés:
  egyenlőségi-kifejezés
  és-kifejezés & egyenlőségi-kifejezés

kizáró-vagy-kifejezés:
  és-kifejezés
  kizáró-vagy-kifejezés ^ és-kifejezés

vagy-kifejezés:
  kizáró-vagy-kifejezés
  vagy-kifejezés | kizáró-vagy-kifejezés

(Az egyenlőségi-kifejezés definíciója a relációknál megtalálható.)

Ha szükséges, akkor a művelet elvégzése előtt a fordító implicit típuskonverziót hajt végre az egész típusú operandusok értékén. Az eredmény típusa az operandusok konverzió utáni típusa. A művelet bitről-bitre valósul meg az operandusok értékén, s egy bitre vonatkoztatva az eredmény így néz ki:

K1K2K1 & K2K1 ^ K2K1 | K2
00000
01011
10011
11101

Befejezve az egyes komplemensnél megkezdett példát, az x|maszk értéke 11111000111100102. Állíthatjuk, hogy az eredményben minden olyan bit egy, ami a maszk-ban az volt. Az x^x eredménye biztosan tiszta zérus. Az x&~maszk értéke 00001000000000102. Megemlítjük, hogy az eredmény minden olyan bitpozícióján 0 van, ahol a maszk bitje 1. Tehát a kifejezés azokat a biteket bizonyosan törölte, ahol a maszk bit 1 volt.

Nem szabad összekeverni a logikai vagy (||) és a bitenként vagy (|), ill. a logikai és (&&) és a bit szintű és (&) műveleteket! Feltéve, hogy két, egész típusú változó értéke: a=2 és b=4, akkor az

a && b -> 1 (igaz)

de az

a & b -> 0

Az ANSI szabvány szerint a bit szintű műveletek signed egészeken implementációfüggők. A legtöbb C fordító azonban signed egészeken ugyanúgy dolgozik, mint unsigned-eken. Például short int-ben gondolkodva a -16 & 99 eredménye 96, mert:

1111111111110000&0000000001100011 -> 0000000001100000

A fájl utolsó módosításának dátumát és idejét egy-egy szóban, azaz C nyelvi fogalmakkal: egy-egy unsigned short int-ben, 16 biten tároljuk. A két szó bitfelosztása legyen a következő:

dátumév - 1980hónapnap
időóraperc2 másodperc
1514131211109876543210

A két szó azonosítója legyen datum és ido! Tételezzük fel, hogy az idő részeit az ora, a perc és az mpunsigned short változókban tároljuk! Az ugyanilyen unsigned short változók a dátum részeire: ev, ho és nap. Akkor az oda-visszaalakítás a következő:

/* Részeiből az idő szó előállítása: */
ido = ora << 11 | perc << 5 | mp >> 1;
/* Az idő szóból a részei: */
ora = ido >> 11;
perc = ido >> 5 & 0X3F;
mp = (ido & 0X1F) << 1;
/* Részeiből a dátum szó előállítása: */
datum = ev - 1980 << 9 | ho << 5 | nap;
/* A datum szóból a részei: */
ev = (datum >> 9) + 1980;
ho = datum >> 5 & 0XF;
nap = datum & 0X1F;

Írjunk unsigned long invertal(unsigned longx, int p, int n) függvényt egy rövid kipróbáló programmal, mely az x paramétere értékét a p.-ik bitpozíciójától n hosszban invertálja (az 1-eseket 0-kra, s a 0-kat 1-esekre cseréli)! A nem említett bitpozíciók értéke maradjon változatlan! Az invertált eredmény a függvény visszatérési értéke.

n
312927252321^p1917151311975310

Az ábra x paramétert, a zérustól induló, 20 értékű p bitpozíciót és az n=5 bitszélességet szemlélteti.

Készítsünk előbb egy ugyancsak unsigned long maszkot, mely p pozíciótól n szélességben 1-es biteket tartalmaz, és az ezen kívüli pozíciók mind nullák!

               ~0: 1111111111111111111111111111111
            ~0<<n: 1111111111111111111111111100000
        ~(~0<<n): 0000000000000000000000000011111
~(~0<<n)<<(p+1-n): 0000000000011111000000000000000

Ezután x egyes komplemenséből bitenkénti éssel kivágjuk a kérdéses invertált bitpozíciókat, azaz: ~x&maszk. Ezt aztán bitenkénti vagy kapcsolatba hozzuk az eredeti x egy olyan változatával, melyben kinulláztuk az érdekes biteket, azaz: x&~maszk. Tehát:

/* PELDA15.C: Bitpoziciok invertalasa. */
#include <stdio.h>
unsigned long invertal(unsigned long x, int p, int n){
  unsigned long maszk=~(~0<<n)<<(p+1-n);
  return ~x&maszk|x&~maszk; }
void binaris(unsigned long x){
  unsigned long maszk;
  for(maszk=0X80000000; maszk; maszk=maszk>>1)
    if(maszk&x) putchar('1'); else putchar('0'); }
void main(void){
  unsigned long x=0X4C59E9FA;
  int p=20, n=5;
  printf("Az eredeti erteket: ");
  binaris(x);
  printf("\nEzt invertaljuk %d bitpoziciotol %d "
        "bitszelessegben.\n", p, n);
  printf("Az invertalt ertek: ");
  binaris(invertal(x, p, n));
  putchar('\n'); }

Vegyük észre, hogy a binaris függvény segítségével unsigned long értéket jelentetünk meg binárisan! A maszk változó 31-es bitpozíciójától indítunk egy 1-est, s a ciklusmag végrehajtása után mindig eggyel jobbra toljuk. A maszk és az érték bit szintű és kapcsolata akkor szolgáltat nem zérust (igazat), ha a kérdéses bitpozíción az értékben 1 van.

Feltételes kifejezés (? :)

Ez a nyelvben az egyetlen három operandusos művelet, melynek prioritása alacsonyabb a kétoperandusos logikai operátorokénál. A definíció:

feltételes-kifejezés:
  logikai-vagy-kifejezés
  logikai-vagy-kifejezés ? kifejezés : feltételes-kifejezés

kifejezés:
  hozzárendelés-kifejezés
  kifejezés , hozzárendelés-kifejezés

A logikai-vagy-kifejezésnek egész, lebegőpontos vagy mutató típusúnak kell lennie, s kiértékelése zérushoz való hasonlítását jelenti:

  • Ha logikai-vagy-kifejezés nem zérus, a kifejezést értékeli ki a fordító. Ez azt jelenti, hogy a kifejezés kiértékelése csak akkor történik meg, ha a logikai-vagy-kifejezés igaz.
  • Ha logikai-vagy-kifejezés zérus, a feltételes-kifejezést határozza meg a fordító, azaz a feltételes-kifejezés kiértékelése csak akkor történik meg, ha a logikai-vagy-kifejezés hamis.

Tehát a logikai-vagy-kifejezést mindenképpen kiértékeli a kód, de a kifejezés és a feltételes-kifejezés közül csak az egyik kiszámítása történik meg: a K1 ? K2 : K3 feltételes kifejezésben K1 értékétől függően K2-t vagy K3-at értékeli ki a fordító. A konstrukció eredményének típusa ilyen alapon K2 vagy K3 operandusok típusától függ a következőképp:

  • Ha mindkettő aritmetikai típusú, akkor az esetleg szükséges implicit típuskonverzió után az eredmény típusa a konvertált típus.
  • Ha a két operandus ugyanolyan struktúra, unió, vagy mutató típusú, akkor az eredmény is a közös típusú.
  • Ha mindkettő void típusú, akkor az eredmény is az.

Például az

if( a > b ) z = a;
else z = b;

utasítás helyettesíthető a

z = a > b ? a : b;

feltételes kifejezéssel. Ha például egy int típusú a tömb első N elemét szeretnénk megjelentetni úgy, hogy egy sorba egymástól szóközzel elválasztva 10 elem kerüljön, akkor ezt "tömör" kódot alkalmazva így is megtehetjük:

for(i=0; i<N; ++i)
  printf("%6d%c", a[i], (i%10==9|| i==N-1) ? '\n': ' ');

Javítsunk ki néhány eddig megírt függvényt a feltételes kifejezés felhasználásával!

  • A pelda15.c binaris függvényében az

    if(maszk&x) putchar('1'); else putchar('0');

    most így is írható:

    putchar((maszk&x)? '1': '0');
  • A pelda14.c atoi rutinjában az

    if(s[i]=='+'||s[i]=='-') if(s[i++]=='-') elojel=-1;

    sor átírható a következőre:

    if(s[i]=='+'||s[i]=='-') elojel=(s[i++]=='-')? -1: 1;

Mindkét példában a feltételes kifejezés logikai-vagy-kifejezése köré zárójelet tettünk. Lássuk azonban be, hogy erre semmi szükség sincs, hisz ennél alacsonyabb prioritású művelet már csak kettő van: a hozzárendelés és a vessző operátor! A felesleges zárójel legfeljebb a jobban olvashatóságot biztosítja.

Hozzárendelés operátorok

A jobbról balra csoportosító hozzárendelés prioritása alacsonyabb, mint a feltételes kifejezésé, s ennél alacsonyabb prioritású művelet már csak a vessző operátor. A művelet a jobb oldali operandus értékét rendeli a bal oldali operandushoz, melyből következőleg a bal oldali operandusnak módosítható balértéknek kell lennie. A hozzárendelés kifejezés értéke ugyan egyezik a bal oldali operandus hozzárendelés végrehajtása utáni értékével, de nem balérték. A jobb oldali operandus értékét a fordító a bal oldali operandus típusára konvertálja a bal operandusba való letárolás előtt a hozzárendelési konverzió szabályai szerint. A bal oldali operandus nem lehet tömb, függvény vagy konstans. Nem lehet természetesen nem teljes (még nem teljesen deklarált) típusú sem. A definíció:

hozzárendelés-kifejezés:
  feltételes-kifejezés
  egyoperandusos-kifejezés hozzárendelés-operátor hozzárendelés-kifejzés

hozzárendelés-operátor: ( a következők egyike!)
  = *= /= %= += -= &= ^= |= <<= >>=

Van tehát egyszerű hozzárendelés operátor (=) és vannak összetettek vagy kombináltak (ezek a többiek).

Foglalkozzunk előbb az egyszerű hozzárendeléssel!

A K1 = K2 kifejezésben K1-nek módosítható balértéknek kell lennie. K2 értéke - esetlegesen a K1 típusára történt konverzió után - felülírja a K1 által kijelölt objektum értékét. Az egész "konstrukció" értéke K2 értéke az esetleg a K1 típusára szükségessé vált hozzárendelési konverzió végrehajtása után.

Reméljük, hogy nem felejtették még el, hogy a balérték (K1) és jobbérték (K2) fogalom éppen az egyszerű hozzárendelésből származik. A balérték a hozzárendelés operátor bal oldalán, a jobbérték pedig a jobb oldalán állhat.

Tudjuk, ha egy kifejezésben hozzárendelés operátor is van, akkor annak a kifejezésnek bizonyosan van mellékhatása.

Emlékezzünk vissza, hogy a definíció megengedi a hozzárendelés operátor

K1 = K2 = K3 = ... = Kn = kifejezés

formájú használatát is, amikor is a kifejezés kiértékelése után jobbról balra haladva az operandusok felveszik a kifejezés értékét. Az egész konstrukció értéke most is a kifejezés értéke lesz. Például

a = b = c = d + 6;

A kombinált hozzárendelés operátorok a

K1 = K1 operátor K2

kifejezést

K1 operátor = K2

módon rövidítik, és K1 kiértékelése csak egyszer történik meg. A megengedett operátorok a definícióban láthatók! Mindegyik megvalósítja azt a műveletet, konverziót és korlátozást, amit a kétoperandusos operátor egyébként realizál, és végrehajtja a hozzárendelést is. A kombinált hozzárendelés operátor operandusai egész vagy lebegőpontos típusúak lehetnek általában. A += és -= bal oldali operandusa mutató is lehet, amikor is a jobb oldali operandus köteles egész típusú lenni. Például:

x = x * ( y + 6); -> x *= y + 6;

Az összetett operátorokat használva kevesebbet kell írni. Például:

t[ t1[i3 + i4] + t2[i1 - i2] ] += 56;

Használjunk kombinált hozzárendelés operátorokat néhány eddig már megírt függvényben!

  • A pelda15.c binaris rutinja:

    void binaris(unsigned long x){
      unsigned long maszk;
      for(maszk=0X80000000; maszk; maszk>>=1)
        putchar((maszk&x)? '1': '0'); }
  • A pelda14.c-beli

    for(ft=ALSO; ft<=FELSO; ft=ft+LEPES)

    most így írható:

    for(ft=ALSO; ft<=FELSO; ft+=LEPES)
Hozzárendelési konverzió

Hozzárendelésnél a hozzárendelendő érték típusát a hozzárendelést fogadó változó típusára konvertálja a fordító. A C megengedi a hozzárendelési konverziót lebegőpontos és egész típusok között azzal, hogy a konverziónál értékvesztés történhet. A használatos konverziós módszer követi az implicit típuskonverzió szabályait és ezen túl még a következőket:

  • Konverzió signed egész típusokról: Nem negatív signed egész nem kisebb méretű unsigned egésszé alakításakor az érték változatlan. Például signed char unsigned char-rá válásakor a bitminta változatlan, de a legmagasabb helyi értékű bit elveszti az előjelbit funkcióját. A konverzió különben a signed egész előjel kiterjesztésével történik. Például signed char unsigned long-gá úgy válik, hogy előjel kiterjesztéssel előbb long lesz belőle, s aztán ez a bitminta megtartásával, és előjelbit funkcióvesztéssel lesz unsigned long. Hosszabb egész típus rövidebbé alakulásakor az egész típus maradó alsó bitjei változatlanok, s a fölösleg egyszerűen levágódik még akkor is, ha értékvesztés történne. long int érték float lebegőpontossá alakításakor nincs értékvesztés, de pontosságvesztés lehet. Ilyen átalakításkor a rövidebb signed egészből előbb long lesz előjel kiterjesztéssel, s csak ezután jön a lebegőpontos konverzió.
    Miután az enum típus definíció szerint int, a felsorolás típusra és típusról való konverzió egyezik az int-ével.
  • Konverzió unsigned egész típusokról: Rövidebb unsigned vagy signed egésszé alakításkor a maradó alsó bitek változatlanok, s a fölösleg egyszerűen levágódik még akkor is, ha értékvesztés történik. Az eredmény legfelső bitje felveszi az előjelbit funkciót, ha signed-dé konvertálás volt. Hosszabb unsigned vagy signed egésszé alakításkor a bejövő magasabb helyi értékű bitek nulla feltöltésűek. Lebegőpontos konverziónál a rövidebb unsigned egészből előbb long lesz nulla feltöltéssel, s csak ezután jön az igazi konverzió. Megállapíthatjuk itt is, hogy lehet pontosságvesztés float-tá alakításkor.
  • Konverzió lebegőpontos típusokról: A rövidebb lebegőpontos ábrázolás hosszabbá konvertálásakor nem változik meg az érték. A hosszabb lebegőpontos ábrázolás float-tá alakítása is pontos, ha csak lehetséges. Pontosságvesztés is bekövetkezhet, ha értékes jegyek vesznek el a mantisszából. Ha azonban az eredmény a float ábrázolási korlátain kívül esne, akkor a viselkedés definiálatlan.
    Ez a definiálatlanság tulajdonképpen a fordítótól függő viselkedést takar!
    A lebegőpontos értéket úgy konvertálja egésszé a fordító, hogy levágja a törtrészt. Az eredmény előre megjósolhatatlan, ha a lebegőpontos érték nem fér be az egész ábrázolási korlátaiba. Különösen definiálatlan a negatív lebegőpontos érték unsigned-dé alakítása.

Konverzió más típusokról: Nincs konverzió a struktúra és az unió típusok között. Explicit típusmódosítással bármilyen érték konvertálható void típusúvá, de csak abban az értelemben, hogy a kifejezés értékét elvetjük. A void típusnak definíció szerint nincs értéke. Ebből következőleg nem konvertálható más típusúra, s más típus sem konvertálható void-ra hozzárendeléssel.

Vessző operátor

Ez a legalacsonyabb prioritású művelet.

kifejezés:
  hozzárendelés-kifejezés
  kifejezés , hozzárendelés-kifejezés

A K1, K2, ..., Kn esetén balról jobbra haladva kiértékeli a fordító a kifejezéseket úgy, hogy a bennük foglalt minden mellékhatás is megvalósul. Az első n - 1 kifejezés void-nak tekinthető, mert az "egész konstrukció" típusát és értékét a legjobboldalibb kifejezés típusa és értéke határozza meg. Például a

regikar = kar, kar = getchar()

esetén regikar felveszi kar pillanatnyi értékét, aztán kar és az egész kifejezés értéke a szabvány bemenetről beolvasott karakter lesz. Tipikus példa még a több kezdőérték adás és léptetés a for utasításban.

Írjuk meg egy rövid kipróbáló programmal a void strrv(char s[]) függvényt, mely saját helyén megfordítja a paraméter karakterláncot!

Az algoritmusról annyit, hogy a karakterlánc első karakterét meg kell cserélni az utolsóval, a másodikat az utolsó előttivel, és így tovább. Két indexet kell indítani a karaktertömbben: egyet alulról és egyet felülről. Az alsót minden csere után meg kell növelni eggyel, a felsőt pedig ugyanennyivel kell csökkenteni. A ciklus tehát addig mehet, míg az alsó index kisebb a felsőnél.

/* PELDA16.C: Karakterlanc megforditasa. */
#include <stdio.h>
#include <string.h> /* Az strlen miatt! */
#define INP 66      /* Az input puffer merete. */
void strrv(char s[]){
  int also, felso, csere;
  for(also=0, felso=strlen(s)-1; also<felso; ++also, --felso){
    csere=s[also]; s[also]=s[felso]; s[felso]=csere; } }
int getline(char s[],int n){
  int c,i;
  for(i=0;i<n&&(c=getchar())!=EOF&&c!='\n';++i) s[i]=c;
  s[i]='\0';
  while(c!=EOF&&c!='\n') c=getchar();
  return(i); }
void main(void){
  char s[INP+1]; /* Az input puffer. */
  printf("A szabvany bemenet sorainak megforditasa.\n"
        "Programveg: ures sor.\n\n");
  while(printf("Johet a sor! "), getline(s,INP)){
    strrv(s);
    printf("Megfordítva: %s\n", s); } }

A string.h fejfájlt bekapcsolva rendelkezésre áll az strrv szabvány könyvtári változata, mely ugyanilyen paraméterezésű és funkciójú, de strrev a neve, s más egy kicsit a visszatérési értékének a típusa.

Vegyük észre, hogy a main-beli while-ban is vesszős kifejezést használtunk!

Olyan szövegkörnyezetben, ahol a vessző szintaktikai jelentésű, a vessző operátort csak zárójelbe tett csoporton belül szabad használni. Ilyen helyek: az inicializátorlista például, vagy a függvény paraméterlistája. A

fv(b, (a = 2, t += 3), 4);

kitűnően mutatja, hogy az fv függvény három paraméterrel rendelkezik. A hívásban a második paraméter vesszős kifejezés, ahol a előbb 2 értékű lesz, aztán t megnő hárommal, s ezt az értéket kapja meg a függvény is második aktuális paraméterként. Ha a függvény prototípusa mást nem mond, akkor a második paraméter típusa t típusa.

Műveletek prioritása

A műveletek prioritását nevezik precedenciának, vagy rendűségnek is. Mindhárom esetben arról van szó, hogy zárójel nélküli helyzetben melyik műveletet kell végrehajtani előbb a kifejezés kiértékelése során.

A következő táblázat csökkenő prioritással haladva mutatja az egyes operátorok asszociativitását. A többször is előforduló operátorok közül mindig az egyoperandusos a magasabb prioritású. Abban a rovatban, ahol több operátor van együtt, a műveletek azonos prioritásúak, és asszociativitásuknak megfelelően hajtja őket végre a fordító. Minden operátor kategóriának megvan a maga asszociativitása (balról jobbra vagy jobbról balra köt), mely meghatározza zárójel nélküli helyzetben a kifejezés csoportosítását azonos prioritású műveletek esetén.

OperátorokAsszociativitás
() [] -> .balról jobbra
! ~ + - ++ -- & * sizeofjobbró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
||balról jobbra
?:jobbról balra
= *= /= %= += -= &= ^= |= <<= >>=jobbról balra
,balról jobbra

A táblázatban felsoroltakon kívül van még a # és a ## operátor, melyek az előfeldolgozónak szólnak.

A prioritási táblázat természetesen tartalmaz eddig még nem ismertetett műveleteket is.

Vannak többjelentésű operátorok is, melyek értelmezése a helyzettől függ. Például:

cimke:          /* utasítás címke */
a?x:y          /* feltételes kifejezés */
a=(b+c)*d;      /* zárójeles kifejezés */
void fv(int n); /* függvénydeklaráció */
a, b, c;        /* vesszős kifejezés */
fv(a, b, c);    /* függvényhívás */

A kifejezés kiértékelési sorrendje nem meghatározott ott, ahol a nyelvi szintaktika erről nem gondoskodik. A fordító a generált kód hatékonyságának javítása érdekében átrendezheti a kifejezést különösen az asszociatív és kommutatív operátorok (*, +, &, ^ és |) esetén, hisz azt feltételezi, hogy a kiértékelés iránya nem hat a kifejezés értékére.

Probléma lehet az olyan kifejezéssel,

  • melyben ugyanazt az objektumot egynél többször módosítjuk, ill.
  • melyben ugyanazon objektum értékét változtatjuk és felhasználjuk.

Például:

i = v[i++]; /* Döntsük el, mit is akarunk i-vel! */
i = a+++b[a]; /* b indexe attól függ, hogy az összeadás
                melyik tagját értékeli ki előbb a fordító. */

A fentiek akkor is igazak, ha kifejezésünket " jól összezárójelezzük":

int osszeg=0;
sum = (osszeg=3)+(++osszeg);/* sum == 4 vagy 7 ? */

Segédváltozók bevezetésével a dolgok midig egyértelműsíthetők:

int seged, osszeg=0;
seged = ++osszeg; /* seged == 1, osszeg == 1. */
sum = (osszeg=3)+seged; /* sum == 4. */

Ha a szintaktika rögzíti a kiértékelési sorrendet (az &&, a ||, a ?: és a vessző operátor esetében ez így van), akkor ott "bármit" megtehetünk. Például:

sum = (i=3, i++, i++); /* OK, sum == 4 és i == 5. */

A kifejezés kiértékelési sorrendjét ugyan () párokkal befolyásolhatjuk:

f = a*(b+c);

de asszociatív és kommutatív operátorok operandusait még "agyonzárójelezve" is összecserélheti a fordító, hisz feltételezheti, hogy a kifejezés értékét ez nem befolyásolja:

f = (a+b) + (c+d);

Határozatlan a függvény aktuális paramétereinek kiértékelési sorrendje is:

printf("%d %d\n", ++n, fv(n));

A bitenkénti operátorok prioritása alacsonyabb a relációkénál, így a

c & 0XF == 8

mindig hamis, mert az előbb kiértékelésre kerülő egyenlőségi reláció sohasem lehet igaz. A kifejezés helyesen:

(c & 0XF) == 8.

Vigyázzunk a hozzárendelés (=) és az egyenlő reláció (==) operátor felcserélésére, mert például az

if( i = 2 ) utasítás1; else utasítás2;

else ágára sohasem jut el a vezérlés tekintettel arra, hogy a 2 hozzárendelése "ebben az életben" sem válik hamissá. Ügyeljünk azokkal a kifejezésekkel is, melyekben csak logikai és (&&) vagy csak logikai vagy (||) műveletek vannak, mert ezeket balról jobbra haladva

  • && esetén csak az első hamis tagig, ill.
  • || operátornál csak az első igaz tagig

fogja kiértékelni a fordító! Az

x && y++

kifejezésben y növelése csak akkor történik meg, ha x nem zérus.

Kifejezés kiértékelése közben adódhatnak "áthidalhatatlan" szituációk. Ilyenek

  • a zérussal való osztás és
  • a lebegőpontos túl vagy alulcsordulás.

Újra felhívjuk azonban a figyelmet arra, hogy a nyelvben nem létezik sem egész túl, sem alulcsordulás!

Feladatok

1. Olyan int egesze(char s[]) függvényt szerettünk volna írni, ami a decimális egész konstans írásszabályát ellenőrzi a paraméter karaktertömbön. Sajnos nem sikerült.

/*01*/#define HSZ sizeof(int)/sizeof(short)*5 /* Az int max. jegyeinek száma. */
/*01*/int egesze(char s[]){
/*02*/  int i = 0; /* Indexeles. */
/*03*/  int kezd; /* Az elso numerikus karakter indexe. */
/*04*/  /* A karakterlanc elejen levo feher karakterek atlepese: */
/*05*/  while(s[i]==' ' || s[i]=='\t' || s[i]=='\n') ++i;
/*06*/  /* Az elojel atlepese: */
/*07*/  if(s[i] == '+' && s[i] == '-') ++i;
/*08*/  kezd = i; /* A szamjegyek itt kezdodnek. */
/*09*/  /* Elorelepes a kovetkezo, elso nem numerikus karakterre: */
/*10*/  while(s[i]>='0' && s[i]<='9' && i<HSZ) ++i;
/*11*/  /* Dontes: */
/*12*/  if(kezd==i || s[i]!=' '&&s[i]!='\t'&&s[i]!='\n'&&s[i]!=0) return 0;
/*13*/  else return 1; }
Jelölje meg azokat a sorokat, amikkel a függvény helyes működésre bírható!
/*05*/  while((s[i]==' ') || (s[i]=='\t') || (s[i]=='\n')) ++i;
/*07*/  if(s[i] == '+' || s[i] == '-') ++i;
/*10*/  while(s[i]>='0' && s[i]<='9' && i-kezd<HSZ) ++i;
/*10*/  while(s[i]>='0' && s[i]<='9' && kezd<HSZ) ++i;
/*10*/  while(s[i]>='0' && s[i]<='9' && kezd-i<HSZ) ++i;
/*12*/  if(kezd==i || s[i]==' '&&s[i]=='\t'&&s[i]=='\n'&&s[i]!=0) return 0;

2. Olyan int hexae(char s[]) függvényt szerettünk volna írni, ami megvizsgálja, hogy paramétere hexadecimális szám-e. Sajnos nem sikerült.

/*01*/int hexae(char s[]) { /* A param. hexadec. karakterlanc-e? */
/*02*/  int i=0; /* Index. */
/*03*/  /* A lanc eleji feher karakterek atlepese: */
/*04*/  while(s[i]==' '|| s[i]=='\t'|| s[i]=='\n') ++i;
/*05*/  /* Elojel nem lehet. */
/*06*/  /* Mig feher karakter nem kovetkezik, vagy vege nincs a
/*07*/    karakterlancnak: */
/*08*/  while(s[i]!=' '|| s[i]!='\t'|| s[i]!='\n'|| s[i]!='\0')
/*09*/    /* Hexadecimalis szamjegy-e? */
/*10*/    if(s[i]>='0'&&s[i]<='9'|| s[i]>='A'&&s[i]<='F'||
/*11*/      s[i]>='a'&&s[i]<='f') ++i;
/*12*/    else return 0; /* Rossz karakterlanc. */
/*13*/  return 1; } /* Jo karakterlanc. */
Jelölje meg azokat a sorokat, amikkel a függvény helyes működésre bírható!
/*04*/  while(s[i]!=' '&& s[i]!='\t'&& s[i]!='\n') ++i;
/*08*/  while(s[i]==' '&& s[i]=='\t'&& s[i]=='\n'&& s[i]!='\0')
/*08*/  while(s[i]!=' '&& s[i]!='\t'&& s[i]!='\n'&& s[i]!='\0')
/*08*/  while(s[i]!=' '&& s[i]!='\t'&& s[i]!='\n'|| s[i]!='\0')

3. Olyan long atoh(char s[]) függvényt szerettünk volna írni, ami hexadecimális karakterláncot alakít egésszé. Sajnos nem sikerült.

/*01*/long atoh(char s[]) { /* Hexa. karakterlanc konverzioja. */
/*02*/  int i=0; /* Index. */
/*03*/  long n=0l; /* A konvertalt ertek. */
/*04*/  /* A karakterlanc eleji feher karakterek atlepese: */
/*05*/  while(s[i]==' '|| s[i]=='\n'|| s[i]=='\t') ++i;
/*06*/  /* Elojel nincs. */
/*07*/  /* Konverzio, mig hexadec. szamjegyek kovetkeznek: */
/*08*/  for(; s[i]>='0'&&s[i]<='9'|| s[i]>='A'&&s[i]<='Z'||
/*09*/        s[i]>='a'&&s[i]<='f'; ++i)
/*10*/    if(s[i]>='0'&&s[i]<='9') n=16*n+s[i]-'0'; /* 0-9 */
/*11*/    /* A-F */
/*12*/    else if(s[i]>='A'&&s[i]<='F') n=16*n+s[i]-'A'+10;
/*13*/    else n=10*n+s[i]-'a'+10; /* a-f */
/*14*/  return(n); }
Jelölje meg azokat a sorokat, amikkel a függvény helyes működésre bírható!
/*08*/  for(; s[i]>='0'&&s[i]<='9'&& s[i]>='A'&&s[i]<='Z'&&
/*08*/  for(; s[i]>='0'&&s[i]<='9'|| s[i]>='A'&&s[i]<='F'||
/*08*/  for(;s[i]>='A'&&s[i]<='Z'|| s[i]>='0'&&s[i]<='9' ||
/*10*/    if(s[i]>='0'&&s[i]<='9') n=10*n+s[i]-'0'; /* 0-9 */
/*13*/    else n=16*n+s[i]-'a'+10; /* a-f */
/*13*/    else n=16*n+s[i]-'a'+16; /* a-f */

4. Olyan int ell210e(char s[]) függvényt szerettünk volna írni, ami teszteli, hogy s 2 és 10 közötti alapú, nem negatív szám-e. A számrendszer alapját fordítási időben változtatni (#define) lehet. Sajnos célunkat nem sikerült elérni.

/*1*/#define ALAP 10
/*2*/int ell210e(char s[]){
/*3*/  int i;
/*4*/  for(i=strlen(s)-1; i>=0 && s[i]>='0' && s[i]<ALAP+'0'; --i);
/*5*/  if(s[i]>='0' && s[i]<ALAP+'0') return 1; else return 0; }
Jelölje meg azokat a sorokat, amikkel a függvény helyes működésre bírható!
/*4*/  for(i=strlen(s); i>=0 && s[i]>='0' && s[i]<ALAP+'0'; --i);
/*4*/  for(i=strlen(s)-1; i>=0 && s[i]>='0' && s[i]<=ALAP+'0'; --i);
/*4*/  for(i=strlen(s)-1; i && s[i]>='0' && s[i]<ALAP+'0'; --i);
/*5*/  if(s[i]>='0' && s[i]<=ALAP+'0') return 1; else return 0; }
/*5*/  if(s[i]>='0' && s[i]<ALAP+'0') return 0; else return 1; }

5. Olyan int ella36e(char s[], int alap) függvényt szerettünk volna írni, ami teszteli, hogy salap alapú, nem negatív szám-e. A számrendszer alapja, vagyis alap értéke 2 és 36 közötti lehet. Sajnos célunkat nem sikerült elérni.

/*1*/int ella36e(char s[], int alap){
/*2*/  int i;
/*3*/  if(alap<10)
/*4*/    for(i=0; s[i]>='0' && s[i]<alap+'0'; ++i);
/*5*/  else
/*6*/    for(i=0; s[i]>='0'&&s[i]<'10' || s[i]>='a'&&s[i]<'a'+alap-10 ||
/*7*/            s[i]>='A'&&s[i]<'A'+alap-10;++i);
/*8*/  if(s[i]='\0') return 0; else return 1; }
Jelölje meg azokat a sorokat, amikkel a függvény helyes működésre bírható!
/*3*/  if(alap<=10)
/*3*/  if(i<10)
/*3*/  if(alap<='a')
/*4*/    for(i=0; s[i]>='0' && s[i]<=alap+'0'; ++i);
/*6*/    for(i=0; s[i]>='0'&&s[i]<'9' || s[i]>='a'&&s[i]<'a'+alap-10 ||
/*6*/    for(i=0; s[i]>='0'&&s[i]<'9' || s[i]>='a' || s[i]<'a'+alap-10 ||
/*8*/  if(s[i]!='\0') return 0; else return 1; }
/*8*/  if(s[i]) return 0; else return 1; }

6. Készítsen long ato36(char s[], int alap) függvényt az atoi mintájára, amely a legfeljebb 36 alapú számrendszerbeli karakterláncot konvertálja egésszé!

long ato36(char s[], int alap){
  int i;
  long n=0l;
  if(alap<=10)
    for(i=0; s[i]>='0'&&s[i]<alap+'0'; ++i) n=alap*n+s[i]-'0';
  else
    for(i=0; s[i]>='0'&&s[i]<'9' || s[i]>='a'&&s[i]<alap+'a'-10 ||
            s[i]>='A'&&s[i]<alap+'A'-10; ++i)
      if(s[i]>='0'&&s[i]<='9') n=n*alap+s[i]-'0';
      else if(s[i]>='a'&&s[i]<'z') n=n*alap+s[i]-'a'+10;
      else n=n*alap+s[i]-'A'+10;
  return n; }
7. Olyan kifejezést szeretnénk írni, ami szolgáltatja az x n db. egymást követő bitje által meghatározott értéket a p pozíciótól kezdve. x, n és p típusa unsigned int. Jelölje meg a helyes megoldást!
(x>>(p-n))&~(~0<<n)
(x>>(p+1-n))&~(~0<<n)
(x<<(p+1-n))&~(~0>>n)
(x>>(p-n))&~(0<<n)

8. Készítse el az unsigned long rotl(unsigned long x) függvényt, ami 1 bittel balra forgatja paramétere értékét. Tehát a 31. pozícióról kicsorgó bit lesz az eredmény 0. bitje.

#define HSZ sizeof(unsigned long)*8 /* A tipus bitszama. */
unsigned long rotl(unsigned long x){ /* x balra forgatasa
  eggyel. Eggyel balra toljuk, s az eredeti legfelso bitet
  berakjuk alulra: */
  return (x<<1 | x>>HSZ-1); }

9. Olyan unsigned rotr(unsigned szam) függvényt szerettünk volna írni, ami paraméterének értékét 1 bittel jobbra forgatva adja vissza (tehát ami a 0. bitnél kicsordul, azt áthelyezi a legmagasabb helyi értékű bitbe). Sajnos célunkat nem sikerült elérni.

/*1*/#define SZH sizeof(unsigned)*8
/*2*/unsigned rotr(unsigned szam){
/*3*/  return (szam>1)|(szam<(SZH-1)); }
Jelölje meg azokat a sorokat, amikkel a függvény helyes működésre bírható!
/*1*/#define SZH sizeof(unsigned)*8;
/*3*/  return (szam>>1)||(szam<<(SZH-1)); }
/*3*/  return (szam>>1)|(szam<<(SZH-1)); }
/*3*/  return (szam>>1)|(szam<<SZH); }