Eddig a programjainkban egyszerű tÃpusokat alkalmaztunk, azonban ezek nem mindig elegendÅ‘ek. Például, ha egy pont adatait szeretnénk leÃrni, akkor kézenfekvÅ‘ lenne, hogy ezeket egy összetett tÃpusban tároljuk. Erre ad lehetÅ‘séget a struct:
struct
{
int x;
int y;
} pont;
A fenti kódrészlet a pont változó tÃpusának egy olyan struktúrát ad, ami két egész számot tartalmaz. Azonban Ãgy a tÃpus egy névtelen tÃpus. Ha más változók esetén is szeretnénk használni ezt a tÃpust, akkor a typedef utasÃtással ki kell egészÃtenünk:
typedef struct
{
int x;
int y;
} pont_t;
Ez az utasÃtássor definiál egy pont_t tÃpust, amit ezt követÅ‘en bárhol használni tudunk a programunkon belül úgy, mintha beépÃtett tÃpus lenne:
point_t pont;
Mivel a C nem objektumorientált, Ãgy nincs lehetÅ‘ségünk konstruktor megadására, ami inicializálja a struktúra tagjait egy kezdőértékre, Ãgy errÅ‘l magunknak kell gondoskodnunk.
A struktúráinkon is alkalmazhatjuk a sizeof() operátort, ami a struktúra elemeinek kombinált méretét adja vissza. Itt érdemes megjegyezni, hogy ha a struktúránk mutatót tartalmaz (pl char *), akkor a mutató által mutatott memóriaterület mérete nem számolódik bele a struktúra méretébe, csupán csak a mutató mérete.
Tárolási szempontból egy érdekes tÃpus C-ben az únió. Ezt az union kulcsszóval definiáljuk. Az únióban elhelyezett változók osztott memóriaterületen helyezkednek el. Ez lehetÅ‘vé teszi, hogy ugyanazt a memória területet többféleképpen kezeljük:
#include <stdio.h>
#include <stdint.h>
typedef union
{
uint32_t egesz;
uint8_t bytes[4];
} number_t;
int main()
{
number_t szam;
szam.egesz = 255;
for (int i=0; i<4; i++)
{
printf("%d ", szam.bytes[i]);
}
return 0;
}
A fenti példában a number_t egy olyan tÃpust definiál, amiben az egesz változó és a bytes változó ugyanazon a memóriaterületen helyezkedik el. Az únió méretét a benne található legnagyobb méretű változó határozza meg. Jelen esetben ez 4 byte lesz, mivel az egesz és az ezt követÅ‘ bytes tömb mérete is azonos.
Jelen esetben a bytes tömb értékének olvasása az egesz szám byte értékeit adja vissza. Az únió tÃpus kifejezetten beágyazott rendszerek esetén hasznos, vagy akkor, ha ugyanazt az adatot több perspektÃvából is használhatóvá szeretnénk tenni anélkül, hogy extra memóriát foglalnánk.
A struktúrák és úniók esetén a tagokat a változó neve utáni pont karakterrel tudjuk cÃmezni, abban az esetben, ha direkt hozzáférésünk van. Azonban ha egy pointeren keresztül van hozzáférésünk a változóhoz, akkor a tagokat a -> operátorral tudjuk kiválasztani:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct
{
char keresztNev[20];
char vezetekNev[20];
} person_t;
int main()
{
person_t *szemely = (person_t *)malloc(sizeof(person_t));
if (szemely == NULL) {
printf("Allokacios hiba");
return 1;
}
strncpy(szemely->keresztNev, "Elek", sizeof(szemely->keresztNev) -1);
strncpy(szemely->vezetekNev, "Teszt", sizeof(szemely->vezetekNev) -1);
printf("%s %s", szemely->vezetekNev, szemely->keresztNev);
free(szemely);
return 0;
}
A harmadik lehetÅ‘ségünk összetett tÃpus definiálásra az enum. Ez egy olyan tÃpusdefinÃció, amely az egész számokhoz rendelt elnevezett konstansok halmazának létrehozására használhatunk.
#include <stdio.h>
typedef enum
{
RED,
GREEN,
BLUE
} color_t;
int main()
{
color_t myColor = BLUE;
switch (myColor)
{
case RED:
printf("Piros.\n");
break;
case GREEN:
printf("Zöld.\n");
break;
case BLUE:
printf("Kék.\n");
break;
default:
printf("Unknown color.\n");
}
return 0;
}
A felsorolás értékeinek számozása itt is 0-tól indul, amit felülbÃrálhatunk explicit értékmegadással. A neveket, amiket használni szeretnénk a felsorolásban, csupa nagy betűvel adjuk meg, a név ütközések elkerülése érdekében. Ez azért fontos, mert mint a fenti példaprogramban is látható: a felsorolások nevesÃtett értékeinél nem szerepel a felsorolás tÃpus neve sehol.
Memóriaszervezés és kitöltés
A struktúrák kapcsán beszélnünk kell egy kicsit a számÃtógépünk memóriaszervezésérÅ‘l. A számÃtógépünkben található processzor nem egyesével bitenként és nem is egyes byteonként olvassa a memóriát, hanem nagyobb egységekben, mégpedig azért, mert Ãgy hatékonyabban tud működni. Struktúrák esetén ez abban nyilvánul meg, hogy a legnagyobb méretű primitÃv adattÃpus határozza meg, hogy mekkora lesz a struktúra alignment-je, mivel ennyi adatot egyszerre be kell olvasnia a processzornak, hogy kezelni tudja az adatot. Nézzünk egy példát:
typedef struct
{
uint32_t a;
uint64_t b;
uint32_t c;
} A;
A fenti példában a struktúránk alignmentje 8 byte lesz, mivel az uint64_t a legnagyobb adattag. Viszont a meglepetés az, hogy ha lekérjük a struktúra méretét a sizeof() operátorral, akkor 16 byte helyett 24-et kapunk eredményül.
Ennek az oka az, hogy a processzor minden esetben a struktúra alignmentje által meghatározott byte-ot olvas minden lépésben, Ãgy kitöltÅ‘ (padding) byte-okat illeszt be a fordÃtó a struktúrába az a és c változó esetén.
A kitöltés sorrendjét a struktúrában a definÃció sorrendje határozza meg. Az alábbi módosÃtott példában ugyanazok a változók szerepelnek, csak más sorrendben definiálva:
typedef struct
{
uint32_t a;
uint32_t c;
uint64_t b;
} B;
Az alignment továbbra is 8 byte, de most a sizeof() 16 byte-ot ad vissza méretként 24 helyett, mivel nem kellett kitöltÅ‘ byte-okat alkalmaznia a fordÃtónak.
Fejlettebb nyelvek esetén ez a memóriaoptimalizáció automatikusan megtörténik. Éppen ezért ha C#-ból lépünk interakcióba egy C kódrészlettel, akkor mindenképpen ki kell tennünk a [StructLayout(LayoutKind.Sequential)] attribútumot a struktúra defincÃciójára, mivel ez arra utasÃtja a fordÃtót és a futtatókörnyezetet, hogy ne rendezze át automatikusan a memóriatartalmat a padding csökkentése érdekében.
A padding alkalmazása letiltható a fordÃtókban. Ennek akkor van értelme, ha fájlba Ãrunk egy adatot. Ilyen esetben minden padding byte felesleges adat. GCC esetén a padding a __attribute__((packed)) módosÃtóval tiltható le:
#include <stdint.h>
#include <stdio.h>
/* Adatméret: 16 byte, de 24 byte lesz*/
typedef struct
{
uint32_t a;
uint64_t b;
uint32_t c;
} A;
/* Adatméret: 16 byte, és 16 byte lesz*/
typedef struct
{
uint32_t a;
uint32_t c;
uint64_t b;
} B;
/* Adatméret: 16 byte, packed */
typedef struct
{
uint32_t a;
uint64_t b;
uint32_t c;
} __attribute__((packed)) NoPadA;
int main()
{
printf("sizefof(A) = %lu\r\n", sizeof(A));
printf("sizefof(B) = %lu\r\n", sizeof(B));
printf("sizefof(NoPadA) = %lu\r\n", sizeof(NoPadA));
return 0;
}
A program kimenete:
sizefof(A) = 24
sizefof(B) = 16
sizefof(NoPadA) = 16
Itt felhÃvnám a figyelmet, hogy a packed módosÃtás egy olyan struktúra esetén, ami a memóriában fog elhelyezkedni és műveleteket végzünk vele, nem a legjobb ötlet, mégpedig azért, mert bizonyos architektúrák (pl. ARM) küszködnek a memóriaolvasással, ha az adat nem aligned formátumban van a memóriában.
x86 és x64 esetén is érdemes az aligned módra törekedni, mivel sokkal több optimalizációt tud Ãgy végrehajtani a fordÃtó a processzor számára. Ennek az optimalizációnak és gyorsaságnak az elrontója a padding, amit ha meg tudunk szüntetni úgy, hogy aligned marad a memória (mint a korábbi példában átrendezéssel), akkor gyorsabb lesz a programunk.