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.
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.
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.
#pragma onceAz 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.
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:
include/budget.h: nyilvános interfész;src/budget_private.h: belső reprezentáció;src/budget.c: létrehozás, megszüntetés és
verzióobjektum;src/budget_take.c: foglalási művelet;app.c: külön kliensprogram.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);
#endifA 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.
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;
};
#endifA 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.
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.
BUDGET_INVALID eredményt ad.
Minden nem null pointernek élő, e könyvtár által létrehozott objektumra
kell mutatnia.BUDGET_EXHAUSTED az eredmény,
állapotváltozás nélkül.| 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
,
a kérés
.
Ha
,
a függvény módosítás nélkül visszatér. Egyébként
,
ezért
á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.
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_demoAz 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.
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.
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;?
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.
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?
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.
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.cVá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.
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.
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.