4.4 Deklarációk, typedef és típusminősítők

C-ben egy deklaráció nemcsak nevet ad valaminek: meghatározza, milyen típusú értékeket és műveleteket enged meg a program. A típus pontos olvasása ezért helyességi kérdés. Nem mindegy, hogy egy pointeren keresztül nem szabad módosítani egy objektumot, maga a pointer nem változtatható meg, vagy az objektumot eleve módosíthatatlanként definiáltuk.

Az Egész típusok és adatmodellek alapján a típusnevet már megkülönböztetjük a konkrét mérettől. A Headerek, deklarációk és include-higiénia részben pedig láttuk, hogyan közölnek deklarációkat a fordítási egységek. Most a deklaráció belső szerkezetét és a hozzá kapcsolódó vállalásokat vizsgáljuk.

4.4.1 A deklaráció olvasása

ISO C17: a deklaráció egy vagy több azonosító tulajdonságait közli. A definíció ennél többet tesz: például egy objektum számára tárolót is biztosít, vagy megadja egy függvény törzsét.

Az alábbiak deklarációs részletek:

extern int pending;       /* Deklaráció, nem objektumdefiníció. */
int completed = 0;        /* Objektumdefiníció. */
int classify(int value);  /* Függvénydeklaráció. */

A deklarációban elkülöníthetjük a közös deklarációmegadókat — például az int típusmegadót és a const típusminősítőt — az egyes nevekhez tartozó deklarátoroktól (declarator). A deklarátor fejezi ki többek között a pointer-, tömb- és függvénykapcsolatot.

int *first, second;

Itt csak first pointer; second típusa int. A * nem a teljes deklaráció minden nevéhez kapcsolódik. Olvashatósági okból az eltérő szerkezetű deklarációkat célszerű külön sorba írni.

A típust a névtől kifelé olvassuk. A zárójelezés elsődleges, a [] és a függvényt jelölő () pedig erősebben kapcsolódik, mint a pointert jelölő *.

Deklaráció A deklarált név típusa
int *p; Pointer int objektumra
int *items[4]; Négy int pointerből álló tömb
int (*row)[4]; Pointer négy int elemből álló tömbre
int convert(int); Egy int paraméterű, int eredményű függvény
int (*operation)(int); Pointer ilyen függvényre

Az int (*row)[4] levezetése: row neve mellett a zárójelben * áll, tehát pointer; a zárójeles egységet [4] követi, tehát tömbre mutat; az elemtípus int. Az int *items[4] esetében viszont először a [4] kapcsolódik a névhez, ezért tömböt kapunk.

Ez egyelőre típustérkép, nem használati útmutató: a deklaráció önmagában nem biztosít érvényes pointerértéket. A részletes olvasási módszert a Deklarátorok: tömb-, pointer- és függvénykapcsolatok rész folytatja.

4.4.2 A typedef típusálnevet hoz létre

ISO C17: a typedef meglévő típushoz rendel másik nevet; nem hoz létre új, attól elkülönülő típust.

typedef unsigned long RequestCount;
typedef int *IntPtr;

A RequestCount és az unsigned long ugyanazt a típust jelöli. Az álnév dokumentálhat mértékegységet vagy szerepet, de önmagában nem akadályozza meg például egy kérésszám és egy időtartam összekeverését.

A typedef nem szöveges helyettesítés. Ez különösen minősítőkkel fontos:

typedef int *IntPtr;
const IntPtr cursor = 0;

A levezetés:

  1. Az IntPtr teljes típusa „pointer int objektumra”.
  2. A const ezt a teljes típust minősíti.
  3. cursor ezért konstans pointer int objektumra, vagyis típusa int *const.

Ez nem azonos a const int * típussal. A példabeli cursor null pointer, nem biztosít objektumhoz hozzáférést; dereferálása UB lenne.

4.4.2.1 Tagnevek és közönséges azonosítók

C-ben a struct, union és enum tagnevei (tag) külön névtérbe tartoznak a közönséges azonosítóktól. A typedef-nevek viszont ugyanabba a névtérbe kerülnek, mint a változó- és függvénynevek, valamint az enumerátornevek.

typedef struct Task Task;

struct Task {
    int state;
};

Az első sorban a struct Task még nem teljes típus: a tagjai nincsenek megadva. A typedef ettől függetlenül létrehozhatja hozzá a Task álnevet. A második deklaráció teljessé teszi a struktúratípust.

A két Task név azért fér meg egymással, mert külön névtérben van. Egy ugyanabban a hatókörben megjelenő int Task; azonban már ütközne a typedef-névvel.

4.4.3 A const hozzáférési korlátozás és objektumtulajdonság

ISO C17: const-minősítésű lvalue-n keresztül nem megengedett az érték módosítása. Ettől külön szabály, hogy egy eredetileg const-minősítésű típussal definiált objektum módosításának kísérlete nem definiált viselkedés.

Deklaráció Mi nem módosítható ezen a hozzáférésen keresztül?
const int *p; A mutatott int
int *const p = 0; A pointerobjektum
const int *const p = 0; Sem a pointer, sem rajta keresztül a mutatott érték

