Egy szolgáltatás naplója terminálon azonnal megjelenhet, pipe-ba irányítva viszont csak később. Egy sikeres írás után pedig még a fájl lezárása is jelezhet hibát. Mindkét jelenség megértéséhez külön kell választani a C könyvtár streamjét, az operációs rendszer I/O-rétegét és a tartós tárolást.
A fejezet az API-paraméterek, az explicit tulajdonlás és cleanup, valamint a sztringek hosszának ismeretére épít. Itt azonban nem sztringeket, hanem tetszőleges adatot is kezelünk: egy nulla értékű C bájt nem jelenti a bemenet végét.
FILE* egy
állapottal rendelkező stream hozzáféréseISO C17: a <stdio.h> fejléc
FILE típusa a stream vezérléséhez szükséges információkat
hordozó objektumtípus. A stream (stream) a program és egy
bemeneti vagy kimeneti adatforrás közötti rendezett adatkapcsolat. A
FILE belső mezőire nem építünk; a hozzáférést könyvtári
függvényeken keresztül végezzük.
POSIX/Linux: a fájlhoz, terminálhoz vagy pipe-hoz
kapcsolt stdio stream jellemzően file descriptorra épül. A
FILE* azonban nem descriptor: saját pufferelést, EOF-jelzőt
és hibajelzőt tart fenn. Egy könyvtári írás ezért nem feltétlenül jelent
azonnali kernelhívást.
A program indulásakor három előre megnyitott stream érhető el:
| Név | Rendeltetés | Szokásos tulajdonlási szerződés |
|---|---|---|
stdin |
Standard bemenet | A segédfüggvény kölcsönzi, nem zárja le |
stdout |
Standard kimenet | A segédfüggvény kölcsönzi; a program felel a végső hibaellenőrzésért |
stderr |
Standard diagnosztikai kimenet | Diagnosztikára használjuk, sikere nem garantált |
Ezek nem feltétlenül terminálhoz kapcsolódnak. Az átirányítás megváltoztatja a háttérben elérhető erőforrást, és ezzel a megfigyelhető pufferelést is.
A fejezet példái bájtorientált stdio-műveleteket használnak; ugyanazon streamen nem keverik őket széles karakteres I/O-val.
ISO C17: az fopen(path, mode) siker
esetén FILE*, hiba esetén null pointer eredményt ad. A mód
nem pusztán jogosultság: a fájl tartalmára gyakorolt hatást is
meghatározza.
| Alapmód | Művelet | Nem létező fájl | Létező fájl |
|---|---|---|---|
"r" |
Olvasás | Hiba | Tartalma megmarad |
"w" |
Írás | Létrehozás | Tartalma megnyitáskor elvész |
"a" |
Hozzáfűzés | Létrehozás | Az írások a mindenkori végére kerülnek |
"r+" |
Olvasás és írás | Hiba | Tartalma megmarad |
"w+" |
Olvasás és írás | Létrehozás | Tartalma megnyitáskor elvész |
"a+" |
Olvasás és hozzáfűzés | Létrehozás | Az írások a végére kerülnek |
A b módosító bináris streamet kér: például
"rb", "wb" vagy "r+b".
ISO C17 szerint a szöveges stream külső reprezentációja
átalakulhat. POSIX/Linux alatt a b nem
változtatja meg a működést, de hordozható bináris kódban így is
indokolt.
A + módú, úgynevezett update stream irányváltásának
szabályai fontosak:
fflush vagy fájlpozicionáló
művelet szükséges.Az fclose kiüríti a függő kimenetet, és lezárja a
kapcsolatot. Siker esetén nullát, észlelt hiba esetén EOF
értéket ad. A hibája ugyanúgy számít, mint az írás hibája.
| Erőforrás vagy tároló | Tulajdonos | Érvényességi határ |
|---|---|---|
Sikeres fopen eredménye |
A megnyitást vállaló kódrész | Az fclose hívásáig |
Függvénynek átadott, kölcsönzött FILE* |
A hívó | A hívó lezárása érvényteleníti |
Saját, setvbuf-nak átadott puffertömb |
A tömböt kezelő kódrész | Legalább a stream lezárásának befejezéséig éljen |
Az fclose hívása után a stream többé nem
használható, akkor sem, ha a lezárás hibát jelzett. Ugyanazzal
a pointerrel nem próbáljuk újra a lezárást.
ISO C17: a
fread(ptr, size, count, stream) és
fwrite(ptr, size, count, stream) visszatérési értéke a
sikeresen feldolgozott teljes elemek száma, nem
általában a C bájtok száma.
Ha egy elem mérete, a kért elemszám, akkor a kért adatmennyiség:
A visszaadott elemszámra . Ha , akkor közvetlenül C bájtszámként értelmezhető. A tárolónak a teljes kért tartományt le kell fednie; a méretszámítás nem csordulhat túl.
Ezért nyers adatmásoláshoz egyszerű szerződés:
fread(buffer, 1, sizeof buffer, input)
Itt buffer tényleges tömb legyen, ne pointer: pointeren
a sizeof nem a mögöttes tároló kapacitását adja.
A rövid olvasás jelenthet fájlvéget vagy olvasási hibát. Ezeket külön kell vizsgálni:
feof(stream) nem nulla, ha a stream EOF-jelzője be van
állítva.ferror(stream) nem nulla, ha a hibajelző be van
állítva.Ezek állapotlekérdezések, nem a következő művelet jóslatai. Az utolsó rendelkezésre álló bájt sikeres beolvasása önmagában még nem feltétlenül állítja be az EOF-jelzőt. Ezért hibás a „míg nincs EOF, olvass” logika: előbb olvasunk, utána értékeljük az eredményt.
A rövid fwrite írási hibát jelez. Nem alkalmazunk rá
automatikusan descriptor-szintű „írd ki a maradékot” ciklust. A
hibajelző törlése sem javítja meg az alapul szolgáló hibát.
int az
fgetc eredménye?Az fgetc egy karaktert ad vissza,
unsigned char értékként int-re konvertálva,
vagy EOF-ot jelez. Az EOF negatív
int konstans, de nem szabad feltételezni, hogy pontosan
-1.
A könyv szokásos, nyolcbites C bájtú laborprofiljában a
karakterértékek
–
közöttiek, ezért az int megkülönbözteti őket az EOF-tól. Ha
az eredményt előbb char változóba szűkítjük, ez az
információ elveszhet: például egy érvényes bájt összekeveredhet az
EOF-fal.
Az EOF eredmény után itt is ferror különíti
el az olvasási hibát a rendes végállapottól.
ISO C17: három pufferelési módot különböztetünk meg:
_IOFBF: teljes pufferelés;_IOLBF: sorpufferelés;_IONBF: pufferelés nélküli működés.Teljes pufferelésnél a kimenet gyűlhet a könyvtár pufferében. Sorpufferelésnél az újsor karakter is kiváltja a továbbítást; a puffer megtelése és az explicit kiürítés szintén számít. A pufferelés nélküli működés sem jelent lemezre történő tartós mentést.
Linux/glibc tipikus viselkedés: a terminálhoz
kapcsolt stdout sorpufferelt, a fájlhoz vagy pipe-hoz
kapcsolt stdout teljesen pufferelt. Ez magyarázza, hogy egy
soronként naplózó program pipe mögött késleltetve jelenítheti meg a
sorokat.
ISO C17 garancia: a stderr kezdetben
nem teljesen pufferelt. Ez nem azonos a minden implementációra érvényes
„biztosan pufferelés nélküli” állítással.
A setvbuf módosítja a pufferelést. A stream megnyitása
után, de minden más streamművelet előtt kell meghívni; a szabvány a
korábbi sikertelen setvbuf hívásra kivételt enged. Nulla a
siker, nem nulla a sikertelenség.
Például a setvbuf(stdout, NULL, _IOLBF, BUFSIZ)
sorpufferelést kér saját puffertömb átadása nélkül. Ezt még az első
kiírás előtt kell ellenőrizni. Saját tömb esetén annak tárolója nem
szűnhet meg a stream lezárása előtt.
fflush?ISO C17: megfelelő kimeneti streamen az
fflush a még ki nem írt adatokat a host környezetnek
továbbítja, hogy azok a fájlba kerüljenek. Siker esetén nulla, írási
hibánál EOF az eredmény. Update streamen akkor alkalmazható
így, ha a legutóbbi művelet nem bevitel volt. Az
fflush(NULL) az erre alkalmas kimeneti streameket
üríti.
Három szintet különítsünk el:
| Művelet | Lényeges határ |
|---|---|
Sikeres fwrite |
A stdio elfogadta a teljes adatmennyiséget |
Sikeres fflush |
A függő stdio-kimenet továbbítása megtörtént |
POSIX fsync |
Az operációs rendszer fájlszinkronizálási szerződését kéri |
Az fflush nem fsync. A
tartósság további feltételeit a fájlrendszer-műveletekről szóló rész tárgyalja.
Az fflush(stdin) nem hordozható „bemenettörlés”.
ISO C17 szerint ilyen bemeneti streamen a viselkedése
nem definiált. A POSIX bizonyos bemeneti streamekre további szabályokat
ad, de ebből nem következik általános terminálbemenet-eldobási
módszer.
A program két különböző fájl között másol.
Előfeltétel: a két útvonal nem jelölheti ugyanazt a
mögöttes fájlt, hard linken keresztül sem; különben a "wb"
megnyitás megsemmisítheti a bemenetet. A célfájl hibánál részleges
maradhat: ez nem tranzakciós másoló.
Minimum: ISO C17, hosted környezet. Választott laborprofil: Linux, x86-64 System V ABI, LP64, glibc 2.40; GCC 14.2.0 vagy Clang 19.1.7. A példa itt nem fordított vagy futtatott mérési eredmény.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
FILE *input = NULL;
FILE *output = NULL;
unsigned char buffer[8192];
int status = EXIT_FAILURE;
if (argc != 3) {
fputs("usage: copy source destination\n", stderr);
return EXIT_FAILURE;
}
input = fopen(argv[1], "rb");
if (input == NULL) {
fputs("cannot open source\n", stderr);
goto cleanup;
}
output = fopen(argv[2], "wb");
if (output == NULL) {
fputs("cannot open destination\n", stderr);
goto cleanup;
}
for (;;) {
size_t n = fread(buffer, 1, sizeof buffer, input);
if (n != 0 && fwrite(buffer, 1, n, output) != n) {
fputs("write failed\n", stderr);
goto cleanup;
}
if (n < sizeof buffer) {
if (ferror(input)) {
fputs("read failed\n", stderr);
goto cleanup;
}
break;
}
}
status = EXIT_SUCCESS;
cleanup:
if (output != NULL && fclose(output) == EOF) {
fputs("destination close failed\n", stderr);
status = EXIT_FAILURE;
}
if (input != NULL && fclose(input) == EOF) {
fputs("source close failed\n", stderr);
status = EXIT_FAILURE;
}
return status;
}Fordítás a megfelelő verziójú driverekkel:
gcc -std=c17 -Wall -Wextra -Wpedantic -O2 copy.c -o copy-gcc
clang -std=c17 -Wall -Wextra -Wpedantic -O2 copy.c -o copy-clangA main mindkét stream tulajdonosa. A könyvtár a
buffer tömböt csak az adott művelet alatt használja; a tömb
a teljes másolás során él. Az fclose minden megszerzett
streamre pontosan egyszer fut le.
Helyességi bizonyításvázlat: minden teljes cikluslépés a beolvasott C bájtot pontosan egyszer adja át az írásnak. Rövid írásnál a program hibával leáll. Rövid olvasásnál megkülönbözteti az olvasási hibát a fájlvégtől. A kezdetben sikeresnek minősített másolást a késleltetett lezárási hiba még sikertelenné teszi.
Külön fflush nem szükséges: az ellenőrzött
fclose elvégzi a kiürítést is. A diagnosztikai
fputs hívások szándékosan best-effort jellegűek; hibájukat
nem jelentjük újabb diagnosztikával. A hibás kilépési állapot ettől még
megmarad.
Egy program minden esemény után újsorral zárt sort ír
stdout-ra. Terminálon gyorsan látszik a sor, pipe mögött
késik. Miért? Garantálna-e tartósságot egy soronkénti
fflush?
Írj copy_chars(FILE *input, FILE *output) függvényt
fgetc és fputc használatával. Adjon nullát
siker, egyet hiba esetén. A streameket kölcsönözze, ne zárja le és ne
ürítse explicit módon.
Mi hibás az alábbi C17-részletben? A streamek egyébként érvényesek.
/* Hibás tanítási példa; nem futtatási minta. */
while (!feof(input)) {
char c = fgetc(input);
fputc(c, output);
}
fflush(stdin);A tipikus Linux/glibc terminálkimenet sorpufferelt, a pipe-kimenet
teljesen pufferelt. Az újsor ezért a második esetben nem feltétlenül
továbbítja a felgyűlt adatot. Az ellenőrzött fflush(stdout)
megszünteti ezt a stdio-szintű várakozást, de tartósságot nem
garantál.
Ellenőrzési feltétel: a magyarázat külön kezelje a könyvtári puffert, a kernelnek történő átadást és a tartós tárolást.
C17-függvényrészlet, a szükséges fejléccel:
#include <stdio.h>
int copy_chars(FILE *input, FILE *output)
{
int c;
while ((c = fgetc(input)) != EOF) {
if (fputc(c, output) == EOF) {
return 1;
}
}
return ferror(input) ? 1 : 0;
}Előfeltétel a két, megfelelő irányban használható, bájtorientált vagy
még orientálatlan stream, kezdetben törölt EOF- és hibajelzőkkel. A hívó
a tulajdonos; a függvény alatt nem zárhatja le őket. A kimenet hibánál
részleges lehet. A késleltetett íráshiba ellenőrzése a hívó későbbi
fflush vagy fclose műveletének feladata.
Ellenőrzési feltétel: üres bemenetet, valamint a
laborprofilban 0x00 és 0xFF bájtokat
tartalmazó bemenetet is helyesen kezeljen.
A feof túl korai vizsgálat: a ciklus sikertelen olvasás
után is megpróbálhat kiírni egy karaktert. A char
elveszítheti az EOF és az érvényes karakter közötti különbséget. Az
olvasási hiba nem feltétlenül állít EOF-jelzőt, ezért a ciklus
végtelenné válhat. Az írás eredményét sem ellenőrzi. Végül az
fflush(stdin) ISO C17 alatt nem definiált viselkedésű.
Javítás és ellenőrzési feltétel: az előző megoldás
szerint előbb az int eredményt vizsgáljuk, ellenőrizzük az
írást, majd a ciklus után az olvasási hibát. EOF-ot soha nem írunk ki
adatként.
A FILE* állapottal és puffereléssel rendelkező streamhez
ad hozzáférést, nem puszta descriptor. A helyes sorrend: művelet,
visszatérési érték ellenőrzése, szükség esetén EOF- és hibajelző
vizsgálata. A teljes másolás sikeréhez a lezárást is ellenőrizni kell.
Az fflush a stdio-kimenet továbbításáról szól, nem
tartósságról, és nem hordozható bemenettörlő művelet.