1. Homepage of Dr. Zoltán Porkoláb
    1. Home
    2. Archive
  2. Teaching
    1. Timetable
    2. Bolyai College
    3. C++ (for mathematicians)
    4. Imperative programming (BSc)
    5. Multiparadigm programming (MSc)
    6. Programming (MSc Aut. Sys.)
    7. Programming languages (PhD)
    8. Software technology lab
    9. Theses proposals (BSc and MSc)
  3. Research
    1. Sustrainability
    2. CodeChecker
    3. CodeCompass
    4. Templight
    5. Projects
    6. Conferences
    7. Publications
    8. PhD students
  4. Affiliations
    1. Dept. of Programming Languages and Compilers
    2. Ericsson Hungary Ltd

Imperatív programozás 4.

Operátorok, kifejezések, utasítások.

Kifejezések

A legelső magasszintű programozási nyelvek, mint pl. a Fortran, egyik elsődleges célkitűzése volt, hogy a programokban matematikai kifejezéseket tudjunk használni. A kifejezések - melyek a matematikai egyenletekhez hasonlítottak - változókból (amelyek egy-egy memória-területet azonosítottak) és operátorokból (melyek a matematikai műveleti jeleknek feleltek meg) álltak. Általánosan, a kifejezéseket a programozási nyelvekben operátorok-ból és konstans értékekből vagy változókból képezzük.

Az alábbi kifejezés például számos programozási nyelvben érvényes:

A + B * C

Hasonlóan a matematikai egyenletekhez, a kifejezésekben is fontos, hogy melyik az “erősebb” művelet, azaz hogyan kell értelmeznünk (zárójeleznünk) egy kifejezést. Ebben az egyes műveleteket leíró operátorok precedenciája (erőssége) az iránymutató. A szorzás például magasabb precedenciájú, mint az összeadás, ezért a fenti kifejezést alábbi módon kell értelmezni:

A + (B * C)

mivel a szorzás magasabb precedenciájú, mint az összeadás. ha ettől eltérő viselkedést szeretnénk, akkor azt zárójelezéssel jelezhetjük. Ilyen értelemben ez a kifejezés hasonlóan működik, mint a megfelelő matematikai képlet. Azért ez ne tévesszen meg bennünket, nem matematikai képleteket írunk a programozási nyelvekben, hanem kifejezéseket (expression), melyek egyrészt viselkedhetnek másképpen, mint azt a matematikában megszoktuk, másrészt lehet mellékhatásuk (side effect), azaz valami egyéb akciót is végrehajthatnak, miközben kiértékeljük (evaluate) a kifejezéseket.

A funkcionális programozási nyelvekben pont ezek a mellékhatások hiányoznak, ezért az ott leírt függvények sokkal inkább matematikai pure jellegűek.

A FORTRAN77 nyelvi verzióban az azonos precedenciájú műveletek sorrendje nem volt meghatározott. Azaz, ha nem írtunk zárójeleket az alábbi kifejezésbe

A * B / C * D

akkor az jelenthette az alábbi zárójelezések bármelyikét:

((A * B) / C) * D
(A * B * D) / C
(A / C) * B * D

Könnyen látható, hogy ha pl. A, B, C, D egész számok (Fortran INTEGER típus), akkor az egész értékű osztás miatt az egyes kiértékelési sorrendek eredménye eltérő lehet. A kerekítési hibák miatt még akkor is kaphatnánk eltérő eredményt, ha az értékek lebegőpontos számok lennének (Fortran REAL vagy DOUBLE PRECISION típus).

A modern programozási nyelvekben az egyes kifejezések értelmét az operátor-__precedencia__ (precedence) mellett az ún. asszociativitás (associativity) határozza meg. Az asszociativitás azt definiálja, hogy azonos precedencia szintű operátorok esetében hogyan (balról-jobbra vagy jobbról-balra) kell (gondolatban) zárójelezni a kifejezéseket.

