Labor, 4. hét: pointerek, sztringek

Pohl László, Czirkos Zoltán · 2023.10.04.

Pointerek (mutatók), tömbök átadása függvényeknek. Sztringek alacsony szintű kezelése és a C sztringkezelő függvényei.

A mai labor feladatai pointerekkel dolgoznak; hol indirekt paraméterátadás, hol tömbök miatt.

Ha egy függvénynek átadunk egy tömböt, akkor csak a kezdőcíme adódik át, mintha egy &t[0] alakú kifejezéssel lekérdeznénk azt. Emiatt általában a tömb méretét is át kell adnunk a függvénynek. A sztringeknél ezzel szemben nincs így; mivel a sztringeknél a tömb tartalma alapján kiderül, hol van a vége (a lezáró nullánál), ezért a méretet legtöbbször felesleges átadni paraméterként. A tömb mérete és a benne lévő sztring hossza amúgy sem egyezik meg. Gondold át minden feladatnál, átadod-e a méretet!

Felkészülés a laborra:

1. Hova mutatnak a pointerek?

Alább egy kis programot látsz, benne néhány változóval – egy valóssal és pointerekkel. A rajz a program futásának 1-essel jelölt helyén mutatja, hogy milyen változók léteznek, és azon belül a pointerek hova mutatnak.

int main(void) {
    double d1 = 3.14;
    double *p1, *p2;
    p1 = &d1;
    p2 = NULL;          // 1
}

Rajzold le papíron az alábbi program futásának buborékkal jelölt helyein, hogy milyen változók léteznek, és hogy hova mutatnak a pointerek! A buborékok, ahogy látod, szándékosan időrendben vannak. Határozd meg, mit ír ki a program – annak futtatása nélkül! Aztán végül egy futtatással ellenőrizd az eredményt.

A c11 szabvány szerint a printf függvény void* típusú paramétert vár a %p-hez. A (void*) explicit típuskonverzió azért szükséges, mert az újabb fordítók figyelmeztető üzenetet adnak, ha automatikus konverzióra támaszkodik a kódunk.

#include <stdio.h>

void func1(int i2) {
    i2 *= 2;
    printf("func1()... i2 = %d\n", i2);     // 4
}

void func2(int *p3) {
    *p3 *= 2;
    printf("func2()... *p3 = %d\n", *p3);
    p3 = NULL;                              // 5, 6
    printf("func2()... p3 = %p\n", (void*)p3);
}

int main(void) {
    int i1 = 2;
    int *p1 = NULL;
    int *p2 = NULL;
    printf("Hova mutat p1 es p2?\n");       // 1

    p1 = &i1;
    printf("&i1 = %p, p1 = %p\n", (void*)&i1, (void*)p1);
    printf("i1 = %d, *p1 = %d\n", i1, *p1); // 2
    
    i1 = 3;  printf("*p1 = %d\n", *p1);
    *p1 = 4; printf("i1 = %d\n", i1);

    p2 = p1;
    *p2 = 5;
    printf("i1 = %d, p2 = %p\n", i1, (void*)p2);   // 3
    printf("-----\n");
    
    func1(i1);
    printf("main()... i1 = %d - de miert?\n", i1);
    printf("-----\n");
    
    func2(&i1);
    printf("main()... i1 = %d - miert?\n", i1);
    printf("-----\n");

    func2(p2);
    printf("main()... i1 = %d - miert?\n", i1);
    printf("main()... p2 = %p - miert?\n", (void*)p2);

    return 0;
}

2. Kocka – cím szerinti paraméterátadás

Írj egyetlen void visszatérési típusú függvényt, amely paraméterként kapja egy kocka oldalhosszúságát (valós szám), majd cím szerint átvett paraméterekben kiszámítja (visszaadja) a kocka felületét és térfogatát! Használd ezt a függvényt a főprogramban egy 2.7 oldalhosszúságú kocka felületének és térfogatának kiszámításához!

