Labor, 4. hét: számábrázolás

Pohl László, Czirkos Zoltán · 2024.09.20.

Számábrázolási kérdések és bitműveletek.

Felkészülés a laborra:

1. Lebegőpontos

Mit írnak ki az alábbi program egyes sorai? Próbáld meg kitalálni! Futtasd le a programot, és magyarázd meg az eredményt!

A printf() %g formátumsztringje azt jelenti, hogy mindig a legrövidebb kiírási formát használja – ha a normálalak rövidebb, akkor azt.

#include <stdio.h>
#include <math.h>

int main(void) {
    printf("1. %f\n", 1.23456789123456789123456789e40);
    printf("2. %g\n", 1e40);
    printf("3. %s\n", 1e3+1 == 1e3 ? "igaz" : "hamis");
    printf("4. %s\n", 1e30+1 == 1e30 ? "igaz" : "hamis");
    printf("5. %g\n", pow(10, 40) / pow(10, -40));
    printf("6. %g\n", powf(10, 40) / powf(10, -40));

    return 0;
}
Megoldás

Eltérő géptípusok és fordítók esetén az eredmények kicsit mások lehetnek.

1. 12345678912345679949757438004016390864896.000000
2. 1e+40
3. hamis
4. igaz
5. 1e+80
6. inf

Az 1-es sorban az látszik, hogy a kiírt 1040 nagyságrendű értéket a gép nem tudja pontosan ábrázolni. A 2-esben megadott %g formátum a rövidsége miatt preferálja a normálalakot, de ez ne tévesszen meg senkit: a szám valójában itt sem pontos, csak kevés a megjelenített tizedesjegyek száma.

Az 3-as és 4-es sor hasonló az előző feladathoz: míg a 103+1 értéke különbözik 103-től, mivel a két szám, 1000 és 1 nem tér el túlzottan egymástól nagyságrendben. A 1030 és 1 esetén ez már nem igaz.

Az 5-ös sorban két double értéket, a 6-osban két float-ot osztunk el egymással. Az eredmény már nagyobb, mint a legnagyobb, float típus által ábrázolható szám, ezért a gép végtelen nagynak veszi.

2. Emeletes tört

Írj programot, amely kiszámítja az alábbi tört valós (tizedestört) értékét! (−0.653333-et kell kapj).

                  4
4 + 2 − (3 − (6 + ─))
                  5
─────────────────────
        3(2−7)
Megoldás

Valahol, leginkább a 4/5 törtnél, szerepelnie kell a kifejezésben egy valós számnak. Különben egész osztás történik. Ezen kívül, figyelni kell a zárójelezésre; zárójelbe kell tenni a számlálót is, és a nevezőt is, különben nem az összeget osztjuk, vagy nem a szorzattal osztunk, az operátorok precedenciája és asszociativitása miatt.

#include <stdio.h>

int main(void) {
    printf("%f", (4+2-(3-(6+4.0/5)))/(3*(2-7)));

    return 0;
}

3. Végtelen ciklus?

Az alábbi program egy olyan ciklust tartalmaz, mely addig fut, amíg egy minden iterációban megnövelt szám pozitív marad. Matematikailag ez nyilvánvalóan végtelen ciklus. No de mi a helyzet a gyakorlatban? Próbáld ki int és short int, signed char típussal is! Magyarázd meg a jelenséget! Miért pont azok a számok jelentek meg, amiket látsz?

#include <stdio.h>
 
int main(void) {
    int i = 1;
    while (i > 0) {
        ++i;
    }
    printf("%d\n", i);

    return 0;
}

4. Kettő hatványai, léptetéssel

Írj programot, amelyik kiszámítja a 2 első 32 hatványát, a 20-tól indulva! Használd ehhez a léptető << operátort! Mit tapasztalsz, ki tudod írni a 2 első 40 hatványát is?

Megoldás

Figyelned kell arra, hogy 231 már nem fér el egy 32 bites signed int típusban. Ezért az 1u értéket kell léptetni, és a printf() számára is jelezni kell, hogy előjel nélküli számról van szó: %u. Ha nem így csináltad, ezt észre fogod venni, amikor megjelenik egy negatív szám a kimeneten.

#include <stdio.h>
 
int main(void) {
    for (int x = 0; x < 32; ++x)
        printf("%d: %u\n", x, 1u << x);
 
    return 0;
}

Ezek után módosítsd úgy a programodat, hogy egy unsigned int típusú változó értékét változtasd benne, mindig eggyel balra léptetve a <<= operátorral! Az előző feladat tapasztalatából kiindulva, a ciklust futtathatod addig, amíg a változó túl nem csordul – nem fixen 32 iterációval.

Megoldás
#include <stdio.h>
 
int main(void) {
    unsigned int x = 1;
    while (x > 0) {
        printf("%u\n", x);
        x <<= 1;
    }
 
    return 0;
}

Ezek alapján olyan programot is lehetne írni, amelyik egy adott típusról megmondja, hogy hány bites: addig kell léptetni, amíg túl nem csordult. Módosítsd úgy a programot, hogy képes legyen megszámolni egy előjel nélküli egész változó bitjeinek számát, más néven szóhosszúságát! Mekkora egy unsigned char, egy unsigned short int, egy unsigned int, egy unsigned long int, és egy unsigned long long int a gépen, amelyiken dolgozol? Figyelj a fordító hibaüzeneteire, a printf() paraméterezését módosítani kell majd az egyes típusoknál!

Megoldás
#include <stdio.h>
 
int main(void) {
    int db = 0;
    unsigned long int x = 1;
    while (x > 0) {
        printf("%lu\n", x);
        x <<= 1;
        ++db;
    }

    printf("Az unsigned long int %d bites.\n", db);
 
    return 0;
}

5. Egy szám bitjei

