6.4 Deklarátorok: tömb-, pointer- és függvénykapcsolatok

Az összetett C-deklarációk olvasása nem különleges memóriafeladat, hanem néhány nyelvtani szabály következetes alkalmazása. Ez rendszerprogramozásban közvetlenül hasznos: egy interfészből meg kell tudnod állapítani, hogy pointerek tábláját, összefüggő tömb címét vagy egy később meghívható függvény címét kapod-e.

A szintaktikai áttekintésből ismert deklarációkat és a többszintű pointerek, illetve a const szabályait használjuk fel. A típus azonban csak a szerződés egyik része: nem mondja meg automatikusan az elemszámot, a tulajdonlást vagy a referált objektum hátralévő élettartamát.

Az alábbi nyelvi szabályok az ISO C17 profilra vonatkoznak. A rövid kódblokkok jelölt deklaráció- vagy függvénytörzs-részletek; a mini labor teljes fordítási egységet tartalmaz.

6.4.1 A deklarátor olvasási algoritmusa

Egy deklarációban a deklarációs specifikátorok (declaration specifiers) adják például az int típust és a típusminősítőket. A deklarátor (declarator) ehhez kapcsolja az azonosítót, valamint a pointer-, tömb- és függvényszerkezetet.

Az alábbi ISO C17 deklarációrészletben az int közös, de a két deklarátor különbözik:

int *p, count;

Csak p pointer; count típusa int. Ezért összetett típusoknál célszerű deklarációnként egy azonosítót használni.

A deklarátornyelvtanban a [] tömbképző és a paraméterlistát tartalmazó () függvényképző szorosabban kapcsolódik, mint a pointert jelölő *. A csoportosító zárójel ezt módosítja. Ez nem kifejezéskiértékelési sorrend: a fordító itt típust épít fel.

Használható olvasási algoritmus:

  1. Keresd meg a deklarált azonosítót, és különítsd el a deklarációs specifikátorokat.
  2. Az aktuális zárójelezési szinten először az azonosítóhoz kapcsolódó jobb oldali [] és függvényparaméter-lista jelentését olvasd el.
  3. Ezután dolgozd fel a bal oldali * jeleket és a hozzájuk tartozó minősítőket.
  4. A csoportosító zárójelből kilépve folytasd a külső szinten.
  5. Végül illeszd hozzá a specifikátorok által megadott típust.

A paraméterlistában szereplő deklarációkat külön kell értelmezni. A módszer ezért pontosabb a feltétel nélküli „mindig jobbra, majd balra” szabálynál.

Például:

/* ISO C17 deklarációrészlet. */
int *a[3];
int (*b)[3];

Az első sorban a után közvetlenül [3] áll:

a háromelemű tömb, amelynek elemei pointerek int objektumokra.

A második sorban a csoportosítás miatt előbb a *b kapcsolatot kell feldolgozni:

b pointer egy háromelemű, int elemeket tartalmazó tömbre.

A zárójel tehát nem díszítés: más típust és más műveleti szerződést hoz létre.

6.4.2 Pointertömb és tömbre mutató pointer

Legyen N pozitív egész konstanskifejezés. A két alaptípus:

Deklaráció A deklarált objektum típusa Referált objektum
int *a[N] N darab int * elemből álló tömb Minden elem külön int objektumra mutathat
int (*p)[N] Egyetlen pointer Egy teljes, N elemű int tömb

A pointertömb pointerértékeket tárol. A referált egész objektumok lehetnek különböző helyeken, ugyanazon tömbben, vagy több pointer mutathat ugyanarra az objektumra. A típus nem garantál összefüggő int tárolót.

A tömbre mutató pointer ezzel szemben egy tömbobjektumot referál. A méret a referált típus része: az int (*)[3] és az int (*)[4] nem kompatibilis pointertípus.

Az alábbi ISO C17 függvénytörzs-részletben minden tároló automatikus tárolási idejű:

enum { N = 3 };

int values[N] = {10, 20, 30};
int *addresses[N] = {&values[0], &values[1], &values[2]};
int (*row)[N] = &values;

*addresses[1] = 25;
(*row)[2] = 35;

Az első írás a második pointertömb-elemen keresztül módosítja values[1] értékét. A másodikban:

  1. row tömbre mutató pointer;
  2. *row a referált tömböt jelöli;
  3. (*row)[2] annak harmadik elemét jelöli.

A row[2] mást jelentene: pointerindexeléssel a harmadik referált tömböt keresné. Ebben a példában nincs ilyen objektum, ezért az ilyen hozzáférés nem definiált viselkedést okozna.

