Operátorok

2. Operátorok: precedencia és asszociativitás

C puska!

Operátorok: a kifejezések építőkockái

  • Pl. matematikai műveletek jelei: +, -, *, /
  • Operandusok: amiken a műveletet végzik

Precedenciák,
asszociativitás:
lásd a két
oldalas C puskát!

Mik az operandusok? – Szabályok

  • Több is lehet: a = -x; unáris (unary), b = x-y; bináris (binary), azaz egy- és kétoperandusú
  • Precedencia: különfélék „erőssége”, pl. 5+2*3 = 5+(2*3)
  • Asszociativitás: egyformák csoportosítása, pl. a/b/c = (a/b)/c

Az operátorok precedenciája és asszocivitása tehát nem azt határozza meg, hogy egy nagyobb kifejezés melyik részkifejezését értékeli ki időben előbb a program, hanem csak azt mondják meg, hogy melyik operátornak mi az operandusa. Pl. egy a*b+c*d kifejezésben mindegy is, hogy előbb az a*b vagy a c*d részkifejezést értékeljük ki. Ellenben az a/b/c kifejezés egészen mást jelentene, ha az osztás jobbról balra lenne asszociatív, mert akkor a/(b/c)-t értenénk alatta, ami viszont nem ugyanazt az eredményt adja.

3. Polimorfizmus és konverziók

Előfordulhat az, hogy egy operátor többféleképp működhet?


int a = 5, b = 2;
double c = 5, d = 2;
double x;

x = a / b;
printf("%g\n", x); // 2
x = c / d;
printf("%g\n", x); // 2.5

x = (double)a / b;
printf("%g\n", x); // 2.5
x = a / (double)b;
printf("%g\n", x); // 2.5

Polimorfizmus (többalakúság)

Az operátorok jelentése függhet az operandusok típusától:

  • a/b: osztás. Ha a és b is int, egész osztás, ami lefelé kerekít!
  • Ha bármelyik lebegőpontos, az eredmény is az.

Az egész osztás sok esetben hasznos. Lásd a bankautomatás feladatot: ha az a kérdésünk, hogy 5500 Ft kifizetéséhez hány ezresre van szükségünk. 5500/1000 = 5 darab ezres, és 5500%1000 = 500 Ft a maradék, amelyet máshogy kell megoldanunk, nem ezresekkel.

Ha valamelyik operandus valós, a másik automatikusan valóssá konvertálódik. Automatikus konverzió egyéb esetekben is történik. Pl. short+int összeadás esetén a short típusú operandus a nagyobb ábrázolási tartományú int típusúvá konvertálódik. Ugyanígy, int+long esetén az összeadás előtt az int konvertálódik automatikusan, az 5+2.3 kifejezésben pedig az 5-ből lesz 5.0. Mindig a nagyobb ábrázolási tartomány felé történik az automatikus konverzió, hogy ne amiatt legyen adatvesztés vagy túlcsordulás.

Kézi konverzió (cast)

  • Ha két int van, de lebegőpontos osztást szeretnénk, jelezni kell
  • Konverziós (cast) operátort használva: (double) x

Fontos megfigyelni, hogy jobb oldalt látható kódban az x=a/b kifejezésben az eredmény még így is 2, hogy utána azt a double x változóba másoljuk! Az értékadás egy újabb operátor, amelynek az osztás eredményébe már nincsen „beleszólása”. Az osztás egész/nem egész jellege nem azon múlik, hogy az elvégzése után mit csinálunk az eredménnyel! Ha az osztás valamelyik operandusát lebegőpontos számmá alakítjuk azáltal, hogy elé a (double) operátort írjuk, már nem egész osztás fog történni. A konverziós operátorok nagyon magas precedenciájúak: mindig közvetlenül arra az értékre vonatkoznak, amelyek elé írjuk őket.

A konverzió segítségével más típusúvá alakítható egy érték. Egy lebegőpontos érték elé (int)-et írva egésszé alakítható az, természetesen a törtrészt elveszítve. Számtani műveletek esetén ritkán kell kézi konverziót alkalmazni. Más típusoknál, a mutatóknál, amelyek egy későbbi előadáson fognak szerepelni, nagyobb szerepet kapnak a konverziók. De ezekről majd később.

4. Az érték és a mellékhatás fogalma

A printf() értéke

123, 456, 1234,
0, 6, 12, 11, 1,
1234577, 93628,
2, 4, 9, 1, 3,

A printf() függvény, amellett hogy kiírja a megadott szöveget a képernyőre, megadja a kiírt karakterek számát is. Ez hasznos lehet, ha azt szeretnénk vizsgálni, a sor végére értünk-e már. A jobb oldalon látható dobozban például az egyes sorok nagyjából egyforma hosszúak, függetlenül attól, hogy az egyes számok hány számjegyből állnak. Kicsi számokból sok kifér egy sorba, nagy számokból kevés. A kimenetet ez a programrész generálta:

int db = 0;
for (i = 0; szamok[i] != -1; i += 1) {
    db += printf("%d, ", szamok[i]);   // !
    if (db > 15) {
        printf("\n");
        db = 0;
    }
}

Erre azt mondjuk, hogy a printf() műveletnek van értéke és mellékhatása is. Az érték a kiírt karakterek száma, a mellékhatás pedig a szöveg kiírása. A mellékhatás a programozásban nem jelent rosszat! A printf()-et általában épp a mellékhatása miatt használjuk: a kiírás a lényeges, az értékkel, a karakterek számával ritkán foglalkozunk. De ettől még a kiírás technikailag egy mellékhatásnak számít.


Érték és mellékhatás: példák

Összehasonlításképp:

sqrt(2)

Érték (value): 1,414
Mellékhatás (side effect): nincs

printf("hello")

Érték: 5
Mellékhatás: kiírt szöveg

5. Operátorok: érték és mellékhatás

Az operátorok esetén az érték és a mellékhatás fogalmát ugyanígy használjuk.

Érték (value)
A műveletnek értéke van, amely behelyettesítődik a kifejezésbe, ahol használjuk.
Mellékhatás (side effect)
Történik valahol egy változás, pl. megváltozik egy változó értéke, megjelenik valami a képernyőn.

Összeadás

x + y

Érték: az összeg
Mellékhatás: nincs

Értékadás

x = y

Érték: a másolt szám
Mellékhatás: x megváltozik

Miért fontos a mellékhatás figyelembe vétele? A mellékhatással nem rendelkező operátorokkal leírt képletek mindig ugyanazt az eredményt adják. Az x+y kifejezésnek nincs mellékhatása, ezért akárhányszor is kiértékeljük, ugyanazt a számot fogjuk kapni. Ezzel szemben, az x+=1 kifejezésnek van mellékhatása: nem ugyanaz lesz az eredmény elsőre, másodjára, harmadjára.


Az értékadás értékének kihasználása

a = b = 1;
a = (b = 1);         // jobbról balra asszociatív
b = 1; a = 1;

A mellékhatás jelenti a változtatás képességét. Az = operátort a mellékhatása miatt használjuk. Az értékével ritkán törődünk, viszont pont az teszi lehetővé a láncolt értékadást: pl. az a=b=1 kifejezés mindkét változóba 1-et tesz. Így rövidebben, kifejezőbben tudjuk leírni, hogy mindkét változónak ugyanazt az értéket szeretnénk adni.