A kifejezéseknek típusa és értéke van. A statikus típusrendszerű programozási nyelvekben (ilyen a C, Java, C#, és sok másik nyelv) a kifejezések típusát a fordítási időben megállapítja a fordítóprogram. A kifejezések értékét legtöbbször csak futási időben lehet megállapítani, de vannak kivételes esetek, amikor ez az érték fordítási időben ismert. Ezeket a kifejezéseket konstans kifejezéseknek (constant expression) nevezzük.

A C nyelv operátorai

A C programozási nyelvre (és leszármazottjaira) jellemző, hogy sok operátort használhatunk, köztük olyanokat is, melyek más nyelvekben utasítások, függvények, vagy egyáltalán nem is léteznek.

Precedencia Operátor Leírás Assoc
Posztfix ++ posztfix növelés L->R
  – – posztfix csökkentés  
  () függvényhívás  
  [] tömb index  
  . struct/union tag elérés  
  -> tag elérés mutatóval  
  (type){list} összetett literál (C99)  
Unáris ++ prefix növelés R->L
  – – prefix csökkentés  
  + pozitívlőjel  
  negatív előjel  
  ! logikai negáció  
  ~ bitenkénti negáció  
  (type) típus konverzió  
  * pointer indiekció  
  & címoperátor  
  sizeof típus/objektum mérete  
  _Alignof igazítási követelmény (C11)  
Multiplikatív * / % szorzás, osztás, maradék L->R
Additív + – összeadás, kivonás L->R
Léptetés « » bitenkénti bal/jobb léptetés L->R
Relációs < <= > >= relációs műveletek L->R
Egyenlőség == != egyenlő, nem egyenlő L->R
Bitenkénti & bitenkénti és (AND) L->R
  ^ bitenkénti kizáró vagy (XOR) L->R
  | bitenknti vagy (OR) L->R
Logikai && logikai és AND L->R
  || logikai vagy OR L->R
Terciális ? : feltételes kifejezés R->L
Értékadás = értékadás R->L
  += –= összetett értékadások  
  *= /= %=    
  «= »=    
  &= |= ^=    
Szekvencia , vessző (szekvencia) operátor L->R

Megjegyzések

Nem kiértékelt operátorok

Néhány operátor ún. nem kiértékelt (unevaluated), azaz futási időben ténylegesen nem történik velük semmi, Ezek a műveletek fordítási időben felhasználható információt szolgáltatnak. C-ben ilyen az _Alignof és a sizeof. Ebből leginkább a sizeof-ot használjuk, ami egy típus méretét adja meg bájtokban. Például:

1
    size_t int_size = sizeof(printf("%d", 42));

nem ír ki semmit sem az outputra, de int_size értéke 4 lesz (4 bájtos integer méret esetén).

Bináris vagy/és precedenciája

Figyeljünk arra, hogy pár operátornak nem túl magától értetődő a percedenciája. Például a bitenkénti és vagy műveletek “gyengébbek”, mint a relációs operátorok. Ebből furcsa hibák következhetnek:

1
    if ( flag & 0xff == 0 )  

valójában

1
    if ( flag & (0xff == 0) )

lesz és mindig hamis. Az ilyen hibák elkerüléséhez mindig írjuk ki a zárójeleket a kifejezéseinkben:

1
    if ( (flag & 0xff) == 0 )

Értékadás vs. egyenlőségvizsgálat

Hasonlóan figyelni kell az értékadás operátor és az egyenlőségvizsgálat különbségére. Az alábbi esetben

1
2
3
    x = 10;
    /* ... */
    if ( x = 0 )

nem egyenlőségvizsgálat, hanem értékadás történik. Miután x felvette a 0 értéket, a kifejezés értéke 0 és hamis lesz. Egy praktikus ötlet: konstanssal való összehasonlításkor írjuk balra a konstanst, így szintaktikus hibát kapnánk, ha elhagyánk egy karaktert:

1
    if ( 0 = x )

Ez utóbbi programozási stílust Yoda conditions-nak nevezik.

Az értékadás operátor és a másolás szemantikája

Az értékadás a programozási nyelvek jó részében utasítás és csak a C nyelv óta használják kifejezésként. Ennek a C-ben csak annyi hatása van, hogy az értékadásnak van eredménye, amit fel lehet használni egy további kifejezésben:

1
2
    int a, b;
    a = 3+(b = 5);  /* a = (3 + (b = 5) )*/

Itt a értéke 8, b értéke 5 lesz. Persze ilyet ritkán csinálunk. Gyakrabban fordul elő, hogy több változónak adunk értéket, de figyeljünk arra, hogy ez nem párhuzamos értékadás, hanem jobbról balra haladó 3 különálló értékadás.

1
2
3
    double a, c;
    int    b;
    a = b = c = 3.14;  /* a = (b = (c = 3.14) ) */

Ami után c értéke 3.14, b értéke 3 és a értéke 3.0 lesz.

Az értékadás működik néhány összetett típusra is, pl. struct és union, de nem működik tömbökre.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

struct X
{
  int    i;
  double d;
  int   *ptr;
};
void f()
{
  int      zz = 1;
  struct X aa;
  struct X bb;

  aa.i   = 1;
  aa.d   = 3.14;
  aa.ptr = &zz;

  bb = aa;  /* 1==bb.i és 3.14==bb.d és *aa.ptr==*bb.ptr */
  ++*aa.ptr; /* 2==zz  és 2==*aa.ptr  és  2==*bb.ptr !!! */  
}

Ilyenkor tagonkénti értékadás történik (valójában egyszerűen aa teljes területe átmásolódik bb-be). Az ilyen értékadások azonban lehetnek veszélyesek, ha pl. az egyik tag pointer, akkor aa.ptr és bb.ptr ugyanoda mutat, tehát ha az egyik módosítja a mutatott területet, akkor a másik is ezt a módosított értéket fogja látni.

Később, objektum-orientált nyelvekben gyakori lesz, hogy egy osztályt úgy implementálunk, hogy egy objektumból egy pointer mutat valami dinamikusan lefoglalt memóriaterületre. Ilyenkor a pointer által mutatott terület logikailag az objektum sajátja, és ha másoljuk az objektumot, akkor nem a pointert, hanem a mutatott tárterületet kéne másolni.

Azokban a nyelvekben, ahol az operátorokat túlterhelhetjük és az értékadás operátor, írhatunk saját értékadás operátort, ami elvégzi a kívánt tevékenységet. Ilyen a C++ másoló konstruktora (copy constructor) és értékadó operátora (assignment operator). Ahol ez nem lehetséges, vagy megtiltjuk az értékadás használatát (ADA private limited típus) vagy valami “szokásos” függvényt (pl. Java clone metódus) hozunk létre. A Java nyelv Cloneable és a C# ICloneable interfésze ez utóbbi módszert támogatja, de erősen vitatott (Java C#) módon.

Konverziók

A kifejezések kiértékelésekor egyes esetekben az operandusok egyike, vagy mind konvertálódhat más típussá.

  • Értékadás, változó inicializálás, paraméterátadás és return utasításkor konverzió történik a cél típusra.
  • Aritmetikai konverziók történnek a szélesebb számábrázolású típusok felé:

    char –> short –> int –> long –> long long

    előjeles egészek –> előjelnélküli egészek

    egészek –> float –> double –> long double

    tömb –> első elemre mutató pointer

A konverziók bonyolult és széles skálája a szabványban és a C könyvekben részletesen le van írva.

Kifejezések kiértékelése

Bár a kifejezések értelmezését egyértelműen meghatározza a precedencia és az asszociativitás, a kifejezések kiértékelésének mikéntjét bizonyos keretek között szabadon meghatározhatja a fordítóprogram.

Mit ír ki az alábbi program:

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
  int i = 1;
  printf( "i = %d, ++i = %d\n", i, ++i );
  return 0;
}
$ ./a.out 
i = 2, ++i = 2   # más platformon i = 1, ++i = 2 is lehet 

