10.1 Függvényhívás, prototípus és paraméterátadás

Egy C API megértéséhez külön kell választani a függvénynek átadott értéket attól az objektumtól, amelyhez az érték hozzáférést biztosít. Egy számláló átadása nem engedi meg a hívó számlálójának módosítását; a címének átadása viszont igen. Ez nem kétféle paraméterátadási mechanizmus: C-ben mindkettő érték szerinti átadás (pass-by-value).

A fejezet a C17 nyelvi szabályait tárgyalja, nem azt, hogy az adott ABI regiszterben vagy memóriában továbbítja az értékeket. Az előzmények közül a struktúrák értékmásolása, a promóciók és konverziók, valamint a függvénypointerek típusszerződése lesz különösen fontos.

10.1.1 Az érték szerinti átadás pontos jelentése

Függvényhíváskor az argumentumkifejezések kiértékelődnek, és a szükséges konverziók után az értékeik inicializálják a függvény paraméterobjektumait. A paraméter a hívott függvény saját objektuma: módosítása önmagában nem módosítja azt az objektumot, amelyből az argumentum értéke származott.

Ez három gyakori esetben a következőt jelenti:

Az utolsó esetben két külön pointerobjektum ugyanarra az objektumra mutathat. A paraméter átállítása csak a helyi pointert változtatja meg, a dereferálás utáni értékadás azonban a közösen elért objektumot.

10.1.1.1 Bizonyításvázlat a pointeres esetre

Legyen a hívó pointerobjektuma pcp_c, a paraméterobjektum pedig pfp_f. Tegyük fel, hogy pcp_c egy élő, írható, megfelelő típusú és igazítású xx objektumra mutat. A hívás kezdetén:

pf=pc=&x. p_f = p_c = \&x.

A p_f = NULL értékadás célobjektuma pfp_f, ezért pcp_c változatlan marad. Ezzel szemben az ezt megelőző *p_f = 42 értékadás célobjektuma xx.

A különbséget tehát az értékadás bal oldala határozza meg. A pointerparaméter nem referencia-paraméter: nem válik a hívó pointerváltozójának másik nevévé.

Ha magát a hívó pointerobjektumát kell módosítani, annak címét kell átadni, például unsigned **slot paraméterként. Ekkor is pointerérték másolódik; a *slot = replacement a hívó pointerobjektumát módosítja.

10.1.2 Kidolgozott példa: három másolat, eltérő hatások

A következő teljes program minimuma ISO C17. Nem igényel konkrét egészszélességet vagy ABI-specifikus elrendezést.

Tervezett laborprofil: Linux x86-64, System V ABI, LP64, glibc 2.39; GCC 14.2.0, illetve Clang 19.1.7. A példát itt nem fordítottuk és nem futtattuk; az ellenőrzések a szabályokból levezetett elvárásokat rögzítik.

Az api_call.c tartalma:

#include <assert.h>
#include <limits.h>
#include <stddef.h>

struct sample {
    unsigned count;
    unsigned *shared;
};

enum add_status {
    ADD_OK,
    ADD_BAD_ARGUMENT,
    ADD_OVERFLOW
};

enum add_status add_unsigned(unsigned a, unsigned b,
                             unsigned *out);

static void change_scalar(unsigned value)
{
    value = 99u;
    (void)value;
}

static void change_record(struct sample value)
{
    value.count = 99u;
    *value.shared = 77u;
}

static void change_through_pointer(unsigned *p)
{
    *p = 42u;
    p = NULL;
    (void)p;
}

int main(void)
{
    unsigned n = 7u;
    struct sample s = {3u, &n};
    unsigned *p = &n;

    change_scalar(n);
    assert(n == 7u);

    change_record(s);
    assert(s.count == 3u);
    assert(n == 77u);

    change_through_pointer(p);
    assert(n == 42u);
    assert(p == &n);

    unsigned result = 123u;
    enum add_status status = add_unsigned(2u, 3u, &result);
    assert(status == ADD_OK);
    assert(result == 5u);

    status = add_unsigned(UINT_MAX, 1u, &result);
    assert(status == ADD_OVERFLOW);
    assert(result == 5u);

    status = add_unsigned(2u, 3u, NULL);
    assert(status == ADD_BAD_ARGUMENT);

    return 0;
}

enum add_status add_unsigned(unsigned a, unsigned b,
                             unsigned *out)
{
    if (out == NULL) {
        return ADD_BAD_ARGUMENT;
    }
    if (b > UINT_MAX - a) {
        return ADD_OVERFLOW;
    }

    *out = a + b;
    return ADD_OK;
}

Fordítás a rögzített profil megfelelő fordítójával:

gcc -std=c17 -Wall -Wextra -Wpedantic -Wstrict-prototypes \
    -Wmissing-prototypes -Wconversion -Werror=implicit-function-declaration \
    -O0 -g api_call.c -o api-call-gcc

