2. hét: C99 nyelvi elemek

Czirkos Zoltán · 2022.06.21.

main() és main(void)

Mi a különbség a main() és a main(void) között?

Megoldás

Attól függ, melyik nyelven. Az f() C-ben azt jelenti, hogy a függvény tetszőlegesen sok, tetszőleges típusú paramétert vehet át. Ez nem jó semmire, csak történelmi okokból megmaradt. Az f(void) pedig azt, hogy a függvény nem kaphat paramétert. Ezzel szemben az f() és az f(void) C++-ban ugyanazt jelenti: a függvény nem kaphat paramétert. C++-ban az f() formát szokás használni, de a C kompatibilitás miatt elfogadja az f(void)-ot is.

C99: tostring_ratio() I.

Az előadás C99 tört osztályához kapcsolódóan.

A printf() hátránya, hogy nem tudjuk neki megtanítani a saját típusainkat. Sztringet viszont tud kiírni, ezért az alábbi, nem túl jó ötletünk adódhat. Mi a baja a kódrészletnek? Mit írnak ki a printf()-ek?

char const *tostring_ratio(Ratio r) {
    static char buf[100];
    sprintf(buf, "%d/%d", r.num, r.den);
    return buf;
}

/* ... */
printf("r1 = %s\n", tostring_ratio(r1));
printf("%s + %s = %s\n", tostring_ratio(r1), tostring_ratio(r2), tostring_ratio(r3));
Megoldás

A baj, hogy csak egyetlen egy buf[] karaktertömb van. Ez az első printf()-nél még nem gond. De mire meghívódik a második printf(), addigra ugyanaz a tömb többször is felül lett írva, r1, r2 és r3 sztring reprezentációjával. Mivel mindhárom hívás ugyanannak a tömbnek a címével tér vissza, a printf() háromszor fogja ugyanazt a törtet kiírni. Hogy melyiket, azt nem lehet tudni, mivel a függvényparaméterek kiértékelési sorrendje kötetlen, és nem tudjuk, az r1, r2 vagy r3-hoz tartozó tostring_ratio() hívódik utoljára.

C99: tostring_ratio() II.

A fentieken felbuzdulva megpróbálhatjuk kijavítani a tostring_ratio() függvényt. Melyik alábbi próbálkozás, miért hibás?

char const *tostring_ratio_1(Ratio r) {
    char buf[100];
    sprintf(buf, "%d/%d", r.num, r.den);
    return buf;
}
char const *tostring_ratio_2(Ratio r) {
    char *buf = (char*) malloc(100 * sizeof(char));
    sprintf(buf, "%d/%d", r.num, r.den);
    return buf;
}
char const *tostring_ratio_3(Ratio r) {
    enum { BUFS = 10 };
    static char buf[BUFS][100];
    static int i = 0;
    i = (i+1) % BUFS;
    sprintf(buf[i], "%d/%d", r.num, r.den);
    return buf[i];
}
Megoldás

A tostring_ratio_1() azért, mert a buf[] lokális változó, ami a függvényből visszatéréskor megszűnik. A hívónak egy érvénytelen pointert (dangling pointer) adunk. A tostring_ratio_2() ugyan megoldja, hogy minden hívásnál külön tömb legyen, de azt a hívónak fel kell majd szabadítania, és így a függvény eddigi használati módja válik helytelenné, ti. nincs felszabadítva a memóriaterület. A tostring_ratio_3() pedig csak addig működik, amíg nem akarunk egy printf()-ből tíznél több törtet kiírni.

C99: tostring_ratio() III.

Észrevehetjük, hogy megoldást jelenthet, ha minden egyes tört objektumnak saját sztring puffere van. Ezt legegyszerűbben úgy érhetjük el, ha a puffert betesszük az objektumba. Eltekintve attól, hogy mekkora pazarlást művel az alábbi kód – helyes egyáltalán?

struct Ratio {
    int num;
    int den;
    char buf[100];
};

char const *tostring_ratio(Ratio r) {
    sprintf(r.buf, "%d/%d", r.num, r.den);
    return r.buf;
}
Megoldás

Helytelen. Az r paraméter a függvény lokális változója; a visszatéréskor az r.buf megszűnik.

C99: tostring_ratio() IV.

Megoldható akkor a probléma? Lehet olyan tostring_ratio() függvényt csinálni C-ben, ami mentes ezektől a memóriakezelési hibáktól?

