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.
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:
[] és függvényparaméter-lista jelentését olvasd
el.* jeleket és a hozzájuk
tartozó minősítőket.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:
aháromelemű tömb, amelynek elemei pointerekintobjektumokra.
A második sorban a csoportosítás miatt előbb a *b
kapcsolatot kell feldolgozni:
bpointer egy háromelemű,intelemeket 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.
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:
row tömbre mutató pointer;*row a referált tömböt jelöli;(*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ő:
mert a tömb N pointerelemet tartalmaz, míg
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.
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:
select_transform(int kind): az azonosító egy
int paraméterű függvényt jelöl.*select_transform(...): a függvény visszatérési értéke
pointer.(int): ez a pointer egy int
paraméterű függvényre mutat.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.
typedef:
olvashatóság és a rejtett constA 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.
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.
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-clangA 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.cAz é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ő.
É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);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.
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;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.
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.
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.
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.