2.2 Headerek, deklarációk és include-higiénia

A header nem külön lefordítandó modul, hanem több fordítási egységbe beilleszthető interfészleírás. Ahogy a Fordítási fázisok és fordítási egységek részben láttuk, a fordító az előfeldolgozás után már a beillesztett deklarációkat látja. Ebből fontos következmény adódik: ugyanaz a header különböző forrásfájlokban, különböző előfeldolgozási környezetben is megjelenhet.

A jó nyilvános header önállóan használható, többszöri beillesztése nem okoz hibát, és csak az implementáció használatához szükséges információt teszi közzé. Ebben az egységben egy két forrásfájlból álló, egyszerű erőforráskeret-könyvtáron építjük fel ezt a szerződést. A nyelvi szabályok alapja ISO C17; a konkrét keresési utak és buildparancsok külön jelölt GCC-laborhoz tartoznak.

2.2.1 Deklaráció, definíció és az interfész határa

A deklaráció (declaration) információt közöl egy azonosítóról: például annak típusáról és nyelvi kapcsolásáról. A definíció (definition) olyan deklaráció, amely függvénynél megadja a függvénytörzset, objektumnál pedig az objektumot is meghatározza, tárolót biztosítva számára.

C17-ben, fájlhatókörben:

Alak Jelentés
int budget_ready(void); Függvénydeklaráció, törzs nélkül
int budget_ready(void) { return 1; } Függvénydefiníció
extern unsigned budget_count; Objektumdeklaráció, nem definíció
unsigned budget_count = 0; Objektumdefiníció
unsigned budget_count; Feltételes definíció (tentative definition)
extern unsigned budget_count = 0; Az inicializáló miatt definíció

A feltételes definíció nem egyszerű „előzetes említés”: ha a fordítási egységben nincs megfelelő másik definíció, a fordítási egység végén meghatározott szabályok szerint definícióvá válik. Ezért a nyilvános headerbe tett unsigned budget_count; több fordítási egység esetén hibás programszerkezethez vezethet. Egyes eszközbeállítások elfedhetik ezt, de erre hordozható programot nem építünk.

Az extern nem linkerparancs. Az inicializáló nélküli, szokásos publikus objektumdeklarációban lehetővé teszi egy máshol definiált objektum használatát. A könyvtárunk egyetlen .c fájlban definiálja a közzétett verzióobjektumot.

Fontos C/C++ különbség: C-ben a fájlhatókörű const önmagában nem teszi belső kapcsolásúvá az objektum nevét. A C++-ból ismert szabályt itt nem alkalmazhatjuk.

2.2.2 Include-keresés és ismételt beillesztés

2.2.2.1 Idézőjel és szögletes zárójel

Az alábbi két forma keresési módot választ, nem hozzáférési jogosultságot:

/* ISO C17 szintaxisrészlet */
#include "budget.h"
#include <stdlib.h>

ISO C17: a keresési helyek és eljárások implementáció által meghatározottak. Az idézőjeles forma sikertelen keresése után a beillesztést a szögletes zárójeles formának megfelelően is meg kell kísérelni.

GCC-labor: különleges keresési kapcsolók nélkül az idézőjeles forma először a beillesztést tartalmazó fájl könyvtárában keres, majd az idézőjeles kereséshez megadott helyeken, végül a szögletes zárójeles keresés útvonalain. A -Iinclude az include könyvtárat hozzáadja a kereséshez; ez mindkét formánál szerepet kaphat. A szögletes zárójel tehát nem jelenti azt, hogy „csak rendszerheader”.

Üzemeltetési szempontból a keresési út a build bemenete. Egy projektbeli stdlib.h nem kívánt módon elfedheti a rendszerheader nevét, ha rossz könyvtár kerül a keresés elejére. A keresési utakat és sorrendjüket ezért ugyanúgy kontrollálni kell, mint a függőségek verzióit.

2.2.2.2 Include guard és #pragma once