Megoldás

Esetleg úgy, hogy a hívó biztosítja a sztring helyét. De ezzel sajnos nem kerülünk közelebb a kényelmes használathoz:

char const *tostring_ratio(Ratio r, char *buf) {
    sprintf(buf, "%d/%d", r.num, r.den);
    return buf;
}

/* ... */
char buf1[100], buf2[100], buf3[100];
printf("r1 = %s\n", tostring_ratio(r1, buf1));
printf("%s + %s = %s\n",
    tostring_ratio(r1, buf1), tostring_ratio(r2, buf2), tostring_ratio(r3, buf3));

C99: tostring_ratio() V.

A fent említett törtes programot az alábbi függvénnyel egészítjük ki. Ez egy törtet alakít sztringgé, és a sztringet a paraméterként kapott pufferbe teszi:

char const *tostring_ratio(Ratio r, char *buf) {
    sprintf(buf, "%d/%d", r.num, r.den);
    return buf;
}

Helyes-e az (1)-es hívás? Mi lenne, ha a sémát felismerve a (2)-vel jelölt makrót írnánk? Így helyes a program? Véleményezd is a kódot!

/* 1 */
printf("%s + %s = %s\n", tostring_ratio(r1, (char[24]){0}),
        tostring_ratio(r2, (char[24]){0}), tostring_ratio(r3, (char[24]){0}));

/* 2 */
#define TOSTRING_RATIO(R) (tostring_ratio((R), (char[24]){0}))
printf("%s + %s = %s\n", TOSTRING_RATIO(r1), TOSTRING_RATIO(r2), TOSTRING_RATIO(r3));
Megoldás

A program helyes, mert az összetett literálisként megadott tömb élettartama a blokk végéig tart. Így létezni fog akkor is, amikor a printf() megkapja a címet – ugyanazt a címet, amit a tostring_ratio() megkapott, és változatlanul visszaadott (még ha a mutatott memóriatartalom változott is). A makró is helyes, mert pont ugyanarra fejtődik ki, ami az eredeti kód is volt.

Az ötlet működőképes, de két hátulütője van. Az egyik, hogy a névtelen tömbök mindig inicializálásra kerülnek, és ez felesleges, mert a függvény úgyis felülírja a tartalmukat. A másik, hogy a makrót könnyű rosszul használni; egy óvatlan pillanatban leírt return TOSTRING_RATIO(r) már hibás.

C++: Nincs setter

Az előadás tört osztályához kapcsolódóan.

Teljesen igaz-e az a kijelentés a fenti szövegben, hogy egy Ratio objektum számlálóját és nevezőjét a) a C++ kód jelenlegi állapotában: nem tudjuk megváltoztatni, b) a C kódban: nem szabad megváltoztatnunk?

Megoldás

Nem igaz. Értékadással meg lehet/szabad változtatni. De ez nem gond, mert értéket adni csak egy másik Ratio objektumból lehet, és annak is a konstruktorral kellett létrejönnie.

C99 és C++: Adattagok sorrendje

Mi történik, ha megcseréljük a num és a den tagváltozók definícióját a) a C programban, b) a C++ programban?

struct Ratio {
    int den;
    int num;
};
Megoldás

A C++ programnak semmi baja nem lesz. A C program viszont elromlik, mivel az alábbi sor nem név szerint, hanem sorrendjük szerint hivatkozik a tagváltozókra:

Ratio new = { num/a, den/a };

C99 és C++: Törtek egyenlősége

Mikor egyenlő két tört? Írd meg a két törtet összehasonlító függvényt C-ben és C++-ban is!

Megoldás

Elég összehasonlítani a számlálókat és a nevezőket, mivel a törtek már egyszerűsítve vannak.

int equal_ratio(Ratio r1, Ratio r2) {
   return r1.num == r2.num && r2.den == r2.den;
}
bool operator==(Ratio r1, Ratio r2) {
    return r1.num() == r2.num() && r1.den() == r2.den();
}

C++: Setter függvények mindenhol

Az OOP-t gyakran kis, egyszerű osztályokon keresztül kezdik tanítani, mint ez a tört is: komplex szám, időpont, dátum. Ezeken keresztül jól meg lehet tanulni a nyelv szintaktikáját: privát és publikus tagok, setter és getter függvények és a többiek. Jó ötlet-e a C++ tört osztálynak .set_num() és .set_den(), számlálót és nevezőt beállító metódust írni?

