1. NHF kiadás – ezen a héten

A házi szabadon választott

  • Kb. 500 soros C program, kötelező elemek
  • Ötletek a honlapon – hasonló nehézségű saját is lehet
  • A választást a laborvezető mindenkinél jóvá kell hagyja!
  • Egyénileg elkészítve!

Admin portál:
NHF 1-2-3-4.

Beadandók (elektronikusan)

  1. Feladat választása: 7. hétig
  2. Pontosított specifikáció: 8. hétig
  3. Félkész program: 10. hétig
  4. Végleges változat: 13. hétig; laboron bemutatva!
    • Kód + felhasználói és programozói dokumentáció

2. NZH – 9. héten

Számonkérés

  • Nagy ZH: nov 3., 8:00.
  • Portálon jelentkezés! Terembeosztás is majd ott.
  • 90 perc, kzh kaliberű feladatokból több
  • Feltételes ajándék 2 pont

NZH anyaga

  • 7. heti előadással bezárólag
  • 8. heti előadáson áttekintés
  • Gyakorolni kell!

Mutatók (pointerek)

4. Az indirekció

A feladatunk egy olyan függvényt írni, amely két egész típusú változó tartalmát megcseréli.

void csere(int a, int b) {
    int temp;
    temp = a;
    a = b;    // nem x változik!
    b = temp;
}

int main(void) {
    int x = 3, y = 4;
    csere(x, y);
    printf("%d %d", x, y); // 3, 4
}

A hibás csere függvény: érték szerinti paraméterátadással

Érték szerinti paraméterátadás

void csere(int *pa, int *pb) {
    int temp;
    temp = *pa;
    *pa = *pb;   // x változik!
    *pb = temp;
}

int main(void) {
    int x = 3, y = 4;
    csere(&x, &y);
    printf("%d %d", x, y); // 4, 3
}

A helyes csere függvény: cím szerinti paraméterátadással

Cím szerinti paraméterátadás

A bal oldalon egy hibás próbálkozás látható. Mint tudjuk, a C nyelvben érték szerinti paraméterátadás történik, ami azt jelenti, hogy a függvény a hívás helyén megadott kifejezések értékét, kiszámolt eredményét kapja meg. Emiatt a csere(x, y) függvényhívás azt jelenti, hogy ki kell olvasni az x és y változók tartalmát, és az ott tárolt számokat átadni a függvénynek. Az átadott két szám, a 3 és a 4 a két paraméterbe, azaz a két lokális változóba másolódik, amelyek tartalmát pedig a függvény hiába cseréli meg. Az két másik változó, amelyeknek a módosítása nincs hatással az eredeti két változó értékeire.

A jobb oldali függvény már működik, mert az egy új nyelvi elemet használ, az indirekciót. Ez azt jelenti, hogy a híváskor a függvény nem a változók értékét, hanem a változók helyét kapja meg paraméterként. Tehát a hívással nem két számot adunk neki, hanem rámutatunk két rekeszre a memóriában: „az egyik változó itt található a memóriában, a másik ott, ezek tartalmát cseréld meg!” Így nem jön létre a hívás idejére két új egész típusú változó, hanem a függvény a hívó által megadott két eredeti változón dolgozik.

A jobb oldali függvényben a paraméterek típusai: int *. Ez egy olyan típus, amely egy int változó helyére mutat a memóriában, máshogy fogalmazva, a mutatott változó címét tárolja. A cím hasonló értelmű, mint a hétköznapi értelemben vett, postai cím: ez mutatja a postásnak, hogy hol van az a hely, ahova a levelet vinnie kell. Az ilyen típusú változót mutatónak (pointer) nevezzük. A függvény ezen a mutatón keresztül közvetetten, indirekt módon látja az eredeti változót. Az indirekció (indirection) kezeléséhez a C nyelvben két operátort is biztosít. Az & címképző operátor előállítja azt a pointert, amelyik a tőle jobbra megadott nevű változóra mutat. A * indirekció (vagy más néven dereferáló) operátorral pedig a tőle jobbra álló pointer által mutatott változót tudjuk elérni.

5. Mutatók és indirekció

Nézzünk meg részletesen az új nyelvi elem működését és használatát! A mutatókról az alábbiakat kell tudni.

Mutató (pointer)

  • Egy változó memóriabeli helye, más néven: címe
  • Cím képzése: az & (címképző operátor), &valtozo.
  • A mutatott változó: * (indirekció operátora): *pointer.
  • A mutató is eltárolható egy változóban.

Indirekció (indirection)

A számítógép memóriájának rekeszei, bájtjai meg vannak számozva. A processzor, ha szeretne egy változóban tárolt értéket kiolvasni, vagy szeretné módosítani azt, akkor a változó helyét jelző cím segítségével hivatkozza meg. Innen tudja a memória, hogy melyik tárolt bájttal kell dolgoznia.

Ezeket a címeket a programunkban is tudjuk használni. Az &x (address of x) kifejezéssel képezzük egy x változó címét. Azt a címet kapjuk meg ezáltal, ahova a fordító a memóriában elhelyezte az x változót. A cím akár eltárolható egy másik változóban. A lenti példában ez a ptr nevű változó, amely double * típusú mutató lévén, pont ilyet tud tárolni, azt, hogy mi a címe a program valamelyik double típusú változójának.

A mutató típusa jelzi a mutatott típust is:

double x;
double *ptr;

ptr = &x;    // cím képzése

*ptr = 3.14; // a mutatott változó
Az indirekció működése