Az include guard egy egyedi makrónévvel védi a header tartalmát. Az első beillesztés definiálja a nevet; a következő beillesztés ugyanabban a fordítási egységben kihagyja a védett részt.

A guard hatóköre nem a teljes build. Két külön .c fájl mindegyike megkapja a header tartalmát. Következésképpen a guard nem javítja meg a headerbe tett külső függvény- vagy objektumdefiníciót.

A #pragma once GCC és Clang alatt elterjedt alternatíva, de a „fájlt csak egyszer illeszd be” jelentés nem ISO C17-szolgáltatás. Használata implementációs függőség; a következő példák hordozható guardot alkalmaznak. A guard neve projektazonosítót tartalmaz, és nem kezdődik fenntartott aláhúzásos mintával.

2.2.3 Két forrásfájlos könyvtár nyilvános headerrel

A könyvtár egy véges keretet kezel. A kliens létrehoz egy keretet, majd egységeket foglal belőle. A sikertelen foglalás nem változtatja meg az állapotot.

A fájlok:

2.2.3.1 A nyilvános header

ISO C17; célarchitektúrától független forráskód. Az include/budget.h teljes tartalma:

#ifndef DEVOPS_BUDGET_H_INCLUDED
#define DEVOPS_BUDGET_H_INCLUDED

struct budget;

enum budget_status {
    BUDGET_OK,
    BUDGET_INVALID,
    BUDGET_EXHAUSTED
};

extern const unsigned budget_api_version;

/* Tulajdont ad at; allokacios hiban NULL az eredmeny. */
struct budget *budget_create(unsigned capacity);

/* NULL eseten nincs hatasa; kulonben ervenyes ownert igenyel. */
void budget_destroy(struct budget *budget);

/* Nem tarolja el a kolcsonzott pointert.
   Hiba eseten az allapot valtozatlan. */
enum budget_status budget_take(struct budget *budget,
                               unsigned amount);

#endif

A struct budget; elődeklaráció (forward declaration): bevezeti a struktúratípus nevét, de nem adja meg a tagjait. A típus itt nem teljes típus (incomplete type). Pointer deklarálható rá, de a kliens nem kérdezheti le a méretét, nem érheti el a tagjait, és nem definiálhat belőle érték szerint objektumot.

Ezt nevezzük opaque, vagyis átlátszatlan típusnak. A reprezentáció nem része a nyilvános headernek.

A header önmagában elegendő (self-contained): használata nem igényel előtte másik #include sort. Jelenleg egyetlen rendszerheaderre sincs szüksége. Ha később size_t jelenne meg az interfészben, közvetlenül be kellene illesztenie a <stddef.h> headert, nem bízhatna a kliens include-sorrendjében.

2.2.3.2 A privát reprezentáció és az implementáció

A src/budget_private.h csak a könyvtár implementációjához tartozik:

#ifndef DEVOPS_BUDGET_PRIVATE_H_INCLUDED
#define DEVOPS_BUDGET_PRIVATE_H_INCLUDED

#include "budget.h"

struct budget {
    unsigned remaining;
};

#endif

A src/budget.c:

#include "budget.h"
#include "budget_private.h"

#include <stdlib.h>

const unsigned budget_api_version = 1u;

struct budget *budget_create(unsigned capacity)
{
    struct budget *budget = malloc(sizeof *budget);
    if (budget == NULL) {
        return NULL;
    }

    budget->remaining = capacity;
    return budget;
}

void budget_destroy(struct budget *budget)
{
    free(budget);
}

A malloc és free itt a dinamikus tároló megszerzését és felszabadítását végzi; részletes memóriaszerződésük későbbi téma. Lényeges, hogy a sizeof *budget használatakor a struktúratípus már teljes.

A src/budget_take.c:

#include "budget.h"
#include "budget_private.h"

enum budget_status budget_take(struct budget *budget,
                               unsigned amount)
{
    if (budget == NULL) {
        return BUDGET_INVALID;
    }

    if (amount > budget->remaining) {
        return BUDGET_EXHAUSTED;
    }

    budget->remaining -= amount;
    return BUDGET_OK;
}

