1.4 A szükséges C szintaxis tömör áttekintése

C-ben a rövid forráskód mögött is pontos típus- és élettartam-szabályok állnak. Most nem a teljes nyelvtant tanuljuk meg: azt a szintaktikai készletet rögzítjük, amellyel a következő memória- és típuslaborok programjai olvashatók.

Az előfeltétel a Reprodukálható fordítási és diagnosztikai labor: különösen az explicit nyelvi mód és a fordítási diagnosztika használata. Az alábbi nyelvi magyarázatok ISO C17 szabályokra épülnek, nem C++-szabályokra.

1.4.1 Deklaráció, definíció, inicializálás és értékadás

A deklaráció (declaration) egy azonosítóról közöl információt a fordítóval: például megadja egy változó típusát vagy egy függvény paramétereit és visszatérési típusát.

A definíció (definition) olyan deklaráció, amely az objektumot vagy függvényt meghatározza: objektumnál tárolót hoz létre, függvénynél megadja a törzset. Nem minden deklaráció definíció.

Az alábbi C17-részletben az első két sor fájlszinten, az utolsó három egy függvény törzsében értendő:

extern int status_code;       /* Deklaráció, nem definíció. */
int twice(int value);        /* Függvénydeklaráció, prototípus. */

/* Függvénytörzsben: */
int pending = 3;              /* Definíció és inicializálás. */
pending = 5;                  /* Értékadó kifejezésutasítás. */
int completed;               /* Definíció, inicializáló nélkül. */

Az inicializálás (initialization) az objektum kezdeti értékének meghatározása. Az értékadás (assignment) egy már létező objektumban tárolt értéket módosít. Bár mindkettőben szerepelhet =, nyelvtanilag és szemantikailag külön műveletek.

A completed automatikus lokális változó nem kap automatikusan nullát. Értéke kezdetben meghatározatlan (indeterminate); ne olvasd, amíg megfelelő értéket nem tároltál benne. Ez nem általános állítás minden inicializáló nélküli objektumról: más tárolási idejű objektumokra később más szabályokat ismerünk meg.

Az utasítás (statement) végrehajtási lépést ír le. A pending = 5; kifejezésutasítás, a return 0; visszatérési utasítás, a { ... } pedig összetett utasítás, más néven blokk. A deklaráció nem kifejezésutasítás, még ha gyakran ugyanúgy pontosvessző zárja is.

1.4.2 Operátorok, precedencia és zárójelezés

Az operátorok operandusokon végeznek műveletet. Az operandusok típusa számít: két int érték osztásakor például 7 / 2 eredménye 3, nem 3.5.

A következő táblázat részleges precedenciatérkép, a szorosabban kötő műveletektől lefelé:

Operátorok Jelentés
!x, -x, ++x Logikai negálás, előjelváltás, prefix növelés
*, /, % Szorzás, osztás, egész maradék
+, - Összeadás, kivonás
<, <=, >, >= Rendezési összehasonlítás
==, != Egyenlőség, különbözőség
&& Logikai ÉS
|| Logikai VAGY
=, +=, -= Értékadás, összetett értékadás

Ezért:

2+3⋅4=2+12=14, 2 + 3 \cdot 4 = 2 + 12 = 14,

míg a C-beli (2 + 3) * 4 eredménye 20.

A zárójel csoportosítást fejez ki, de nem általános kiértékelésisorrend-vezérlő eszköz. A left() + right() esetén a + nem írja elő, melyik függvényhívás történjen előbb.

A && és || viszont rövidzáras (short-circuit) operátor. A jobb operandus csak akkor értékelődik ki, ha az eredmény eldöntéséhez szükséges, és ilyenkor a bal operandus kiértékelése megelőzi azt. Például:

/* C17-részlet; divisor inicializált int. */
if (divisor != 0 && 12 / divisor > 2) {
    /* Az osztás csak nem nulla osztóval történik meg. */
}

C-ben a nulla hamis, a nem nulla igaz feltételnek számít. Az összehasonlító és logikai operátorok int típusú 0 vagy 1 eredményt adnak.

Két gyakori tévedés:

1.4.3 Blokkok, lokális változók és vezérlés