Hogy működik egy ilyen láncolt értékadás? Az = értékadó operátor jobbról balra asszociatív, tehát az a=b=1 kifejezés pont ugyanúgy működik, mintha a=(b=1) formában zárójeleztük volna. Így már érthető: az 1-es érték a b változóba másolódik (mellékhatás), a b=1 részkifejezés értéke pedig a másolt érték, az 1. Ezt kell helyettesíteni a kifejezésbe a b=1 helyére: a=(1), innen pedig már látszik, hogy az a-ba is 1 kerül.

Az értékadás az egyik legfontosabb operátorunk, mert ezzel tudjuk változtatni a változók értékét a program futása során. Nem minden kifejezés állhat azonban az értékadás bal vagy jobb oldalán: a=1 és szamok[2]=5 helyes, viszont 1=a és a+2=3 helytelen kifejezések. Értéket adni változóknak, tömbelemeknek tudunk: ezeket balértéknek (left, l-value) nevezzük, mert ilyenek állhatnak egy értékadó operátor bal oldalán is. Az értékadó operátor kizárólag jobb oldalára írható értékeket pedig jobbértéknek (right value, r-value) nevezzük. Általában véve a balértékek valamiféle változók szoktak lenni, a memóriában megjelölt helyek; a jobbértékek pedig számértékek, számítások eredményei, amik nem kötődnek helyhez.

A balérték, jobbérték kifejezéseket ismerni kell, mert a fordítók hibaüzeneteiben gyakran megjelennek. Például az 5=6 kifejezésre az „lvalue required as left operand of assignment” jelzést kapjuk, vagyis hogy az értékadás bal oldali operandusaként egy balérték kellene szerepeljen.

Mindezek érvényesek az összes kombinált értékadó operátorra is: +=, -= stb.

printf("%d", a = b); // neeeee
a = b;
printf("%d", a);

Az értékadás kifejezés értéke miatt olyanokat is lehet írni, mint a fenti printf – de nem érdemes. Az ilyesmi csak zavart okoz. Bár a fordító által készített gépi kód úgyis teljesen ugyanaz lesz, inkább kerüljük a felesleges tömörítést! Jobb külön, két sorba leírni a két, egymástól logikailag független teendőt (értékadás, kiírás).

6. Kiértékelés: vajon mit ír ki?

#include <stdio.h>

int main(void) {
    printf("%d %d\n",
           printf("hello,\n"),
           printf("vilag\n"));

    return 0;
}

Ha mellékhatások vannak a programban, akkor nem mindegy a műveletek sorrendje. Az a = b; b += 1; nem ugyanazt csinálja, mint a b += 1; a = b; utasítássorozat: nem mindegy, hogy a növelés előtti vagy utáni értéket másoljuk.

Egy ezzel kapcsolatos furcsaságot szemléltet a fenti programrész. A printf("%d %d\n", ...) kiírás előtt elő kell állítani a printf() paramétereit, a két számot. A printf("hello,\n") értéke 7 (5 betű, a vessző és a sortörés), a printf("vilag\n") értéke 6 lesz. Azonban nem mindegy, hogy melyik történik meg előbb: a világ, vagy a helló szöveget látjuk előbb a képernyőn. Így a kimenet lehet „helló, világ 7 6”, de esetleg lehet „világ helló, 7 6” is.

Ha kicsit utánajárunk, kiderül, hogy nem tudjuk megmondani, melyik lesz. A paraméterek kiértékelési sorrendjét a fordító szabadon megválaszthatja, a mellékhatások esetleg a nem várt sorrendben történhetnek meg. Így még az is előfordulhat, hogy egyik fordítóval kipróbálva „helló, világ 7 6”-ot kiíró programot, másik fordítót használva „világ helló, 7 6”-ot kiíró programot kapunk.

Emiatt tudnunk kell, hogy nem szabad ilyen kódot írni. Miért olyan fontos ez? Adódhatnak olyan esetek, ahol nem ennyire egyértelmű a hiba. Tegyük fel, hogy van egy beolvas() nevű függvényünk, amely a billentyűzetről kér egy számot, és visszaadja értékként. A beolvas() + beolvas() kifejezés kiértékeléséhez kétszer is használni kell ezt a függvényt, a felhasználó két számot kell kapjon. Kérdés, hogy melyik lesz az összeadás bal és jobb oldali operandusa? Az összeadás kommutatív, ezért mindegy – viszont ha beolvas() - beolvas() kifejezést írunk, akkor már korántsem ez a helyzet! Nem mindegy, hogy melyik a kisebbítendő és a kivonandó, mert a rossz sorrend esetén a várt eredmény ellentettjét kapjuk.

Ilyen esetben különálló utasításokat kell írnunk, mert úgy különálló, ún. kiértékelési pontokat helyezünk el a programban. A fenti példák helyesen:

int h = printf("hello,\n");
int v = printf("vilag\n");
printf("%d %d\n", h, v);

int kisebbitendo = beolvas();
int kivonando = beolvas();
printf("Különbség: %d", kisebbitendo - kivonando);

7. Mik azok a kiértékelési pontok?

A mellékhatások kiértékelési pontokig (sequence point) érvényesülnek.

  • Utasítás végén: ; vagy }
  • if, while, for feltétele után
  • Néhány operátornál menet közben: ?: és && és || és a ,
  • Függvényhívás előtt az összes paraméter kiértékelődik

„A szabvány által nem definiált”

  • A kiértékelési pontok között a mellékhatások sorrendje kötetlen!
  • A függvényparaméterek kiértékelési sorrendje kötetlen
  • Ha kell, több utasításba szedéssel, segédváltozók használatával kényeszeríthetjük a sorrendet.
  • Helytelen: a = a++;
  • Viszont helyes: while (scanf("%c", &c) == 1 && c != '\n')

Ne írjunk olyan kódot, ahol többször használjuk egy változó értékét, amelyre mellékhatás is van! Be kell tartani a következő szabályokat:

  • Ne zsúfoljunk egy kifejezésbe több mellékhatással rendelkező műveletet!
  • Ne keverjük a mellékhatással rendelkező és a rövidzáras operátorokat!
  • Ne tegyünk az if, while… utasítások feltételébe fölöslegesen mellékhatás kifejezést!

Ilyenekre úgysem lesz szükség programozás közben. Ha mégis megsértjük ezeket a szabályokat, nemcsak azt kockáztatjuk, hogy követhetetlen és olvashatatlan lesz a programunk, hanem azt is, hogy egyszerűen nem fog működni. Különböző fordítók (de még akár ugyanaz a fordító is, más beállítások mellett) másképpen fogják értelmezni a kódot! Ettől nem rossz a C. Sőt emiatt lehet gyors, és emiatt van minden elképzelhető fajta számítógépre C fordító. Csak be kell tartanunk a játékszabályokat.

A fentiek miatt nemcsak értelmetlen, hanem még hibás is az a = a++; utasítás. Ez nem csak amiatt rossz, mert a ++ operátornak amúgy is van mellékhatása (az már amúgy is megváltoztatja az a változót), hanem mert nem lehet megmondani, mi lesz az eredménye. Itt egy kifejezésen belül, tehát ugyanazon kiértékelési pont előtt az a változót két mellékhatás is érinti. Az egyik az értékadó operátor mellékhatása, a másik pedig a posztinkremens operátoré. Ezekről nem lehet tudni, hogy milyen sorrendben fognak megtörténni. Ha előbb az értékadás történik meg, utána az inkrementálás, akkor a értéke megnő eggyel. Ha előbb az inkrementálás, és csak utána az értékadás, akkor a értéke nem változik, mivel a posztinkremens kifejezés értéke a változó növelés előtti értéke.

