KURZUS: Programozás alapjai

MODUL: IV. modul

11. lecke: Struktúrák és uniók

Ebben a fejezetben megismerkedünk a struktúrákkal, melyekkel egy tetszőleges entitás különböző tulajdonságait tudjuk egyetlen, összetett változó segítségével, egységként kezelni. A szintaktikailag nagyon hasonló uniókkal lehetővé válik, hogy valamilyen tulajdonság értékétől függően mindig más-más típust használjunk egy adat leírására, vagy ugyanannak az adatnak a különböző részeit (pl. egy 4 bájtos egész bájtjait) könnyebben érjük el. Röviden kitérünk még a bitmezőkre is, melyeknek főként hardver-közeli alkalmazásokban vehetjük hasznát.

A lecke végére a hallgatónak tisztában kell lennie

  • Struktúrák és tagjaik deklarálásával, a struktúrákhoz kapcsolódó fogalmakkal (pl. címke, tag), struktúra inicializálásával, a tagok eléréséhez használt operátorok használatával, struktúrák függvényeknek paraméterként történő átadásával és visszatérési értékként történő használatával, struktúráknak a tárban történő elhelyezésével.
  • Uniók használatával, a struktúrák és uniók közötti hasonlóságokkal és különbségekkel.
  • Bitmezők alkalmazásának módjával.

A lecke kulcsfogalmai: struktúra, unió, bitmező, összetett típus, felhasználói típus, tag, címke, címkézetlen struktúra, szelekciós operátor/tagszelektor, tárillesztés, bájt/szó/dupla szóhatárra igazítás, MSB, balérték/jobbérték, módosítható balérték, névterület.

Struktúrák és uniók áttekintése

A struktúra és az unió aggregátum. Egy vagy több, esetleg különböző típusú változó (elnevezett tag) együttese, melynek önálló azonosítója van. A struktúrát más nyelvben rekordnak nevezik. Azt teszi lehetővé, hogy a valamilyen módon összetartozó változók egész csoportjára egyetlen névvel hivatkozhassunk, azaz hogy a változócsoport kezelése egyszerűbb legyen.

Tulajdonképpen minden struktúrával és unióval új, összetett, felhasználói adattípust hozunk létre.

Az ANSI szabvány megengedi, hogy a struktúrákat át lehessen másolni egymásba, hozzá lehessen rendelni, és átadhatók legyenek függvényeknek, ill. rutinok visszatérési értéke is lehessen struktúra. Képezhető természetesen a struktúra címe (&), mérete (sizeof), és benne lehet explicit típusmódosító szerkezetben is, de a struktúrák nem hasonlíthatók össze.

A struktúrában felsorolt változókat struktúratagoknak (member) nevezik. Struktúratag kis megszorításokkal - melyre később kitérünk - akármilyen típusú lehet. Lehet alap és származtatott típusú bármilyen sorrendben.

A deklarációbeli típusspecifikátor egyik alternatívája a

struktúra-vagy-unió-specifikátor:
  struktúra-vagy-unió<azonosító>{struktúratag-deklarációlista}
  struktúra-vagy-unió azonosító

struktúra-vagy-unió:
  struct
  union

A struktúratag-deklarációlista struktúra, ill. uniótag deklarációk sorozata:

struktúratag-deklarációlista:
  struktúratag-deklaráció
  struktúratag-deklarációlista struktúratag-deklaráció

struktúratag-deklaráció:
  típusspecifikátor-lista struktúra-deklarátorlista

típusspecifikátor-lista:
  típusspecifikátor
  típusspecifikátor-lista típusspecifikátor

struktúra-deklarátorlista:
  struktúra-deklarátor
  struktúra-deklarátorlista, struktúra-deklarátor

A struktúra-deklarátor többnyire a struktúra, ill. az unió egy tagjának deklarátora. A struktúratag azonban meghatározott számú bitből is állhat, azaz lehet ún. bitmező (bit field) is, mely a nyelvben struktúrákon kívül nem is használható másutt. A mező bitszélességét a kettőspontot követő, egész értékű konstans-kifejezés határozza meg.

struktúra-deklarátor:
  deklarátor
  <deklarátor>: konstans-kifejezés

Struktúradeklaráció

Alakja tehát a következő:

<tárolási-osztály-specifikátor> struct <struktúracímke> <{
  struktúratag-deklarációlista }> <azonosítólista>;

Például:

struct datum{
  int ev, ho, nap, evnap;
  long datumssz; /* Dátumsorszám. */
  char datumlanc[11]; }
d, dptr, dt[10]; /* Azonosítólista. */

ahol:

  • A tárolási-osztály-specifikátor elhagyásával, megadásával és ennek értelmezésével nem foglalkozunk újra!
  • A datum azonosító ennek a struktúrának a címkéje (struktúracímke), mely azt biztosítja, hogy később struct datum módon hivatkozni tudjunk a felhasználói típusra. Például:

    struct datum *sptr =
      (struct datum *)malloc(sizeof(struct datum));
  • Az ev, a ho, a nap és az evnap a struct datum típusú struktúra int típusú tagjainak, a datumssz a long típusú tagjának és a datumlanc a struktúra char* típusú tagjának az azonosítói. A tagneveknek csak a struktúrán belül kell egyedieknek lenniük, azaz a tagazonosítók nyugodtan egyezhetnek például más közönséges változók neveivel, vagy a struktúracímkékkel.
  • A d struct datum típusú változó, a dptr és az sptr struct datum típusú objektumra mutató mutatók, és a dt tíz, struct datum típusú elemből álló tömb azonosítója, azaz a dtstruktúratömb.
  • A fordító a struktúrának éppen annyi helyet foglal a memóriában, hogy benne a struktúra minden tagja elférjen. A struktúratagok a deklaráció sorrendjében, folyamatosan növekvő címeken helyezkednek el, s így a struktúradefinícióban később deklarált tag címe mindig nagyobb.

Az azonosítólista nélküli struktúradeklarációt, ahol van struktúratag-deklarációlista, azaz megadottá válik a struktúra szerkezete, szokás struktúradefiníciónak is nevezni, ugyan nincs memóriafoglalása.

  • Minden struktúradeklaráció egyedi struktúra típust hoz létre, s így

    struct A {
      int i, j;
      double d; } a, a1;
    struct B {
      int i, j;
      double d; } b;

    az a és az a1 objektumok struct A típusú struktúrák, de az a és a b objektumok különböző struktúra típusúak annak ellenére is, hogy a két struktúra szerkezete azonos.
  • Az azonosítólista nélküli, de struktúracímkével és tag-deklarációlistával ellátott struktúradefiníció nem foglal ugyan helyet a memóriában, de biztosítja azt a lehetőséget, hogy a struktúradeklaráció hatáskörében később ilyen típusú struktúrával azonosítólistát is megadva helyet foglalhassunk változóinknak, mutatóinknak és tömbjeinknek. Például:

    struct datum{
      int ev, ho, nap, evnap;
      long datumssz; /* Dátumsorszám. */
    char datumlanc[11]; };
    /* . . . */
    struct datum d, *dptr=&d, dt[10];

    Fedezzük fel, hogy a felhasználó definiálta típusnév struct datum. Hasonlításul:

    double d, *dptr=&d, dt[10];

    Struktúra, unió, enum deklarációt, definíciót záró kapcsos zárójel után kötelező pontosvesszőt tenni, mert ez zárja az azonosítólistát!

    struct struki{
      int a, b;
      float matrix[20][10];
      char nev[26]; }; /* Itt nem elhagyható a ; a } után! */
  • A struktúracímkének egyedinek kell lennie a struktúra, unió és enum címke névterületen!
  • Mint a tömböknél, struktúráknál is megadható nem teljes típusdeklaráció. Például a

    struct datum;

    még akkor is létrehozza az aktuális hatáskörben a struct datum nem teljes típust, ha ilyen befoglaló, vagy külső hatáskörben is létezne. Könnyen belátható, hogy ez struct datum típusú struktúra objektum definíciójára nem használható a struktúra szerkezetének közbenső, ugyanezen hatáskörbeli definiálása nélkül, hisz ismeretlen a memóriaigény. Arra azonban ez is alkalmas, hogy deklarációban, typedef-ben használjuk a típusnevet, vagy, hogy struct datum típusú objektumokra mutató mutatókat hozzunk létre:

    struct datum *dptr1, *dptr2, *dptrt[20];

    Az ilyen mutatók akár más struktúra tagjai is lehetnek:

    struct A; /* Nem teljes típusdeklaráció. */
    struct B{ /* Itt tag a nem tejes típusra */
      struct A *pa;}; /* mutató mutató. */
    struct A{
      struct B *pb;}; /* Ez most már teljes típus
                         lesz.*/