A fenti kifejezés hibás, nemdefiniált viselkedésű (undefined behavior) mert i és ++i ugyanazt a memóriaterületet éri el és egyikük módosítja is azt. Ha két kifejezés kiértékelése ugyanazt a memória-területet éri el és legalább az egyik módosítja is azt, akkor konfliktusban vannak (conflicting). Erősen leegyszerűsítve, ahhoz, hogy a programok helyes viselkedését biztosítsuk, az ilyen konfliktusban levő kifejezéseket el kell választanunk ún. szekvencia pontokkal (sequence point). A szekvencia pont garantálja, hogy az előzőleg elkezdett kiértékelések befejeződjenek a szekvencia pontig és a rákövetkező kifejezések csak a szekvencia pont után kezdődjenek el. Így a kiértékelések nem kerülnek konfliktusba. A precíz leírás a C szabvány 5.1.2.3 pontja alatt olvasható.

Az utasítások eleje és vége szekvencia pont. Ezen kívül van néhány operátor, amelyik maga is szekvencia pontként viselkedik. Ilyenek

  1. a rövidzáras logikai operátorok ( && és || )
  2. a feltételes operátor feltételének a kiértékelése ( ? : )
  3. a vessző operátor ( , )

Hasonlóan, amikor egy függvényt meghívunk, akkor az összes paramétere kiértékelődik, mielőtt a függvény törzsének végrehajtása elkezdődne. Ugyanakkor a paraméterek kiértékelésének egymás közötti sorrendje nem meghatározott.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
int f()
{
  printf("f\n");
  return 2;
}
int g()
{
  printf("g\n");
  return 1;
}
int h()
{
  printf("h\n");
  return 0;
}
void func()
{
  printf("(f() == g() == h()) == %d", f() == g() == h());
}
int main()
{
  func();
  return 0;
}
$ gcc -ansi -pedantic -Wall f.c
f.c: In function ‘func’:
f.c:20:44: warning: suggest parentheses around comparison in operand of ‘==[-Wparentheses]
printf("func: (f() == g() == h()) == %d\n", fpar == gpar == hpar);
                                               ^