Megoldás

Rossz ötlet. Törteket lehet összeadni, összeszorozni, de olyan műveletről nem hallott még senki, hogy „x-re cseréljük egy tört számlálóját”. Ha mégis ilyet szeretnénk, ahhoz nem kell új tagfüggvény, megoldható a mostani osztállyal is:

Ratio new_numerator(Ratio r, int num) {
    return Ratio(num, r.den());
}

C++: Teljes tört osztály

Az előadás tört osztályához kapcsolódóan.

Dolgozd ki a C++ tört osztályt! Valósítsd meg a + (egyoperandusú), +=, - (egy- és kétoperandusú), -=, *, *=, /, /=, >> (beolvasás), (double) cast operátorokat! Figyelj a referenciák helyes használatra! Mennyi a legkevesebb új tagfüggvény (szintaktikai értelemben), amennyivel ez megoldható? Oldd meg úgy a feladatot, hogy a lehető legkevesebb legyen!

Figyelj arra, mit jelentenek az „egyoperandusú” és „kétoperandusú” szavak! Ezek nem az operátorokat megvalósító függvények paraméterszámát adják meg. Például egyoperandusú mínusz az ellentett: -x, és kétoperandusú mínusz a kivonás: a-b.

Megoldás

Egyet, a cast-ot. A C++ szintaktikai szabályai szerint az kötelezően tagfüggvény. A többi lehet globális is, és jobb is úgy megírni őket. Például:

/* ez tag */
Ratio::operator double() const {
    return (double) num_ / (double) den_;
}

/* minden más visszavezethető a konstruktorra és a két getterre */
Ratio & operator+=(Ratio &r1, Ratio r2) {
    r1 = r1 + r2;
    return r1;
}

/* egyoperandusú - (negálás) */
Ratio operator-(Ratio r) {
    return Ratio(-r.num(), r.den());
}

/* a konstruktor egyszerűsíti is a beolvasott törtet! */
std::istream & operator>>(std::istream &is, Ratio &r) {
    int num, den;
    char c;
    is >> num >> c >> den;
    r = Ratio(num, den);
    return is;
}

A fenti kódrészletek a szintaktikát hivatottak bemutatni. Egy „komoly” változatnál figyelni kellene, hogyan kezeljük a nullával osztást (hibás nevező) és az előjeleket.

C++: Komplex osztály

A törteket nem nagyon érdemes máshogy ábrázolni, mint számlálóval és nevezővel. A komplex számoknál azonban már kétféle választási lehetőségünk van: algebrai alakban, re + im×j, vagy trigonometrikus alakban r × e is tárolhatjuk a számot. Ha gyakran kell összeadni és kivonni, az előbbi célravezetőbb; ha a szorzás és az osztás a gyakori művelet, akkor az utóbbi. Írj két külön C++ komplex osztályt, az egyik algebrai, a másik trigonometrikus alakot használjon! Írd meg a négy alapműveletet is átdefiniált operátorral! Hogyan lehet elérni azt, hogy a négy alapművelet kódja ne függjön a belső reprezentációtól, sőt akár azt, hogy karakterről karakterre megegyezzenek az operátorfüggvények?

Megoldás

Az alábbi két osztály tökéletesen csereszabatos. És igen, a konstruktorok szándékosan privát elérésűek. Mivel akár valós és képzetes részből, akár hosszból és szögből szeretnénk létrehozni egy komplex számot, mindkét esetben két double lenne a konstruktor paramétere, sajnos nem tudjuk kihasználni a függvénynév túlterhelést. Marad a névvel rendelkező konstruktor, mint szokásos nyelvi fordulat (named constructor idiom). A statikus tagfüggvények nyelvileg is részét képezik az osztálynak, így elérik a privát konstruktort.

class Complex {
  public:
    static Complex make_complex_reim(double re, double im) {
        return Complex(re, im);
    }
    static Complex make_complex_rfi(double r, double fi) {
        return Complex(r*cos(fi), r*sin(fi));
    }

    double get_re() const {
        return re_;
    }
    double get_im() const {
        return im_;
    }
    double get_r() const {
        return sqrt(re_*re_ + im_*im_);
    }
    double get_fi() const {
        return atan2(im_, re_);
    }

