Seminář 2

void pointer, přetypování

V jazyce C existuje pointer bez informace o typu hodnoty, na kterou pointer ukazuje. To je užitečné například v situaci, kdy pracujeme čistě s pamětí a nepotřebujeme znát, jakého je typu. Například při kopírování, porovnávání apod. (K tomu se ještě dostaneme). Typ takového pointeru je void*, programátoři mu říkají void pointer.

Void pointer lze implicitně přetypovat na pointer jakéhokoliv typu, a pointer jakéhokoliv typu lze implicitně přetypovat na void pointer. Přetypování mezi ostatními typy pointerů je nutno provádět explicitně (jinak překladač vypíše warning nebo chybu).

Příkladem jednoduché funkce pracující s pamětí je tisk jejího obsahu na obrazovku. Jako argumenty funkce postačuje adresa začátku paměti a její velikost v bajtech.

void dump_mem(void *mem, size_t size) {

Pamět budeme tisknout po bajtech do tabulky, která bude mít na každém řádku 8 bajtů. Jeden bajt vytiskneme jako dvouciferné hexadecimální číslo (zamyslete se proč je to vhodné). Ve funkci využijeme toho, že typ unsigned char má vždy velikost jeden bajt. Na začátku funkce (implicitně) přetypujeme pointer mem na pointer na unsigned char a můžeme se na pamět dívat jako na pole bajtů. Poté stačí toto pole projít a vytisknout.

void dump_mem(void *mem, size_t size) {
    unsigned char *bytes = mem; // tady pretypujeme
    for (size_t i = 0; i < size; i += 1, bytes += 1) {
        if (i && !(i % 8)) {
            printf("\n"); 
        }
        printf("%.2X ", *bytes); 
  }
  printf("\n");
}

Chceme-li funkci použít, musíme získat její argumenty: získat adresu začátku paměti a její velikost.

int x = 300;
dump_mem(&x, sizeof(x));  // tady dochazi k implicitnimu pretypovani int* na void*

float f = 2.4;
dump_mem(&f, sizeof(f));

struct { char c; int a; } s = {'x', 55 };
dump_mem(&s, sizeof(s));

char a[] = "ahoj svete";
dump_mem(a, strlen(a) + 1);

double b[5] = { 1.1, 2.2, 3.3, 0, 4.4 }
dump_mem(b, sizeof(b));

Přetypování z void* na unsigned char je vždy bezpečné. Jinde může dojít k nedefinovanému chování. Prvním důvodem jsou tzv. trap reprezentace. Mohou existovat typy, kde ne každý obsah paměti odpovídá platné hodnotě daného typu. Takový obsah paměti, který neodpovídá platné hodnotě je past: pokus o dereferenci vede k nedefinovanénu chování. Podle standardu jediný typ, který zaručeně nemá trap reprezentaci právě unsigned char. Ostatní typy trap reprezentaci mít mohou, ale nemusí. Dalším problémem může být tzv. zarovnání (anglicky alignment): na některých architekturách je vyžadováno, aby vícebajtové objekty začínaly na adrese s určitou vlastností, například na adrese dělitelné velikostí objektu. Dereference nezarovnané adresy vede k nedefinovanému chování.

Pro explicitní konverze mezi pointery jiných typů existují ve standardu pravidla (strict aliasing rules), která svým rozsahem do kurzu nepatří. Doporučuji se prozatím takovým konverzím vyhýbat. (Mimo void pointer jsou výjimkou i unsigned char pointery).

Některé funkce ze standardní knihovny

Pro manipulaci s pamětí lze využít některé funkce ze string.h. Jsou to zejména:

Další podrobnosti o zmíněných funkcích si čtenář nastuduje z referenční příručky. Důležité je podívat zejména okrajové chování (např. když je některý argument 0).

Úkoly

  1. Upravte funkci dump_mem tak, aby na začátku každého řádku vypsala adresu prvního bajtu na tomto řádku. Přidejte možnost vypisovat bajty jako posloupnost 8 bitů (je nutné využít bitových operátorů).

  2. Naprogramujte vlastní verze funkcí ‘memcpy, memcmp, memset’. Funkce mohou být implementovány naivně.

  3. Najděte způsob, jak bez použití sizeof programově zjistit velikost nějakého typu, například float.

  4. Pro typ unsigned int napište funkce pro detekci endianity a pro převod mezi big endian a small endian. Wiki o endianitě. Funkce mohou být implementovány naivně.