Szereplő Tároló és tulajdonosi felelősség Élettartam és érvénytelenítés
values A blokk által kezelt egészérték-tároló A blokk elhagyásáig él
addresses Saját pointerelemeket tárol; az egész értékeket csak kölcsönzi A values élettartamának vége érvényteleníti a referált hozzáférést
row Egy pointerértéket tárol; a tömböt csak kölcsönzi Ugyanaz az élettartamkorlát érvényes

Itt nincs dinamikus allokáció és nincs visszaadott hibakód. A tartomány- vagy élettartam-szerződés megsértése nem automatikusan ellenőrzött hiba.

A méretkülönbség számszerű ABI-feltételezés nélkül is levezethető:

sizeof(addresses)=N⋅sizeof(int*) \operatorname{sizeof}(addresses) = N\cdot\operatorname{sizeof}(int *)

mert a tömb N pointerelemet tartalmaz, míg

sizeof(*row)=N⋅sizeof(int). \operatorname{sizeof}(*row) = N\cdot\operatorname{sizeof}(int).

Ezzel szemben sizeof row egy pointerobjektum mérete. Minden méret C bájtban értendő; itt sem az int, sem a pointer konkrét szélességét nem feltételezzük.

Gyakori tévedés, hogy a tömbre mutató pointert int ** típussal helyettesítik. Az int ** egy int * objektumra mutat, nem int elemek tömbjére. A tömbkifejezések pointerré alakulását részletesen a tömbobjektum és decay tárgyalja; ez az átalakulás sem teszi azonossá ezeket a típusokat.

6.4.3 Függvénypointerek és függvénypointert visszaadó függvények

A függvényképző ugyanúgy részt vesz az olvasási algoritmusban:

/* ISO C17 deklarációrészlet. */
int *produce(void);
int (*transform)(int);

A produce paraméter nélküli függvény, amely int * értékkel tér vissza. A transform pointer egy olyan függvényre, amely egy int paramétert fogad és int eredményt ad.

C17-ben a (void) azt jelenti, hogy a függvénynek nincs paramétere. Egy int f(); deklaráció üres zárójele nem ugyanez: nem ad információt a paraméterek számáról és típusáról.

Most tekintsük ezt:

/* ISO C17 deklarációrészlet. */
int (*select_transform(int kind))(int);

Az olvasás közbülső lépései:

  1. select_transform(int kind): az azonosító egy int paraméterű függvényt jelöl.
  2. *select_transform(...): a függvény visszatérési értéke pointer.
  3. A külső (int): ez a pointer egy int paraméterű függvényre mutat.
  4. A kezdeti int: a referált függvény visszatérési típusa.

Vagyis egy stratégia-választó függvényt deklaráltunk, amely egy másik függvény címét adja vissza. Ilyen szerkezet használható például feldolgozási mód vagy ellenőrzési szabály kiválasztására.

C-függvény nem térhet vissza közvetlenül tömb- vagy függvénytípussal, de visszaadhat ezekre mutató pointert.

Egy függvénypointer meghívható transform(value) vagy (*transform)(value) alakban. Előfeltétel, hogy nem null, megfelelő függvényre mutató értéket tartalmazzon, és a referált függvény típusa kompatibilis legyen a híváshoz használt függvénytípussal. A cast nem javítja ki az inkompatibilis hívási szerződést.

Függvénypointerekből tömb is képezhető:

/* ISO C17 deklarációrészlet. */
int (*operations[2])(int);

Ez két pointerből álló tömb, nem függvényeket tartalmazó tömb. A callbackek és a hozzájuk társított állapot szerződését a következő egység részletezi.

6.4.4 typedef: olvashatóság és a rejtett const

A typedef meglévő típushoz rendel nevet; nem hoz létre új, elkülönült típust. Jó felhasználása az összetett típus rétegenkénti megnevezése:

/* ISO C17 deklarációrészlet. */
typedef int transform_fn(int);
typedef transform_fn *transform_ptr;
typedef transform_ptr selector_fn(int);

transform_ptr select_transform(int kind);

A transform_fn függvénytípus, nem pointertípus. A transform_ptr már pointer erre a függvénytípusra. A selector_fn olyan függvénytípus, amely transform_ptr értéket ad vissza.

Ez ugyanazt a szerkezetet fejezi ki, mint a korábbi összetett deklaráció, de a fogalmi határok nevet kapnak. Hasznos szabály: az aliasnevek jelezzék, ha pointert rejtenek.

A legfontosabb minősítőcsapda:

/* ISO C17 függvénytörzs-részlet. */
typedef int *int_ptr;

int value = 7;
int_ptr const fixed = &value;
const int *readonly = &value;

A fixed típusa int * const: maga a pointer nem írható át, a referált int viszont módosítható. A readonly típusa const int *: ezen az elérési úton nem módosítható az egész objektum, a pointerérték viszont lecserélhető.