A *ptr kifejezés a ptr pointer által mutatott változót adja. A kifejezés kiértékelése úgy működik, hogy a ptr nevű változóból kiolvasódik a memóriacím, utána pedig az így kapott memóriacím által hivatkozott helyen tárolt változóval dolgozunk tovább, mintha csak az eredeti nevén neveztük volna. Mivel a ptr a fenti példában az x változóra mutat, *ptr-t írva tulajdonképp az x-ről beszélünk. A címet gyakran referenciának (reference) is szokás nevezni. Ezért mondják a * operátorra, hogy dereferál (dereference), azaz megszünteti a referenciát: általa már a mutatott változót látjuk.

Az említett két operátor, a * dereferáló és az & címképző operátor precedenciája viszonylag magas. A jobb oldalon álló (indexelő [], függvényhívó () stb.) operátorokénál alacsonyabb, azonban minden kétoperandusú aritmetikai műveletnél (pl. +, - stb.) magasabb.

Mutató típusú változót úgy definiálunk, hogy a definícióban egy *-ot teszünk a neve mellé. A mutató típusához hozzátartozik a mutatott változó típusa is, hiszen az általa mutatot hely hivatkozásakor tudnia kell a fordítónak azt, hogy milyen típusú érték található ott – egyáltalán hány bájtból álló adatot kell kezelni. A mutató a program futása során tetszőlegesen másik változókra állítható át, ha azok a változók megfelelő típusúak. A fenti példában a mutató típusa double *, ezért a program bármelyik double típusú változójának címét tárolhatná. A pointer típusú változó maga is egy teljesen szokványos változóként viselkedik: értéket kell adni neki használat előtt! A pointerek pedig a szokványos módon átadhatók függvénynek paraméterként, és lehetnek függvények visszatérési értékei is.

Fontos megjegyzés. A „cím szerinti paraméterátadás” igazából csak egy trükk C-ben, mert ebben a nyelvben tényleg csak érték szerinti paraméterátadás létezik. Csak a pointer átadásánál az érték nem a változó által tárolt érték, hanem egy másik érték, a változó memóriacíme. A csere() függvény most is mindent másolatként kap; annak a pa nevű pointerében van egy másolat az x változó & operátor segítségével lekérdezett címéről, a pb nevű paraméterben pedig egy az y változó címéről. Ezek viszont a külső változókra mutatnak, a dereferálás után az eredeti x és y változókat lehet elérni és akár megváltoztatni.

Címek és mutatott értékek: program

Az alábbi programban a p pointert hol az egyik, hol a másik változóra állítjuk be.

#include <stdio.h>

int main(void) {
    double x = 5, y = 10;
    double *p;

    p = &x;
    printf("p=%p \n *p=%f \n\n", p, *p);

    p = &y;
    printf("p=%p \n *p=%f \n\n", p, *p);

    return 0;
}

Egy pointer értékét, amely a memóriacím maga (vagyis a fenti példában az x és y helye a memóriában), a printf()-fel a %p konverzióval lehet kiírni. Ez akkor lehet jó, ha a programunkban hibát keresünk. A pointer értéke egyébként általában nem más, mint egy szám, a hivatkozott memóriarekesz sorszáma.

Ugyan a scanf %p képes beolvasni egy pointert, de azzal sokra nem megyünk. Nincs értelme pl. kiírni egy fájlba egy pointert és újra használni a program későbbi újrafuttatásánál, hiszen minden egyes futtatáskor máshova kerülhetnek a memóriában a változók. (Próbáld ki, futtasd le többször a programot!)

6. NULL: a sehova nem mutató pointer

NULL pointer: ami nem mutat sehova

int *ptr;
int i;

ptr = &i;    // most az i-re mutat
*ptr = 5;

ptr = NULL;  // most nem mutat sehova

A NULL pointer egy olyan mutatót jelent, amely nem mutat semmilyen változóra. Bármilyen típusú pointer (int*, double*, struct Pont* stb.) lehet NULL értékű.


NULL-e? Nem NULL-e?

if (ptr != NULL)
  printf("Mutat valahova.\n");
if (ptr == NULL)
  printf("Sehova sem.\n");
if (ptr)
  printf("Mutat valahova.\n");
if (!ptr)
  printf("Sehova sem.\n");

A pointerek a logikai kifejezésekhez hasonlóan használhatók. Igazra értékelődnek ki, ha mutatnak valahova, és hamisra, ha nem. Így aztán a ! tagadó operátor is működik: !ptr igazra értékelődik ki, ha ptr nem mutat sehova, vagyis NULL pointer. „Ha nincs ptr, akkor” – így meg lehet jegyezni. Emiatt if (ptr!=NULL) és if (ptr!=0) és if (ptr) mind ugyanazt jelentik. Ahogyan az if (ptr==NULL), if (ptr==0) és if (!ptr) is tökéletesen egyenértékűek.

Gyakran szokott vita lenni abból még gyakorlott programozók között is, hogy ugyanaz-e a 0 és a NULL. A C szabvány megengedi azt, hogy a NULL, vagyis a sehova nem mutató pointert 0-val jelöljük, ha a programkód szövegéből kiderül, hogy azt pointerként kell értelmezni (lásd: ISO/IEC 9899:1999, § 6.3.2.3 (3)). Vagyis ez a kódsor tökéletesen helyes:

int *p = 0;

Mindez kifejezetten a C nyelv sajátja, más nyelvekben nem feltétlenül van így! Gyakran emiatt a C-ben a „0”-t polimorf (többalakú) literálisnak is nevezik (polymorphic literal), hiszen jelenthet számot és pointert is. Semelyik másik literális nem képes ilyenre: se egy másik egész (pl. 1), se egy valós szám, se egy sztring.