Vigyázat! Struktúradefinícióban a struktúra típusa a struktúra-tagdeklarációlistában csak akkor válik teljessé, ha elérjük a specifikátor bezáró kapcsos zárójelét (}).

  • A struktúradeklaráció általános alakjából látható volt, hogy belőle a struktúracímke is elhagyható. Ha ezt megtesszük, ún. név nélküli, vagy címkézetlen struktúrához jutunk. Világos, hogy ebben az esetben a nem teljes típusdeklarációnak

    struct;

    semmi értelme (szintaktikai hiba is) sincs, de az olyan deklarációnak sincs, amiben csak a struktúra szerkezetét adjuk meg:

    struct {int tag1, tag2; /* ... */};

    hiszen később nem tudunk a típusra hivatkozni, s ebből következőleg ilyen típusú objektumokat deklarálni. Név nélküli struktúradeklarációban nem hagyható el tehát az azonosítólista, azaz:

    struct {int tag1, tag2; /* ... */} az1, az2[14];
Típusdefiníció
  • Az előbb vázolt probléma típusdefiníció alkalmazásával áthidalható:

    typedef struct{ int tag1, tag2; /* ... */} CIMKETLEN;
    /* Most sincs címke. */
    CIMKETLEN y, *y, ytomb[12];
  • A típusdefiníció a címkézett struktúrával is használható lett volna:

    typedef struct datum{
      int ev, ho, nap, evnap;
      long datumssz; /* Dátumsorszám. */
      char datumlanc[11]; } DATUM;
    /* . . . */
    DATUM d, *dptr, dt[10];

Összesítve: A typedef címke nélküli struktúrák, uniók és enum-ok típusdefiníciójára is alkalmas. Struktúrák esetében használjunk azonban struktúracímkét, vagy typedef-es szerkezetet, de a kettőt együtt nem javasoljuk!

  • Egy kicsit összetettebb példát véve:

    typedef char nev[30];
    typedef enum{no, ferfi, egyeb} sex;
    typedef struct{
      nev csalad, kereszt; /* Két 30 elemű karaktertömb. */
      sex fino; /* no vagy ferfi értékű enum. */
      /* . . . */
      double osztondij; } hallgato;
    typedef hallgato evf[100]; /* 100 elemű, fenti szerkezetű
                                  struktúratömb. */
    evf evf1, evf2, evf3; /* Három darab, 100 elemű, fenti
                             szerkezetű struktúratömb. */
Struktúratag deklarációk

A { }-ben álló struktúratag-deklarációlista a deklarátor szintaktikát követve meghatározza a struktúratagok neveit és típusait.

  • A struktúratag bármilyen típusú lehet a void, a nem teljes, vagy a függvény típustól eltekintve. A struktúratag deklaráció nem tartalmazhat azonban tárolási osztály specifikátort vagy inicializátort. Struktúratag nem lehet az éppen definíció alatt álló struktúra sem:

    struct szoszlo{
      static char *szo;                   /* HIBÁS */
      int szlo=0;                         /* HIBÁS */
      struct szoszlo elozo, kovetkezo; }; /* HIBÁS */
  • Struktúratag lehet azonban nem teljes típusú struktúrára, így akár az éppen deklaráció alatt állóra mutató mutató:

    struct szoszlo{
      char *szo;
      int szlo;
      struct szoszlo *elozo, *kovetkezo; }; /* OK */
  • Struktúratag lehet tömb, sőt már definiált szerkezetű struktúra is:

    struct sor_lanc{
      int sorszam;
      char megjegyzes[32];
      struct sor_lanc *kovetkezo; };
    struct kereszthivatkozas{
      char *szo;
      int szlo;
      struct kereszthivatkozas *elozo, *kovetkezo;
      struct sor_lanc elso; };
  • A struktúrának nem lehet függvény tagja, de függvényre mutató mutató persze lehet tag:

    struct pelda{
      char *szoveg;
      int (*hasonlit)(const char *, const char *);};
  • A struktúratag azonosítójának egy struktúrán belül kell egyedinek lennie, vagyis másik struktúrában nyugodtan létezhet ugyanilyen nevű tag.
  • A beágyazott struktúra ugyanúgy elérhető, mint a fájl hatáskörben deklarált, azaz a következő példa helyes:

    struct a{
      int x;
      struct b{
        int y; } v2;
    } v1;
    /* . . . */
    struct a v3;
    struct b v4;
  • A beágyazott struktúra gyakran névtelen:

    struct struki{
      struct { int x, y; } pont;
      int tipus; } v;
Struktúrák inicializálása

A struktúrát konstans kifejezésekből álló inicializátorlistával láthatjuk el kezdő értékkel. Az inicializátorlista elemek értékét a struktúratagok a deklarációbeli elhelyezkedés sorrendjében veszik fel:

struct struki {
  int i;
  char lanc[25];
  double d; } s = {20, "Jancsika", 3.14};

Pontosítsunk még néhány dolgot!

  • Lokális élettartamú struktúrák esetén az inicializátor inicializátorlista, vagy kompatibilis struktúra típusú egyszerű kifejezés lehet:

    struct struki s = {20, "Juliska", 3.14}, s1 = s;
  • Lokális (auto) struktúra persze akár ilyen típusú struktúrát visszaadó függvény hívásával is inicializálható:

    struct struki fv(int, char *, double);
    struct struki s2=fv(2, "Boszi", 1.4);
  • Ha a struktúrának struktúra vagy tömb tagja is van, akkor azt egymásba ágyazott { }-kel lehet inicializálni.

    struct struki {
      int i;
      long darab[3];
      double d; } s = { 20, { 1l, 2l, 3l}, 3.14};
  • Tudjuk, hogy az inicializátorlista elemeinek száma nem haladhatja meg az inicializálandó struktúratagok számát! Ha az inicializátorlista kevesebb elemű, mint az inicializálandó objektumok száma, akkor a maradék struktúratagok a statikus élettartamú implicit kezdőérték adás szabályai szerint tölti fel a fordító, azaz nullázza:

    struct struki{
      int cipomeret, /* s.cipomeret==42 */
          magassag;  /* s.magassag==180 */
      char nev[26];  /* az s.nev "Magas Lajos" */
      char cim[40];  /* s.cim üres karakterlánc ("")
                        kezdőértékű. */
      double fizetes;/* s.fizetes==0.0. */
    } s = { 42, 180, "Magas Lajos"};
  • Névtelen bitmező tag nem inicializálható!

Ha az inicializátorlistában nincs beágyazott inicializátorlista, akkor az ott felsorolt értékek az alaggregátumok, s őket a deklaráció sorrendjében veszik fel az aggregátum elemei. Kapcsos zárójelek ugyanakkor akár az egyes inicializátorok köré is tehetők, de ha a fordítót nem kívánjuk "becsapni", akkor célszerű őket az aggregátum szerkezetét pontosan követve használni!

typedef struct { int n1, n2, n3; } triplet;
triplet nlist1[2][3] = { /* Helyes megoldás: */
  {{11, 12, 13}, {4, 5, 6}, {7, 18, 9}},/* Első sor. */
  {{1, 2, 3}, {14, 15, 16}, {7, 8, 9}}  /* 2. sor. */ };
triplet nlist2[2][3] = { /* Hibás megoldás: */
  {11, 12, 13}, {4, 5, 6}, {7, 18, 9}, /* Első sor. */
  {1, 2, 3}, {14, 15, 16}, {7, 8, 9}  /* 2. sor. */ };

A sizeof-ot struktúrákra alkalmazva mindig teljes méretet kapunk akár a típust adjuk meg operandusként, akár az ilyen típusú objektumot. Például:

#include <stdio.h>
struct st{
  char *nev; /* A mutató mérete bájtban. */
  short kor; /* + 2 bájt. */
  double magassag; }; /* + 8 bájt. */
struct st St_Tomb[ ] = {
  {"Jancsika", 18, 165.4}, /* St_Tomb[0] */
  {"Juliska", 116, 65.4}}; /* St_Tomb[1] */