  private:
    double re_, im_;
    Complex(double re, double im): re_(re), im_(im) {}
};
class Complex {
  public:
    static Complex make_complex_reim(double re, double im) {
        return Complex(sqrt(re*re+im*im), atan2(im, re));
    }
    static Complex make_complex_rfi(double r, double fi) {
        return Complex(r, fi);
    }

    double get_re() const {
        return r_*cos(fi_);
    }
    double get_im() const {
        return r_*sin(fi_);
    }
    double get_r() const {
        return r_;
    }
    double get_fi() const {
        return fi_;
    }

  private:
    double r_, fi_;
    Complex(double r, double fi): r_(r), fi_(fi) {}
};

C99: return 0;

Az alábbi C99 programban nem volt return 0 a főprogram végén. Baj ez?

#include <stdio.h>

int main(void) {
    printf("Hello world!\n");
}
Megoldás

Nem. A C99-ben és a C++-ban a main() speciális függvény. Ha kihagyjuk belőle a return-t, a fordító úgy veszi, 0-val tért vissza. A C89-ben ez még nem volt így.

C99: Tömb literálisok

Adott az alábbi függvény:

void print_chars(char const *p) {
    while (*p != '\0') {
        putchar(*p);
        p++;
    }
    puts("");
}

Az alábbi módokon hívjuk meg. Helyesek ezek a kódok?

char const *chars1(void) {
    return "hello";
}

char const *chars2(void) {
    return (char[]) { 'h', 'e', 'l', 'l', 'o', '\0' };
}

print_chars(chars1());
print_chars(chars2());
Megoldás

Az első igen, a második nem. Az elsőben globális változó a karaktertömb, a másodikban lokális.

És ez?

char const *chars2(void) {
    static char const *p = (char[]) { 'h', 'e', 'l', 'l', 'o', '\0' };
    return p;
}
Megoldás

Így már le sem fordítható a kódrészlet. A függvény statikus lokális, azaz adatszegmensben tárolt, globális élettartamú p pointeréhez olyan inicializáló érték kellene, amely fordítási idejű konstans. Mivel azonban a megadott tömb a veremben van, a címe nem konstans. A static kulcsszóval amúgy sem a tömb, hanem a pointer élettartamát változtatjuk, hiszen az a p definíciójához tartozik.

C99: Változóval megadott típusú pointer

Vajon mi történik akkor, ha rossz típusú, változóval megadott méretű tömböt adunk értékül egy hasonló pointernek? Pl. az alábbi kód ránézésre rossz, de mi történik futási időben?

int arr[3][5];
n = 7;
int (*parr)[n] = arr;
Megoldás

A „szokásos”: definiálatlan. Valószínűleg helytelenül fog működni a programunk. A fordító nem ellenőrzi ezt az értékadást; ránk van bízva, hogy helyesen használjuk.

C99: változóval megadott méretű tömb egy struktúrában

Definiálj struktúrát, amely egy tetszőleges szélességű és magasságú, valós számokat tartalmazó kétdimenziós mátrixot, és annak méreteit tárolja!

Megoldás

Vigyázat! VLA csak veremben lehet!

/* jó megoldás */
struct Matrix {
    int width, height;
    double **data;
};
/* rossz megoldás */
struct Matrix {
    int width, height;
    double data[width][height];
};

C99: flexibilis tömb egy struktúrában

Definiálj struktúrát, amely egy tetszőleges szélességű és magasságú, valós számokat tartalmazó kétdimenziós mátrixot, és annak méreteit tárolja!

Megoldás

Vigyázat! A flexibilis tömb nem lehet kétdimenziós!

/* jó megoldás lehet */
struct Matrix {
    int width, height;
    double **data;
};
/* rossz megoldás */
struct Matrix {
    int width, height;
    double data[][];
};

C99: flexibilis tömböt tartalmazó struktúra paraméterként

Mi történik, ha egy flexibilis tömböt tartalmazó struktúrát érték szerint adunk át egy függvénynek? Visszatérhet-e egy függvény egy flexibilis tömböt tartalmazó struktúrával?

Megoldás

Csak a flexibilis tag előtti része másolódik, mivel az ilyen struktúra (szinte) minden szempontból úgy viselkedik, mint a flexibilis tag nélküli változata. A tömb nem másolódhat, mivel annak méretéről a fordítónak nincs tudomása.