A typedef nem szöveghelyettesítés. A const az alias által megnevezett teljes pointertípust minősíti.

Mindkét pointer value nem tulajdonló elérése; a blokk elhagyása megszünteti a célobjektum élettartamát. Egy tiltott értékadás fordítási diagnosztikát igényel, míg az élettartam megsértését a típus önmagában nem feltétlenül jelzi.

6.4.5 Incomplete típusokra mutató pointerek

Nem teljes típus (incomplete type) esetén még nem áll rendelkezésre az objektumméret meghatározásához szükséges összes információ. A pointertípus ettől még teljes lehet:

/* ISO C17 deklarációrészlet. */
struct session;
typedef struct session Session;

Session *current;

A fordító ismeri current pointertípusát, de nem ismeri a struct session tagjait és méretét.

Ezért sizeof current megengedett, sizeof *current viszont ezen a ponton nem. Tagelérés és current + 1 sem használható: az utóbbihoz teljes referált objektumtípus kell.

A deklaráció nem hoz létre Session objektumot, és nem határozza meg a tulajdonlást. Egy későbbi interfésznek kell előírnia, ki hozza létre, meddig tartja életben és ki szünteti meg a referált objektumot. Erre épül az opaque handle, de itt csak a típusszintű alapot rögzítjük.

Ismeretlen méretű tömb is lehet incomplete:

/* ISO C17 deklarációrészlet. */
extern int samples[];
int (*unknown_row)[];

A samples deklarációja nem adja meg a tömb elemszámát. Az unknown_row pedig ilyen tömbre mutató pointer. A pointer tárolható, de sizeof *unknown_row és unknown_row + 1 nem megengedett, amíg a referált típus nem teljes.

6.4.6 Fordítóval ellenőrizhető típus-egyezési mini labor

A következő declarators.c teljes ISO C17 program. A _Generic típuskompatibilitás szerint választ ágat, a _Static_assert pedig fordításkor ellenőrzi az így kapott konstans állítást. Ez nem bizonyítja a futásidejű élettartam- vagy tartományhelyességet.

#include <stddef.h>

enum { N = 3 };

typedef int row_type[N];
typedef int transform_fn(int);
typedef transform_fn *transform_ptr;
typedef transform_ptr selector_fn(int);

struct session;
typedef struct session Session;

/* Nyers deklarátor; a definíció ugyanazt aliasokkal írja le. */
int (*select_transform(int kind))(int);

static int identity(int value)
{
    return value;
}

static int nonnegative(int value)
{
    return value < 0 ? 0 : value;
}

transform_ptr select_transform(int kind)
{
    return kind == 0 ? identity : nonnegative;
}

_Static_assert(
    _Generic(&select_transform, selector_fn *: 1, default: 0),
    "selector type mismatch"
);

int main(void)
{
    int values[N] = {-2, 5, 7};
    int *addresses[N] = {
        &values[0], &values[1], &values[2]
    };
    row_type *row = &values;
    transform_ptr operations[2] = {
        select_transform(0), select_transform(1)
    };
    Session *session = NULL;

    _Static_assert(
        _Generic(row, int (*)[N]: 1, default: 0),
        "row pointer type mismatch"
    );
    _Static_assert(
        _Generic(&addresses[0], int **: 1, default: 0),
        "pointer element address mismatch"
    );
    _Static_assert(
        sizeof addresses == N * sizeof(int *),
        "pointer array size mismatch"
    );

#ifdef NEGATIVE_TYPES
    row = addresses; /* Szándékos constraint-sértés. */
#endif

    (*row)[1] = operations[1]((*row)[0]);
    (void)session;

    return values[1] == 0 && operations[0](7) == 7 ? 0 : 1;
}

A lokális egészérték-tároló kezelője a main blokkja; a row és az addresses csak kölcsönöz. Érvényességük nem lépi túl a values élettartamát. A függvénypointerek a programban definiált függvényeket jelölik; nincs tulajdonátruházás vagy dinamikus kódbetöltés. A session null érték, ezért nem jelöl elérhető Session objektumot, és a program nem is dereferálja.

Választott laborprofil: Linux x86-64, System V ABI, LP64; GCC 14.2.0 vagy Clang 19.1.7, glibc 2.39 fejlécekkel. A példában nincs ABI-specifikus méretfeltételezés. A parancsok a megfelelő verziójú gcc és clang elérhetőségét feltételezik; más kiadások használatát külön rögzítsd.

gcc -std=c17 -Wall -Wextra -Wpedantic -Werror \
    declarators.c -o declarators-gcc

clang -std=c17 -Wall -Wextra -Wpedantic -Werror \
    declarators.c -o declarators-clang

A negatív próba szándékosan hibás fordítási egységet képez:

