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 × ejφ
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.