Dolgozz tovább abból a feltételezésből kiindulva, hogy a gépeden az unsigned long int típus 32 bites. Írj egy olyan programrészletet, amelyik egy ilyen típusú változóban tárolt szám bitjeit kiírja! Pl. 29 esetén ez a bitminta 00000000000000000000000000011101, mert 29 = 1×24+1×23+1×22+0×21+1×20, azaz 2910 = 111012.

Tipp

Emlékezz vissza, a >> operátorral tudsz jobbra léptetni egy számot. Ha kíváncsi vagy valamelyik bitjére, akkor annyiszor lépteted jobbra, ahányadik bit az érdekes – utána pedig a bitenkénti ÉS operátorral kivágható a legalsó bit.

Megoldás

Az alábbi mintamegoldásban a jobbra léptetés >> után az ÉS operátorral & kivágjuk a legalsó bitet. Mivel így az eredmény vagy 0 lesz, vagy 1 (tehát vagy csak a legalsó biten lesz egy 1-es, vagy még ott sem), ez rögtön kiíratható.

#include <stdio.h>

int main(void) {
    unsigned long int value = 29;

    for (int i = 31; i >= 0; --i)
        printf("%lu", value>>i & 1);
    
    return 0;
}

6. Bitek titkai

Az előző feladat folytatása: a bitenként kiíró programodat írd át úgy, hogy az 1-esek helyére '#' karaktert, a 0-k helyére ' ' (szóköz) karaktert tegyen! (Ehhez használhatod a ?: operátort is.)

Utána pedig úgy, hogy ne csak egy szám bitmintáját írja ki ilyen formában, hanem az alábbi tömb összes számát, egymás alá! Mit látsz?

unsigned long szamok[8] = { 0U, 1931988508U, 581177634U, 581374240U,
							581177632U, 581177634U, 1919159836U, 0U };
Megoldás
#include <stdio.h>

int main(void) {
	unsigned long szamok[8] = { 0U, 1931988508U, 581177634U, 581374240U,
								581177632U, 581177634U, 1919159836U, 0U };

    /* az összes szám */
    for (int y = 0; y < 8; ++y) {
        /* az egyik szám bitmintája */
        for (int x = 31; x >= 0; --x)
            printf("%c", szamok[y]>>x & 1 ? '#' : ' ');
        printf("\n");
    }
    
    return 0;
}

Ha megvan, amit látni kell, akkor próbáld ki, hogy a kirajzolás előtt az összes tömbelemet megváltoztatod az alábbi módokon:

  • tömbelem = tömbelem & 65535
  • tömbelem = tömbelem & ~65535
  • tömbelem = tömbelem | 65535
  • tömbelem = tömbelem | ~65535
  • tömbelem = tömbelem ^ 65535
  • tömbelem = tömbelem ^ ~65535

Magyarázd meg a kapott eredményeket! (Segít, ha megvizsgálod a 65535 és a ~65535 bitmintáját is előbb.)

Megoldás

65535 bitmintája 00000000000000001111111111111111, 16 darab 0 és 16 darab 1 bit. ~65535 ugyanez, csak bitenként negálva, tehát 11111111111111110000000000000000. Ha ezt a két értéket bitenkénti ÉS kapcsolatba hozzuk a tömb számaival, akkor a kép ba vagy jobb oldala marad meg, mivel X ÉS 0 = 0, illetve X ÉS 1 = X (lásd az igazságtáblázatot). A bitenkénti VAGY kapcsolattal 1-be billennek a bitek. A bitenkénti KIZÁRÓ VAGY (XOR) kapcsolattal pedig negálni lehet a kiválasztott biteket.

7. Rajzok

Az előző feladat tömbje egy fekete-fehér rajzot tárolt a számokban. Ebben a feladatban ezt kell továbbfejlesztened. Hozz létre egy 32×24 pontból álló rajzot, és dolgozz azon! A rajz szélessége (32) az unsigned long int méretéből adódik; a magasság (24) a tömbméretből. A konzolablak 24 vagy 25 sor magas szokott lenni, így el fog férni a rajzod.

Írj programrészt, amelyik:

  • Letörli a rajzot, azaz mindegyik bitet „feketévé”, 0-vá változtatja!
  • Rajzol egy pontot, azaz „fehér”, 1-es bitet tesz egy adott (x, y) pozícióra!

Készíts a fentiek használatával egy rajzot! Rajzolj pl. kört vagy téglalapot, vagy egyéb formát!

Megoldás

Az alábbi program egy behajtani tilos táblát rajzol:

#include <stdio.h>
#include <math.h>

int main(void) {
    unsigned long int rajz[24];

    /* törlés */
    for (int y = 0; y < 24; ++y)
        rajz[y] = 0;

    /* kör */
    double r = 9.5;
    for (double alfa = 0; alfa < 360; alfa += 3) {
        int x = 16 + r*cos(alfa*3.14/180);
        int y = 12 + r*sin(alfa*3.14/180);
        rajz[y] |= 1 << (31-x);       /* 1-be állítás */
    }

    /* téglalap */
    int x1 = 9, y1 = 10;
    int x2 = 22, y2 = 14;
    for (int y = y1; y <= y2; ++y) {
        rajz[y] |= 1 << (31-x1);
        rajz[y] |= 1 << (31-x2);
    }
    for (int x = x1; x <= x2; ++x) {
        rajz[y1] |= 1 << (31-x);
        rajz[y2] |= 1 << (31-x);
    }

    /* kirajzolás */
    for (int y = 0; y < 24; ++y) {
        /* az egyik szám bitmintája */
        for (int x = 31; x >= 0; --x) {
            printf("%c", rajz[y]>>x & 1 ? '#' : '.');
        }
        printf("\n");
    }
    
    return 0;
}