clang -std=c17 -Wall -Wextra -Wpedantic -Wstrict-prototypes \
    -Wmissing-prototypes -Wconversion -Werror=implicit-function-declaration \
    -O0 -g api_call.c -o api-call-clang

A parancsok a tanuló külön laborjához tartoznak; a gcc és clang tényleges verzióját ellenőrizni kell. Az assert ellenőrzéseihez ne legyen definiálva az NDEBUG.

A végrehajtásból elvárt állapotok:

Hívás után n s.count p
change_scalar(n) 7 3 &n
change_record(s) 77 3 &n
change_through_pointer(p) 42 3 &n

A struktúramásolás nem mély másolás. A value.count külön tagobjektum, de a value.shared pointerértéke ugyanaz, mint az s.shared értéke. A referált n objektumból nem készül másolat.

10.1.2.1 Tulajdonlás és élettartam

Objektum vagy hozzáférés Tulajdonos, tároló Érvényesség és érvénytelenítés
n, s, p, result A hívó, a main automatikus tárolójában A befoglaló blokk végéig élnek
s.shared, p Kölcsönzött hozzáférés n-hez n élettartamának végén elveszítik használhatóságukat
Pointerparaméterek A hívott függvény helyi pointerobjektumai A visszatéréskor megszűnnek; a referált objektum ettől még élhet
out A hívó eredményobjektumát kölcsönzi A függvény nem tárolja el, nem szabadítja fel

A módosító segédfüggvények érvényes, írható objektumra mutató pointert követelnek. Nincs hibastátuszuk; null vagy dangling pointer dereferálása nem definiált viselkedés lenne. Az add_unsigned a null értéket külön kezeli, de egy nem null pointer érvényességét nem tudja általánosan ellenőrizni.

10.1.3 Deklaráció, prototípus és definíció

A deklaráció közli a függvény nevét és típusát. A prototípus olyan függvénydeklaráció, amely a paraméterek típusinformációját is megadja. A definíció ezen túl a függvénytörzset is tartalmazza.

A példában:

/* C17 deklarációrészlet: prototípus, függvénytörzs nélkül. */
enum add_status add_unsigned(unsigned a, unsigned b,
                             unsigned *out);

A deklaráció lehetővé teszi, hogy a később definiált függvényt a main megfelelő típusinformáció birtokában hívja. A paraméternevek nem részei a függvénytípusnak, de dokumentációként hasznosak.

Könyvtárban a prototípus közös headerbe kerüljön, amelyet a hívó és a megvalósítás is beilleszt. A linker sikeres névfeloldása önmagában nem bizonyítja a típusok kompatibilitását.

10.1.3.1 f(void) és f() C17-ben

Alak C17 jelentés
int f(void); Prototípus: a függvény nem fogad argumentumot
int f(); Nem prototípus: nincs információ a paraméterek számáról és típusáról
int f() { return 0; } Paraméter nélküli definíció, de nem prototípusos alak

Az int f(); nem azt jelenti, hogy tetszőleges argumentumlista helyes, és nem variadikus deklaráció. Csupán hiányzik az a típusinformáció, amellyel a fordító a hívást teljesebben ellenőrizhetné.

C23-változás: az üres () paraméterlista nulla paramétert meghatározó prototípusos alak. C17-kompatibilis kódban továbbra is az egyértelmű f(void) alakot használjuk. A fordító C23-módjának támogatását külön kell ellenőrizni.

A deklaráció teljes hiánya más probléma: C17-ben az implicit függvénydeklaráció nem megengedett. Az olyan hívás, amelynél a függvény neve nincs megfelelően deklarálva, diagnosztikát igényel.

10.1.4 Argumentumkonverzió és inkompatibilis hívás

Látható prototípus mellett az argumentumok a megfelelő paramétertípushoz igazodnak. Ez nem jelent veszteségmentességet. Az int paraméternek átadott 3.75 például 3 értékké konvertálódik; tartományon kívüli lebegőpontos értéknél a helyzet már nem biztonságos.

Nem prototípusos C17-hívásnál az alapértelmezett argumentumpromóciók (default argument promotions) érvényesülnek: például a float érték double lesz, a kisebb egész típusok pedig egészpromóción mennek át. A hívó ilyenkor nem tud a tényleges paraméterlistához igazodni.

Három hibát érdemes elkülöníteni:

  1. Látható prototípus megsértése: például egy void stop(void) függvény hívása stop(1) alakban. Ez fordítási diagnosztikát igényel.
  2. Fordítási egységek közötti eltérés: a hívó és a definíció inkompatibilis függvénytípust használ. A hagyományos linkelés ezt nem feltétlenül ismeri fel.
  3. Inkompatibilis függvénypointeren keresztüli hívás: egy cast nem teszi kompatibilissé a függvénytípusokat. Például egy long(long) függvény int (*)(int) típuson keresztüli meghívása nem definiált viselkedés.

A javítás nem a warning elnémítása, hanem az egységes prototípus és a helyes konverziós szerződés.