Példa: két visszatérési értékű függvény

Egy lehetséges használatra a lenti kód mutat példát: a függvény kiszámolja a két paraméter összegét és szorzatát, amelyeket az ossz és szorz változókba tesz. A számolás mindkét esetben csak akkor történik meg, ha nem NULL pointert kapott az adott változóhoz. Vagyis megtehetjük azt, hogy csak az összeg vagy csak a szorzat kiszámolására kérjük meg a függvényt.

void szamol(int a, int b, int *possz, int *pszorz) {
    if (possz != NULL)
        *possz = a+b;
    if (pszorz != NULL)
        *pszorz = a*b;
}

Használata:

int ossz, szorz;

szamol(5, 7, &ossz, &szorz);    // kiszámolja mindkettőt

szamol(9, 3, NULL, &szorz);     // csak a szorzatot

7. Cím aritmetika (pointer arithmetic)

C-ben egy tömbbel egy valamit lehet csinálni: a nevét írva megkapjuk a tömb kezdőcímét, vagyis az első elemének helyét a memóriában.

Mivel a tömbelemek egymás után helyezkednek el, a címeik kiszámíthatóak!


int tomb[5], *p1, *p2, tav;

p1 = &tomb[0];   // kezdőelem címe

p1 = tomb;       // (így rövidíthető)


p2 = &tomb[4]-1; // tomb[4-1] címe


tav = p2-p1;     // távolság: 3

A pointer aritmetika azt jelenti, hogy memóriacímekkel végzünk számításokat. Ennek tömböknél van értelme, hiszen ezáltal a tömb kezdőcímének és a típusának ismeretében meghatározható az egyes elemek címe. (Ahogyan azt is meg tudjuk mondani, mi a szomszédos ház címe.) Ezt a kódban úgy jelöljük, hogy a pointerhez hozzáadunk egy egész számot – azt a számot, hogy a tömbben a címtől számítva hányadik elem címére vagyunk kíváncsiak. A hozzáadás a tömb vége felé, a kivonás a tömb eleje felé való mozgást jelent. Két pointert akár ki is vonhatunk egymásból, hogy megkapjuk a közöttük lévő távolságot (ugrásszámot). Sőt még a <=, > stb. operátorokat is használhatjuk. Természetesen ezeknek csak akkor van értelme, ha a két pointer ugyanazon tömb belsejére mutat.

Emiatt is fontos a pointerek típusa. A típusból tudja a fordító, hogy az adott változó, amire a pointer mutat, hány bájtból áll. Ha például a pointer egy 8 bájtos double típusra mutat, a p+1 azt jelenti, hogy 8 bájtot ad hozzá a p pointer értékéhez. Ezzel azonban nekünk nem kell foglalkozni, a fordító ezt automatikusan megoldja a háttérben! Nekünk csak arra kell gondolni, hogy p+1 a következő double címe, p+2 az azt követő címe stb. A bájtok számolgatását végző kódért a fordító felel.

Tudni kell azt, hogy egy önálló int változóra mutató int *p pointer esetén is helyes szintaktikailag a p+1 és a *(p+1) kifejezés. Vagyis a program lefordítható, csak szemantikailag helytelen. Ugyanis nem tudhatjuk, hogy milyen változót helyezett el a fordító az adott egész után, vagy van-e ott egyáltalán változó. Ilyen hibákat elkövetve ahhoz hasonló misztikus hibákat és programlefagyásokat kelthetünk, mint amilyeneket például tömb túlindexeléssel is.

Néhány szó a fenti változódefiníciókról. Az

int tomb[5], *p1, *p2, tav;

definíciók azt jelentik, hogy négy változót hozunk létre egyszerre, amelyek mind int-ekkel, egész számokkal kapcsolatosak. Nevezetesen: megfelelő módon használva a változókat, mindegyik által egész számokhoz juthatunk. Hogy hogyan jutunk el az int-ekhez, azt pedig a változók neve mellett használt operátorok adják meg. A négy változó definícióját megadó sor magyar nyelvű olvasata tehát az alábbi:

  • int – változókat fogunk létrehozni, amelyek int-ekkel kapcsolatosak.
  • tomb[5] – az első a tomb nevű változó, amelyen az indexelő [] operátor használható (tehát ez egy tömb), és azt használva kapunk int-eket. Tehát tomb típusa int[5], azaz int-ek tömbje.
  • , – ezen kívül...
  • *p1 – szeretnénk létrehozni egy p1 nevű változót, amelyen a dereferálás * operátorát használva (tehát ez egy pointer) jutunk egy int-hez. Így p1 típusa int *, azaz int-re mutató pointer.
  • , – ezen kívül...
  • *p2 ugyanígy.
  • , – ezen kívül...
  • tav – kérünk még egy tav nevű változót is, amin semmiféle operátort nem kell használni, hogy int legyen, tehát önmaga egy int.
  • ; – és most ennyi.
int* pi, i;

Az elv lényege az, hogy C-ben a változók típusát a használat módja alapján kell megnevezni. Ezért szokás a *-ot a változók neve mellé tenni, nem pedig a típus neve, itt az int mellé – bár úgy is lehetne, és úgy is ugyanazt jelentené. Ha több pointert hozunk létre egy sorban, akkor mindegyik neve mellé oda kell tennünk a csillagot, mint fent p1 és p2 esetében. Hiába tesszük a csillagot az int mellé, a jobb oldali példában akkor is egy pointert és egy egész számot hozunk létre. Ezért inkább ne használjuk így, hanem a változó neve mellett, különben csak félrevezetjük vele a kódot olvasó embert! (A gépnek persze mindegy.)

