3. Alapműveletek és kifejezések
3.7. Bitműveletek
c = (1 > 2 ? 4 : 7) * (2 < 3 ? 4 : 7) ; //28
3.7. Bitműveletek
Régebben a számítógépek igen kevés memóriával rendelkeztek, így nagyon értékesek voltak azok a megoldások, amelyek a legkisebb címezhető egységen, a bájton belül is lehetővé tették több adat tárolását és feldolgozását. Bitműveletek segítségével egy bájtban akár 8 darab logikai értéket is elhelyezhetünk.
Napjainkban ez a szempont csak igen ritkán érvényesül, és inkább a program érthetőségét tartjuk szem előtt.
Van mégis egy terület, ahol a bitműveleteknek napjainkban is hasznát vehetjük, nevezetesen a különböző hardverelemek, mikrovezérlők programozása. A C++ nyelv tartalmaz hat operátort, amelyekkel különböző bitenkénti műveleteket végezhetünk előjeles és előjel nélküli egész adatokon.
3.7.1. Bitenkénti logikai műveletek
A műveletek első csoportja, a bitenkénti logikai műveletek, lehetővé teszik, hogy biteket teszteljünk, töröljünk vagy beállítsunk:
Operátor Művelet
~ 1-es komplemens, bitenkénti tagadás
& bitenkénti ÉS
| bitenkénti VAGY
^ bitenkénti kizáró VAGY
A bitenkénti logikai műveletek működésének leírását az alábbi táblázat tartalmazza, ahol a 0 és az 1 számjegyek a törölt, illetve a beállított bitállapotot jelölik.
a b a & b a | b a ^ b ~a
0 0 0 0 0 1
0 1 0 1 1 1
1 0 0 1 1 0
1 1 1 1 0 0
A számítógép hardverelemeinek alacsony szintű vezérlése általában bizonyos bitek beállítását, törlését, illetve kapcsolását igényli. Ezeket a műveleteket összefoglaló néven „maszkolásnak” nevezzük, mivel minden egyes művelethez megfelelő bitmaszkot kell készítenünk, amellyel aztán logikai kapcsolatba hozva a megváltoztatni kívánt értéket, végbemegy a kívánt bitművelet.
Mielőtt sorra vennénk a szokásos bitműveleteket, meg kell ismerkednünk az egész adatelemek bitjeinek sorszámozásával. A bitek sorszámozása a bájton a legkisebb helyértékű bittől indulva 0-val kezdődik, és balra haladva növekszik. Több bájtból álló egészek esetén azonban tisztáznunk kell a számítógép processzorában alkalmazott bájtsorrendet is.
A Motorola 68000, SPARC, PowerPC stb. processzorok által támogatott „nagy a végén” (big-endian) bájtsorrend esetén a legnagyobb helyiértékű bájt (MSB) a memóriában a legalacsonyabb címen tárolódik, míg az eggyel kisebb helyiértékű a következő címen és így tovább.
Ezzel szemben a legelterjedtebb Intel x86 alapú processzorcsalád tagjai a „kicsi a végén” (little-endian) bájtsorrendet használják, amely szerint legkisebb helyiértékű bájt (LSB) helyezkedik el a legalacsonyabb címen a memóriában.
A hosszú bitsorozatok elkerülése érdekében a példáinkban unsignedshort int típusú adatelemeket használunk.
Nézzük meg ezen adatok felépítését és tárolását mindkét bájtsorrend szerint! A tárolt adat legyen 2012, ami hexadecimálisan 0x07DC!
big-endian bájtsorrend:
little-endian bájtsorrend:
(A példákban a memóriacímek balról jobbra növekednek.) Az ábrán jól látható, hogy a hexadecimális konstans értékek megadásakor az első, „nagy a végén” formát használjuk, ami megfelel a 16-os számrendszer matematikai értelmezésének. Ez nem okoz gondot, hisz a memóriában való tárolás a fordítóprogram feladata.
Ha azonban az egész változókat bájtonként kívánjuk feldolgozni, ismernünk kell a bájtsorrendet. Könyvünk további részeiben a második, a „kicsi a végén” forma szerint készítjük el a példaprogramjainkat, amelyek azonban az elmondottak alapján az első tárolási módszerre is adaptálhatók.
Az alábbi példákban az unsigned short int típusú 2525 szám 4. és 13. bitjeit kezeljük:
unsigned short int x = 2525; // 0x09dd
Művelet Maszk C++ utasítás Eredmény
Bitek beállítása 0010 0000 0001 0000 x = x | 0x2010; 0x29dd
Bitek törlése 1101 1111 1110 1111 x = x & 0xdfef; 0x09cd
Bitek negálása (kapcsolása)
0010 0000 0001 0000 x = x ^ 0x2010;
x = x ^ 0x2010;
0x29cd (10701) 0x09dd (2525)
Az összes bit negálása 1111 1111 1111 1111 x = x ^ 0xFFFF; 0xf622
Az összes bit negálása x = ~x; 0xf622
Felhívjuk a figyelmet a kizáró vagy operátor (^) érdekes viselkedésére. Ha ugyanazzal a maszkkal kétszer végezzük el a kizáró vagy műveletet, akkor visszakapjuk az eredeti értéket, esetünkben a 2525-öt. Ezt a működést felhasználhatjuk két egész változó értékének segédváltozó nélküli felcserélésére:
int m = 2, n = 7;
m = m ^ n;
n = m ^ n;
m = m ^ n;
Nehezen kideríthető programhibához vezet, ha a programunkban összekeverjük a feltételekben használt logikai műveleti jeleket (!, &&, ||) a bitenkénti operátorokkal (~, &, |).
3.7.2. Biteltoló műveletek
A bitműveletek másik csoportjába, a biteltoló (shift) operátorok tartoznak. Az eltolás balra (<<) és jobbra (>>) egyaránt elvégezhető. Az eltolás során a bal oldali operandus bitjei annyiszor lépnek balra (jobbra), amennyi a jobb oldali operandus értéke.
Balra eltolás esetén a felszabaduló bitpozíciókba 0-ás bitek kerülnek, míg a kilépő bitek elvesznek. A jobbra eltolás azonban figyelembe veszi, hogy a szám előjeles vagy sem. Előjel nélküli (unsigned) típusok esetén balról 0-ás bit lép be, míg előjeles (signed) számoknál 1-es bit. Ez azt jelenti, hogy a jobbra való biteltolás előjelmegőrző.
short int x;
Értékadás Bináris érték Művelet Eredmény decimális (hexadecimális)
bináris
x = 2525; 0000 1001 1101 1101 x = x << 2; 10100 (0x2774)
0010 0111 0111 0100
x = 2525; 0000 1001 1101 1101 x = x >> 3; 315 (0x013b)
0000 0001 0011 1011
x = -2525; 1111 0110 0010 0011 x = x >> 3; -316 (0xfec4)
1111 1110 1100 0100 Az eredményeket megvizsgálva láthatjuk, hogy az 2 bittel való balra eltolás során az x változó értéke négyszeresére (22) nőtt, míg három lépéssel jobbra eltolva, x értéke nyolcad (23) részére csökkent. Általánosan is megfogalmazható, hogy valamely egész szám bitjeinek balra tolása n lépéssel a szám 2n értékkel való megszorzását eredményezi. Az m bittel való jobbra eltolás pedig 2m értékkel elvégzett egész osztásnak felel meg.
Megjegyezzük, hogy egy egész szám 2n-nel való szorzásának/osztásának ez a leggyorsabb módja.
Az alábbi példában a beolvasott 16-bites egész számot két bájtra bontjuk:
short int num;
unsigned char lo, hi;
// A szám beolvasása
cout<<"\nKerek egy egesz szamot [-32768,32767] : ";
cin>>num;
// Az alsó bájt meghatározása maszkolással lo=num & 0x00FFU;
// Az felső byte meghatározása biteltolással hi=num >> 8;
Az utolsó példában felcseréljük a 4-bájtos, int típusú változó bájtsorrendjét:
int n =0x12345678U;
n = (n >> 24) | // az első bájtot a végére mozgatjuk, ((n << 8) & 0x00FF0000U) | // a 2. bájtot a 3. bájtba, ((n >> 8) & 0x0000FF00U) | // a 3. bájtot a 2. bájtba, (n << 24); // az utolsó bájtot pedig az elejére.
cout <<hex << n <<endl; // 78563412
3.7.3. Bitműveletek az összetett értékadásban
A C++ mind az öt kétoperandusú bitművelethez összetett értékadás művelet is tartozik, melyekkel a változók értéke könnyebben módosítható.
Operátor Műveleti jel Használat Művelet
Balra eltoló értékadás <<= x <<= y x bitjeinek eltolása balra y
bittel,
Operátor Műveleti jel Használat Művelet
Jobbra eltoló értékadás >>= x >>= y x bitjeinek eltolása jobbra y bittel,
Bitenkénti VAGY
értékadás
|= x |= y x új értéke: x | y,
Bitenkénti ÉS értékadás &= x &= y x új értéke: x & y,
Bitenkénti kizáró VAGY értékadás
^= x ^= y x új értéke: x ^ y,
Fontos megjegyeznünk, hogy a bitműveletek eredményének típusa legalább int vagy az int-nél nagyobb egész típus, a bal oldali operandus típusától függően. Biteltolás esetén a lépések számát ugyan tetszőlegesen megadhatjuk, azonban a fordító a típus bitméretével képzett maradékát alkalmazza az eltoláshoz. Például a 32-bites int típusú változók esetében az alábbiakat tapasztalhatjuk:
unsigned z;
z = 0xFFFFFFFF, z <<= 31; // z ⇒ 80000000 z = 0xFFFFFFFF, z <<= 32; // z ⇒ ffffffff z = 0xFFFFFFFF, z <<= 33; // z ⇒ fffffffe