A kapcsos zárójelek blokkot határolnak. Egy szokásos lokális változó neve a deklarációjától a blokk végéig használható; ezt a név hatókörének (scope) nevezzük. A hatókör nem azonos az objektum élettartamával, még ha egyszerű példákban határaik szorosan kapcsolódnak is.

C17-ben a deklarációk és utasítások keverhetők. Célszerű a változót az első szükséges használata közelében, lehetőleg inicializálva definiálni.

A /* ... */ komment több soros lehet, de nem ágyazható önmagába. A // komment a sor végéig tart.

1.4.3.1 A vezérlési szerkezetek rövid referenciája

Az alábbi C17-utasításrészletekben a használt változók megfelelően deklarált, inicializált egész változók.

Szerkezet Példa és működés
if–else if (ready != 0) { count = 1; } else { count = 0; } – a feltétel alapján választ ágat
for for (int i = 0; i < 3; ++i) { total += i; } – inicializálás, feltétel, törzs, léptetés
while while (remaining > 0) { --remaining; } – elöl tesztel, akár egyszer sem fut
do–while do { ++attempts; } while (attempts < 3); – hátul tesztel, legalább egyszer fut

A for inicializáló része egyszer hajtódik végre. A feltételt minden iteráció előtt ellenőrzi; a léptetés a törzs után következik. A példabeli i a cikluson kívül már nem használható.

A switch egész típusú kifejezés alapján választ. A case címkék egész konstanskifejezéseket tartalmaznak; a default a fel nem sorolt értékek ága. A kiválasztott címkétől a végrehajtás továbbhalad, ezért a következő case nem zárja le automatikusan az előző ágat.

A break a legbelső körülvevő ciklusból vagy switch-ből lép ki. A continue a legbelső ciklus következő iterációjához vezet; for esetén előbb a léptetés történik meg. A return az egész függvényből tér vissza.

Használj kapcsos zárójeleket egyutasításos ágaknál is. Az if (ready); utáni pontosvessző üres utasítás: az utána írt blokk már nem feltételes.

1.4.4 Teljes program és közös kilépési ág

A következő önálló program minimumkövetelménye ISO C17, hosted környezet. A GCC-parancs profilja a központi profiltábla lab-gcc profilja, a Clang-parancsé a lab-clang.

A syntax.c tartalma:

#include <stdio.h>
#include <stdlib.h>

int contribution(int state)
{
    switch (state) {
    case 0:
        return 0;
    case 1:
        return 2;
    default:
        return 1;
    }
}

int main(void)
{
    int result = EXIT_FAILURE;
    int total = 0;

    for (int state = 0; state < 3; ++state) {
        total += contribution(state);
    }

    if (printf("total=%d\n", total) < 0) {
        goto cleanup;
    }

    if (fflush(stdout) == EOF) {
        goto cleanup;
    }

    result = EXIT_SUCCESS;

cleanup:
    return result;
}

A main(void) paraméter nélküli, int visszatérési típusú belépési függvény. A <stdio.h> deklarálja a használt I/O-függvényeket, és elérhetővé teszi többek között a stdout és EOF neveket. A <stdlib.h> adja az EXIT_SUCCESS és EXIT_FAILURE makrókat.

A contribution definíciója megelőzi a hívást, ezért itt külön előzetes prototípusra nincs szükség. A switch ágai return utasítással fejeződnek be, így nem kell utánuk break.

A ciklus közbülső eredményei:

state Visszakapott érték total a hozzáadás után
0 0 0
1 2 2
2 1 3

Sikeres kimeneti műveletek mellett az elvárt sor total=3. A %d egy int érték formázására szolgál, a \n újsorjel.

A printf negatív eredménye hibát jelez. Mivel a standard kimenet pufferelt lehet, a program a kikényszerített kiírás, az fflush eredményét is ellenőrzi. Ezek a visszatérési értékek, nem a képernyőn látott szöveg döntik el a hibakimenetet.

1.4.4.1 A goto szerepe

A goto cleanup; ugyanazon függvény cleanup címkéjére ugrik. Itt még nincs saját erőforrás, amelyet fel kellene szabadítani: ez a későbbi közös takarítási ág előképe. A stdout a standard könyvtár által kezelt stream; a program nem vesz át külön tulajdonjogot felette.

Mindkét hibaág megőrzi az előre beállított EXIT_FAILURE értéket. A sikerértéket csak az összes ellenőrzés után állítjuk be. A címke előtt álló result minden odavezető úton inicializált.