A fentiek alapján a * karakter a programkódban kétféle dolgot jelenthet, attól függően, hogy deklaratív helyen van (vagyis egy változó típusának megadásakor), vagy kifejezésben. Deklaratív helyen: int *pi azt jelenti, hogy a nevezett pi változó egy pointer legyen. Kifejezésben: *pi = 5 jelentése az, hogy a pi pointert dereferáljuk, azaz az általa mutatott változóról beszélünk. A két jelentés persze nincs távol egymástól: az előbbinél megmondjuk, hogy hogyan fogjuk használni, az utóbbi pedig a konkrét használat.

8. Cím aritmetika – az indexelés működése

Tömbök és pointerek

int tomb[10];

*(tomb+2) = 3;
tomb[2] = 3;   // ugyanaz
int *p = tomb;

*(p+2) = 3;
p[2] = 3;      // ugyanaz

A tömbök egy elemének elérésekor mindig két művelet történik. Az egyik a kérdéses elem címének kiszámítása (ez egy pointer aritmetikai művelet), a másik pedig az elem elérése (az indirekció a kiszámított pointerrel.) A tömb nevének használatával ezekben a kifejezésekben mintha azt kérnénk a fordítótól, hogy adja meg, hol található a tömb a memóriában. Ilyenkor automatikusan képzi a címet, és egy pointert kapunk, amelyen a [] indexelő operátort használva címszámítást és dereferálást is végzünk.

A C nyelv az összes tömbi műveletet így értelmezi. Ez történt a 3. előadás óta az összes tömbös programban, csak mindezidáig hallgattunk róla. Ezt mutatja a bal oldalon látható kódrészlet is: *(tomb+2) ugyanazt jelenti, mint t[2], egy címszámítást és egy dereferálást. A szögletes zárójel [] operátor a C szemléletében nem egyéb, mint egy rövidítés: az emberi gondolkodás számára nehezebben követhető kerek zárójeles kifejezést tudjuk vele egyszerűen megfogalmazni.

A jobb oldalon a p pointert ugyanazon tömb elejére állítjuk (a 0. indexű elemre). Így a p+2 kifejezés értéke egy pointer, amely a tömb 2. elemére mutat, *(p+2) pedig ez a pointer dereferálva, vagyis a 2. indexű elem maga. A p pointeren keresztül is a tomb nevű tömb elemeit érjük el, mivel ez a pointer a tömb elejére mutat.

A kétféle módon leírt indexelés egyébként tökéletesen egyenértékű. Mivel a csillagos-pluszos (pointeres) forma nehezebben olvasható, mint a szögletes zárójeles (tömbös), a szögletes zárójelest szoktuk használni; legyen az indexelt változó akár tömb, akár pointer.


Tömbös ciklusok

double t[100];
int i;

/* i = 0→99, 100 már nem */
for (i = 0; i != 100; ++i)
    t[i] = 0.0;
double t[100];
double *p;

/* p=t+0→t+99, t+100 nem */
for (p = t; p != t+100; ++p)
    *p = 0.0;

A tömbökön végigmenő ciklusokat nem csak indexeléssel, hanem pointerek használatával is megírhatjuk. (Némelyik tömbös algoritmusnál egyszerűbb így gondolkodni.) A bal oldalon látható a szokásos, indexelő operátort használó forma. A jobb oldalon a pointeres. A ciklus kezdetén a pointert beállítjuk a tömb elejére, és egészen addig fut a ciklus, amíg el nem éri a pointer a tömb 100. indexű elemét. Mivel a tömb csak 0…99-ig indexelődik, a t+100 cím használata már túlindexelés lenne, ezért a ciklus itt megáll.

A két forma egymással teljesen egyenértékű. Mindkét esetben egyébként balról zárt, jobbról nyílt intervallummal dolgoznak a ciklusok: az i=0 indexű, azaz a t+0 című elemet feldolgozzák, az i=100 indexű, azaz t+100 címűt pedig nem.

Nagyon fontos megjegyezni: a tömbökön és a pointereken is használható az indexelő operátor. Mindkét esetben ugyanazt jelenti a használata, egy címszámítást és egy dereferálást.

9. Tömböt átvevő függvények

Fejléc és törzs

Az alábbi függvények tömböt vesznek át paraméterként. A kiir() kiírja az elemeiket, a beolvas() pedig feltölti a tömböt a billentyűzetről beolvasott értékekkel.

void kiir(double *tomb, int meret) {    // kezdőcím és méret
    int i;
    for (i = 0; i != meret; ++i)
        printf("%g ", tomb[i]);         // indexelő operátor
    printf("\n");
}

void beolvas(double tomb[], int meret); // ugyanazt jelenti (!)

A függvénynek átadjuk a tömb elejére mutató pointert. Az még csak egy pointer, abból nem fogja tudni az elemszámát – ezért átadjuk neki a tömb méretét is. Nagy előny, hogy így a függvény bármekkora tömbön használható.

Ha tömböt adunk át függvénynek, át kell adni a tömb méretét is – hacsak máshonnan nem tudja kitalálni a függvény, hol van a vége. (Például ha végjeles, akkor nem kell.)

Ha tömböt adunk át egy függvénynek, akkor a függvény formális paramétereinek listájában használható a tomb[] jelölés is. Ez azonban ne tévesszen meg senkit: ilyenkor is csak egy pointer adódik át. Tökéletesen ugyanazt jelenti, mint a *tomb forma! Nem a teljes tömb lesz az átadott adat, hanem csak a kezdőcím.

