7.1 Tömbobjektum, decay és függvényparaméter

C-ben a tömbobjektum ismeri a méretét a típusán keresztül, egy rá mutató pointer azonban önmagában nem hordoz elemszámot. Ez a különbség különösen függvényhatáron fontos: az int a[] paraméter nem „mérettel együtt átadott tömb”, hanem pointerparaméter. A biztonságos interfésznek ezért külön kell közölnie, hány elemhez szabad hozzáférnie.

Az alábbi nyelvi szabályok az ISO C17 alapra vonatkoznak. A Pointeraritmetika, objektumhatár és provenance részben megismert fél-nyílt tartományokat, valamint a Deklarátorok: tömb-, pointer- és függvénykapcsolatok típusolvasási módszerét használjuk: először mindig azt döntjük el, hogy az adott objektum tömb vagy pointer, és csak utána vizsgáljuk a kifejezésbeli viselkedését.

7.1.1 A tömbobjektum és inicializálása

A tömb (array) azonos elemtípusú objektumok folytonosan tárolt sorozata. Egy rögzített méretű T a[N] deklarációban N pozitív egész konstanskifejezés, T pedig megfelelő, teljes objektumtípus. Az érvényes indexek tartománya [0,N)[0,N).

A következő C17-részlet három valódi tömbobjektumot definiál:

int a[4] = {3, 7};       /* értékek: 3, 7, 0, 0 */
int b[] = {10, 20, 30};  /* a típus int[3] */
int zeros[5] = {0};      /* mind az öt elem értéke nulla */

Az inicializálóból kimaradó elemek itt nulla értéket kapnak. Ez nem azonos az inicializáló teljes elhagyásával: egy blokkban definiált, automatikus tárolási idejű int values[5]; elemei indeterminate állapotúak; nem feltételezhető róluk, hogy nullák.

A b[] üres szögletes zárójele nem dinamikus méretet jelent. A fordító az inicializálóból határozza meg a háromelemes tömbtípust. A nulla hosszúságú tömb nem szabványos C17 tömbobjektum.

Az elemek között nincs külön, tömb által hozzáadott kitöltés. Az elemtípus saját mérete azonban tartalmazhat kitöltést. Az alábbi táblázat ABI-független összefüggéseket mutat; minden méret és eltolás C bájtban, vagyis a sizeof egységében értendő.

Objektum Eltolás a tömb elejétől Méret
a[0] 00 sizeof(int)
a[1] sizeof(int) sizeof(int)
a[2] 2 * sizeof(int) sizeof(int)
a[3] 3 * sizeof(int) sizeof(int)
Teljes a tömb 00 4 * sizeof(int)

A tömb nem értékadható úgy, mint egy egész változó: két már létező tömb között az a = other; nem megengedett tömbmásolás, hanem fordításkor diagnosztizálandó szabálysértés.

7.1.2 Array-to-pointer conversion és kivételei

A tömbkifejezés a legtöbb kifejezéskörnyezetben az első elemére mutató pointerértékké konvertálódik. Ennek neve array-to-pointer conversion, gyakori informális neve decay.

/* C17-részlet, az előző a tömb deklarációja után. */
int *p = a;

Itt két külön objektum létezik:

Nem maga a tömb változik pointerré. Az a kifejezésből keletkezik pointerérték; sem elemmásolás, sem tulajdonátadás nem történik. A p kölcsönzött hozzáférés, amely csak a élettartama alatt használható az elemek elérésére.

7.1.2.1 A konverzió kivételei

C17-ben különösen fontosak a következő esetek:

A C17 konverziós szabályának kivétellistájában az _Alignof is szerepel. Ennek szabványos C17-szintaxisa azonban típusnevet fogad: _Alignof(int[4]) megengedett, _Alignof a nem szabványos C17 kifejezésalak.

Az előző deklarációk mellett:

Kifejezés Jelentés
a a p = a értékadásban Az első elemre mutató int * érték
sizeof a A teljes tömb mérete
sizeof p A pointerobjektum mérete
&a Az egész tömbre mutató int (*)[4] érték
sizeof(a + 0) Pointerméret, mert az összeadásban már megtörténik a konverzió

Az a + 1 egy int elemmel lép előre. Az &a + 1 egy teljes int[4] tömbbel lép tovább, és az egész tömb utáni one-past pointert adja. A típus tehát meghatározza a lépés egységét.

7.1.3 Elemszám, indexelés és határok

Valódi, teljes tömbtípusú a objektumra:

sizeof(a)=N⋅sizeof(a[0]). \operatorname{sizeof}(a) = N\cdot\operatorname{sizeof}(a[0]).

Mivel egy objektumtípus mérete pozitív:

N=sizeof(a)sizeof(a[0]). N=\frac{\operatorname{sizeof}(a)} {\operatorname{sizeof}(a[0])}.

Bizonyításvázlat: a tömb pontosan NN folytonosan elhelyezett elemet tartalmaz; minden elem mérete sizeof a[0]. A teljes méret ezért ezek összege, az osztás pedig visszaadja az elemszámot.