8. A ++ és -- értékadó operátorok

A ++ és -- operátorokat inkremens (increment) és dekremens (decrement) operátoroknak nevezzük, és az értékadó operátorok közé tartoznak. Jelentésük „következő” és „előző”. Mindkét operátornak két változata van, egy poszt (post) és egy pre forma. A kettőt az különbözteti meg, hogy az operátort a változó neve elé vagy mögé írjuk: az a++ kifejezés a posztinkremens operátort, a ++a pedig a preinkremenst használja.

A mellékhatása mindkét alaknak ugyanaz. A ++a és az a++ kifejezés is megnöveli az a változót eggyel. De míg a posztinkremens kifejezés értéke még régi, növelés előtti állapotot mutatja (poszt, kiértékelés után növelés), a preinkremensnél a kifejezés értéke a már megnövelt értékkel lesz egyenlő (pre, azaz kiértékelés előtt növelés). Ezt onnan lehet megjegyezni, hogy a posztinkremensnél: a++ az operátor a változó neve után van, vagyis előbb vesszük az értékét, és később növeljük, a preinkremensnél a változó neve előtt.

Ennek megfelelően, ha a kifejezések értékét is használjuk, a két forma eltérően írható át külön utasításokra:

Preinkremens kifejezés kifejtve

/* tömören */
a = 5;
printf("%d", ++a);  /* 6-ot ír ki */

/* részletesen */
a = 5;
a += 1;            // előre megnöveli
printf("%d", a);

Posztinkremens kifejezés kifejtve

/* tömören */
a = 5;
printf("%d", a++);  /* 5-öt ír ki */

/* részletesen */
a = 5;
printf("%d", a);
a += 1;            // csak utólag nő

A dekremens párjaik ugyanígy működnek. Az alábbi programot csak ki kell próbálni, mindent megmutat:

#include <stdio.h>

int main(void) {
    int a;

    a = 5;
    printf("előtte: %d\n", a);
    printf("értéke: %d\n", ++a);
    printf("utána: %d\n", a);
    printf("\n\n");

    a = 5;
    printf("előtte: %d\n", a);
    printf("értéke: %d\n", a++);
    printf("utána: %d\n", a);

    return 0;
}

9. A ++ és -- tipikus használata

Ciklusban vagy önmagában

i = i+1;
i += 1;
++i;
for (i = 1; i <= 10; ++i)
   …

Nem használjuk az értékét. Itt mindegy, melyik formát írjuk.

Gyakori félreértés, hogy a fenti ciklusba i++-t írva más számokat kapunk eredményül, de ez nem igaz. Hiába van posztinkremens az egyik, preinkremens a másik ciklusban, mind a kettő 1-től 10-ig írja ki a számokat:

for (i = 1; i <= 10; i++)
    printf("%d ", i);

A kiértékelési pontok ismeretében könnyű megmagyarázni, miért lesz ugyanaz a kimenet. Az egyik kiértékelési pont a ciklustörzs vége után van, azaz a printf() után; a másik kiértékelési pont pedig a ciklusfeltétel, az i <= 10 ellenőrzése előtt. Mire ide ér a végrehajtás, addigra az i változó garantáltan megnőtt, függetlenül attól, hogy pre- vagy posztinkremens operátort használtunk. Az utasítások mindkét esetben önmagukban állnak, két kiértékelési pont között.


Tömb feltöltése

szamok[db++] = szam_beolvas();
0.1.2.3.4.
1243

Posztfix: a darabszám régi értéke az index; oda beírjuk, utána növeljük.
Pl. ha db = 2, az új elem szamok[2] helyre kerül, utána db = 3 lesz.

Ami pont stimmel, hiszen az 5 elem a szamok[0]…szamok[4] helyeken van. Vegyük észre: kényelmes dolog, hogy 0-tól számozódnak a tömbindexek!

10. Feltételes kiértékelés: a ?: operátor

A „3 operandusú operátor” (ternary operator)

  • Formája: feltétel ? igaz_kif : hamis_kif
  • Értéke: mint Excel-ben a HA() függvény, ha igaz a feltétel, igaz_kif, különben hamis_kif

Ezt az operátort kérdőjel–kettőspont operátornak, vagy feltételes operátornak szokás nevezni. Néha, főleg angol nyelvű szakirodalomban három operandusú operátornak is nevezik, mivel ez az egyetlen, amelyiknek három operandusa van.


Használata

if (x < 0)
    y = -x;
else
    y = x;
helyett:
y = x<0 ? -x : x;

„abszolút érték”

if (a > b)
    nagyobb = a;
else
    nagyobb = b;
helyett:
nagyobb = a>b ? a : b;

„a nagyobbik”

11. A rövidzár tulajdonság

A logikai &&, || rövidzár tulajdonsága

Ha a bal oldal alapján eldől az eredmény, a jobb ki sem értékelődik!

  • A && B: ha A=HAMIS, nem számít B, az egész biztosan HAMIS
  • A || B: ha A=IGAZ, a kifejezés értéke biztosan IGAZ
  • Ha kiderül az elsőből az eredmény, a másodikat nem értékeli ki
    if (b != 0 && a/b > 3)  // elkerüljük a 0-val osztást!

A ?: feltételes operátor rövidzár tulajdonsága

z = x>y ? x++ : y++;        // neeee

A rövidzár tulajdonság olykor hasznos, de mellékhatásokkal kombinálva veszélyes, mert áttekinthetetlen, érthetetlen programokhoz vezet. Ilyen a fenti is. Első ránézésre úgy tűnik, hogy mindkét változó értéke megnő eggyel. De ez nem igaz, hanem a kettő közül csak a nagyobbik fog nőni, mert x>y esetén csak az x++, amúgy csak az y++ kifejezés értékelődik ki, és csak annak a mellékhatása történik meg. Lehetőleg kerüljük az ilyesmit, ne írjunk ilyeneket! Ne használjunk olyan kifejezést a rövidzár tulajdonsággal rendelkező operátorok operandusaként, amelynek mellékhatása van!

12. A vessző operátor

Több kifejezés egymás utáni végrehajtása egy kifejezésben; értéke az utolsó kifejezés értéke. Leggyakrabban for ciklus fejlécében:

erőltetett
példa
unsigned int i, j;

for (i = 0, j = 1; i < 4; ++i, j *= 2)
    printf("Kettő %u. hatványa: %u\n", i, j);
Kettő 0. hatványa: 1
Kettő 1. hatványa: 2
Kettő 2. hatványa: 4
Kettő 3. hatványa: 8

Vigyázat:
függvényhívás paraméterei: az nem vessző operátor!

Logikai ÉS, VAGY operátorok helyett se használjuk! Tipikus hiba ciklusfeltételbe vesszőt írni valamelyik logikai operátor helyett:

while (oszto < szam, !vanoszto) {   // HIBÁS!
    ...
}

Ez a C számára nem azt jelenti, hogy „amíg osztó kisebb számnál ÉS nincs osztó”, hanem azt, hogy „amíg nincs osztó”. A vessző operátor kiértékeli a bal oldali részkifejezést (szám és osztó összehasonlítása), utána ezt eldobja, és a ciklus feltételének a !vanoszto értékét tekinti. Tehát oszto>=szam esetén is fut tovább a ciklus!

Számítógép, számábrázolás, számrendszerek

14. Számítógép: elvi felépítés