Mindkét forrás először a saját nyilvános headert illeszti be. Ez hasznos ellenőrzési gyakorlat: kisebb eséllyel takarja el egy előző include a publikus header hiányzó függőségét.

2.2.3.3 Az interfész pontos szerződése

A capacity és az amount az unsigned típus értéktartományába tartozó egységszám; nem feltételezünk konkrét bitszélességet.

Szerep Tulajdonos és élettartam Érvénytelenítés, hibakimenet
Tulajdonosi pointer A kliens felel a pontosan egyszeri megszüntetésért A pointer másolása nem hoz létre új objektumot
Dinamikus tároló Sikeres létrehozástól a budget_destroy hívásáig él A megszüntetés felszabadítja
A budget_take paramétere A hívás idejére kölcsönzött hozzáférés; nem tárolódik el Null pointerre hibakód; dangling pointerre nincs biztonságos hibafelismerés
További pointermásolatok Nem hosszabbítják meg az objektum élettartamát Megszüntetés után érvénytelenek; ismételt használatuk UB-hez vezethet

Helyességi bizonyításvázlat. Legyen az aktuális keret rr, a kérés aa. Ha a>ra>r, a függvény módosítás nélkül visszatér. Egyébként 0≤a≤r0\leq a\leq r, ezért r−ar-a ábrázolható az unsigned típusban, és az új keret nem nagyobb a réginél. A kivonást tehát olyan ellenőrzés előzi meg, amely kizárja az alulcsordulást.

2.2.3.4 Kliens és build

Az app.c teljes, ISO C17 kliens:

#include "budget.h"

#include <stdlib.h>

int main(void)
{
    struct budget *budget = budget_create(3u);
    if (budget == NULL) {
        return EXIT_FAILURE;
    }

    int ok =
        budget_take(budget, 2u) == BUDGET_OK &&
        budget_take(budget, 2u) == BUDGET_EXHAUSTED &&
        budget_take(budget, 1u) == BUDGET_OK;

    budget_destroy(budget);
    return ok ? EXIT_SUCCESS : EXIT_FAILURE;
}

A választott parancsprofil: Linux x86-64, System V ABI, LP64; GCC 14.2.0, glibc 2.40, GNU Binutils 2.43.1, explicit C17 móddal. Ez laborprofil, nem itt elvégzett fordítás vagy futtatás.

A projekt gyökeréből:

gcc -std=c17 -Wall -Wextra -pedantic-errors -Iinclude \
    -c src/budget.c -o budget.o
gcc -std=c17 -Wall -Wextra -pedantic-errors -Iinclude \
    -c src/budget_take.c -o budget_take.o
ar rcs libbudget.a budget.o budget_take.o

gcc -std=c17 -Wall -Wextra -pedantic-errors -Iinclude \
    -c app.c -o app.o
gcc app.o libbudget.a -o budget_demo

Az archívum a két implementációs objektumfájlt tartalmazza; a kliens csak a nyilvános headert használja. Várt eredmény: sikeres allokáció esetén a kliens sikeres kilépést jelez, és közvetve ellenőrzi, hogy az elutasított foglalás nem fogyasztott keretet.

2.2.4 Minimális függőség és konfigurációs egyezés

A minimális include nem az include-ok mechanikus törlését jelenti. Minden fájl közvetlenül illessze be azokat a headereket, amelyek deklarációit használja. Ne építsen arra, hogy egy másik header véletlenül továbbadja őket.

Ciklikus függőségnél az include guard legfeljebb a végtelen beillesztést akadályozza meg; a hiányzó típusinformációt nem pótolja. Ha két struktúra csak pointerrel hivatkozik egymásra, elődeklarációval megszakítható a függőség. Érték szerinti taghoz viszont teljes típus kell; a kölcsönös érték szerinti beágyazás nem oldható meg include-sorrenddel.