Emlékeztető

A cím szerint átvett paraméter esetén a függvény módosítani is tudja a neki átadott változó értékét. Gyakran ezt úgy hívjuk, hogy „paramétersoron adja vissza az eredményt.” Gondolj a printf()scanf() függvénypárra: a scanf() megváltoztatja a neki paraméterként átadott változót, a printf() nem. Ezért a scanf() cím szerint veszi át azt, a printf()-nek elég az értéke:

int x;
scanf("%d", &x);
printf("%d", x);
Megoldás
#include <stdio.h>
#include <math.h>

void kocka(double oldal, double *pfelszin, double *pterfogat) {
    *pfelszin = pow(oldal, 2) * 6;
    *pterfogat = pow(oldal, 3);
}

int main(void) {
    double a, v;
    kocka(2.7, &a, &v);
    printf("V=%f A=%f\n", v, a);

    return 0;
}

3. Tömb és függvény

Készíts függvényt, amely paraméterként vesz át egy egész számokból álló tömböt, továbbá egy számot, amit meg kell keresnie a tömbben! Térjen vissza a megtalált számnak az indexével, vagy −1-gyel, ha nem találta meg! (Ha több előfordulás is van, akkor mindegy, melyik sorszámával tér vissza.)

Írasd ki a tömböt sorszámozott elemekkel, és ellenőrizd a függvény működését! Írd ki az indexet, vagy a „nincs találat” szöveget!

Emlékeztető

Amikor függvénynek tömböt adunk át, a tömb nem másolódik le! A függvény mindig csak a tömb elejére mutató pointert kapja meg, akár int t[] formában, akár int *t formában definiáltuk a fejlécében a formális paramétert. Így tetszés és ízlés szerint mindkét forma használható. De sosem szabad elfelejteni, hogy a függvényparaméter kontextusban mindkettő pointert jelent! Ezért kell mindig átadni a tömb méretét is a függvénynek, ha máshonnan nem tudná.

Alakítsd át a programot úgy, hogy ne a találat indexét, hanem annak memóriacímét adja vissza! Mit tud visszaadni ilyenkor a függvény, ha nem talált semmit? Ha ezt a függvényt kell használnod, hogyan tudod segítségével az indexet meghatározni, egy újabb keresés nélkül?

Emlékeztető

p1 + integer = p2: mutató a tömb elejére + egész szám → mutató a tömb valahányadik elemére. Emiatt p2 - p1 = integer is igaz kell legyen – és tényleg, ki lehet számolni, hányat kell ugrani az egyik memóriacímtől a másikig.

Alakítsd át úgy is a programot, hogy a ciklusok tömbindexek helyett pointerekkel dolgozzanak, az előadáson bemutatott módon.

Emlékeztető

Ha p pointer típusú változó esetén p + 1 leírható, akkor p = p+1 is. Akkor pedig p += 1 is és ++p is. Így lehet lépkedni a tömbben.

Megoldás
#include <stdio.h>

int keres(int *tomb, int meret, int keresett) {
    for (int i = 0; i != meret; ++i)
        if (tomb[i] == keresett)
            return i;
    return -1;
}

int *keres_ptr(int *tomb, int meret, int keresett) {
    for (int i = 0; i != meret; ++i)
        if (tomb[i] == keresett)
            return tomb+i;
    return NULL;
}

int *keres_ptr_aritmetika(int *tomb, int meret, int keresett) {
    for (int *p = tomb; p != tomb+meret; ++p)
        if (*p == keresett)
            return p;
    return NULL;
}