Számítógépek elvi felépítése Notebook számítógép CPU-ja
Processzor
CPU (central processing unit). Ez hajtja végre a fordító által gépi kóddá alakított programjainkat.
Memória (memory)
Ez tárolja az adatokat és a programokat.
Perifériák (peripheral device)
HDD, DVD, megjelenítő, billentyűzet, egér, hangkártya, óra, …
Busz v. sín (bus)
Vezetékek, amelyeken a kommunikáció történik.

A számítógép hardverének részegységei ún. buszokon keresztül vannak összekötve. Egy valódi számítógép lényegében csak abban különbözik a fenti elvi ábrától, hogy több busza van, és az egyes részegységek hierarchikusan vannak azon elrendezve.

15. Adatok és programok

Notebook számítógép memóriája

Memória működése: számok tárolása

  • Karakterek → számok
  • Képek → fényesség értékek → számok
  • Hang → levegőnyomás értékek → számok
  • Gépi utasítások (mov, add) → számok

Érdekesség: A végrehajtott program, és az adatok, amelyeken a program dolgozik, lehetnek külön memóriában is. Az első automatikusan működő számítógépek a programot nem a belső memóriájukban tárolták, hanem papírszalagról vagy lyukkártyáról olvasták be azt futás közben. Ennek egyszerűen az volt az oka, hogy nagyon költséges és bonyolult volt relékből (lásd lent) memóriát csinálni. A tervezők pedig ott spóroltak, ahol tudtak. Ahogyan a technika fejlődött, úgy vált lehetővé, hogy a programot is a központi memóriában tárolják. Ezt az elvet Neumann János (John von Neumann) javasolta kollégáival, és ma Neumann-féle architektúrának nevezzük. Az első ilyen elven működő számítógép az EDVAC nevet viselte. Ez egyben az első kettes számrendszert használó, már nem elektromechanikus, hanem teljesen elektronikus számítógép is volt. A külön programmemória elve a fentiek ellenére nem halt ki: ezt ma Harvard architektúrának nevezzük, az EDVAC-nál régebbi Mark I számítógép nyomán.


CPU működése

  • Elemi, egyszerű lépések: gépi utasítások
  • A fordítóprogram állítja elő a C forráskódból
x+=2;
81 10 55    mov   eax, [1055]   ; x memóriából processzorba
83 00 02    add   eax, 2        ; processzor hozzáad 2-t
79 10 55    mov   [1055], eax   ; eredmény memóriába

A számítógép processzora rendkívül egyszerű, gépi nyelvű utasításokat tud csak végrehajtani. A gépi nyelvnél legtöbbször még egy egyszerű változónövelést is három lépésre kell bontani: 1. a változó értékének kiolvasása a memóriából, 2. az érték növelése, 3. az érték visszaírása a memóriába. A processzor a működés közben így legtöbbet a memóriával kommunikál, jóval többet, mint bármelyik perifériával. Főleg, hogy általában a számítógépeknek közös memóriájuk van az adatok és programok számára.

16. Számrendszerek (numeral system)

A római számokkal bizonyos műveleteket, pl. az összeadást nagyon könnyű elvégezni: pl. III + VIII = VIIIIII = VVI = XI. Más műveletek, a szorzás és az osztás bonyolultak. A matematikusok nagy találmánya a nullás számjegy: ez teszi lehetővé azt, hogy tömören (kevés számjeggyel), ugyanakkor helyiértékenként egységes jelölésrendszerrel tudjuk leírni a számokat. A nullás számjegy a leírt számokban helyőrzőként szerepel: a 203 számban pl. azt jelzi, hogy az 2-es a százasok számát tárolja. A hindu-arab számírásban nincs külön jele az egynek, tíznek, száznak, ezernek. (Vegyük észre: a tízes csoportosítás ettől független! A római számokban is létezik már az 1-10-100-1000 fogalma. Nem a tízes csoportosítás a lényeg, hanem a jelölésmód, ami a nulla bevezetésével válik lehetővé.)

A mindennapi életben a tízes számrendszert használjuk. Ebben az egyes helyiértékek a 10 hatványait követik. Ennek oka nagyon egyszerű: azért alakult így ki, mert tíz ujjunk van. Más számrendszerek is használhatóak, és a hindu-arab számírás logikus felépítése miatt ezekben a szabályok pontosan ugyanazok, mint a tízes számrendszerben.

1
1
1

számrendszerek
alappélda
10 decimális1203tíz = 1·103 + 2·102 + 0·101 + 3·100
8 oktális377nyolc = 3·82 + 7·81 + 7·80 = 255tíz
2 bináris1101kettő = 1·23 + 1·22 + 0·21 + 1·20 = 13tíz

A létező legegyszerűbb számrendszer a kettes alapú. Ebben csak kétféle számjegy van, a 0 és az 1. Hogy miért ez a legegyszerűbb? Mert ebben az összeadó és a szorzótábla nem tízszer tízes, hanem mindössze kétszer kettes.

bináris összeadás
+01
001
1110
bináris szorzás
×01
000
101

17. Érdekesség: kapcsolók generációi

A mai számítógépek digitális elven működnek. Csak egész számokkal tudnak dolgozni, amelyeket kettes számrendszerben tárolnak. A kettes számrendszer előnye az, hogy csak két számjegy van benne: 0 és 1. Ez elektronikusan könnyen kezelhető (nincs áram, van áram), ezért a működést kapcsolók adják. Bármi, ami kapcsolóként tud működni, az használható számítógép építésére is.

Az alábbi fényképek a számítógépek generációit mutatják. Ezek elvben nem különböznek egymástól, csak a gyakorlatban, méghozzá abból az egyetlen szempontból, hogy milyen elektronikus, vagy esetleg még elektromechanikus eszközt használtak kapcsolónak. Az első három képen lévő eszköz egyetlen kapcsolónak felel meg, míg a jobb alsó képen látható integrált áramkörön már sok millió kapcsoló van. Összehasonlításképp: 3-4000 kapcsoló használatával már egészen jól használható processzor tervezhető, egy Core i7 processzorban viszont már 730 millió darab van.

Relék (relay): az áram hatására a bennük lévő tekercsben (jobb oldalt) mágneses tér keletkezik, és így lehet vezérelni a kapcsolót (bal oldalt). Egy ilyen eszköz kb. 3-4 cm nagy. Manapság is használnak ilyet nagyobb áramok kapcsolására, pl. autókban is.

Elektroncső (tube): a bennük lévő vákuumban repülő elektronok mozgása vezérelhető az elektromos tér változtatásával. Ezek is viszonylag nagyok: 3-4 cm, ráadásul fűteni kell a belsejüket, hogy az elektronok kilépjenek a fémből.

Tranzisztor (transistor): a félvezető anyagok vezetőképessége (ellenállása) elektromos úton szabályozható, így kapcsolónak is használhatóak. A képen látható tranzisztorban a félvezető szilícium darabka 1 mm-nél is kisebb. A védő fém vagy műanyag tokozás nagyobb, 3-4 mm-es.

Integrált áramkör (integrated circuit): ebben is tranzisztorok vannak, azonban az előzőnél jóval kisebbek. Egy 1 cm2 méretű szilícium lapra akár több tízmillió transzisztor integrálható, amelyek egyesével alig néhány tíz nanométeresek (vagyis méretük egy ember hajszál vastagságának ezrede). A fenti processzor mikroszkóp alatt forgatva egy videón is látható.

18. Binary digit = bit