int main(void){
  printf("\nSt_Tomb elemeinek száma = %d\n",
        sizeof(St_Tomb)/sizeof(struct st);
printf("\nSt_Tomb egy elemének mérete = %d\n",
      sizeof(St_Tomb[0]));
return 0; }
Struktúratagok elérése

A struktúra és az uniótagok eléréséhez ugyanazokat a tagelérés operátorokat alkalmazza a nyelv. A tagelérés operátort szelekciós operátornak, tagszelektornak is szokás nevezni. Prioritásuk magasabb az egyoperandusos műveletekénél, s közvetlenül a () és a [] után következik. Kétfajta tagelérés operátor van:

  • az egyik a közvetlen szelekciós operátor (.) és
  • a másik a közvetett (->).

A közvetlen tagelérés operátor alakja:

utótag-kifejezés.azonosító

Az utótag-kifejezésnek struktúra típusúnak, s az azonosítónak e struktúra típus egy tagja nevének kell lennie. A konstrukció típusa az elért tag típusa, értéke az elért tag értéke, s balérték akkor és csak akkor, ha az utótag-kifejezés az, és az azonosító nem tömb.

A közvetett tagelérés operátor formája:

utótag-kifejezés->azonosító

Az utótag-kifejezésnek struktúra típusra mutató mutatónak, s az azonosítónak e struktúra típus egy tagja nevének kell lennie. A konstrukció típusa és értéke az elért tag típusa és értéke. Balérték, ha az elért tag nem tömb.

Feltéve, hogy s struct S típusú struktúra objektum, és sptr struct S típusú struktúrára mutató mutató, akkor ha t az struct S struktúrában deklarált, típus típusú tag, az

s.t

és az

sptr->t

kifejezések típusa típus, és mindkettő a struct S struktúra t tagját éri el. A következők pedig szinonimák, ill. azt is mondhatjuk, hogy a -> szelekció operátoros kifejezés a másik rövidítése:

sptr->t  (*sptr).t

Az s.t és az sptr->t balértékek, feltéve, hogy t nem tömb típusú. Például:

struct S{
  int t;
  char lanc[23];
  double d; } s, *sptr = &s, Stomb[20]={
    { 0, "nulla", 0.}, { 1, "egy", 1.}};
/* . . . */
s.t = 3;
sptr->d = 4.56;
  • Az Stomb 20 elemű, struct S struktúrából álló struktúratömb, melynek első (Stomb[0]) és második (Stomb[1]) elemét kivéve nincs explicit kezdőértéke, azaz Stomb[2], . . ., Stomb[19] { 0, "", 0,} értékű. A következő példák a struktúratömb tagelérést szemléltetik:

    Stomb[3].t=3;
    strcpy(Stomb[3].lanc, "három");
    Stomb[3].d=3.3;

    vagy:

    sptr=Stomb+5;
    sptr->t=5; /* Stomb[5].t */
    strcpy(sptr->lanc, "öt"); /* Stomb[5].lanc */
    sptr->d=5.5; /* Stomb[5].d */
  • Ha struct B struktúrának van struct A struktúra típusú tagja, akkor az ilyen struct A tagokat csak a tagszelekciós operátorok kétszeri alkalmazásával lehet elérni:

    struct A {
      int j;
      double x; };
    struct B {
      int i;
      char *nev;
      struct A a;
      double d; } s, *sptr = &s;
    /* . . . */
    s.i = 3;
    s.a.j = 2;
    sptr->d = 3.14;
    (sptr->a).x = 6.28;
  • A szelekciós operátorok balról jobbra kötnek, tehát a következők teljesen azonosak:

    (sptr->a).x sptr->a.x;
    (s.a).x s.a.x;
  • Említettük már, hogy a tagszelektorok prioritása magasabb az egyoperandusos műveleteknél. Ezért a

    ++sptr->i ++(sptr->i)

    azaz a kifejezés struct B i tagját inkrementálja, s nem sptr-t. Ha mégis ezt szeretnénk (bár a konkrét példánál ennek semmi értelme sincs), akkor a ++sptr kifejezés részt zárójelbe kell tenni, azaz:

    (++sptr)->i

    A

    ++(sptr++)->i

    ugyancsak s.i-t inkrementálja, de a kifejezés mellékhatásaként sptr is megnő eggyel. Ha a zárójeleket elhagyjuk, persze akkor is idejutunk:

    ++sptr++->i ++(sptr++)->i
  • Ugyanígy a nev címen levő karaktert éri el a

    *sptr->nev

    hisz az indirekciót az előbbiekből következőleg az sptr-rel elért nev címen hajtja végre a fordító. Vegyük még a

    *sptr++->nev++

    kifejezést, aminek ugyanaz az értéke, mint az előző kifejezésé, de mellékhatásként sptr és nev inkrementálása is megtörténik.

Ha már mindig hozzárendelési példákat hoztunk, akkor itt kell megemlítenünk, hogy struktúrákat csak akkor lehet egymáshoz rendelni, ha a forrás és a cél struktúra azonos típusú. (Az azonos szerkezetű, de eltérő címkéjű, a fordító által különböző típusúként kezelt struktúrák közötti hozzárendelést esetleg helyettesíthetjük egy memmove hívással.)

struct A {
  int i, j;
  double d; } a, a1;
struct B {
  int i, j;
  double d; } b;
/* . . . */
a = a1; /* OK, a hozzárendelés megy tagról-tagra. */
a = b; /* HIBÁS, mert eltér a két struktúra típusa.*/
a.i = b.i; /* Tagról-tagra persze most is megy a dolog. */
a.j = b.j;
a.d = b.d;

Vegyünk valamilyen példát a struktúratömbökre!

Keressük meg a megadott, síkbeli pontok közt a két, egymástól legtávolabbit! A pontok száma csak futás közben dől el (n), de nem lehetnek többen egy fordítási időben változtatható értéknél (N). Az x koordináta bevitelekor üres sort megadva, a bemenet előbb is befejezhető, de legalább két pont elvárandó! Az input ellenőrzendő, s minden hibás érték helyett azonnal újat kell kérni. Az eredmény közlésekor meg kell jelentetni a két pont indexeit, koordinátáit és persze a távolságot is.

/* PELDA28.C: A ket, egymastol legtavolabbi pont
              megkeresese. */
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <math.h>
#define INP 28 /* Az input puffer merete. */
#define N 128  /* Pontok maximalis szama. */

A feladatot síkbeli pontot leíró struktúra segítségével fogjuk megoldani:

struct Pont {  /* A Pont struktura. */
  double x, y; };
int getline(char s[],int n); {
  int c,i;
  for(i=0; i<n&&(c=getchar())!=EOF&&c!='\n'; ++i) s[i]=c;
  s[i]='\0';
  while(c!=EOF&&c!='\n') c=getchar();
  return(i); }
int lebege(char s[]) {
  int i=0, kezd;
  /* Feher karakterek atlepese a lanc elejen: */
  while(isspace(s[i])) ++i;
  /* A mantissza elojele: */
  if(s[i]=='+'||s[i]=='-') ++i;
  kezd=i; /* A szamjegyek itt kezdodnek. */
  /* A mantissza egesz resze: */
  while(isdigit(s[i])) ++i;
  /* A mantissza tort resze: */
  if(s[i]=='.') ++i;
  while(isdigit(s[i])) ++i;
  /* Nincs szamjegy, vagy csak egy . van: */
  if(i==kezd||kezd+1==i&&s[kezd]=='.') return 0;
  /* Kitevo resz: */
  if(toupper(s[i])=='E') {
    ++i;
    if(s[i]=='+'||s[i]=='-')++i;
    /* Egy szamjegynek lennie kell a kitevoben! */
    if(!isdigit(s[i])) return 0;
    while(isdigit(s[i])) ++i; }
  /* Vege: */
  if(isspace(s[i])||!s[i]) return 1;
  else return 0; }
int main(void) {
  char sor[INP+1];    /* Input puffer. */

A struktúratömb definíciója:

  struct Pont p[N];    /* Strukturatomb. */
  int n=0;            /* Pontok szama. */
  double max=-1., d;  /* Pillanatnyi maximum es */
  int i, j, tavi, tavj;/*segedvaltozok. */
  printf("A ket egymestol legtavolabbi pont a sikban.\n"
        "Adja meg a pontok koordinataparjait rendre!\n"
        "Vege: ures sor az X koordinata megadasanal.\n\n");
  for(n=0; n<N; ++n) {
    printf("A(z) %d pont koordinatai:\n",n+1);
    if(printf("X: "),getline(sor,INP)<=0)break;
    if(lebege(sor)) p[n].x=atof(sor);
    else {
      --n;
      continue; }
    while(printf("Y: "), getline(sor,INP),
          !lebege(sor));
    p[n].y=atof(sor);  }
  if(n<2) {
    printf("Legalabb ket pontot meg kene adni!\n");
    return(1);  }
  for(i=0; i<n-1; ++i)
    for(j=i+1; j<n; ++j)

Kitűnően látszik, hogyan kell elérni a struktúra tömbelem tagjait!

      if((d=sqrt((p[j].x-p[i].x)*(p[j].x-p[i].x)+
                (p[j].y-p[i].y)*(p[j].y-p[i].y))) > max) {
        max=d;
        tavi=i;
        tavj=j; }
  printf( "A maximalis tavolsagu ket pont:\n"
          "P[%d]: (%10.1f, %10.1f) es\n"
          "P[%d]: (%10.1f, %10.1f),\n"
          "s a tavolsag: %15.2f\n",
          tavi+1, p[tavi].x, p[tavi].y,
          tavj+1, p[tavj].x, p[tavj].y, max);
  return(0); }
Struktúrák és függvények

Említettük már, hogy a struktúra másolható, hozzárendelhető, elérhetők a tagjai, képezhető a címe, ill. tömb is előállítható belőle, de függvény is visszaadhat struktúrát vagy erre mutató mutatót. Az fv1 visszaadott értéke struct struki struktúra.

struct struki fv1(void);

Az fv2 viszont struct struki struktúrára mutató mutatót szolgáltat.

struct struki *fv2(void);

A függvény paramétere is lehet struktúra e két módon. Az fv3 struct struki struktúrát fogad paraméterként.

void fv3(struct struki s);

Az fv4 viszont struct struki struktúrát címző mutatót fogad.

void fv4(struct struki *sp);

A következő példa a "helytelen" gyakorlatot szemlélteti. A függvény paraméterei és visszaadott értéke egyaránt struktúra. Struktúra persze akármekkora is elképzelhető.

typedef struct{
  char nev[20];
  int az;
  long oszt; } STUDENT;
STUDENT strurend(STUDENT a, STUDENT b){
  return((a.az < b.az) ? a : b); }
/* . . . */
STUDENT a, b, c;
/* . . . */
c = strurend(a, b);

Amíg a strurend fut, hat darab STUDENT struktúra létezik: a, b, c, aztán a és b másolata és a függvény visszaadott értéke a veremben. Célszerű tehát nem a struktúrát, hanem arra mutató mutatót átadni a függvénynek, ill. vissza is kapni tőle, ha lehet, azaz:

STUDENT *strurnd(STUDENT *a, STUDENT *b){
  return((a->az < b->az) ? a : b); }
/* . . . */
STUDENT *z;
/* . . . */
z = strurnd(&a, &b);

Prototípus hatásköre ellenére a benne megadott struct hatásköre globális, azaz figyelmeztető üzenet nélkül nem hívhatjuk meg a következő függvényt:

void fv(struct S *);

A probléma elhárításához deklarálni vagy definiálni kell a struktúrát prototípus előírása előtt:

struct S;
/* . . . */
void fv(struct S *);

Készítsünk struktúrát és kezelő függvénycsaládot dátumok manipulálására!

A datum struktúrában nyilvántartjuk a dátum évét (ev), hónapját (ho), napját (nap), a Krisztus születése óta a dátumig eltelt napok számát: az ún. dátumsorszámot (datumssz), a dátum karakterlánc alakját (datumlanc) és azt, hogy a dátum az év hányadik napja (evnap). Az egészet úgy képzeljük el, hogy

  • vagy megadják a dátumot év, hó és nap alakban, és a DatumEHN függvénnyel meghatározzuk a struktúra összes többi adatát (a main-ben d1 objektum így kap értéket),
  • vagy karakterlánc alakú dátumból a DatumKAR-ral állítjuk elő a datum struktúra tagjainak értékeit (a főprogramban d2 e módon jut értékhez).
  • Mindkét struct datum objektumnak értéket adó rutin végül dátumellenőrzést végez a Datume függvénnyel, s ezt a logikai értéket szolgáltatja.
  • A NapNev visszaadja a dátum héten belüli napjának nevét. Pontosabban a név karakterláncának címét.
  • A további rutinok műveleteket végeznek a dátum struktúrákkal. A DatumKul megállapítja két dátum különbségét, s szolgáltatja ezt a napszámot. A DatumMegEgy inkrementálja, és visszaadja a dátum objektumot. A DatumMegint pozitív, egész értéket ad hozzá.
  • A dátumfüggvények mind a MINDATUM és MAXDATUM közötti tartományban dolgoznak.
  • A main ki is próbálja az összes dátumfüggvényt.

Figyeljük meg, hogy mindegyik függvény struct datum objektumra mutató paraméterként kapja meg a manipulált dátum struktúrá(ka)t! A DatumMegEgy és a DatumMegint visszatérési értéke struct datum struktúra.

Fedezzük fel, hogy a globális MAXDATUM, a hónapi napszámokat tartalmazó honap tömb, és a Datume függvény static tárolási osztálya miatt lokális a datum.c modulra! Nincs is prototípus a Datume-re a datum.h fejfájlban. Az értéküket nem változtató, de csak a rutin blokkjából elérendő oszto változó, és a napnév karakterláncokra mutatókból álló hetnev mutatótömb statikus élettartamúak, de hatáskörük lokális.

Vegyük még észre a datum.h-ban, hogy a _DATUMH makró egyetlen struct datum definíciót tesz lehetővé akkor is, ha a fordítási egységben többször kapcsolnák be a fejfájlt.

/* DATUM.H: Datumok kezelese. */
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>

#if !defined(_DATUMH)
#define _DATUMH
struct datum{
  int ev, ho, nap, evnap;
  long datumssz;
  char datumlanc[11]; };
#endif

const char *NapNev(struct datum *);
int DatumEHN(int,  int, int, struct datum *);
int DatumKAR(const char *, struct datum *);
long DatumKul(struct datum *, struct datum *);
struct datum DatumMegEgy(struct datum *);
struct datum DatumMegint(struct datum *, int);

/* DATUM.C: Datumok kezelese. */
#include "datum.h"

#define MINDATUM 366
static const long MAXDATUM =
  9999*365l + 9999/4 - 9999/100+9999/400;
static int honap[]= {
  0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };

const char *NapNev(struct datum *pd) {
  static char *hetnev[]= {"vasarnap", "hetfo", "kedd",
                          "szerda", "csutortok","pentek","szombat" };
  return hetnev[(pd->datumssz)%7l]; }

A dátumsorszámból 7-tel képzett modulus alapján állapítja meg a héten belüli napindexet a NapNev rutin.

static int Datume(struct datum *pd) {
  int i;
  honap[2]=28+(!(pd->ev%4)&&pd->ev%100||(!pd->ev%400));
  if(pd->ev<1 || pd->ev>9999 || pd->ho<1 || pd->ho>12 ||
    pd->nap<1 || pd->nap>honap[pd->ho]) {
    pd->evnap=0;
    pd->datumssz=0l;
    return 0;
  } else {
    sprintf(pd->datumlanc, "%04d.%02d.%02d",
            pd->ev, pd->ho, pd->nap);
    pd->evnap=pd->nap;
    for(i=1; i<pd->ho; ++i) pd->evnap+=honap[i];
    pd->datumssz=(pd->ev-1)*365l+pd->evnap+
                pd->ev/4-pd->ev/100+pd->ev/400;
    return 1; } }

A Datume ugyanúgy a dátumot ellenőrzi, s logikai választ ad a "formálisan jó-e a dátum?" kérdésre, mint a korábbi datume függvények. Nem karakterláncból dolgozik azonban, hanem a struktúra ev, ho, nap tagjaiból, melyeket a DatumEHN, ill. a DatumKAR készítettek oda. Ha hibás a dátum, akkor nullázza a rutin az evnap és a datumssz tagokat.

Ha jó a dátum, akkor a Datume képezi a datumlanc-ba a dátum karakterlánc alakját, s meghatározza az evnap és a datumssz értékét. Az evnap a dátum napszámáról indul, s a rutin hozzáadogatja a megelőző hónapok maximális napszámait. A dátumsorszám megállapításához a szökőév vizsgálatához használatos kifejezést alkalmazza a függvény.

Az stdio.h bekapcsolásával rendelkezésre álló sprintf ugyanúgy működik, mint printf társa, de nem a szabvány kimenetre, hanem az első paramétereként kapott karaktertömbbe dolgozik.

A formátumspecifikációkban a mezőszélesség előtt álló 0 hatására a jobbra igazított számok balról nem szóköz, hanem '0' feltöltést kapnak. Magyarán a 932.2.3. dátumból 0900.02.03 karakterlánc lesz.

Szóltunk már róla, hogy a Datume nem hívható más forrásmodulból. A datum.c- ben is csak a DatumEHN és a DatumKAR idézi meg utolsó lépéseként.

int DatumEHN(int e, int h, int n, struct datum *pd) {
  pd->ev=e;
  pd->ho=h;
  pd->nap=n;
  if(e>=0&&h>=0&&n>=0&&e<10000&&h<100&&n<100)
    sprintf(pd->datumlanc, "%04d.%02d.%02d", e, h, n);
  else *pd->datumlanc=0;
  return Datume(pd); }
int DatumKAR(const char *lanc, struct datum *pd) {
  pd->ho=pd->nap=0;
  strncpy(pd->datumlanc, lanc, 10);
  pd->datumlanc[10] = 0;
  pd->ev=atoi(lanc);
  while(isdigit(*lanc))++lanc;
  if(*lanc!=0) {
    ++lanc;
    pd->ho=atoi(lanc);
    while(isdigit(*lanc))++lanc;
    if(*lanc) {
      ++lanc;
      pd->nap=atoi(lanc); } }
  return Datume(pd); }

A DatumEHN a kapott, int típusú év, hó, nap segítségével tölti fel az utolsó paraméterként elért dátum struktúrát.

Az sprintf hívás előtti vizsgálatra azért van szükség, hogy a rutin ne tudja túlírni a 11 elemű karaktertömb, datumlanc tagot a memóriában valamilyen egészen "zöldség" év, hó, nap paraméter miatt. Ilyenkor üres lesz a datumlanc.

A DatumKAR átmásolja a karakterlánc alakban kapott dátum első 10 karakterét a datumlanc-ba. Nullázza a honapot és napot. Látszik, hogy a függvény nem köti meg olyan szigorúan sem az év, sem a hónap és nap jegyszámát, mint a korábbi datume, ill. elválasztó karakterként csak valamilyen nem numerikust vár el. A karakterlánc egésszé konvertált elejét évnek, az első elválasztó karakter utáni részt hónapnak, s a második elválasztó karakter mögöttieket napnak tekinti a rutin, hacsak időközben vége nem lesz a karakterláncnak.

Végül mindkét függvény meghívja a Datume-t, s ennek visszatérési értékét szolgáltatja.

long DatumKul(struct datum *pd1, struct datum *pd2) {
  if(pd1->datumssz>pd2->datumssz)
    return pd1->datumssz-pd2->datumssz;
  else return pd2->datumssz-pd1->datumssz; }

A DatumKul képzi a két paraméter dátum struktúra dátumsorszám tagjainak különbsége abszolút értékét.

struct datum DatumMegEgy(struct datum *pd) {
  int e=pd->ev, h=pd->ho, n=pd->nap;
  struct datum d;
  honap[2]=28+(e%4==0 && e%100 || e%400==0);
  if(++n>honap[h]) {
    n=1;
    ++h;
    if(h>12) {
      h=1;
      ++e; } }
  if(!DatumEHN(e, h, n, &d)) d=*pd;
  return d; }

A paramétere dátumot inkrementáló DatumMegEgy munkaváltozókba rakja az évet, a hónapot és a napot. Meghatározza az év szerinti február pontos napszámát. Növeli eggyel a napot. Ha ez túlmenne a hónap szerinti maximális napszámon, akkor 1 lesz, és a hónapszám növelése jön. Ha ez 13 lenne, akkor 1 lesz, és az évszám növelése következik.

A megállapított, új év, hó, nap alapján a DatumEHN feltölti a lokális, d dátum objektumot. Ha az új dátum érvénytelen volt, akkor a változatlanságot jelzendő a rutin hozzárendeli d-hez a paraméter címen levő, eredeti dátumot.

A hozzárendelés a paraméter címen levő (ezért kell elé az indirekció) struct datum minden tagját egy az egyben átmásolja a balérték, d, lokális dátum objektum tagjaiba rendre.

A visszatérés során a DatumMegEgy létrehoz a veremben egy ideiglenes dátum objektumot, melybe tagról-tagra bemásolja a d lokális dátum változót. Visszatérés után aztán a main hozzárendeli az ideiglenes dátum objektumot a main-ben lokális d-hez.

struct datum DatumMegint(struct datum *pd, int np) {
  int e=pd->ev, h=pd->ho, n=pd->nap;
  long dpd = pd->datumssz + np + 365;
  /* A tiszta jo konstans 365.24223 lenne kezi
    szamitas szerint!!!! */
  static double oszto=365.24225;
  struct datum d=*pd;
  if(np <= 0) return d;
  if(dpd > MAXDATUM) {
    e=9999;
    h=12;
    n=31;
  } else {
    e= (int)dpd/oszto;
    n=dpd-e*365l-e/4+e/100-e/400;
    honap[2]=28+(e%4==0 && e%100 || e%400==0);
    for(h=1; n > honap[h]; ++h)n-=honap[h]; }
  if(!DatumEHN(e, h, n, &d)) d=*pd;
  return d; }

Csak a kezdetét és a végét tekintve a pozitív napszámot a dátumhoz adó DatumMegint a DatumMegEgy-gyel megegyezően dolgozik.

Látszik, hogy negatív, hozzáadandó napszámot, vagy a művelet végén érvénytelen dátumot kapva, az eredeti dátum objektumot szolgáltatja a rutin változatlanul.

A dátumsorszámot megnöveli a napszámmal és még 365-tel. Ha így meghaladná a 9999.12.31-et, akkor ezt adná vissza. Ha nem, akkor az új dátumsorszámot elosztja a tapasztalati alapon a [MINDATUM, MAXDATUM] tartományban érvényes évenkénti átlagos napszámmal, s ez lesz az új évszám. Visszaszámolja belőle az új év pontos napszámát, s a két érték különbségéből hónap és napszámot képez.

/* PELDA29.C: A datumok kezelesenek kiprobalasa. */
#include "DATUM.H"

void main(void) {
  long kul;
  struct datum d1, d2, d;
  printf("Datum műveletek:\n\n");
  DatumEHN(2003, 12, 31, &d1);
  DatumKAR("2003-2-13", &d2);
  printf("A(z) %s. es a(z) %s. kulonbsege %ld nap!\n",
        d1.datumlanc, d2.datumlanc,
        (kul=DatumKul(&d1, &d2)));
  d=DatumMegEgy(&d1);
  printf("A(z) %s. + 1 a(z) %s.\n", d1.datumlanc,
        d.datumlanc);
  d=DatumMegint(&d2, (int)kul);
  printf("A(z) %s. + %ld a(z) %s.\n", d2.datumlanc,
        kul, d.datumlanc);
  printf("A(z) %s. %s.\n", d2.datumlanc, NapNev(&d2)); }
Struktúra tárillesztése

A fordító a struktúratagokat deklarációjuk sorrendjében növekvő memória címeken helyezi el. Minden adatobjektum rendelkezik tárillesztési igénnyel is. A fordító olyan eltolással helyezi el az adatobjektumot, hogy az

eltolás % tárillesztési-igény == 0

zérus legyen. Struktúrák esetén ez a szabály a tagok elhelyezésére vonatkozik. Ha példának vesszük a

struct struki {
  int i;
  char lanc[3];
  double d; } s;

struktúrát, akkor tudjuk, hogy az s objektumot növekvő memória címeken úgy helyezi el a fordító, hogy

1.négy bájtot (32 bites esetben) foglal az int tagnak,
2.aztán a 3 bájtos karakterlánc következik, és
3.végül 8 bájtot rezervál a double taghoz.

Bizonyos fordító opciók, vagy valamilyen #pragma direktíva segítségével vezérelhetjük a struktúra adatok memóriabeli illeszkedését. Ez azt jelenti, hogy az adatokat 1-gyel, 2-vel, 4-gyel stb. maradék nélkül osztható címeken: bájthatáron, szóhatáron, dupla szóhatáron stb. kell elhelyezni. Felkérjük az olvasót, hogy nézzen utána a dolognak a programfejlesztő rendszere segítségében! Bárhogyan is, eme beállítások hatására a fordító minden struktúratagot, az elsőt követően, olyan határon tárol, mely megfelel a tag tárillesztési igényének.

A bájthatárra igazítás azt jelenti, hogy

  • a struktúra objektum elhelyezése bármilyen címen kezdődhet, és
  • a struktúratagok ugyancsak bármilyen címen elhelyezhetők típusuktól függetlenül.

A példa s objektum összesen 15 bájtot foglal el ilyenkor, és a memória térkép a következő:

A szóhatárra igazítás azt jelenti, hogy

  • a struktúra objektum kezdőcíme páros kell, hogy legyen, és
  • a struktúratagok - a char típustól eltekintve - ugyancsak páros címeken helyezkednek el.

A példa s objektum így összesen 16 bájtot foglal el. Egy bájt elveszik, és a memória térkép a következő:

A dupla szóhatárra igazítás azt jelenti, hogy

  • a struktúra objektum elhelyezése néggyel maradék nélkül osztható címen (dupla szóhatáron) történik meg,
  • a char típusú struktúratagok bájthatáron kezdődnek,
  • a short típusú tagok szóhatáron indulnak és
  • a többi típusú tag dupla szóhatáron (néggyel maradék nélkül osztható címen) kezdődik.

Dupla szóhatárra igazítva az s objektum megegyezik az előzővel. A határra igazítási "játék" folytatható értelemszerűen tovább.

Persze a struktúrát nem ilyen "bután" definiálva igazítástól függetlenül elérhetjük, hogy egyetlen bájt elvesztése se következzék be:

struct struki {
  double d;
  int i;
  char lanc[3]; } s;
Uniók

Az unió típus a struktúrából származik, de a tagok között zérus a címeltolás. Az unió azt biztosítja, hogy ugyanazon a memória területen több, különféle típusú adatot tárolhassunk. Az

union unio{
  int i;
  double d;
  char t[5]; } u, *pu = &u, tu[23];

definícióban az u union unio típusú objektum, a pu ilyen típusú objektumra mutató mutató, és a tu egy 23 ilyen típusú elemből álló tömb azonosítója. Az u objektum - például - egyazon memória területen biztosítja az i nevű int, a d azonosítójú double és a t nevű karaktertömb típusú tagjainak elhelyezését, azaz:

&u  &u.i  &u.d  ...

Ennek szellemében aztán igaz, hogy az unió objektumot címző mutató annak egyben minden tagjára is mutat.

(pu=&u)  &u.i  &u.d  ...

Természetesen a dolog csak a mutatók értékére igaz, mert az &u (union unio *), az &u.i (int *) és az &u.d (double *), azaz típusban eltérnek. Ha azonban uniót megcímző mutatót explicit típusmódosítással tagjára irányuló mutatóvá alakítjuk, akkor az eredmény mutató magára a tagra mutat:

u.d=3.14;
printf("*(&u.d) = %f\n", *(double *)pu);

Az unió helyfoglalása a tárban akkora, hogy benne a legnagyobb bájtigényű tagja is elfér, azaz:

sizeof(union unio)  sizeof(u)  8.

Tehát 4 bájt felhasználatlan, ha int adatot tartunk benne, ill. 3 bájt elérhetetlen, ha karaktertömböt rakunk bele. Az unió egy időben csak egyetlen tagját tartalmazhatja.

Láttuk már, hogy az uniótagokat ugyanazokkal a tagszelektor operátorokkal érhetjük el, mint a struktúratagokat:

u.d = 3.15;
printf("u.d=%f\n", u.d); /* OK: u.d=3.15 jelenik meg. */
printf("u.i=%d\n", u.i); /* Furcsa eredmény születik. */
printf("u.t[0]=%c\n", u.t[0]);/* Valami csak megjelenik,
                                vagy sem. */
printf("u.t=%s\n", u.t); /* "Csoda" karakterlánc látszik.
                        Ki tudja, hol van a lánc vége!*/
strcpy(pu->t, "Hohó");
printf("u.t=%s\n", pu->t);/* OK: a "Hohó" látszik. */
printf("u.i=%d\n", pu->i);/* Furcsa eredmény születik. */
printf("u.d=%f\n", pu->d);/* Nagyon szorítsunk, hogy ne
          legyen lebegőpontos túl vagy alulcsordulás! */

Valahonnan tehát célszerű tudni - például úgy, hogy nyilvántartjuk - milyen típusú adat is található pillanatnyilag az unió objektumban, és azt szabad csak elérni.

Ha egy unió többféle, de azonos kezdő szerkezetű struktúrával indul, és az unió tartalma e struktúrák egyike, akkor lehetőség van az unió közös kezdeti részére hivatkozni. Például:

union{
  struct{ int tipus;} t;
  struct{ int tipus; int iadat;} ti;
  struct{ int tipus; double dadat;} td;
  /* . . . */ } u;
/* . . . */
u.td.tipus = DOUBLE;
u.td.dadat = 3.14;
/* . . . */
if(u.t.tipus == DOUBLE) printf("%f\n", u.td.dadat);
else if(u.t.tipus == INT) printf("%d\n", u.ti.iadat);
else /* . . . */

Uniókkal pontosan ugyanazok a műveletek végezhetők, mint a struktúrákkal. Hozzárendelhetők, másolhatók, hozzáférhetünk a tagjaikhoz, képezhető a címük, átadhatók függvényeknek és rutinok visszatérési értékei is lehetnek.

Uniódeklarációk

Az általános deklarációs szabály azonos a struktúráéval. Az eltérések a következők:

  • Az uniók tartalmazhatnak bitmezőket. Mindegyik bitmező azonban az unió kezdetétől indul, s így közülük csak egy lehet aktív.
  • Az unió tagja nem lehet void, nem teljes, vagy függvény típusú. Nem lehet a definíció alatt álló unió egy példánya, de ilyenre mutató mutató persze lehet.
  • Uniók esetében a deklarációban csak az elsőnek deklarált tagnak adható explicit kezdőérték. Például:

    union unika{
      int i;
      double d;
      char t[6]; } u = { 24 };

    Csak az u.i kaphatott, és kapott is 24 kezdőértéket. Ha kicsit bonyolultabb esetet nézünk:

    union{
      char x[2][3];
      int i, j ,k; } y = {{{'1'}, {'4'}}};

    Az y unió változó inicializálásakor aggregátum inicializátort használunk, mert az unió első tagja kétdimenziós tömb. Az '1' inicializátor a tömb első sorához tartozik, így az y.x[0][0] felveszi az '1' értéket, s a sor további elemei tiszta zérusok lesznek az implicit kezdőérték adás szabályai szerint. A '4' a második sor első elemének inicializátora, azaz y.x[1][0] = '4', y.x[1][1] = 0 és y.x[1][2] = 0.
  • Lokális élettartamú uniók esetén az inicializátor kompatibilis unió típusú egyszerű kifejezés is lehet:

    union unika{
      int i;
      double d;
      char t[6]; } u = { 24 }, u1 = u;
  • Az uniódeklarációban is elhagyható az uniócímke. Az uniók előfordulhatnak struktúrákban, tömbökben, és tömbök, ill. struktúrák is lehetnek tagok uniókban:

    #define MERET 20
    struct {
      char *nev;
      int adat;
      int u_tipus; /* Az unióban aktuálisan tárolt */
      union{ /* típus nyilvántartásához. */
        int i;
        float f;
        char *mutato; } u;
    } tomb[MERET];

    Ilyenkor a tomb i-edik eleme i uniótagjához való hozzáférés alakja:

    tomb[i].u.i

    és a mutato tag mutatta első karakter elérésének formája:

    *tomb[i].u.mutato
Bitmezők (bit fields)

Bitmezők csak struktúra vagy unió tagjaként definiálhatók, de a struktúrában és az unióban akár keverten is előfordulhatnak bitmező és nem bitmező tagok. A bitmező struktúratag deklarációs szintaktikája kicsit eltér a normál tagokétól:

típusspecifikátor <deklarátor> : konstans-kifejezés;

ahol a típusspecifikátor csak

  • signed int,
  • unsigned int vagy
  • int

lehet az ANSI C szabvány szerint. Az int tulajdonképpen signed int. A deklarátor a bitmező azonosítója, mely el is maradhat. Ilyenkor a névtelen bitmező specifikálta bitekre nem tudunk hivatkozni, s a bitek futásidejű tartalma előre megjósolhatatlan. A konstans-kifejezés csak egészértékű lehet. Zérus és sizeof(int)*8 közöttinek kell lennie, s a bitmező szélességét határozza meg.

Bitmező csak struktúra vagy unió tagjaként deklarálható. Nem képezhető azonban bitmezők tömbje. Függvénynek sem lehet visszaadott értéke a bitmező. Nem megengedett a bitmezőre mutató mutató és tilos hivatkozni a bitmező tag címére, azaz nem alkalmazható rá a cím (&) operátor sem.

A bitmezők az int területen (dupla szóban, vagy szóban) deklarációjuk sorrendjében többnyire az alacsonyabb helyi értékű bitpozícióktól a magasabbak felé haladva foglalják el helyüket.

Az int pontos mérete, bitmezővel való feltöltésének szabályai és sorrendje a programfejlesztő rendszertől függ. Célszerű tehát a segítségben utánanézni a dolognak. Maradjunk meg azonban az előző bekezdésben említett szabálynál, és a könnyebb szemléltethetőség végett még azt is tételezzük fel, hogy az int 16 bites! Ilyenkor például a:

struct bitmezo{
  int i: 2;
  unsigned j: 5;
  int : 4, k: 1;
  unsigned m: 4; } b, *pb = &b;

által elfoglalt szó bittérképe a következő:

Ha az m bitmező tag szélessége 4-nél nagyobb lett volna, akkor új szót kezdett volna a fordító, s az előző szó felső négy bitje kihasználatlan maradt volna. Általánosságban: a (dupla)szón túllógó bitmező új (dupla)szót kezd, s az előző (dupla)szóban a felső bitek kihasználatlanok maradnak.

Ha a deklarációban valamely (névtelen) bitmezőnél zérus szélességet adunk meg, akkor mesterségesen kényszerítjük ki ezt a következő (dupla) szóhatárra állást.

A bitmezőnek elég szélesnek kell lennie ahhoz, hogy a rögzített bitminta elférjen benne! Például a következő tagdeklarációk illegálisak:

int alfa : 37;
unsigned beta : 42

A bitmezők ugyanazokkal a tagszelektor operátorokkal (. és ->) érhetők el, mint a nem bitmező tagok: b.i vagy pb->k

A bitmezők kis signed vagy unsigned egész értékekként viselkednek (rögtön átesnek az egész-előléptetésen), azaz kifejezésekben ott fordulhatnak elő, ahol egyébként aritmetikai (egész) értékek lehetnek. signed esetben a legmagasabb helyi értékű bit (MSB - most significant bit) előjelbitként viselkedik, azaz az int i : 2 lehetséges értékei például:

00: 0, 01: +1, 10: -2, 11: -1

Az unsigned m : 4 lehetséges értékei:

0000: 0, 0001: 1, . . ., 1111: 15

Általánosságban:

unsigned x : szélesség; /* 0 <= x <= 2szélesség-1 */
signed y : szélesség; /* -2szélesség-1<= y <=+2szélesség-1-1 */

A nyelvben nincs sem egész alul, sem túlcsordulás. Ha így a bitmezőnek ábrázolási határain kívüli értéket adunk, akkor abból is lesz "valami". Méghozzá az érték annyi alsó bitje, mint amilyen széles a bitmező. Például:

b.i = 6; /* 110 -> 10, azaz -2 lesz az értéke! */

A bitmezők ábrázolása gépfüggő, mint már mondottuk, azaz portábilis programokban kerüljük el használatukat!

Vegyük elő ismét a Bit szintű operátorok részben tárgyalt dátum és időtárolási problémát! Hogyan tudnánk ugyanazt a feladatot bitmezőkkel megoldani?

Dátum: Bitpozíció:Idő:Bitpozíció:
év - 19809 - 15óra11 - 15
hónap 5 - 8perc5 - 10
nap0 - 4két másodperc0 - 4

A dátum és az idő adatot egy-egy szóban, azaz C nyelvi fogalmakkal egy-egy unsigned short int-ben tartjuk. A két szó bitfelosztása az ábrán látható!

A bitmezős megoldás például a következő is lehetne:

struct datum{
  unsigned short nap: 5, ho: 4, ev: 7; }
  d = { 8, 3, 1996-1980 };
struct ido{
  unsigned short mp2: 5, perc: 6, ora: 5; }
  i = { 2, 59, 11 };
/* . . . */
int ev=1996, ho=3, nap=8, ora=11, perc=59, mp=4;
/* Részeiből a dátum és az idő előállítása: */
d.ev = ev - 1980;
d.ho = ho;
d.nap = nap;
i.ora = ora;
i.perc = perc;
i.mp2 = mp >> 1;
/* Ugyanez visszafelé: */
ev = d.ev + 1980;
ho = d.ho;
nap = d.nap;
ora = i.ora;
perc = i.perc;
mp = i.mp2 << 1;
Balérték - jobbérték

Most már tökéletesen pontosíthatjuk a balérték kifejezést a C-ben, mely:

  • Egész, lebegőpontos, mutató, struktúra vagy unió típusú azonosító.
  • Indexes kifejezés, mely nem tömbbé (hanem elemmé) értékelhető ki.
  • Tagszelektoros kifejezés (->, .).
  • Nem tömbre hivatkozó, indirekciós kifejezés.
  • Balérték kifejezés zárójelben.

A const objektum nem módosítható balérték, hisz csak a deklarációban kaphat kezdőértéket.

A jobbérték (rvalue) olyan kiértékelhető kifejezés, melynek értékét balérték veheti fel. Például:

a = c + d; /* OK */
c + d = a; /* HIBÁS */

A balérték (lvalue) olyan kifejezés, mely eléri az objektumot (a hozzá allokált memória területet). Triviális például egy változó azonosítója. Lehet azonban *P alakú is, ahol a P kifejezést nem NULL mutatóra értékeli ki a fordító. Onnét is származtatható a két fogalom, hogy a balérték állhat a hozzárendelés operátor bal oldalán, s a jobbérték pedig a jobb oldalán.

Beszélhetünk módosítható balértékről is! Módosítható balérték nem lehet tömb típusú (a tömbazonosító praktikusan cím konstans), nem teljes típusú, vagy const típusmódosítóval ellátott objektum. Módosítható balérték például a konstans objektumra mutató mutató maga, miközben a mutatott konstans objektum nem változtatható. Például

int tomb[20];

esetén balértékek:

tomb[3] = 3; *(tomb+4) = 4;

A következő deklarációban viszont a kar nem balérték, hisz konstansságára való tekintettel értéket egyedül a definíciójában kaphat:

const char kar = 'k';

Azonban ha van egy

char *pozicio(int index);

függvény, akkor balérték lehet a következő is:

*pozicio(5) = 'z';
Névterületek

A névterület az a "hatáskör", melyen belül egy azonosítónak egyedinek kell lennie, azaz más-más névterületen konfliktus nélkül használható ugyanaz az azonosító, s a fordító meg tudja különböztetni őket. A névterületeknek a következő fajtái vannak:

  • Utasítás címke névterület: Az utasítás címkéknek abban a függvényben kell egyedinek lenniük, amelyben definiálták őket.
  • Struktúra, unió és enum címke névterület: A struktúra, az unió és az enum címkék ugyanazon a névterületen osztoznak. Deklarálásuk blokkjában kell egyedinek bizonyulniuk. Ha minden függvény testén kívül adják meg őket, akkor viszont fájl hatáskörben kell egyedinek lenniük.
  • Struktúra és uniótagok (member) névterülete: A tagneveknek abban a struktúrában vagy unióban kell egyedinek lenniük, amelyben deklarálták őket. Különböző struktúrákban és uniókban előfordulhatnak ugyanazon tagazonosítók akár más típussal, s eltolással. Összesítve: mindenegyes struktúra és unió külön névterülettel rendelkezik.
  • Normál azonosítók névterülete: Idetartozik minden más név, ami nem fért be az előző három névterületbe, azaz a változó, a függvény (beleértve a formális paramétereket, s a lokális változókat) és az enumerátorazonosítók. Abban a hatáskörben kell egyedinek bizonyulniuk, ahol definiálják őket. Például a fájl hatáskörű azonosítóknak ugyanebben a hatáskörben kell egyedinek lenniük.
  • Típusdefiníció (typedef) nevek: Nem használhatók azonosítóként ugyanabban a hatáskörben. Magyarán a típusdefiníciós nevek a normál azonosítók névterületén vannak, de nem futásidejű azonosítók! Tehát, ha a helyzetből eldönthető, akkor lehet a típusdefiníciós név, és például egy lokális hatáskörű változó azonosítója egyforma is:

    typedef char FT;
    int fv(int lo){
      int FT; /* Ez az FT egy int típusú lokális
                 változó azonosítója. */
    /* . . . */ }

Nézzünk néhány példát!

struct s{
  int s; /* OK: a struktúratag újabb névterületen
            helyezkedik el. */
  float s;/*HIBÁS: így már két azonos tagnév lenne egy
            struktúrán belül. */
} s; /* OK: a normál változók névterülete különbözik
        minden eddig használttól. */
union s{ /* HIBA: az s struktúracímke is ezen a
            névterületen van. */
  int s; /* OK: hisz új tag névterület kezdődött. */
  float f;/*OK: más azonosítójú tag. */
} f; /* OK: hisz ez az f az normál változók
        névterületén található. */
struct t{
  int s; /* OK: hiszen megint újabb tag névterület
            kezdődött. */
/* . . . */
} s; /* HIBA: s azonosító most már minden
        névterületen van. */
goto s; /* OK: az utasítás címke és a struktúracímke
          más-más névterületen vannak. */
/* . . . */
s: ; /* Utasítás címke. */
Feladatok

1. Fejlessze tovább a pelda28.c-t úgy, hogy a tárolt síkbeli pontokat rendezi az origótól való távolságuk csökkenő sorrendjében, majd meg is jeleníti a pontokat és távolságukat fejléccel ellátva, táblázatosan és lapozhatóan!

/* PELDA28X.C: A ket, egymastol legtavolabbi pont keresese,
majd a pontok origotol mert tavolsaguk csokkeno sorrendjeben
rendezett listajanak lapozhato megjelenitese. */
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <math.h>
#define INP 28 /* Az input puffer merete. */
#define N 128 /* Pontok maximalis szama. */
#define LAPSOR 20 /* Egy lapra kiirhato sorok szama. */
struct Pont { /* A Pont struktura. */
  double x, y, tav;
};/* tav tag=origotol mert tavolsag. */
int getline(char *s, int n) { /* Sor beolvasasa s-be. */
  int c; /* A hosszat adja vissza. */
  char *t=s; /* Indexeles helyett a t mutatoval
haladunk az s karaktertombon. A t mindig a kovetkezo
szabad helyre mutat a tombben, melynek tulirasat az n
elfogyasa akadalyozza meg. */
  while(n-->0&&(c=getchar())!=EOF&&c!='\n') *t++=c;
  *t='\0';
  while(c!=EOF&&c!='\n') c=getchar();
  /* A karakterlacot zaro '\0' cimebol a tomb kezdocimet
  kivonva, eppen a karakterlanc hosszat kapjuk. */
  return(t-s); }
int lebege(char *s) { /* Atirva mutatosra! */
  char *i=s, *kezd;
  /* Feher karakterek atlepese a lanc elejen: */
  while(isspace(*i)) ++i;
  /* A mantissza elojele: */
  if(*i=='+'||*i=='-') ++i;
  kezd=i; /* A szamjegyek itt kezdodnek. */
  /* A mantissza egesz resze: */
  while(isdigit(*i)) ++i;
  /* A mantissza tort resze: */
  if(*i=='.') ++i;
  while(isdigit(*i)) ++i;
  /* Nincs szamjegy, vagy csak egy . van: */
  if(i==kezd||kezd+1==i&&*kezd=='.') return 0;
  /* Kitevo resz: */
  if(toupper(*i)=='E') {
    ++i;
    if(*i=='+'||*i=='-')++i;
    /* Egy szamjegynek lennie kell a kitevoben! */
    if(!isdigit(*i)) return 0;
    while(isdigit(*i)) ++i; }
  /* Vege: */
  if(isspace(*i)||!*i) return 1;
  else return 0; }
int main(void) {
  char sor[INP+1]; /* Input puffer. */
  struct Pont p[N], /* Strukturatomb. */
      cs; /* Rendezesnel a cserehez. */
  int n=0; /* Pontok szama. */
  double max=-1., d; /* Pillanatnyi maximum es */
  int i, j, tavi, tavj; /* segedvaltozok. */
  printf("A ket egymastol legtavolabbi pont a sikban, "
        "majd a\npontok listaja origotol mert tavolsaguk "
        "sorrendjeben.\n\n"
        "Adja meg a pontok koordinataparjait rendre!\n"
        "Vege: Ures sor az X koordinata megadasanal.\n\n");
  for(n=0; n<N; ++n) {
    printf("A(z) %d pont koordinatai:\n",n+1);
    if(printf("X: "), getline(sor, INP)<=0)break;
    if(lebege(sor)) p[n].x=atof(sor);
    else {
      --n;
      continue; }
    while(printf("Y: "), getline(sor,INP),
          !lebege(sor));
    p[n].y=atof(sor); /* A tavolsag! */
    p[n].tav=sqrt(p[n].x*p[n].x + p[n].y*p[n].y); }
  if(n<2) {
    printf("Legalabb ket pontot meg kene adni!\n");
    return(1); }
  /* A maximalis tavolsagu ket pont megkeresese: */
  for(i=0; i<n-1; ++i)
    for(j=i+1; j<n; ++j)
      if((d=sqrt((p[j].x-p[i].x)*(p[j].x-p[i].x)+
                (p[j].y-p[i].y)*(p[j].y-p[i].y))) > max) {
        max=d;
        tavi=i;
        tavj=j; }
  printf("A maximalis tavolsagu ket pont:\n"
        "P[%d]: (%10.1f, %10.1f) es\n"
        "P[%d]: (%10.1f, %10.1f),\n"
        "s a tavolsag: %15.2f\n",
        tavi+1, p[tavi].x, p[tavi].y,
        tavj+1, p[tavj].x, p[tavj].y, max);
  /* Rendezes az origotol mert tavolsag szerint
  csokkenoleg: */
  for(i=0; i<n-1; ++i) {
    for(j=i+1, tavi=i; j<n; ++j) if(p[j].tav>p[tavi].tav)
        tavi=j;
    if(i!=tavi) {
      cs=p[i];
      p[i]=p[tavi];
      p[tavi]=cs; } }
  /* Listazas: */
  printf("\nA listazas inditasahoz usson Enter-t! ");
  while(getchar()!='\n');
  for(i=0; i<n; ++i) {
    if(i%LAPSOR==0) { /* Lapvaltas? */
      if(i) { /* Varakozas a lapvegen. */
        printf("A tovabblistazashoz usson Enter-t! ");
        while(getchar()!='\n'); }
      /* Fejlec. */
      printf("\n\n%10s%10s%10s\n", "X", "Y",
            "Tavolsag");
      printf("------------------------------\n"); }
    /* Sorok: */
    printf("%10.1f%10.1f%10.2f\n", p[i].x, p[i].y,
          p[i].tav); }
  return(0); }

2. Egyszeri programozó megpróbált olyan függvényt írni, amely egy dátum struktúra paraméterből dátum karakterláncot állít elő oly módon, hogy abban a hónap nem számmal, hanem szövegesen szerepel. A megoldás sajnos nem hibátlan.

/*01*/struct datum{
/*02*/  int ev, ho, nap }
/*03*/#include<stdio.h>
/*04*/char* datum(const struct datum* d) {
/*05*/  static char* honapNevek[] =
/*06*/    { "januar", "februar", "marcius", "aprilis",
/*07*/      "majus", "junius", "julius", "augusztus",
/*08*/      "szeptember", "oktober", "november", "december" };
/*09*/  char datumLanc[21];
/*10*/  if(d.ev<0||d.ev>9999||d.ho<1||d.ho>12||d.nap<1||d.nap>31)
/*11*/    return NULL;
/*12*/  sprintf(datumLanc, "%d. %s %d.",
/*13*/          d->ev, honapNevek[d->ho-1], d->nap);
/*14*/  return datumLanc; }
Jelölje meg azokat a sorokat, amelyekkel a függvény működőképessé válik!
/*02*/  int ev, ho, nap; }
/*02*/  int ev, ho, nap };
/*02*/  int ev, ho, nap; };
/*04*/char* datum(const datum* d) {
/*09*/  static char datumLanc[21];
/*10*/  if(d->ev<0||d->ev>9999||d->ho<1||d->ho>12||d->nap<1||d->nap>31)
/*13*/          d.ev, honapNevek[d.ho-1], d.nap);
/*13*/          d.ev, honapNevek[d.ho], d.nap);
/*13*/          d->ev, honapNevek[d->ho], d->nap);