int main(void) {
    int szamok[10] = { 5, 8, 3, 6, 9, -2, 4, 6, 7, 1 };

    int keresett = -2;
    
    int index = keres(szamok, 10, keresett);
    if (index != -1) {
        printf("A(z) %d tömbbeli indexe: %d\n", keresett, index);
    } else {
        printf("A(z) %d nincs a tömbben\n", keresett);
    }
    
    int *p1 = keres_ptr(szamok, 10, keresett);
    if (p1 != NULL) {
        printf("A(z) %d tömbbeli memóriacíme: %p, indexe: %ld\n", keresett, p1, p1-szamok);
    } else {
        printf("A(z) %d nincs a tömbben\n", keresett);
    }
    
    int *p2 = keres_ptr_aritmetika(szamok, 10, keresett);
    if (p2 != NULL) {
        printf("A(z) %d tömbbeli memóriacíme: %p, indexe: %ld\n", keresett, p2, p2-szamok);
    } else {
        printf("A(z) %d nincs a tömbben\n", keresett);
    }
}

Hasonló feladatok

Ha ez a feladat nehezen ment, példatárban itt találsz hasonlókat, amiken a labor után gyakorolhatsz otthon.

4. Függőlegesen

Készíts programot, mely bekér és eltárol egy keresztnevet, majd azt betűnként függőlegesen lefelé kiírja. Például ha a név „Imre”, akkor az eredmény:

I
m
r
e
Tipp

Hogy lehet beolvasni egy szót a billentyűzetről? Mekkora karaktertömbre van ehhez szükség? Mi történik akkor, ha túl kicsi a karaktertömb, ahhoz képest túl hosszú a felhasználó neve?

Megoldás
#include <stdio.h>

int main(void) {
    char nev[100];

    printf("Írd be a keresztneved!\n");
    scanf("%s", nev);

    for (int i = 0; nev[i] != '\0'; ++i)
        printf("%c\n", nev[i]);

    return 0;
}

Hasonló feladatok

Ha ez a feladat nehezen ment, megoldhatsz pár hasonló feladatot a példatárból, mielőtt a következő feladatra rátérsz.

Vigyázz, a laborfeladatokat erősen ajánlott az utolsó feladatig megoldani, hogy a jövő hétre felkészült legyél. Ha nem sikerül, fejezd be őket otthon!

5. Trimmer

Gyakori feladat, hogy egy sztring elejéről és végéről el kell távolítani a szóközöket. Ezt a függvényt gyakran trim()-nek szokták hívni.

Írj függvényt, amelyik egy paraméterként kapott sztring elejéről és végéről eltávolítja a szóköz karaktereket (a többi maradjon)! Pl. ha a bemenet "  helló, mizu?   ", akkor az új sztring "helló, mizu?" legyen. A függvény vegyen át két paramétert: egy forrás tömböt (ebben van a szóközös sztring) és egy cél tömböt (ebbe kerül a szóköz nélküli).

Meg kell ennek a függvénynek kapnia a tömb méretét? Miért?

Megoldás

Részfeladatok: 1) előbb megkeresi az első nem szóköz karaktert a sztring elején, 2) aztán megkeresi a sztring végét, 3) és onnan visszafelé indulva megkeresi az első nem szóköz karaktert. Így megkapja a kivágandó darabot. Ezt a darabot a cél tömbbe másolja, és lezárja nullával.

#include <stdio.h>

void trim(char *forras, char *cel) {
    int eleje = 0, vege = 0;

    /* elején a szóközök? */
    while (forras[eleje] != '\0' && forras[eleje] == ' ')
        eleje++;
    /* hol a vége? utána visszafelé a szóközökön */
    while (forras[vege] != '\0')
        vege++;
    vege--;
    while (vege >= 0 && forras[vege] == ' ')
        vege--;
    /* másolás az elejére, és lezárás */
    int i;
    for (i = 0; i <= vege-eleje; i++)
        cel[i] = forras[eleje+i];
    cel[i] = '\0';
}
int main(void) {
    char s1[100] = "      Hello mizu?   ";
    char s2[100];
    
    trim(s1, s2);
    printf("[%s] [%s]\n", s1, s2);

    return 0;
}

6. További feladatok

Ha elkészültél, gyakorlásnak folytathatod a példatár sztringes feladataival!