A számrendszerek közötti átalakítás könnyű: csak el kell osztanunk (vagy meg kell szoroznunk) a számokat, számjegyeket az adott számrendszer alapszámának hatványaival.

Átalakítás kettesből tízesbe

Az átalakítás lépései: a szám számjegyeit (alaki értékek) összeszorozzuk kettő megfelelő hatványaival (helyi értékek). Az így kapott számok (valódi értékek) összege adja az eredményt.

Kettesből tízesbe
számjegy 
helyiérték×64×32×16×8×4×2×1
valódi érték 
Szám: kettő        tíz

Átalakítás tízesből kettesbe

Az átalakítás lépései: a számot leosztjuk kettő első olyan hatványával, amely kisebb nála. Az eredmény egy számjegy, a hányadost pedig felírjuk a következő oszlopba. Így folytatjuk az egyre kisebb hatványokkal, amíg el nem érünk 0-ig. (A legutolsó esetben eggyel osztunk, aminek a maradéka biztosan nulla lesz.) Az osztások során sehol nem kaphatunk 1-nél nagyobb értéket; ha ilyen történne, akkor kettő egy nagyobb hatványától kell indulnunk.

Tízesből kettesbe
maradék 
helyiérték/64/32/16/8/4/2/1
számjegy 
Szám: tíz        kettő

A más számrendszerekbe átalakítás ugyanígy működik, csak az ottani alap hatványait kell használni.

HDD: akár 2 TiB

Bitek és bitcsoportok

bit
Az információ alapegysége: 0 vagy 1.
bájt (byte): bitek csoportja
A memória legkisebb egysége: általában 8 bit.
szó (word): több bájtos adategység
Általában 4 bájt (32 bit), vagy 8 bájt (64 bit).

DVD: 4.3 GiB

Előtagok (prefix)

A kettes számrendszerbeli működés miatt a szokásos mértékegységeknek megvan a bináris párja. Bár a kilo- előtag általában ezret jelent, a számítástechnikában inkább 1024-et, azaz 210-t. Ezt azért választották meg így, mert a kettő között nagyon kicsi a különbség. Sajnos gyakran keverik is a kettőt. A merevlemezgyártók például előszeretettel használják a kilo=1000 jelölést, mert így nagyobb kapacitást írhatnak rá az eladott merevlemezekre. Hogy ne kelljen mindig hozzátenni, ha pontosak akarunk lenni, hogy a bináris vagy a decimális prefixumról beszélünk, bevezették a kibibájt, mebibájt stb. jelöléseket. Egy DVD kapacitása így 4,7 gigabájt, azaz 4,3 gibibájt.

kilobájt (kB) és kibibájt (KiB)
103=1000 ≈ 210=1024 bájt.
megabájt (MB) és mebibájt (MiB)
106=1000000 ≈ 220=1048576 bájt.
gigabájt (GB, GiB), terabájt (TB, TiB)
109≈230 és 1012≈240 bájt.

Érdekesség: A „binary digit”, azaz bináris számjegy szókapcsolatot eredetileg „bigit” vagy „binit” néven rövidítették. Később a „bit” szót John Tukey, amerikai matematikus javasolta. (Ő találta ki a „software” szót is.) A bit szó tudományos írásban először Claude Shannon M.Sc. diplomamunkájában szerepelt, amelynek címe A Symbolic Analysis of Relay and Switching Circuits. Ebben megmutatta, hogy az addig telefonközpontokban használt relék segítségével logikai problémák is megoldhatóak. Pár évvel később megépült az első relékből felépített, kettes számrendszert használó számítógép, a Harvard Mark I. Azóta gyakorlatilag nem készül olyan számítógép, amely nem bináris elven működne.

19. Egész típusok a C-ben

Mivel a számítógép nem végtelen nagy, a tárolt adatok sem lehetnek azok. Ha szeretnénk egy számot eltárolni, akkor arra egy véges, fix méretű memóriaterületet kell kijelölnünk. Ezáltal keletkezik „legkisebb” és „legnagyobb” szám, bár matematikailag ez értelmetlenül hangzik. A számítógépen használt számok emiatt a matematikai számfogalomnak csak tökéletlen modelljei. A matematika számai lehetnek egészek, racionálisak, irracionálisak – a programjainkban ezzel szemben rögzítenünk kell, hogy egy változó típusa egész-e vagy nem, sőt még a tartományban és a pontosságban is be vagyunk korlátozva.

Egész számok tipikus méretei
névtipikus
méret
tipikus tartományprintf,
scanf
megjegyzés
signed char
unsigned char
8 bit-128…127
0…255
legkisebb,
mindig 1 bájt
signed short int
unsigned short int
16 bit-32768…32767
0…65535
%hd
%hu
signed int
unsigned int
32 bit-231…231-1 (±2·109)
0…232-1 (4·109)
%d
%u
tipikus szóhossz
a gépen
signed long int
unsigned long int
32 bit-231…231-1 (±2·109)
0…232-1 (4·109)
%ld
%lu
néha akár
64 bit
signed long long int
unsigned long long int
64 bitkb. ±9·1018
kb. 0…1,8·1019
%lld
%llu
signed long a;
short b;
unsigned c;

Egy egész szám tárolására egy vagy több bájtot foglalunk le. A számítógép ezeket általában „hardverből”, áramkörileg tudja kezelni. Az egész típus neve a C-ben int, amelyhez társulhatnak ún. módosító jelzők (specifier). Pl. az unsigned jelző az előjel nélkülit jelenti, a signed pedig az előjelest. Vannak bizonyos alapértelmezések:

  • Az int típus (méret megadása nélkül) mindig azt a méretet jelenti, amely az adott számítógépen az optimális, vagyis amellyel a processzor leggyorsabban tud dolgozni. Ez manapság a 32 bitest szokta jelenteni, de lehetnek eltérések.
  • Ha nincs signed vagy unsigned megadva, akkor signed, azaz előjeles lesz a változó. Kivétel a char: ennél nincs alapértelmezés. Ajánlatos így használni:
    • char: betű
    • signed char: 1 bájtos, előjeles egész szám
    • unsigned char: 1 bájtos, előjel nélküli egész szám
  • Ha van módosító megadva, akkor az int szó elhagyható. Pl. long x; ugyanaz, mint long int x; és signed x; ugyanolyan típusú változót hoz létre, mint a signed int x; utasítás.

A szabvány nem köti meg a típusok pontos méretét, csak a minimális méreteket, hogy minél többféle számítógépen működhessenek a C programok. A fenti táblázat a tipikus értékeket mutatja. Ritka, de van, ahol a char 9 bites vagy az int csak 16! Erre figyelni kell, amikor hordozható (portable) programokat szeretnénk írni, amelyek különféle gépeken működnek. Főleg, ha Interneten kommunikálnak egymással vagy egymás fájljait kell olvassák.

Újabb C szabványokban a printf() és a scanf() a signed és unsigned char-t is ismeri; %hhd és %hhu a hozzájuk tartozó formátumjelző.

20. Túlcsordulás és kettes komplemens

A számokat tehát kettes számrendszerben, bitekkel tároljuk. A biteket az alsó helyiértékektől számozzuk, aszerint, hogy 2 hányadik hatványának felel meg. 20 → 0. bit, 21 → 1. bit stb. A helyiértékek matematikából megszokott neve alapján a legkisebb helyiértékű bitet (least significant bit, LSB) legalsónak, a legnagyobb helyiértékűt (most significant bit, MSB) legfelsőnek nevezzük.

Túlcsordulás: ha nem fér el az eredmény

