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.
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.
Legyen a hívó pointerobjektuma , a paraméterobjektum pedig . Tegyük fel, hogy egy élő, írható, megfelelő típusú és igazítású objektumra mutat. A hívás kezdetén:
A p_f = NULL értékadás célobjektuma
,
ezért
változatlan marad. Ezzel szemben az ezt megelőző *p_f = 42
értékadás célobjektuma
.
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.
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-clangA 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.
| 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.
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.
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.
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:
void stop(void) függvény hívása stop(1)
alakban. Ez fordítási diagnosztikát igényel.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.
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:
out a sikeres művelet eredményének helye.Szerződése szerint hiba esetén az eredményobjektum változatlan. A túlcsordulás-ellenőrzés levezetése matematikai egészekkel, jelöléssel:
Mivel
,
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.
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?
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.
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);
}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.
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
mellett a feltétel
.
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.
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.
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.