gcc -std=c17 -Wall -Wextra -pedantic-errors \
    -DNEGATIVE_TYPES -fsyntax-only declarators.c

clang -std=c17 -Wall -Wextra -pedantic-errors \
    -DNEGATIVE_TYPES -fsyntax-only declarators.c

Az értékadás jobb oldalán az addresses kifejezés az első pointerelemre mutató int ** értékké alakul. Ez nem kompatibilis a bal oldal int (*)[N] típusával.

Az elvárt ellenőrzési eredmény: a pozitív változat lefordul, a negatív változatot a megadott kapcsolókkal a fordító elutasítja. A pozitív program elvárt kilépési állapota 0. Ezek elvárások, nem dokumentált fordítási vagy futtatási eredmények; a diagnosztika pontos szövege verziófüggő.

6.4.7 Feladatok

6.4.7.1 Magyarázó feladat

Értelmezd az alábbi ISO C17 deklarációrészlet minden sorát. Melyik deklarál függvényt, és melyik objektumot?

int *slots[4];
int (*block)[4];
int (*actions[4])(int);
int (*choose(void))(int);

6.4.7.2 Kódolási feladat

Alakítsd át az actions és a choose deklarációját typedef használatával. Külön nevezd meg a függvénytípust és a pointertípust. Adj fordítási idejű ellenőrzést arra, hogy choose valóban paraméter nélküli, megfelelő függvénypointert visszaadó függvény.

6.4.7.3 Diagnosztikai feladat

Az alábbi ISO C17 függvénytörzs-részlet két szándékos hibát tartalmaz. Azonosítsd és javítsd őket cast nélkül.

typedef int *int_ptr;

int x = 1;
int y = 2;
int_ptr const p = &x;
p = &y;

struct record;
struct record *r = 0;
(void)sizeof *r;

6.4.8 Megoldások

6.4.8.1 A deklarációk értelmezése

A slots négy int * elemet tartalmazó tömbobjektum. A block pointerobjektum, amely négy int elemből álló tömbre mutathat.

Az actions négy függvénypointert tartalmazó tömbobjektum. Minden referált függvény egy int paramétert fogad és int eredményt ad.

A choose függvény: nincs paramétere, és egy int paraméterű, int eredményű függvényre mutató pointert ad vissza.

Az ellenőrzés kulcsa az első kapcsolódó típusművelet: actions esetén tömbképzés, choose esetén függvényképzés történik.

6.4.8.2 Az aliasos változat

Az alábbi ISO C17 részlet fájlszinten helyezhető el:

typedef int action_fn(int);
typedef action_fn *action_ptr;
typedef action_ptr chooser_fn(void);

action_ptr actions[4];
action_ptr choose(void);

_Static_assert(
    _Generic(&choose, chooser_fn *: 1, default: 0),
    "choose type mismatch"
);

Az ellenőrzés azért az &choose kifejezést használja, mert annak típusa pointer a deklarált függvénytípusra. A részlet önmagában szintaktikai ellenőrzéssel vizsgálható; egy ténylegesen choose-t hívó teljes programhoz függvénydefiníció is kell.

6.4.8.3 A két hiba javítása

A p konstans pointer, ezért nem rendelhető hozzá új cím. Ha az átirányítás szükséges, a deklaráció legyen int_ptr p = &x;. Ha csak a referált értéket kell módosítani, az eredeti deklaráció mellett *p = 2; megengedett. A két javítás eltérő célt valósít meg.

A struct record nem teljes típus, ezért mérete nem kérdezhető le. Ha a pointer mérete kell, sizeof r a javítás. Ha a struktúra mérete szükséges, előbb teljes struktúradefiníciót kell adni.

A második hiba nem a null pointer dereferálásából ered: ebben az esetben a sizeof nem értékeli ki az operandusát. Az operandus nem teljes típusa miatt hibás. Az ellenőrzési feltétel mindkét javításnál az, hogy a választott szándékhoz illő változat C17 módban diagnosztika nélkül fordítható legyen.

6.4.9 Rövid összefoglaló

Az összetett deklarátorokat az azonosítótól kifelé, a zárójelezési szinteket követve olvasd. A [] és a függvényparaméter-lista szorosabban kapcsolódik, mint a *.

A pointertömb, a tömbre mutató pointer és a pointerre mutató pointer különböző típus és különböző tárolási szerződés. A typedef olvashatóvá teheti ezeket, de nem szöveghelyettesítés: a pointeraliasra alkalmazott const magát a pointert minősíti.

A fordítóval ellenőrizhető típuskompatibilitás fontos első kapu. A tulajdonlást, az élettartamot és az érvényes hozzáférési tartományt azonban továbbra is külön kell megadni és betartani.