1.4.4.2 Fordítás

A profilnak megfelelő fordítóval:

gcc -std=c17 -Wall -Wextra -Wpedantic -O0 -g syntax.c -o syntax-gcc
clang -std=c17 -Wall -Wextra -Wpedantic -O0 -g syntax.c -o syntax-clang

Ezek GCC/Clang-parancsok, nem ISO C-szintaxis. Buildjegyzőkönyv: a GCC-parancs a lab-gcc profilban figyelmeztetés nélkül fordított, a program total=3 kimenettel, 0 kóddal futott; a Clang-sort nem ellenőriztük. A figyelmeztetésmentes fordítás önmagában egyébként sem bizonyítaná a helyességet.

1.4.5 Feladatok

  1. Fogalmi: Osztályozd a következő, függvénytörzsbe szánt sorokat. Melyik deklaráció, definíció, inicializálás vagy értékadás?

    int quota;
    quota = 4;
    int used = 0;
    int check(int value);
  2. Kódolási: Írj int sum_even(int limit) függvényt, amely a [0, limit] intervallum páros egészeit összeadja. Csak 0 <= limit && limit <= 100 fogadható el; más bemenetre térjen vissza -1 értékkel.

  3. Diagnosztikai: Az alábbi C17-részletben a kívánt eredmény kizárólag az idle sor. Miért nem ezt kapjuk? Keress két külön hibát. Feltételezd a <stdio.h> beillesztését és a sikeres I/O-t.

    int ready = 0;
    int mode = 1;
    
    if (ready = 1) {
        mode = 2;
    }
    
    switch (mode) {
    case 1:
        puts("idle");
    case 2:
        puts("active");
        break;
    default:
        puts("unknown");
        break;
    }

1.4.6 Megoldások

1.4.6.1 Fogalmi megoldás

Az int quota; deklaráció és definíció, explicit inicializálás nélkül. A quota = 4; értékadás, kifejezésutasítás. Az int used = 0; deklaráció, definíció és inicializálás. Az int check(int value); függvénydeklaráció, de nem függvénydefiníció.

Ellenőrzési feltétel: a quota első olvasása csak az értékadás után történhet.

1.4.6.2 Kódolási megoldás

Az alábbi C17-függvénydefiníció a teljes program main függvénye elé illeszthető:

int sum_even(int limit)
{
    if (limit < 0 || limit > 100) {
        return -1;
    }

    int total = 0;
    for (int value = 0; value <= limit; ++value) {
        if (value % 2 == 0) {
            total += value;
        }
    }
    return total;
}

Bizonyításvázlat: minden iteráció elején total a már megvizsgált páros értékek összege. A feltételes hozzáadás ezt megőrzi. Kilépéskor minden megengedett értéket megvizsgáltunk. A legnagyobb összeg:

2+4+⋯+100=2(1+2+⋯+50)=2⋅50⋅512=2550. 2+4+\cdots+100 =2(1+2+\cdots+50) =2\cdot\frac{50\cdot51}{2} =2550.

Ez a C17 által minimálisan megkövetelt int-tartományba is belefér.

Ellenőrzési feltétel: a -1, 0, 5, 100, 101 bemenetekre rendre -1, 0, 6, 2550, -1 az elvárt eredmény.

1.4.6.3 Diagnosztikai megoldás

Az if (ready = 1) egyet tárol ready változóba, és igaz feltételt eredményez. Emiatt mode kettő lesz, tehát az eredeti részlet active sort ír.

Az első javítás if (ready == 1). Ezután viszont az idle után végrehajtódik az active kiírása is, mert hiányzik a break az első ágból.

Ellenőrzési feltétel: mindkét javítás után ready nulla, mode egy marad, és sikeres I/O mellett csak az idle sor jelenik meg.

1.4.7 Összefoglaló

C-kód olvasásakor különítsd el a nevek deklarálását, az objektumok definícióját, az inicializálást és a későbbi értékadást. A precedencia csoportosítást, nem általános végrehajtási sorrendet határoz meg. A blokkok a lokális nevek hatókörét rendezik; a vezérlési szerkezetek pedig azt, mely utasítások hajtódnak végre. A közös kilépési ág már most segít láthatóvá tenni a siker és a hiba szerződését.