Bár a függvény pointert kap, azon belül tömbként használhatjuk, mivel a C nyelv megengedi azt, hogy pointeren használjuk az indexelő operátort. Fel sem tűnik a különbség, mivel ez az operátor ugyanúgy működik a pointeren, mintha „igazi” tömb lenne! (Ne felejtsük: ha tömbön használjuk az indexelő operátort, akkor is ugyanez történik!) Mivel a függvény a tömböt a címével veszi át, bele is tud írni. Ez természetesen független attól, hogy a fejlécében *tomb vagy tomb[] formában hivatkozunk rá, mert a kettő egy és ugyanaz.


Hívás (használat)

double szamok[10];
beolvas(szamok, 10);                       // neve → kezdőcím
kiir(szamok, 10);

A híváskor a tömb nevét adjuk első paraméternek, ilyenkor a függvény a tömb kezdőcímét kapja meg. Természetesen a tömb méretét is meg kell adni. Fontos viszont, hogy mivel a függvény cím szerint veszi át a tömböt, meg is tudja változtatni az elemeit! Ebből a szempontból nagy a különbség a beépített típusok és a tömbök függvény paraméterként történő átadása között. De, mint azt eddig láttuk, igazából semmi különbség nincsen – ilyenkor is érték adódik át, csak az érték a tömb kezdőcíme (ami pointerként egy beépített típus), nem pedig a teljes tartalma. A beolvas() függvény egyébként így képes ellátni a feladatát, hogy a billentyűzetről számokkal töltse fel a tömböt.

10. Többdimenziós tömbök – röviden

int matrix[3][4]; // 3 sor, 4 oszlop

A kétdimenziós tömbök sorfolytonosan helyezkednek el a memóriában:

  • Első sor vége után a második sor eleje
  • Indexelés: tömb[sor][oszlop]
  • A fordító az tömb + y*szélesség+x képletet használja.

Emiatt kétdimenziós tömböt úgy kell átadni függvénynek, hogy a szélességét is meg kell adnunk, már a típusban:

void fuggveny(int tomb[][4], int magassag); // int (*tomb)[4]

A void fuggveny(int tomb[][]) szabálytalan!

11. Pointerek: így már minden érthető

Első előadás: scanf()

Ismerős?!
int a;
scanf("%d", &a);  // cím szerinti átadás

Így már érthető! Különben nem tudná beleírni a beolvasott számot.


A többi furcsaság

  • Az elmondottak miatt nincs t1=t2 tömb értékadás
  • Ezért nincs sztring értékadás (azok is tömbök)
  • Ezért nem lehet sztringeket == operátorral összehasonlítani. A címüket hasonlítja össze, nem a tartalmukat!

"Sztringek"

13. A sztringek létrehozása

Sztringek

A sztringek a C-ben nullával ('\0' vagy 0) lezárt karaktertömbök.

hello\0 ±¤%X§»"$»

A NUL nevű ASCII vezérlőkódot a sztringek végének jelölésére tartjuk fenn. A karakter kódja 0. Ezt a forráskódban '\0' és 0 formában írhatjuk. Ez nem keverendő a '0'-val, ami a nullás számjegyet jelöli, és a kódja 48!


Sztring létrehozása és inicializálása

char szoveg1[50] = { 'h', 'e', 'l', 'l', 'o', '\0' };
char szoveg2[50] = "hello";
char szoveg3[50];

A fenti utasítások nem értékadások, hanem inicializálások. Az = jel itt azt jelenti, hogy létrehozunk egy tömböt, amelyet kezdeti értékekkel töltünk fel. Nem értékadást – hiszen tömbök közötti értékadás nincs.

A karaktertömb tartalma: a karakterek és a lezáró nulla. Ha a "Hello" formát írjuk, akkor is hozzáteszi a fordító a lezáró nullát. Ezért a fenti két inicializálás tökéletesen ugyanazt jelenti, de természetesen az alsót használjuk inkább. Az ilyesmit szintaktikai édesítőszernek (syntactic sugar) szokás nevezni – szebb, olvashatóbb kódot kapunk. (Figyeljünk a szintaktikára: ha a karaktereket egyesével adjuk meg, akkor szükség van a vesszővel elválasztott sorozat köré a {} kapcsos zárójelekre. Ha idézőjelben adjuk meg a karaktersorozatot, akkor már nincs arra szükség: C-ben "egy ilyen" eleve karaktertömböt jelent.)

Egy adott méretű tömbbe méret−1 hosszú, azaz egy karakterrel rövidebb szöveg fér csak! A lezáró 0-nak is kell hely!

Erre a szabályra nagyon fontos emlékezni! Az „alma” szó eltárolásához például egy 5 (öt!) elemű karaktertömbre van szükség: char szoveg[5]="alma". Négy nem elég, mert a lezáró nulla akkor már nem férne bele. Fent mindkét tömb 50 karakterből áll. Abból a szöveg 5 bájtos, de végülis legalább 6 bájt kell neki, mert 1 bájtot a lezáró nulla is igényel.

Példa: sztring, mint karaktertömb

[0] h
[1] e
[2] l
[3] l
[4] o
[5]\0
[6]
[7]
...
#include <stdio.h>

int main(void) {
   char str[50] = "hello";

   printf("1. %s\n", str);

   str[0] = 'H';
   printf("2. %s\n", str);

   str[5] = '!';
   str[6] = '\0';
   printf("3. %s\n", str);

   return 0;
}