Különösen veszélyes a makrókkal módosított reprezentáció. Az alábbi hibát szemléltető C17-részlet a privát struktúra alternatív definíciója:

struct budget {
#ifdef BUDGET_DIAGNOSTICS
    unsigned failed_requests;
#endif
    unsigned remaining;
};

Ha a létrehozást és a foglalást végző forrás eltérő makróbeállítással fordul, ugyanahhoz az objektumhoz eltérő típusleírást használhatnak. A linkelés ettől még sikerülhet; a futás nem válik helyessé. Konkrét layout vagy hibakimenet nem garantálható, a program UB-be kerülhet.

Az include guard itt sem segít. Sőt, egy fordítási egységen belül az első beillesztéskor érvényes konfigurációt rögzíti: későbbi makróváltoztatás nem dolgoztatja fel újra a headert.

Javításként a reprezentációt befolyásoló konfiguráció legyen központi, minden érintett fordítási egységre egységes buildbemenet. Változtatásakor minden függő objektumfájlt újra kell fordítani. A nyilvános API alakját lehetőleg ne változtassuk kliensenként beállítható makrókkal.

2.2.5 Feladatok

  1. Magyarázó: miért helyes a headerben az extern const unsigned budget_api_version;, és miért nem megfelelő helyette a const unsigned budget_api_version = 1u;?

  2. Kódolási: készíts tests/header_check.c fájlt, amely elsőként, majd ismételten is beilleszti a nyilvános headert. Ellenőrizd fordítással, linkelés nélkül.

  3. Diagnosztikai: a budget.c makró nélkül, a budget_take.c pedig BUDGET_DIAGNOSTICS mellett fordult a fenti alternatív struktúrával. A linker nem jelez hibát. Mit bizonyít ez, és hogyan javítanád a buildet?

2.2.6 Megoldások

2.2.6.1 Deklaráció és egyetlen definíció

Az extern deklaráció megadja az objektum típusát, de nem definiál új objektumot minden fordítási egységben. Az inicializált alak viszont definíció lenne minden olyan egységben, amely a headert beilleszti. A helyes szerkezet: publikus deklaráció a headerben, egyetlen definíció az implementációban.

Ellenőrzési feltétel: a könyvtár és több külön kliensfordítási egység együtt is csak egy verzióobjektum-definíciót tartalmazzon.

2.2.6.2 Önállóság és ismételhetőség ellenőrzése

A teljes tesztfájl:

#include "budget.h"
#include "budget.h"

int header_check(void)
{
    return budget_api_version == 1u;
}

Azonos GCC-laborprofilban:

gcc -std=c17 -Wall -Wextra -pedantic-errors -Iinclude \
    -fsyntax-only tests/header_check.c

Várt ellenőrzési feltétel: nincs diagnosztika. Az első include az előzetes függőség nélküli használatot, a második a guard működését vizsgálja. Linkelés hiányában ez még nem ellenőrzi a verzióobjektum definíciójának meglétét.

2.2.6.3 Konfigurációs eltérés felismerése

A sikeres linkelés csak azt mutatja, hogy az adott eszköz elfogadta az összekapcsolást; nem bizonyítja a fordítási egységek típusleírásainak egyezését.

A javítás egységes konfiguráció, az érintett objektumfájlok tiszta újrafordítása, majd az előfeldolgozott struktúradefiníciók összevetése.

Ellenőrzési feltétel: mindkét implementáció kompatibilis teljes típust lásson. Egyetlen sikeres próbahívás ehhez nem elegendő bizonyíték.

2.2.7 Rövid összefoglaló

A nyilvános header deklarációkat és használati szerződést közöl; az implementáció biztosítja a definíciókat. Az önállóságot közvetlen függőségekkel, az ismételhetőséget include guarddal, a reprezentáció elrejtését opaque típussal érjük el. A keresési út és a makrókonfiguráció a build lényegi bemenete: eltérésük olyan hibát is okozhat, amelyet a linker nem vesz észre.