/* C17-részlet; size_t miatt szükséges: #include <stddef.h> */
size_t count = sizeof a / sizeof a[0];

Ez a módszer csak valódi tömbön határozza meg az elemszámot. A sizeof p / sizeof p[0] lefordulhat, de pointerméretet oszt elemmérettel. Az eredménynek nincs általános kapcsolata az elérhető elemek számával.

Rögzített méretű tömbnél a fenti sizeof operandusai nem értékelődnek ki; a sizeof a[0] nem olvassa ki az első elem értékét. A változó hosszúságú tömbök külön szabályait a Többdimenziós tömbök, VLA és layout rész tárgyalja.

Az indexelés definíció szerint pointeraritmetikára épül:

p[i]≡*(p+i). p[i]\equiv *(p+i).

Ha p az első elemre mutat, és n elem használható, akkor dereferáláshoz 0≤i<n0\le i<n szükséges. A p + n lehet érvényes one-past érték, de nem dereferálható. A folytonos fizikai tárolás nem teszi megengedetté a tömbhatár átlépését.

7.1.4 Tömbparaméter: deklarációs adjustment

A függvényparaméter tömbalakja külön szabály alapján módosul pointerparaméterré. Ez a parameter adjustment, nem egy függvényen belüli tömbobjektum későbbi konverziója.

A következő C17-prototípusok ugyanazt a függvénytípust deklarálják:

void process(int a[]);
void process(int a[10]);
void process(int *a);

A függvény tehát pointerértéket kap. Nem jön létre tízelemű helyi tömb, és nem érkezik rejtett elemszám. A függvénytörzsben a sizeof a pointerméretet ad.

A deklarációban szereplő [10] önmagában még azt sem követeli meg, hogy legalább tíz elem legyen elérhető. Erre külön paraméteralak szolgál.

7.1.4.1 const és static a paraméterben

Paraméteralak Módosított paramétertípus Jelentés
int a[] int *a Az elemek írhatók
const int a[] const int *a Az elemek ezen a hozzáférésen át nem írhatók
int a[const] int *const a A definíció törzsében a helyi pointer nem értékadható újra
int a[static 8] int *a Minden híváskor legalább nyolc elemhez kell hozzáférést biztosítani

A const int a[] nem teszi az eredeti tömböt minden hozzáférésen át változtathatatlanná: más, megfelelő pointeren keresztül továbbra is módosítható lehet.

A szögletes zárójelen belüli const viszont a pointerparamétert minősíti. A függvénytípus kompatibilitásánál ez a felső szintű minősítés nem különbözteti meg az interfészt.

A [static 8] itt nem tárolási időt jelent. A hívó minimumkapacitási vállalása, amelyre a fordító is támaszkodhat. Nem futásidejű ellenőrzés: null pointer vagy túl rövid tartomány átadása megsérti a szerződést, és nem definiált viselkedéshez vezet. A const int a[static 8] a minimumkapacitást csak olvasható hozzáféréssel kombinálja.

Ha a típussal valóban meg akarjuk őrizni a négyes kiterjedést, használható az int (*a)[4] paraméter. Ez pointer egy négyes tömbre; a sizeof *a tömbméretet ad. Ettől azonban a pointer érvényességét továbbra is külön biztosítani kell.

7.1.5 Explicit hosszparaméteres API

Az általános tömbinterfész két adatot kap: kezdőpointert és elemszámot. A hossz nem bájtméret, hanem az adott elemtípusban értelmezett darabszám.

A következő teljes program minimum C17-et igényel. A laborhoz előírt profil: Linux, x86-64 System V ABI, LP64, GCC 14.2.0 vagy Clang 19.1.7, glibc 2.40. A program nem támaszkodik ezek konkrét pointer- vagy int-méretére.

/* array_api.c */
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>

static bool int_fill(int *a, size_t n, int value)
{
    if (a == NULL && n != 0) {
        return false;
    }

    for (size_t i = 0; i < n; ++i) {
        a[i] = value;
    }
    return true;
}

int main(void)
{
    int values[4] = {3, 7};
    int *p = values;
    size_t n = sizeof values / sizeof values[0];

    if (!int_fill(p, n, 9)) {
        return 1;
    }

    for (size_t i = 0; i < n; ++i) {
        if (values[i] != 9) {
            return 1;
        }
    }

    if (!int_fill(NULL, 0, 9)) {
        return 1;
    }

    printf("count=%zu array_bytes=%zu pointer_bytes=%zu\n",
           n, sizeof values, sizeof p);
    return 0;
}

Fordítás a külön tanulói laborban:

gcc -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O0 -g \
    array_api.c -o array_api-gcc

clang -std=c17 -Wall -Wextra -Wpedantic -Wconversion -O0 -g \
    array_api.c -o array_api-clang

Ezek elvégzendő ellenőrzések, nem dokumentált fordítási vagy futtatási eredmények. A nyelvi szabályokból következő elvárt elemszám négy; a kiírt bájtméretek a célplatformtól függenek.

Az API szerződése:

Szerep vagy feltétel Követelmény
Tulajdonos és tároló A hívó kezeli a tömb élettartamát; itt main helyi values objektuma biztosítja a tárolót
Kölcsönzött hozzáférés p és a függvény a paramétere nem tulajdonos
Nem üres tartomány Legalább n élő, írható int elem legyen elérhető
Üres tartomány n == 0 esetén a null pointer is elfogadott
Érvénytelenítés A tároló élettartamának vége megszünteti az elemekhez való hozzáférés érvényességét
Hibakimenet Null pointer és pozitív hossz esetén false, módosítás nélkül

A függvény nem tárolja el a pointert. A kitöltés módosítja az értékeket, de nem érvényteleníti az elemekre mutató pointereket.

Korlát: a függvény nem tudja ellenőrizni, hogy a nem null pointer mögött ténylegesen van-e n elem. A túlbecsült hossz továbbra is szerződéssértés és határon túli íráshoz vezethet.

A ciklus minden belépéskor i < n feltételt ellenőriz, ezért igaz hossz esetén csak érvényes indexet használ. Üres tartománynál sem dereferencia, sem pointeraritmetika nem történik. A legrosszabb műveletszám pozitív n mellett O(n)O(n), üres tartománynál O(1)O(1); nincs allokáció vagy amortizált költség. A hozzáférés szekvenciális, ezért jó térbeli lokalitású.

7.1.6 Gyakori hibák

A „tömb egy pointer” rövidítés eltakarja a lényegi különbséget: a tömb elemeket tartalmaz, a pointer egy hozzáférési értéket tárol. Emiatt a függvényparaméteren végzett sizeof nem alkalmas elemszámlálásra.

A másik tipikus hiba a <= n ciklusfeltétel. Az utolsó elem indexe n - 1, nem n; üres tartománynál pedig még a n - 1 kiszámítása is kerülendő. A természetes bejárási alak i < n.

Végül a static paraméteralak nem helyettesíti a bemenetellenőrzést. Erősebb előfeltételt ad, nem biztonsági hálót.

7.1.7 Feladatok

7.1.7.1 Típus és méret

Adott int a[6]; és int *p = a;. Mit jelent a sizeof a, sizeof p, sizeof(a + 0) és &a? Melyikből határozható meg az elemszám?

7.1.7.2 Ellenőrzött elemírás

Írj bool int_set(int *a, size_t n, size_t index, int value) függvényt! Hibás index vagy null pointer esetén adjon false értéket, módosítás nélkül. Egyébként pontosan egy elemet írjon át.

7.1.7.3 Hibás tömbparaméter diagnózisa

Az alábbi C17-részlet hibás API-megvalósítás; a size_t deklarációjához a <stddef.h> szükséges:

void clear(int a[8])
{
    size_t n = sizeof a / sizeof a[0];

    for (size_t i = 0; i <= n; ++i) {
        a[i] = 0;
    }
}

Azonosítsd a két független méretkezelési hibát! Megoldaná-e őket a [static 8] használata?

7.1.8 Megoldások

7.1.8.1 Típus és méret

A sizeof a értéke 6 * sizeof(int). A sizeof p pointerméret; a sizeof(a + 0) szintén az int * típus mérete. Az &a típusa int (*)[6].

Az elemszámot a sizeof a / sizeof a[0] adja. Ellenőrzési feltétel: ez minden megfelelő C17-implementáción hat, függetlenül a pointermérettől.

7.1.8.2 Ellenőrzött elemírás

A teljes program fejléceivel használható C17-részlet:

static bool int_set(int *a, size_t n,
                    size_t index, int value)
{
    if (index >= n || a == NULL) {
        return false;
    }

    a[index] = value;
    return true;
}

A hívó a tulajdonos; a pointer csak a hívás idejére kölcsönzött. A sikeres ág előfeltétele a megadott hosszúságú, élő és írható tartomány. A tároló élettartamának vége érvényteleníti a hozzáférést.

Ellenőrizendő: index == n esetén nincs írás; n == 0 mindig hibát ad; érvényes indexnél kizárólag a kijelölt elem változik.

7.1.8.3 Hibás tömbparaméter diagnózisa

Az első hiba, hogy a pointerparaméter: a méretarány nem nyolcat, hanem pointerméret és elemméret hányadosát számolja. A második hiba a <=: még helyes elemszám esetén is eggyel túl sok elemhez férne hozzá.

A [static 8] egyik hibát sem javítja. Az előfeltételt erősítené, de a paraméter továbbra is pointer maradna.

A javítás külön size_t n paraméter és i < n feltétel; például használható az int_fill(a, n, 0). Ellenőrzési feltétel: igaz hossz mellett pontosan n elem módosuljon. Határon túli hozzáférés esetére nincs garantált kimenet vagy garantált összeomlás.

7.1.9 Összefoglaló

A tömbobjektum elemeket és típusbeli kiterjedést jelent; a pointer önmagában nem közöl hosszt. A decay, a paraméter-adjustment és a sizeof külön szabályok. Megbízható C API-ban a pointer mellé explicit elemszám, élettartam- és írhatósági előfeltétel, valamint világos hibaszerződés tartozik.