A 2. lépésnél a sztring legelső karakterét, a 'h'-t felülírjuk egy nagybetűs 'H'-val. Mivel tömbről van szó, az első karaktere a 0. indexű.

A 3. lépésben egy új karaktert fűzünk a sztringhez. Az eredeti sztringben a Hello szöveg betűi a tömb 0–4. indexű elemeit foglalták el; az 5. indexen volt a lezáró nulla. Azt a lezáró nullát felülírjuk egy felkiáltójellel, és a következő üres helyre elhelyezünk egy új lezáró nullát, hiszen annak mindig lennie kell. Így lesz az új tartalom "Hello!". A printf %s pedig a lezáró nulla alapján tudja, hol van vége a sztringnek, azért nem írja ki mind a húsz karaktert.

14. A sztringek átadása függvénynek

Miért nem adjuk most át a függvénynek a tömb méretét?

A tömb méretét
nem veszi át!
int sztring_hossza(char *sztring) {
   int i;
   for (i = 0; sztring[i] != '\0'; ++i)
      ;  /* üres */
   return i;
}

Az üres ciklus igazából nem üres: ++i.

Ezt a ciklust írhattuk volna i = 0; while (sztring[i] != '\0') ++i; formában is, ami teljesen ugyanezt jelentené. Azért írtuk így, mert a C szemlélete szerint „ez még belefér” számlálásos ciklusnak; honnan? – a nulladiktól, meddig? – a sztring végéig, hányasával? – egyesével. A ciklus törzse így látszólag üresnek tűnik. Ilyen esetben illik külön sorban írni az üres utasítást, esetleg kommenttel is megjelölni, hogy látszódjon, szándékosan írtuk így.


char str[20] = "Hello";

printf("%d", sztring_hossza(str)); // 5

De tényleg, miért, mikor az előbb a tömbök/függvények témakörben azt mondtuk, hogy mindig át kell adni?! Hát azért, mert a tömb méretének nincs köze a sztring hosszához! Azért, mert a lezáró 0-ból tudni fogjuk, hol van a sztringnek vége. És persze azért, mert épp azt várjuk a függvénytől, hogy számolja meg. :)

012345
Hello\0

A ciklus i=5-nél fog megállni, mivel a sztringben az 5. indexű elem a lezáró nulla. Ez egyben pont a sztring hossza is, vagyis a benne lévő hasznos karakterek száma (a lezáró nullán kívül). Ez azért jön ki pont így, mivel az értékes karakterek a 0. indextől kezdődően találhatóak a tömbben.

Konklúzió: a lezáró nulla pont annyiadik indexű elem a tömbben, mint amilyen hosszú benne a szöveg.

15. A sztringek változtatása függvényben

toupper()
a→A
#include <ctype.h>

void sztringet_nagybetusit(char *sztring) {
   int i;
   for (i = 0; sztring[i] != '\0'; ++i)
      sztring[i] = toupper(sztring[i]);
}

A sztringet címével látja, ezért meg is változtathatja azt!


char str[] = "Hello";
sztringet_nagybetusit(str); // Hello → HELLO

Figyelem: nem kell & operátor a sztring átadásánál! A sztring egy tömb, amelynek a nevét írva már eleve pointert kapunk a tömb elejére a paraméterátadás helyén!

16. Sztring másolása

void sztringet_masol(char *ide, char *innen) {
   int i;

   for (i = 0; innen[i] != '\0'; ++i) // ugyanolyan indexűeket
      ide[i] = innen[i];
   ide[i] = '\0';                     // és még a lezáró nulla!
}

A ciklus átmásolja az „értékes” karaktereket.

  • Utána pedig még a lezáró nullát kell, pont az ide[i] helyre

Túlindexelés veszélye: a függvény nem tudja, mekkora a cél tömb!

  • Emiatt nem tud felelősséget vállalni ezért! Ha túl kicsi, túlírja!
  • A függvény hívója felel érte, hogy elég nagy legyen!

A klasszikus megoldás

A K&R könyv

Alapmű: Brian Kernighan and Dennis Ritchie: The C Programming Language.

void masol(char *ide, char *innen) {
    while (*ide++ = *innen++)
        ;
}

A ciklus feltételében szándékosan értékadás van! Ennek a kifejezésnek az értéke a másolt karakter kódja. Ha a lezáró nulla, az logikai hamisként értékelődik ki. Ettől megáll a ciklus, de azt még átmásolta, mivel annak a kifejezésnek a mellékhatása a karakter másolása. A másik mellékhatás az, hogy mindkét pointer a következő karakterre mutat (postincrement, utólagos).

17. Beépített sztringkezelő függvények

Sztringeket kezelő függvények

#include <string.h>

char str[50], *hol;

strcpy(str, "alma");
strcat(str, "fa");
printf("%d", strlen(str));
hol = strstr(szenakazal, tu);

Sztringek bevitele/kiírása

#include <stdio.h>

gets(str); // problémás
puts(str);
scanf("%s", str); // szó!
printf("str: %s\n", str);
sprintf(str, "x=%d", 19);
sscanf(str, "%d", &i);