255+1 = 0
0-1 = 255

Számítások végzése közben könnyen a véges bitszám korlátaiba ütközhetünk. Ha egy eredmény nem fér el az adott bitszámban, túlcsordulás történik. Emiatt pl. 8 biten: 255+1 = 0, és 0-1 = 255.

 11111111

+00000001
─────────
100000000

kettes komplemens
bitekérték
11111110-2
11111111-1
000000000
000000011
000000102

Negatív számok tárolása: a kettes komplemens

Negatív számokat úgy tudunk tárolni, hogy egy bitet felhasználunk az előjelnek. A számolás sémája a jobb oldali táblázatban látható. A gondolatmenet lényege az, hogy a −1-et egy olyan bitsorozatnak kellene jelentenie, amelyhez 1-et adva 0-t kapunk. a −2-t egy olyannak, amelyhez 2-t adva nullát kapunk, és így tovább. Így alakul ki a jobb oldali táblázat.

Ennek az ún. kettes komplemens ábrázolásnak az előnye, hogy a túlcsordulások miatt automatikusan előállnak a helyes pozitív/negatív értékek – nem kell megkülönböztetnie a hardvernek (az áramköröknek) a pozitív és negatív számokkal végzett összeadásokat, kivonásokat. Egy négy bites példát tekintve: 1110+0011=0001, azaz −2+3 = 1, ami éppen a helyes eredmény. Ezért mindenhol ezt szokás használni. (Egyébként ugyanez működik tízes alapú számrendszerben is. Pl. 3 számjegyen 17+999=1016, amiből ha eldobjuk a túlcsordulás miatti ezrest, akkor 16-ot kapunk, ami pont 17-1.)

Egy bitenként adott, kettes komplemens ábrázolású számsort az alábbiak szerint lehet értelmezni.

  • Az előjelet a legfelső bit dönti el
  • Legfelső bit 0 → nemnegatív szám.
    00000011 → értéke 3tíz
  • Legfelső bit 1 → negatív szám.
    Értéke a többi bit negáltja + 1: 11111110 0000001kettő + 1 = 2tíz → a szám −2
  • Lásd: Digit!

Szerencsére ezt a számítógép hardvere megoldja helyettünk. Csak azért kell tudni róla, hogy értsük, két pozitív szám összeadásakor hogyan kaphatunk negatív értéket. A témakör a Digitális technika tárgyból még fog szerepelni részletesen.

21. A túlcsordulás élőben

#include <stdio.h>

int main(void) {
   unsigned char kicsi;
   int i;

   kicsi = 1;
   for (i = 0; i < 20; i += 1) {
      printf("%u\n", kicsi);
      kicsi *= 2;
   }

   return 0;
}

Típusok, amelyekkel érdemes kipróbálni: unsigned char, signed char, short. Bármekkorával is próbáljuk, előbb-utóbb előjön a probléma. Ha kettővel szorozgatjuk a számot, akkor előbb-utóbb nulla lesz az eredmény, így könnyű tetten érni a hibát. Ha hárommal szorzunk a ciklusban, akkor már kevésbé!

22. Valós számok ábrázolása

Mivel a digitális elven működő hardver csak egész számokkal képes dolgozni, a törtek tárolását vissza kell vezetni egész számokra. Ez megoldható egy normálalakszerű sémával, ahol a kitevő lehet negatív is. A normálalak természetesen a gép (és a tervezőmérnökök) kényelme érdekében nem tízes, hanem kettes alapú.

Lebegőpontos ábrázolás (floating point)

± mantissza · 2karakterisztika
  • Pl. 4 = 1·22 = 100kettő, ¼ = 1·2-2 = 0.01kettő
  • Véges a méret: adott bitszám a mantisszának és a kitevőnek
  • Korlátos az ábrázolható számtartomány és a pontosság is

Lebegőpontos számok a számegyenesen

Emiatt a számítások eredménye sokszor pontatlan! Még az is előfordulhat, hogy a+b=a, ahol a egy nagy, b pedig egy kicsi szám. Ez történik (pirossal az értékes jegyek):

a    10000000000000.0000000
b   +             0.0000001
    ───────────────────────
a+b  10000000000000.0000001 → 10000000000000 lesz!

Így az == és != operátorokat valós számokon elvileg nem is lenne szabad használni, de a többi is adhat váratlan eredményt. Ehelyett a módszer az, hogy az összehasonlításokat egy adott tűréssel végezzük. Például egyenlőség helyett ezt a kifejezést használuk: fabs(a-b)<0.0001. Tehát ha a két szám különbsége kevesebb egy tízezrednél, akkor egyenlőnek tekintjük őket.

23. Lebegőpontos típusok C-ben

Lebegőpontos típusok
névtipikus
tartomány
pontosság
(tizedesjegy)
printfscanf
float±10±38kb. 7%f
double±10±308kb. 15%f%lf
long double±10±4932kb. 18%Lf

Ha valós szám kell, legtöbbször double-t használunk. A támogatott típusok itt is eltérnek számítógépenként. A long double gyakran nem létezik, hanem a double szinonímája csak. Figyeljünk arra is, hogy a printf()-nek és a scanf()-nek adott formátumkód a double esetében nem ugyanaz. A kiíráshoz ennél %f kell, a beolvasáshoz %lf. (Ennek mélyebb, történelmi okai vannak, amelyek itt nem lényegesek. Némelyik környezettel működik a %lf is, de inkább a %f-et használjuk a printf()-nél!)

24. Lebegőpontos ábrázolás: furcsaságok

Néhány példa a valós számábrázolás pontatlanságaira:

#include <stdio.h>

int main(void) {
    if (0.1 + 0.2 == 0.3)
        printf("Egyenlőek!\n");
    else
        printf("Nem egyenlőek!\n");
    printf("\n");

    printf("%.16f\n", 9.95);
    printf("\n");

    double szam;
    for (szam = 0; szam < 1; szam += 0.1)
        printf("%f\n", szam);
    printf("\n");

    return 0;
}

A tizedek nem ábrázolhatóak pontosan kettes számrendszerben, mert 1/10 = 1/(2*5), tehát nem 2 hatványa. (Vegyük észre, hogy tízes számrendszerben pont azok a törtek ábrázolhatóak pontosan, amelyek nevezőjének prímtényezői 2 és 5. Minden másból végtelen, szakaszos tizedes tört lesz.) Emiatt sem a 0,1, sem a 0,2 nem ábrázolható, csak közelítőleg; az összegük a kerekítési hiba miatt nem adja ki a 0,3 értéket. Hogy ez a pontatlanság tényleg előjön-e, az persze a számítógép típusától is függ.

A 9,95 egy olyan szám, ahol ez közvetlenül tetten is érhető, akár műveletvégzés nélkül is. Ha pénzről van szó, általában századokkal szoktunk dolgozni (pl. euró és cent). Banki szoftverekben a századok miatt nem szoktak lebegőpontos számokat használni, hiszen még egy ilyen egyszerű pénzösszeg, mint a 9,95 € (9 euró 95 cent), sem adható meg pontosan.