Az argumentumok kiértékelési sorrendjére sem szabad balról jobbra történő végrehajtást feltételezni. A C17 szerinti f(i++, i++) hívás nem definiált viselkedés: az érték szerinti átadás nem rendezi sorba a mellékhatásokat.

10.1.5 Visszatérési érték és output paraméter

A return kifejezés értéke a függvény visszatérési típusára konvertálódik. Skalár vagy struktúra érték szerinti visszaadása nem igényli a lokális eredményobjektum további életben maradását. Lokális objektum címének visszaadása viszont nem hosszabbítja meg annak élettartamát.

Tervezési szabályként minden szabályosan visszatérő, nem void út adjon explicit eredményt. C17-ben a nem void függvény végére jutás után a hívás értékének felhasználása UB; a main végére jutás speciálisan nulla visszaadásának felel meg.

Az add_unsigned két csatornát választ szét:

Szerződése szerint hiba esetén az eredményobjektum változatlan. A túlcsordulás-ellenőrzés levezetése matematikai egészekkel, M=𝚄𝙸𝙽𝚃_𝙼𝙰𝚇M=\texttt{UINT\_MAX} jelöléssel:

a+b≤M⇔b≤M−a. a+b \le M \quad\Longleftrightarrow\quad b \le M-a.

Mivel 0≤a≤M0\le a\le M, az M - a kivonás ábrázolható. Ezért előbb ellenőrizhető a feltétel, és csak utána szükséges összeadni.

Gyakori hiba előbb írni az *out objektumba, majd hibát jelezni: ez megsértené a „hiba esetén változatlan” ígéretet. Ugyanilyen hiba a státusz figyelmen kívül hagyása és az eredmény automatikus érvényesnek tekintése.

10.1.6 Feladatok

  1. Magyarázat: miért marad változatlan az s.count, miközben az n megváltozik a struktúraparaméteres hívásban? Miért nem lesz null a hívó p változója?

  2. Kódolás: készíts multiply_unsigned függvényt ugyanolyan státusz- és output-szerződéssel, mint az összeadás. Null output esetén jelezzen argumentumhibát, ábrázolhatatlan szorzatnál túlcsordulást.

  3. Diagnosztika: két külön C17 fordítási egység lényegi részletei következnek. A második szándékosan hibás hívást tartalmaz. Mi történik az argumentummal, és hogyan javítanád?

/* scale.c — C17 részlet */
int scale(int value)
{
    return value;
}
/* client.c — hibás C17 részlet */
int scale();

int invoke(void)
{
    return scale(1.0f);
}

10.1.7 Megoldások

10.1.7.1 A másolat és az elért objektum

A value.count a paraméterstruktúra tagja, ezért módosítása nem érinti az s.count tagot. A value.shared viszont ugyanarra az n objektumra mutat, mint az eredeti pointertag. Hasonlóképpen a p = NULL csak a helyi pointerparamétert módosítja.

Ellenőrzési feltétel: külön meg kell nevezni az értékadás célobjektumát és a referált objektumot.

10.1.7.2 Ellenőrzött szorzás

Az előző program deklarációit és include-jait használó C17-részlet:

enum add_status multiply_unsigned(unsigned a, unsigned b,
                                  unsigned *out)
{
    if (out == NULL) {
        return ADD_BAD_ARGUMENT;
    }
    if (a != 0u && b > UINT_MAX / a) {
        return ADD_OVERFLOW;
    }

    *out = a * b;
    return ADD_OK;
}

Pozitív aa mellett a feltétel b≤⌊M/a⌋b\le\lfloor M/a\rfloor. A nullás eset külön kezelése kizárja a nullával osztást. A tulajdonos továbbra is a hívó; az out csak a hívás idejére kölcsönzött, írható hozzáférés.

Ellenőrzendő: szorzás nullával, UINT_MAX szorzása eggyel és kettővel, null output, valamint az eredmény változatlansága hibánál.

10.1.7.3 A hiányzó prototípus következménye

A float argumentum a nem prototípusos hívásban double értékké promótálódik, miközben a definíció int paramétert vár. A hívás viselkedése nem definiált; nincs garantált eredménye.

A javítás közös headerben az int scale(int value); prototípus, mindkét fordítási egységbe beillesztve. Ekkor az 1.0f argumentum szabályosan int értékké konvertálódik. Az ellenőrzési feltétel nem pusztán a sikeres linkelés, hanem a közös, kompatibilis deklaráció használata.

10.1.8 Összefoglaló

C-ben a skalár, a struktúra és a pointer egyaránt érték szerint adódik át. A pointermásolat közös objektumhoz adhat hozzáférést, de nem lesz a hívó pointerváltozójának referenciája. A megbízható API látható prototípussal, kompatibilis definícióval, explicit élettartam-feltételekkel és egyértelmű sikeres, illetve hibás kimeneti állapottal rendelkezik.