A fontosabb függvények:

  • char* strcpy(char *ide, char *ezt) – sztringet másol.
  • char* strcat(char *ehhez, char *ezt) – „ehhez” sztringhez hozzáfűzi „ezt”.
  • size_t strlen(char *str) – visszatér a sztring hosszával (size_t egy egész szám).
  • gets(char *str) – beolvas egy egész sort a billentyűzetről.
  • puts(char *str) – kiírja a sztringet és új sort kezd.
  • printf("%s", str) – kiírja a sztringet.
  • scanf("%s", str) – beolvas egy szót (szóköz, enter, tabulátor karakterig).
  • sprintf(str, formátum, ...) – ugyanaz, mint a printf(), de a sztringbe ír, nem a szabványos kimenetre.
  • sscanf(str, formátum, ...) – ugyanaz, mint a scanf(), de a sztringből olvas, nem a szabványos bemenetről.
  • int strcmp(char *a, char *b) – összehasonlít két sztringet. A visszatérési értéket lásd lentebb.
  • char* strchr(char *str, int c); – c karakter első előfordulásának címe. NULL, ha nincs.
  • char* strrchr(char *str, int c); – az utolsó előfordulás címe vagy NULL.
  • char* strstr(char *szenakazal, char *tu) – megkeresi a tűt (needle) a szénakazalban (haystack). Ha megtalálta, pointert ad rá, ha nem, NULL pointer.

Fontos, hogy a sztring paramétereknél sehol nem kell & címképző operátor, még a scanf()-nél sem! Emlékezzünk arra, hogy a sztringek C-ben tömbök, amelyeknek a neve önmagában pointert jelent.

Azoknál a függvényeknél, amelyek egy sztringet írnak, a hívó felelőssége megfelelő méretű tömböt biztosítani! Pl. az strcpy() esetén az ide[] tömb legalább akkora kell legyen, mint strlen(ezt)+1. Sok függvénynek van n betűs párja: strncpy(), strncat() stb., amelyek figyelembe tudják venni a cél tömb méretét is. Azonban ezek nem pontosan úgy működnek, ahogy várnánk. Pl. az strncpy() nem biztos, hogy lezárja nullával a cél tömböt!

Ez a probléma különösen a gets()-nél jelentkezik, mivel ott a sor hossza a felhasználótól függ. Emiatt azt veszélyes függvénynek szokták tartani, hiszen sokszor használták már ki ezt a dolgot számítógépek feltöréséhez (crack), jogosulatlan hozzáférés megszerzéséhez. Ajánlott az fgets() függvényt használni helyette.


Összehasonlítás (compare)

strcmp(a,b) egész szám, értéke:

  • 0, ha a==b
  • negatív, ha a<b
  • pozitív, ha a>b
int strcmp(char *a, char *b);

if (strcmp(s1, s2) == 0)
    printf("s1 == s2\n");
if (strcmp(s1, s2) < 0)
    printf("s1 < s2\n");

Vigyázat: a strcmp() értéke egyezés esetén nulla, ami a C szabályai szerint hamis értéket jelent, ha logikai értékként tekintünk rá! Emiatt az alábbi kódrészlet hibás:

if (strcmp(s1, s2))         // HIBÁS!
    printf("Egyformák.\n");

Itt pont akkor megy be a végrehajtás a feltétel igaz ágába, ha nem egyformák a sztringek. Ha egyezést vizsgálunk, írjuk ki az ==0-t!

Legegyszerűbb ezt úgy megjegyezni, hogy az strcmp() hívás értéke és a 0 egész szám közé lehet tenni azt az operátort, amelyet a két sztring közé tennénk. Pl. a!=bstrcmp(a,b)!=0; a>=bstrcmp(a,b)>=0.

18. Nagy példa: Gipsz Jakab

A feladat: megcserélni egy névben a keresztnevet és a vezetéknevet, és az eredményt egy másik tömbbe írni.

char eredeti[] = "Gipsz Jakab";
char forditott[20];

/*
 * Itt bármit csinálhatunk...
 */

printf("%s\n", forditott);  // Jakab Gipsz



Első megoldás: karakterenként

#include <stdio.h>

int main(void) {
    char eredeti[] = "Gipsz Jakab", forditott[20];
    int i;

    for (i = 0; eredeti[i] != ' '; ++i) // szóköz helye
        ;
    int szokoz = i;

    int cel = 0;
    for (i = szokoz + 1; eredeti[i] != '\0'; ++i) // keresztnév
        forditott[cel++] = eredeti[i];
    forditott[cel++] = ' ';
    for (i = 0; i != szokoz; ++i)                 // vezetéknév
        forditott[cel++] = eredeti[i];
    forditott[cel++] = '\0';

    printf("%s\n", forditott);
}

Az első lehetőség: „kézzel” megoldjuk a problémát, egyszerű tömbkezelési feladatnak tekintve a problémát. Először is, megkeressük a szóközt a név közepén, mert tudjuk, hogy ami előtte van, az a vezetéknév, ami utána, az pedig a keresztnév. Aztán pedig ezeket fordított sorrendben a forditott[] tömbbe másoljuk: az elejére a keresztnevet (az eredeti tömbben a szóköz utántól a végéig), középre teszünk egy szóközt, a végére pedig a vezetéknevet (az eredeti tömbben az elejétől a szóköz előttig).

Az eredményt tároló forditott[] tömbbe a karaktereket folyamatosan írjuk: hogy mindig tudjuk, hányadik indexű helyre kerül a következő karakter, egyszerűbb bevezetni egy új változót (cel), minthogy bonyolult képletekkel számolnánk ki, az eredeti tömb hányadik karaktere az új tömb hányadik karaktere lesz. Az írást a C-ben megszokott forditott[cel++] = következő_karakter; fordulattal végezzük, kihasználva, hogy a posztinkremens operátornál a kifejezés értéke még a növelés előtti érték. A tömb végére a lezáró nulla ugyanígy kerül be.

Második megoldás: beépített függvényekkel

#include <stdio.h>
#include <string.h>