$ ./a.out 
f
g
h
func: (f() == g() == h()) == 1
$

A fenti példában a kifejezés jelentését egyértelműen meghatározza a precedencia és az asszociativitás szabály. Ugyanakkor az egyes függvények meghívási sorrendjéről a fordító szabadon dönthet. Más fordítóprogramok, vagy akár ugyanaz a fordító más platformokon más sorrendet eredményezhet.

A hiányzó szekvencia pont súlyos hibát okozhat a programunkban. A lenti programban a 11. sorban az i változó két elérése (köztük az i++ módosító) konfliktusos akció, ezért ez a program nemdefiniált viselkedésű (undefined behavior). A nemdefiniált viselkedésű programok hibásak, még akkor is, ha egyes platformokon lefutnak. Könnyen lehet, hogy a hiba csak akkor jön elő, ha egy másik fordítóval fordítjuk a programot.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
 * BAD!
 */
#include <stdio.h>
int main()
{
  int t[10];
  int i = 0;
  while( i < 10 )
  {
    t[i] = i++;
  }
  for ( i = 0; i < 10; ++i )
  {
    printf("%d ", t[i]);
  }
  return 0;
}
$ gcc -ansi -pedantic -Wall -W  f.c
f.c: In function ‘main’:
f.c:9:13: warning: operation on ‘i’ may be undefined [-Wsequence-point]
   t[i] = i++;
           ^
$ ./a.out 
613478496 0 1 2 3 4 5 6 7 8 
$

A helyes megoldás:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
 * OK
 */
#include <stdio.h>
int main()
{
  int t[10];
  int i = 0;
  while( i < 10 )
  {
    t[i] = i;
    ++i;
  }
  for ( i = 0; i < 10; ++i )
  {
    printf("%d ", t[i]);
  }
  return 0;
}