Az első eset nem bizonyítja, hogy a mutatott objektum minden más hozzáférés számára is módosíthatatlan. A következő, függvénytörzsbe illeszthető részlet szabályos:

int value = 4;
const int *view = &value;
int *writer = &value;

*writer = 9;
/* *view értéke most 9. */

A tárolót a helyi value objektum adja; view és writer kölcsönzött hozzáférések. A módosítás nem érvényteleníti őket, de value élettartamának vége igen. Az azon túli dereferálás UB.

Ezzel szemben az alábbi hibás, UB-t szemléltető részlet, nem futtatandó teszt:

const int frozen = 4;
int *writer = (int *)&frozen;
*writer = 9;  /* UB: az eredetileg const objektum módosítása. */

A cast csak a kifejezés típusát változtatja meg, az objektum definícióját nem. Ha az eredeti objektum nem const, a minősítés eltávolítása után végzett módosítás önmagában nem feltétlenül UB; az API szerződése azonban ezt akkor is tilthatja.

4.4.3.1 A const nem konstans kifejezés

C17-ben ez a blokkhatókörű részlet változó hosszúságú tömböt, VLA-t deklarál:

const int count = 4;
int values[count];

A count nem egész konstans kifejezés (integer constant expression), hiába kapott konstans inicializálót. A VLA-támogatás C17-ben opcionális. Rögzített tömbmérethez itt megfelelő lehet:

enum { ITEM_COUNT = 4 };
int values[ITEM_COUNT];

A const a szálbiztonságot sem garantálja. Egy const pointer mögötti, valójában módosítható objektumot más szál írhat; megfelelő szinkronizáció hiányában ebből adatverseny lehet.

4.4.4 A volatile és a restrict eltérő vállalásai

4.4.4.1 Volatile: megfigyelhető hozzáférés, nem szinkronizáció

ISO C17: a volatile olyan objektumokhoz való hozzáférést támogat, amelyek értéke a szokásos programfolyamon kívül változhat, illetve amelyek hozzáférése külső mellékhatással járhat. A volatile-hozzáféréseknek az absztrakt gép szabályait kell követniük; azt, hogy pontosan mi számít ilyen hozzáférésnek, az implementáció határozza meg.

Platformfeltétel: memóriába térképezett eszközregisztereknél a volatile csak egy része a szerződésnek. A leképezés, a hozzáférési szélesség és az esetleges hardveres barrier külön platformkövetelmény.

A volatile nem garantál:

Ezért egy volatile jelzőváltozó nem helyettesíti a mutexet vagy az atomikus műveletet. A részletek a volatile, compiler barrier és hardver barrier, illetve a C atomics és memory order részekhez tartoznak.

4.4.4.2 Restrict: vállalás a hozzáférési utakról

ISO C17: a restrict pointerekhez kapcsolható típusminősítő. Nem futásidejű ellenőrzés, hanem az optimalizáló által felhasználható aliasing-szerződés.

Első munkamodellként: ha egy objektumot egy restricted pointerből származtatott hozzáférésen keresztül érünk el, és az objektum módosul, akkor az érintett blokk végrehajtása során az objektum többi releváns hozzáférésének is ugyanahhoz a restricted hozzáférési úthoz kell igazodnia. A teljes szabály ennél részletesebb.

Ez nem azt jelenti, hogy más pointerérték nem létezhet ugyanarra az objektumra. A tényleges hozzáférések számítanak. Két, kizárólag olvasásra használt pointer átfedése sem általánosan tiltott.

A következő másolófüggvényhez egyszerű, elegendő szerződést választunk: a forrás- és céltartomány nem fedheti egymást.

4.4.5 Kidolgozott példa: olvasható nézet és külön céltartomány

A példa minimuma ISO C17. A választott laborprofil Linux x86-64 System V, glibc 2.39, GCC 14.2.0 vagy Clang 19.1.7, bővítmények nélkül. Az alábbi ellenőrzések és kimenet levezetett elvárások, nem közölt futtatási eredmények.

#include <assert.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>

enum { ITEM_COUNT = 3 };
enum Status { STATUS_OK = 0, STATUS_BAD = 1 };

static void copy_values(size_t n,
                        int *restrict dst,
                        const int *restrict src)
{
    for (size_t i = 0; i < n; ++i) {
        dst[i] = src[i];
    }
}

int main(void)
{
    int source[ITEM_COUNT] = {3, 5, 8};
    int target[ITEM_COUNT] = {0};
    const int *view = source;

    source[0] = 13;
    copy_values(ITEM_COUNT, target, view);

    bool ready = -4;
    enum Status status = ready ? STATUS_OK : STATUS_BAD;

    assert(view[0] == 13);
    assert(target[0] == 13);
    assert(target[1] == 5);
    assert(target[2] == 8);
    assert(ready == true);

    printf("%d %d %d\n", target[0], (int)ready, (int)status);
    return 0;
}

Fordítás types.c fájlból, a megfelelő kiadásra beállított driverrel:

gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion \
    -Wcast-qual -O2 types.c -o types-gcc

clang -std=c17 -Wall -Wextra -Wpedantic -Wconversion \
    -Wcast-qual -O2 types.c -o types-clang

Pozitív n esetén mindkét pointer mögött legalább n megfelelően inicializált, illetve írható int elemnek kell rendelkezésre állnia. A két tartomány legyen diszjunkt. Nulla n esetén a függvény nem végez pointerműveletet vagy memóriahozzáférést.

Tulajdonosi szerep és tároló Kölcsönzött hozzáférés Élettartamhatár
A main helyi source tömbje view, majd a hívás src paramétere A main blokkjának elhagyása
A main helyi target tömbje A hívás dst paramétere A main blokkjának elhagyása

A függvény nem tárolja el a pointereket, nem allokál, és nem érvénytelenít hozzáférést. Nem jelez szerződéssértést: az érvénytelen tartomány vagy a restricted hozzáférések tiltott átfedése UB-t okozhat.

A source[0] módosítása szabályos, mert a tömb nem const; view csak olvasási hozzáférést biztosít. A várt kimenet 13 1 0.

4.4.6 Enum, bool és literálok C17-ben

Az enum Status felsorolt típus. C17-ben az enumerátornevek, például STATUS_OK, int típusú konstansok; értéküknek ábrázolhatónak kell lennie int-ként. Maga az enumerált típus nem egyszerűen az int másik neve: kompatibilis egész típusának kiválasztása implementáció által meghatározott.

A _Bool beépített C17 típus. A <stdbool.h> a bool, true és false makrókat adja: rendre _Bool, 1 és 0 jelentéssel. Skalár érték _Bool-ra konvertálásakor a nullával egyenlő értékből 0, minden másból 1 lesz. Ezért lesz a példában -4 értékéből igaz.

Néhány fontos literáltípus:

Kifejezés Típus C17-ben
12, 12U, 12L Rendre int, unsigned int, long
1.0, 1.0f, 1.0L Rendre double, float, long double
'A' int, nem char
true a megfelelő header után int típusú 1 makrókifejtés

Nagyobb egész literálok típusát a számrendszer, az utótag és az ábrázolhatóság együtt határozza meg; ezt az egész típusok rész tárgyalja. A közönséges sztringliterál C17-ben char elemtípusú tömb, módosításának kísérlete mégis UB. A „nem const típusú” tehát nem minden esetben jelent módosíthatóságot.

4.4.7 Feladatok

  1. Típusmagyarázat. Mit jelentenek az alábbi, egymástól független deklarációk? Miben különbözik a és b?

    typedef int *Handle;
    const Handle a = 0;
    const int *b = 0;
    int (*c)[3];
  2. Kódolás. Írj status_is_ok függvényt a teljes példához, amely enum Status paraméterből bool eredményt készít. Miért hibás erre egyszerűen a státusz közvetlen logikai konverziója?

  3. Diagnosztika. Értékeld külön-külön:

4.4.8 Megoldások

4.4.8.1 Típusmagyarázat

a konstans pointer int objektumra; b módosítható pointer const int hozzáféréssel; c pointer háromelemű int tömbre. Az ellenőrzési feltétel: a újraértékadása fordítási diagnosztikát igényel, b újraértékadása önmagában megengedett. A null pointerek dereferálása egyik esetben sem megengedett.

4.4.8.2 Kódolás

A teljes példa elé illeszthető függvényrészlet:

static bool status_is_ok(enum Status status)
{
    return status == STATUS_OK;
}

A siker kódja nulla, amely közvetlen _Bool-konverzióval hamis lenne. A függvény a státusz jelentését, nem puszta nullaságát képezi le. Ellenőrzésként a main törzsében legyen igaz a status_is_ok(STATUS_OK) és a !status_is_ok(STATUS_BAD) kifejezés.

4.4.8.3 Diagnosztika

Az első eset UB: a cast nem változtatja meg a ténylegesen const objektumot.

A második esetben a céltartomány source[1] és source[2], a forrás pedig source[0] és source[1]. A source[1] objektumot külön restricted hozzáférési utakon olvassuk és írjuk, ezért a hívás sérti a szerződést. Külön céltömb használata megfelelő javítás.

A harmadik állítás hamis: a volatile nem hoz létre szálak közötti szinkronizációt. A helyességhez megfelelő atomikus protokoll vagy zárolás szükséges; pusztán sikeres tesztfutás nem ellenőrzési bizonyíték.

4.4.9 Rövid összefoglaló

A deklarátort a névtől kifelé olvassuk, a typedef pedig teljes típushoz rendel álnevet. A const módosítási korlátozás, nem általános konstanskifejezés-jelölő vagy szálbiztonsági garancia. A volatile a hozzáférések megfigyelhetőségéhez, a restrict a hozzáférési utak aliasing-szerződéséhez kapcsolódik. Helyes használatukhoz mindig a tényleges objektumot, annak élettartamát és az összes releváns hozzáférést kell vizsgálni.