int main(void) {
    char eredeti[] = "Gipsz Jakab", forditott[20];

    char* szokoz_helye = strchr(eredeti, ' ');
    strcpy(forditott, szokoz_helye+1);
    strcat(forditott, " ");
    strncat(forditott, eredeti, szokoz_helye-eredeti);

    printf("%s\n", forditott);
}

Ez a megoldás ugyanúgy működik, mint az előző, csak az összes részfeladat egy beépített függvény segítségével van megoldva. A szóköz helyének megkeresését az strchr() függvény végzi; ez visszaad egy pointert az eredeti tömbben a szóközre.

A strcpy() hívás a keresztnevet másolja. Ez kerül be a forditott[] tömb elejére. Vegyük észre, hogy ez másolandó sztringként nem az eredeti sztringet kapja, hanem „behazudjuk” neki kezdőcímként a szóköz utáni első karaktert, azaz a vezetéknév első betűjét. A strcpy()-nak ez mindegy, az úgyis csak előrefelé halad a sztringben, és megáll az eredeti keresztnév utáni lezáró nullánál – közben a cél tömbbe is tesz egy lezáró nullát. Ebben a pillanatban a cél tömb tartalma "Jakab".

A strcat() hívás ezt a lezáró nullát megkeresi, és felülírja; a cél tömbhöz hozzáfűz egy szóközt. Persze utána tesz egy új lezáró nullát, tehát a tömb tartalma "Jakab " lesz.

Végül pedig, ehhez fűzzük hozzá a vezetéknevet, a strncat() függvény használatával. Ez ugyanúgy összefűzésre használható, de a sima strcat()-hoz képest egy további paramétere is van, amelyikkel a másolt karakterek száma korlátozható. Mivel az eredeti tömbben a vezetéknév után nincs vége a sztringnek (hanem egy szóköz és a keresztnév követi), erre a korlátra szükségünk is van, ha csak a vezetéknevet szeretnénk másolni. Tehát korlátként megadjuk a vezetéknév hosszát, amelyet címaritmetikával számolunk ki: szokoz_helye-eredeti. A strncat() is tesz lezáró nullát a cél tömbbe, tehát készen vagyunk: a tartalom eddigra "Jakab Gipsz" lett.

Harmadik megoldás: a scanf()printf() trükk

#include <stdio.h>

int main(void) {
    char eredeti[] = "Gipsz Jakab", forditott[20];

    char vezeteknev[10], keresztnev[10];
    sscanf(eredeti, "%s %s", vezeteknev, keresztnev);
    sprintf(forditott, "%s %s", keresztnev, vezeteknev);

    printf("%s", forditott);
}

Láttuk, hogy a scanf "%s" nem sort, hanem szót olvas be, azaz beolvasásnál nem csak a sortörés, hanem a szóköz karakternél is megáll. Ezen kívül, láttuk azt is, hogy a scanf()-printf()-nek vannak sztringes párjaik: az sscanf()-sprintf() sztringből olvasnak, sztringbe írnak.

Innentől a feladat nagyon egyszerű: olvassuk be a két szót, mintha a billentyűzetről jönnének, az eredeti tömbből... Aztán írjuk bele, mintha a képernyőre írnánk, a cél tömbbe a két szót. Persze fordítva.

Mindehhez szükségünk van két segédtömbre is, amelyekbe a szétválasztáskor a vezetéknév és a keresztnév kerül. Sebaj, még jó is, mert érthetőbbé is válik tőlük a kódrészlet.

Negyedik megoldás: fordít, fordít és megint fordít

#include <stdio.h>
#include <string.h>

void fordit(char *tomb, int n) {
    int i;
    for (i = 0; i < n/2; ++i) {
        char temp = tomb[i];
        tomb[i] = tomb[n-1-i];
        tomb[n-1-i] = temp;
    }
}

int main(void) {
    char eredeti[] = "Gipsz Jakab", forditott[20];

    strcpy(forditott, eredeti);
    fordit(forditott, strlen(forditott));
    int szokoz = strchr(forditott, ' ') - forditott;
    fordit(forditott, szokoz);
    fordit(forditott + szokoz+1, strlen(eredeti)-szokoz-1);

    printf("%s", forditott);
}

Ha megfordítjuk a sztringet, "bakaj zspiG" lesz a tömbben. Ezzel helyet cserélt a keresztnév és a vezetéknév, csak az a baj, hogy a szavak is megfordultak. Sebaj, azokat külön vissza kell fordítani. Előbb a keresztnevet: "Jakab zspiG", aztán a vezetéknevet: "Jakab Gipsz", és kész. Mivel a feladatkiírás úgy szólt, hogy az eredménynek egy másik tömbben kell lennie, ezért az eredeti sztringet átmásoljuk a cél tömbbe, és ott zsonglőrödünk vele.

A sztring megfordítása, mint részfeladat, ebben a megoldásban háromszor is szerepel. Ezért jobb egy függvényt írni erre, hogy aztán csak háromszor meg kelljen hívni azt, különféle paraméterezéssel.

Jobban belegondolva, nem egy sztring megfordítása ez a bizonyos részfeladat, hanem egy sztring egy részletének megfordítása. Amikor a keresztnevet fordítjuk (bakaJ↔Jakab), akkor nem a lezáró nulláig, hanem a szóközig terjedő részt kell megfordítani. Ezért a segédfüggvényünknek ez is paramétere kell legyen: vagy az, hogy mit tekintsen határoló karakternek (a lezáró nullát, illetve a szóközt), vagy egyszerűen a megfordítandó tömbrészlet hossza. A fenti megoldás az utóbbi utat követi.