A harmadik rész ciklusa adhat olyan kimenetet, amelyben 11-szer történik meg a kiírás. A dolog külön érdekessége, hogy a ciklus, bár feltételében szam<1 van, gyakorlatilag 1-nél is lefut! Nem 10-szer (0; 0,1; … 0,9), hanem 11-szer! És ez teljesen esetleges; szam=1, szam<2 esetén csak 10-szer fut le, mivel ott úgy alakulnak a kerekítési hibák. Az eredmény géptípusonként változhat, a pontosság függvényében. Ha a printf() formátumsztringjébe %.16f-et írunk, akkor egyből látszik, mi okozza a hibát: a helyzet ugyanaz, mint az előbb. A jelenség azért érhető ilyen könnyen tetten, mert a tízes számrendszerben „kerek” számok, mint a 0,1, kettes számrendszerben nem azok. A 0,1 például azért nem, mert 1/10 = 1/2/5, és 2-nek nem osztója az 5. Kettes számrendszerben „kerek” törtek esetén (0,5; 0,25; 0,125; 0,0625…) nincs ilyen probléma. Érdemes kipróbálni!

25. Számok a C nyelvben

dechex
10a
11b
12c
13d
14e
15f

Számok megadása C-ben (numerikus literálisok)

  • 23: egész szám
    • Bináris konstans C-ben nincsen!
    • 012 = 12nyolc (0-val kezdődik), 0xFCE2 = FCE2tizenhat (0x-szel kezdődik)
    • Ritkán: 12U, 12u – unsigned, 12L, 12l – long
  • 3.14: valós (van benne . vagy e)
    • .45 = 0,45 (a legelső 0 elhagyható), 6.022e23 = 6,022·1023
    • Ritkán: 12.3F – float, 12.3L – long double

A 16-os számrendszer

  • 4-es csoportokban (nibble) átalakíthatóak
  • Pl. 10011111kettő = 9ftizenhat
  • A 10…15 alaki értékek jelölésére az a…f vagy A…F betűket használjuk

A kettes számrendszerben leírt számok nagyon sok számjegyből állnak. Ezért sokszor helyette a tizenhatos (hexadecimális) számrendszert szoktuk használni. Ez „rokon” a bináris számrendszerrel. Mivel 24=16, a bitek négyes csoportokban adnak egy hexadecimális számjegyet.

Használhatnánk a nyolcas számrendszert is azzal a céllal, hogy spóroljunk a számjegyekkel. Azzal azonban van egy kis probléma. Manapság szinte mindegyik számítógépen nyolc bites a bájt. Ha egy ilyet nyolcas számrendszerben írunk le, akkor 2-3-3 bites csoportok adódnak: 10'101'111kettő=257nyolc. Ezzel önmagában nem is lenne probléma, azonban ha egy két bájtos, azaz 16 bites számot szeretnénk átírni, akkor az egymás mellé tett bájtok nyolcas átírása eltér attól, mint a két bájtté külön. Ha az előző bitsorozatot kétszer egymás mellé írjuk, annak átírása: 1'010'111'110'101'111kettő=127657nyolc, nem pedig 257257nyolc, ahogyan a két nyolcas számrendszerbeli egymás után írása miatt gondolnánk. A tizenhatos számrendszerrel nincs ilyen probléma, mert ott nem három, hanem négy bit van egy csoportban, és egy bájt nyolc bitje pontosan két csoportot ad. A négybites csoportok angol neve a nibble (esetleg nybble).

Bitműveletek

27. Boole-féle algebra

Lásd:
Digit

A számítógépek processzorai – mivel maguk is bitekkel dolgoznak – általában tartalmaznak olyan gépi utasításokat, amelyekkel a tárolt számok egyes bitjeit tudjuk állítgatni, mégpedig a Boole-féle algebrából ismert műveletekkel. Ebben az algebrában a változóknak két értékük lehet: HAMIS és IGAZ, vagy bitekben gondolkodva 0 és 1. A sokféle művelet közül a fontosabbak a következők.

NEM
(NOT)
AA
01
10
ÉS
(AND)
ABAB
000
010
100
111
VAGY
(OR)
ABA+B
000
011
101
111
KIZÁRÓ VAGY
(XOR)
ABA⊕B
000
011
101
110

Érdemes megvizsgálni ezen műveletek tulajdonságait egy különös szemszögből: az egyik bemenetet változatlannak gondolva azt figyelni, hogyan reagál a kimenet a másik bemenet megváltozására. Az igazságtáblák alapján:

  • A VAGY műveletnél: ha függőlegesen a 0-t választjuk, akkor a kimeneten a vízszintesen kiválasztott bit jelenik meg. Ha függőlegesen az 1-et, akkor pedig fixen 1. Szóval a VAGY művelet lemásolja az egyik bemenetét, ha a másik 0, és fixen 1-et ad a tőle függetlenül, ha a másik 1.
  • Az ÉS művelet ugyanezt csinálja nullákkal. Ha az egyik bemenet 0 (függőleges), akkor a másik bemenet (vízszintes) értékétől függetlenül 0-t ad a kimeneten. Ha az előbbi bemenet 1-es, akkor az utóbbit lemásolja.
  • Az XOR (exclusive or), ha az egyik bemenet 0, akkor a másikat lemásolja. Ha az előbbi 1-es, akkor pedig az utóbbit másolja – de negálva.

A következő bitműveletes feladatoknál egyértelmű lesz, hogy miért hasznosak ezek a megfigyelések. A programozásban ezekkel dolgozunk. Pl. a VAGY művelet fenti tulajdonsága miatt használható arra, hogy egy többites szám egy adott bitjét 1-be állítsuk, miközben a többi változatlanul marad.

Bitműveletek használata

A bitműveleteket sok területen alkalmazhatjuk:

  • Minden bit kihasználása: egy 8 bites unsigned char változóban 8 IGAZ/HAMIS értéket tárolhatunk.
  • Hardverközeli programozás: hardvereszközben adott bit 1-esbe állításával bekapcsolunk egy funkciót (pl. grafikus kártyában egérmutató láthatósága).
  • Hálózatprogramozás: egy internet adatcsomag adott bitjei jelzik, hogy kapcsolat létrehozása, lebontása stb. történik-e.
  • Kriptográfia: titkosítási és ellenőrző összegeket előállító algoritmusok.
  • Véletlenszámok: pszeudo-véletlenszámokat előállító algoritmusok.

Egy nagyon fontos megjegyzés: a bitenkénti műveletek nem keverendőek a logikai műveletekkel! Míg a bitenkénti műveletek egy vagy két szám azonos helyiértékű bitjein dolgoznak páronként, addig a logikai műveletek egy „teljes” szám (a változó méretének megfelelő) értékével. Míg a bitenkénti ~ művelet egy egész szám összes bitjét ellentettjére állítja, a logikai ! művelet nem nulla számból nullát csinál, és nullából nem nullát (egyet). Ugyanígy, a bitenkénti | nem ugyanaz, mint a logikai ||, és a bitenkénti & mást csinál, mint a logikai && művelet. A ^ kizáró vagy műveletnek nincsen logikai párja.

28. Bitműveletek: léptetés (shift)

A léptető operátorok egy szám bitjeit eltolják.

Balra léptetés: << operátor

10000110  előtte
00110000  balra 3-mal
x = 134 << 3;
  • A felső három elveszik, alulról három nulla jön be.
  • Mintha szoroznánk 23-nal

Jobbra léptetés: >> operátor

01010110 előtte
00010101 jobbra 2-vel
x = 86 >> 2;
  • Az alsó kettő elveszik, felülről két nulla jön be.
  • Mintha osztanánk 22-nal