3. Kezdő programozó megpróbált készíteni egy olyan függvényt, ami a paraméterként kapott óó:pp:mm formátumú karakterláncból időt reprezentáló struktúrát készít, majd annak címét visszaadja. Hiba esetén NULL-t szolgáltat. A megoldás azonban nem tökéletes.

/*01*/typedef struct {
/*02*/  char ora, perc, mp; } IDO;
/*03*/#include<ctype.h>
/*04*/#include<stdlib.h>
/*05*/IDO idoKonv(const char* s) {
/*06*/  int i;
/*07*/  IDO* is;
/*08*/  if(s[2]!=':'||s[5]!=':')
/*09*/    return NULL;
/*10*/  for(i=0; s[i]; i++) {
/*11*/    if(i==2||i==5) continue;
/*12*/    if(!isdigit(s[i]))
/*13*/      return NULL; }
/*14*/  if(i==8) return NULL;
/*15*/  is = (IDO*)malloc(sizeof(IDO));
/*16*/  if(!is) {
/*17*/    is->ora = atoi(s);
/*18*/    is->perc = atoi(s+3);
/*19*/    is->mp = atoi(s+6);
/*20*/    if(is->ora>23||is->perc>59||is->mp>59) {
/*21*/      calloc(1, sizeof(is));
/*22*/      return NULL; } }
/*23*/  return is; }
Jelölje meg azokat a sorokat, melyekkel működőképessé tehető!
/*05*/IDO* idoKonv(const char* s) {
/*11*/    if(i==2||i==5) break;
/*12*/    if(!isdigit(*s))
/*14*/  if(i!=8) return NULL;
/*15*/  is = (IDO)malloc(sizeof(IDO));
/*16*/  if(is) {
/*18*/    is->perc = atoi(s[3]);
/*19*/    is->mp = atoi(s[6]);
/*20*/    if(is.ora>23||is.perc>59||is.mp>59) {
/*21*/      free(is);

4. Kezdő programozó megpróbálta számítógépének IPv4 címét kiírni egy darab 4 bájtos értékként, és 4 darab 1 bájtos adatként is.

/*01*/#include<stdio.h>
/*02*/void main(void) {
/*03*/  int i;
/*04*/  union ipu {
/*05*/    unsigned teljes;
/*06*/    unsigned char bajt[4];
/*07*/  } ip = {{ 1, 2, 3, 4 }};
/*08*/  printf("Teljes cim: %u\n", ip.teljes);
/*09*/  printf("Bájtonként: ");
/*10*/  for(i=sizeof(unsigned)-1; i>=0; i--)
/*11*/    printf("%4u ", ip.bajt[i]);
/*12*/  putchar('\n'); }
Jelölje meg azokat a sorokat, amelyek használatával hibás programja működni fog!
/*05*/    unsigned char bajt[4];
/*06*/    unsigned teljes;
/*11*/    printf("%4u ", ip->bajt[i]);