Figyelem! A bitenkénti léptetés operátorok ugyanolyanok, mint az összeadás vagy a szorzás: két operandusuk van, és létrehoznak egy harmadik számot, az eredményt. Eközben a két operandusukat nem változtatják meg, nincs mellékhatásuk! A b = a<<3 kifejezés így az a változó értékét változatlanul hagyja, és csak b változik! A többihez hasonlóan viszont ezeknek is megvan a rövidített, értékadó párjuk:

  • x <<= 3 ugyanaz, mint x = x<<3, és
  • x >>= 2 ugyanaz, mint x = x>>2.

Kettő hatványainak kiírása bitművelettel

int i;
for (i = 0; i < 32; ++i)
    printf("%2d. %10u\n", i, 1U << i);

"%10u", mert unsigned, előjel nélküli számot írunk ki. És 1U, mert az 1-et unsigned int-nek kell tekinteni, nem pedig int-nek. int-nek tekintés esetén a 31-edik léptetés már túlcsordulna, az előjelbitbe csúszna az 1-es.

29. A bitenkénti VAGY művelet: |

Ez a művelet két szám bitjeit hozza VAGY kapcsolatba páronként, vagyis az egyik szám 0. bitjét a másik szám 0. bitjével, 1. bitet az 1. bittel stb. A VAGY műveletnél „az eredmény 1, ha bármelyik 1”. Ezt adott bitek 1-be állítására szokás használni.

|
„pipe”,
álló vonal
VAGY művelet – kattints a számokra!
76543210
A 01001010
B 00100000
A|B 00000000

Feladat: állítsuk egy szám 5. bitjét 1-be!

  • VAGY: „ha valamelyik 1, az eredmény 1”
  • Olyan maszk (mask) kell, amiben az 5. bit 1-es, többi 0
  • Ez a szám az 1<<5:
    00000001 1
    00100000 1<<5
  • x = x | 1<<5; vagy egyszerűbben: x |= 1<<5;

30. A bitenkénti kizáró vagy: ^

A két operandusának ugyanolyan sorszámú bitjeit hozza KIZÁRÓ VAGY kapcsolatba. Mivel a KIZÁRÓ VAGY műveletnél az eredmény akkor 0, ha egyformák a bemeneti bitek, és akkor 1, ha nem egyformák, ezt adott bitek negálására szokás használni.

Digites terminológiában: a KIZÁRVÓ VAGY kapu a vezérelhető (ki/bekapcsolható) inverter.

^
„caret”,
kalap
KIZÁRÓ VAGY művelet – kattints a számokra!
76543210
A 01001010
B 00011000
A^B 00000000

Feladat: negáljuk egy szám 3. és 4. bitjét!

  • KIZÁRÓ VAGY: „ha valamelyik 1, az eredmény a másik negáltja”
  • A maszk: 1<<3 | 1<<4
  • Tehát: x = x ^ (1<<3 | 1<<4);
  • Vagy másképpen: x = x ^ 1<<3 ^ 1<<4;

Az x=x^y kifejezés rövidítve is írható: x^=y, vagyis a fenti példa rövid változata: x ^= 1<<3 | 1<<4;

31. A bitenkénti ÉS művelet: &

A két operandusának ugyanolyan sorszámú bitjeit hozza ÉS kapcsolatba.

&
„et” vagy
„és” jel
ÉS művelet – kattints a számokra!
76543210
A 01001010
B 00001000
A&B 00000000

Feladat: ellenőrizzük, egyes-e a szám 3. bitje!

  • Vágjuk ki csak azt a bitet, minden másikat nullázva
  • ÉS: „ha az egyik 0, a kimenet is az lesz”
  • A maszk: 1<<3
  • Tehát: if ((x & 1<<3) != 0) …
  • A zárójelezés a műveletek erőssége (precedenciája) miatt szükséges. Különben 1<<(3!=0)-t jelentené a kifejezés jobb oldala.

32. A bitenkénti tagadás: ~

A ~ operátor a bitenkénti negálás jele C-ben. Ezt egy szám vagy változó neve elé írva olyan számot kapunk, amelyben minden bit ellenkezőjére (negáltjára) van váltva.

~
„tilde”,
hullámvonal
NEM művelet – kattints a számokra!
76543210
A 11110111
 ~A 00000000

Feladat: állítsuk egy szám 3. bitjét 0-ba!

  • ÉS: „az eredmény 0, ha bármelyik 0”
  • Maszk, amelyben csak a 3. bit 0: ~(1<<3), mert:
    00000001 = 1
    00001000 = 1<<3
    11110111 = ~(1<<3)
  • Tehát: x = x & ~(1<<3); vagy rövidebben: x &= ~(1<<3);

33. Példa: Eratoszthenész szitája, spórolósan

Egy bájtban, amennyiben az 8 bites, 8 logikai értéket lehet tárolni. Írjuk meg a gyakorlat „Eratoszthenész szitája” feladatát úgy, hogy egy unsigned char típusba tömörítse 8 egymás melletti szám prím/nem prím tulajdonságát – azaz csökkentsük nyolcadára az ottani program memóriahasználatát!

Ebben a programban 8000-ig fogjuk vizsgálni a prímszámokat. A létrehozott tömb most bool prim[8000] helyett unsigned char prim[1000] típusú. Az eredeti változatban 8000 logikai érték (bool) volt; most 8 logikai értéket, bitet sűrítünk egy bájtba (unsigned char), és így a tömbnek már csak 1000 eleműnek kell lennie. Mindegyik logikai érték megtalálható ebben a tömbben, valahányadik bájt valahányadik bitjeként. Mivel minden bájt 8 bitből épül fel, egy vizsgálandó sz sorszámhoz a bájt sorszámát az sz/8 kifejezéssel kaphatjuk meg. Azon belül a bit sorszámát pedig az sz%8 kifejezés adja.

[0]01234567
[1]89101112131415
[2]1617181920212223
......
#include <stdio.h>

int main(void) {
    unsigned char prim[1000];
    int sz, t;

    for (sz = 0; sz < 1000; sz++)
        prim[sz] = ~0;
    for (sz = 2; sz < 8000; sz++) {
        if ((prim[sz/8] & 1<<(sz%8)) != 0) {
            for (t = sz*2; t < 8000; t += sz)
                prim[t/8] &= ~(1<<(t%8));
        }
    }
    for (sz = 2; sz < 8000; sz++)
        if ((prim[sz/8] & 1<<(sz%8)) != 0)
            printf("%8d", sz);
    
    return 0;
}

A program elején „üres”, csupa 1-ekből álló tömbbel indulunk: mindent prímszámnak tekintünk. Ezért a tömböt feltöltjük olyan értékekkel, amely csupa 1-es bitekből áll. Ehhez a 0-t bitenként tagadjuk: ~0.

Ezután megyünk végig a szitán az sz-es ciklussal. A tömb 1000 elemű, de minden elem 8 bitet tárol, ezért 8000-ig tudjuk vizsgálni a számokat! Amint találunk egy prímet, annak a többszöröseit húzzuk majd ki. A bit vizsgálatához a bitenkénti ÉS & műveletet használjuk: előállítjuk az 1<<(sz%8) maszkot, amely csupa 0-ból áll, kivétel az sz%8-adik bitet, ahol 1-es van; ezt a maszkot „ÉS”-elve a tömb eleméhez 0-t kapunk, ha a vizsgált bit nulla értékű, amúgy pedig nem nullát.

A t-s ciklus húzza ki az sz többszöröseit a tömbből, vagyis jelöli be, hogy nem prímek. Ehhez nullába kell állítania a többszöröshöz tartozó bitet, ami a bitenkénti ÉS & művelettel tehető meg.