CALMARIUS referencia – a mindenes

A C/C++ nyelv, és sok más hasznos cucc

Ez a referencia nem egy tankönyv, de röviden, tömören (majdnem) mindent elmagyaráz a C nyelvről. Aki nagyjából tudja, mi fán terem a programozás, annak, szerintem, nem lesz túl sok gondja ezzel. Ebben a referenciában elejétől a végéig is lehet haladni, de beleugorhatsz a közepébe is a következő hiperlinkekkel:

Hiperlinkek:

C/C++ nyelv:

Vágjunk bele

Mi a különbség C és a C++ között?

Egyszerű szöveg kiíratása

Vezérlő karakterek kiíratása

Képernyőtörlés

Várakozás egy billentyű leütésére

Változók

Konstansok

Makrók

Operátorok

Formátum jelek

Pointerek

Dinamikus memóriakezelés

Referenciák

Tömbök

Szövegek

Adatbekérés

Feltételes elágazás

Feltételes elágazás többfelé

Számlálós ciklus

Elöltesztelő ciklus

Hátultesztelő ciklus

Alprogramok

Rekurzió

Pointerek és tömbök kapcsolata

Típusmegadás

Struktúrák

Uniók

Típuskonverzió (kasztolás)

Fájlok kezelése

Objektumokról általában

Objektumok publikus és privát tagjai

Objektumok származtatása

Objektumok védett tagjai

Objektumok virtuális metódusai

Konstruktorok

Destruktorok

Virtuális destruktorokról bővebben

Algoritmusok

Rendezések

Buborékos rendezés

Gyorsrendező algoritmus

Hilbert-görbe rajzolása

Hasznos tanácsok kezdőknek és haladóknak

Előszó

Úgy is kihagyod… De néhány infót azért beírok ide. Először is, semmi felelősséget nem vállalok a helyesírási, mondattani, nyelvhelyességi hibákért. Továbbá remélem, hogy nem írtam hülyeséget. Szabadon terjeszthető, de anyagi haszonért nem sokszorosítható.

Vágjunk bele!

Ha C-t, vagy akármilyen nyelvet tanulsz, akkor először mindig a DOS részével kell kezdened, azért, hogy elsajátítsd az alapokat. Használj olyan fordítóprogramot, amely támogatja a DOS-os programok írását, pl. a Borland C++ 3.0-át, az a legalkalmasabb DOS-os programok írására.

A programodba írhatsz megjegyzéseket, ha azokat /* és */ jelpárok közé rakod. // jelpár a sorvégi megjegyzéshez.

Ha szerkesztőbe ezt írod be, akkor nem fog kiakadni, de semmit sem csinál:

void main()
{
}

Ez a program belépési pontja, a fő része. Azok az utasítások fognak a programodban végrehajtódni, amiket a két kapocszárójel közé beírsz.

FONTOS: A legtöbb programnyelvvel ellentétben a C különbséget tesz kis és nagybetű között!

Mi a különbség C és a C++ között?

A C és C++ között az a különbség, hogy a C++ többet tud, mint a C nyelv. Egy C++ nyelvű környezetben lefordíthatók a C nyelven írt programok, de ez fordítva nem igaz. C++ nyelvű egy program, ha használ objektumokat, templátokat stb. De mindezekről kicsit később.

Egyszerű szöveg kiíratása

A C-ben, a legtöbb programozási nyelvvel ellentétben, nincsenek előredefiniált eljárásai, csak az alaputasítások, de azok önmagukban nem sokra jók. Tehát, ha ezeket használni akarod, akkor a hozzájuk tartozó fájlt be kell szúrni. Így egy egyszerű szöveget kiíró program így néz ki:

#include "stdio.h"

void main()
{
     printf("Borland C++");
}

Az az #include utasítja a fordítóprogramot, hogy szúrja be az STDIO.H fájlt, olyan, mintha beírnád. Abban a fájlban van definiálva a printf utasítás, amivel a képernyőre írhatsz ki szöveget. A program kiír egy szöveget, majd kilép. Az ALT+F5-tel nézheted meg, hogy mit alkottál. Tehát, ha valamilyen printf-féle utasítást akarsz alkalmazni, mindig be kell szúrnod az stdio.h-t, különben nem ismeri fel a fordítód.

Vezérlő karakterek kiíratása

Ha szövegeket írsz ki, akkor kiemelt szerepe lesz a szövegbe írt „\” jelnek, ez egy vezérlő karakter. A következő felsorolás mutatja, hogy mire jó:

\              Így önmagában alkalmas arra, hogy a következő sor elején folytassad a szöveget, ha nem férne ki egy sorba. Bármilyen egy sorba való dolog a következő sorba tolására alkalmas, pl. a programod nem akad ki, ha az #include sort így írod be:

                #include \
"stdio.h"

\n            Új sor karakter (ASCII 10). A leggyakrabban használt kód. Ez az új sor karaktert jelenti, amit utána írsz, az a következő sorba fog kerülni.

\t             Tabulátor karakter (ASCII 9)

\r             „Kocsivissza” karakter (ASCII 13), ettől a kurzor sor elejére fog visszaugorni, és onnét folytatja az írást, ennek a karakternek a Windows programok írásánál lesz majd haszna, ugyanis ott a \r\n karakter páros jelenti a sorvégét.

\a            Sípolás (ASCII 7), ha beírod a géped sípolni fog, mikor ezt kiírja.

\b            Backspace karakter (ASCII 8), Eggyel visszalépteti a kurzort, de nem törli ki azt a karaktert.

\f             Formfeed karakter (ASCII 12), (???)

\v            Függőleges tabulátor (ASCII 11), (???)

\"            Idézőjel

\’             Aposztróf

\\             Backslash karakter. Ezt kell használni, ha a szövegbe visszapert akarsz. Nagyon gyakran elrontják.

\xxx         Visszaper után egy 3jegyű 8-as számrendszerbeli számmal adhatsz meg egy ASCII karaktert.

\xHH       \x után egy kétjegyű hexadecimális számmal adhatsz meg egy ASCII karaktert.

Az előző kettőt ne nagyon használd, mert nagyon macerás tud lenni, egyedül akkor, ha a NUL karakter akarod írni, Ennek az ASCII kódja nulla.

A C-ben egy szöveg a 0 ASCII kódú karakterig tart, innen fogja a géped tudni, hogy vége a szövegnek. Amikor idézőpöckök között szöveget írsz be, akkor gép automatikusan a végére érti ezt a karaktert, így a szövegek mérete egyenlő a karakterek száma + 1-gyel. Amúgy az ilyen szöveget angolul null-terminated string-nek, vagy ASCIIZ string-nek nevezik.

Példa:

#include "stdio.h"

 

void main()

{

printf("Sima szöveg, utána soremelés\n");

printf("Sípol:\a\n");

printf("A\tB\tC\n");

printf("Idézőjelek: \"\"\"\n");

printf("Több sorra\

törve\n");

printf("Sor elején lesz a kurzor\r");

}

Képernyőtörlés

A programod elején ennek az utasításnak a használatához be kell szúrnod a CONIO.H fájlt is. Az utasítás egyszerűen ez:

clrscr();

Várakozás egy billentyű leütésére

Ez is a CONIO.H-ban van leírva. Az utasítás ez:

getch();

Addig vár, amíg le nem nyomsz egy gombot. A programod végén jó, ha van egy, hogy ne az ALT+F5-tel kelljen megnézni az eredményeket.

Változók

A C nyelv is ugyanúgy változókat használ az adatok tárolására, mint a legtöbb programnyelv. A C-ben nincs kikötve, hogy hol adod meg a változóidat, de a lényeg az, hogy még a használat előtt deklaráld őket.

Egész számú változó használatához írd ezt:

int változó(k);

Példa:

int i;

Valós számhoz egy hasonlót:

float változó(k);

Példa:

float Gyok1,Gyok2;

Vagy általában:

Típusnév változó(k);

A C-ben a következő előre definiált típusnevek vannak:

Típusnév

Értelmezési tartomány

Méret (bájt)

Egész számok

 

 

char

[-128; 127]

1

unsigned char

[0; 255]

1

short

[-32768; 32767]

2

unsigned short

[0; 65535]

2

int

Platformfüggő

2 v. 4

unsigned int

Platformfüggő

2 v. 4

long

[-2147483648; 2147483647]

4

unsigned long

[0; 4294697295]

4

Valós számok

 

 

float

[±3,4E-38; 3,4E+38]

4

double

[±1,7E-308; 1,7E+308]

8

long double

[±3,4E-4932; 3,4E+4932]

10

Itt egy példaprogram ehhez:

#include "stdio.h"

#include "conio.h"

 

int a,b,c;

 

void main()

{

a=3;

b=4;

c=a+b;

printf("A két szám összege: %d\n",c);

getch();

}

Fontos, hogy minden változónak olyan értéket adjunk, amilyet szabad (Szám típusú változónak számot, struktúratípusúnak struktúrát). Ha nem így teszünk, hibaüzenetet kapunk.

Láthatjuk, hogyan kell értéket adni egy kifejezésnek. A = jel valójában egy operátor, de utasításértékű, és az a=4 kifejezésnek is van értéke, méghozzá 4, így a fordítóprogramod nem fog kiakadni, ha ezt írod: a=b=c=4. Ez esetben az a, b, c változók értéke 4 lesz, ezt úgy csinálja, hogy először kiértékeli a c=4-et, ennek az értéke 4 lesz, ezt kapja meg a b, ennek a kifejezésnek az érteke szintén négy lesz, és ezt kapja meg a c. Így lehet egyszerre egy csomó változót beállítani, de én külön-külön szoktam beállítgatni őket.

A következő alcím témája az a %d lesz ott a printf-ben (formátumjelek rész).

A C nyelvben lehetőségünk van, hogy rögtön kezdőértéket adjunk egy változónak, ekkor csak egy egyenlőségjel után kell írnunk a kívánt értéket:

int f=4,h=32;

Ilyen egyszerű.

Konstansok

Megadása megegyezik a változók megadásával, csak annyiban különbözik, hogy elé kell írni, hogy const, és kezdőértéket is kap mindig:

const <típus> <konstans> = <érték>;

Innentől kezdve a konstanst nem lehet módosítani, ha mégis megpróbálnád, akkor hibaüzenetet kapsz. Persze, ha nagyon ki akarod játszani a fordítóprogramot, akkor sikerülhet:

const int my_age = 17;

...

*(int *)&my_age = 35; // Elfogadja

...

Ez azonban fizikailag is benne lesz a memóriában, egy szám, mai feleslegesen foglalja a helyet, mi lenne, ha a számnak adnánk egy nevet, és így fordítód fogja tudni, hogy az azt a számot jelenti, és kicseréli vele, mikor fordítja a programot? Van erre megoldás, használd a #define-t:

#define <Név> <Érték>

Ez nem a C nyelv eleme, hanem a fordítóprogramé ez annyit tesz, hogy a név helyére azt az értéket fogja érteni, amit oda írsz, ez nem csak egy szám lehet, hanem akármilyen szöveg, de fontos, hogy a végére ne tegyél pontosvesszőt, mert akkor azt is bele fogja érteni. Amúgy az így létrehozott konstansokat szimbolikus konstansoknak nevezzük.

Példák:

#define PI 3.1415

Tehát, ahova azt írod, hogy PI, oda a géped azt fogja hinni, hogy 3.1415. Ilyen egyszerű.

Makrók

A #define-nal nem csak szimbolikus konstansokat, hanem makrókat is lehet definiálni. Baloldalra olyat írsz, ami nem úgy néz ki, mint egy azonosító, hanem, mint egy függvény. Ekkor a jobboldalra egy kifejezést kell írnod. Ekkor a fordítód ki fogja cserélni azokat az általad megadott kifejezésre, mikor fordít.

Hadd említsek néhány példát:

#define RAD(x) (x)/PI*180

#define until(x) while(!(x))

#define BINARY(b7,b6,b5,b4,b3,b2,b1,b0) (BYTE)(b7)*128+(BYTE)(b6)*64+(BYTE)(b5)*32+\

(BYTE)(b4)*16+(BYTE)(b3)*8+(BYTE)(b2)*4+(BYTE)(b1)*2+(BYTE)(b0);

Mit is tesznek ezek a sorok? A RAD(x) átváltja a fokban megadott szöget radiánba. Az until(x)-et a do ciklusoknál használhatjuk, azok részére jó, akik hozzászoktak a pascalhoz. A binary egy bitekkel megadott, kettes számrendszerbeli számot vált át tízes számrendszerbe. Ennél azért van mindenhol típuskasztolás, hogy 8 db figyelmeztetést kerüljük el vele, a fordító magától is elvégezné ezt a konverziót, de így jobb. Ezeknek a makróknak a végére nem tehetünk semmilyen lezáró jelet, a sorvégjel zárja le a makrót, ha mi mégis több sorba szeretnénk őket tenni, akkor egy \ jelet kell a sor végére tenni, ekkor írhatjuk több sorba is. Ezek, a makrók, így nem lesznek benne az EXE kódba, de egyszerűbbé tehetik a forráskódot. De azért kerüljük a túl bonyolult makrókat, mert azok lelassíthatják a programunkat. Ne felejtsük el, hogy a fordítóprogram a baloldalra írt kifejezést csak, egyszerűen kicseréli a jobb oldalra írttal.

Használatra példa:

x=sin(RAD(60));

...

do

{

...

}

until (x<0);

Operátorok

A C-ben tulajdonképpen minden operátor, ami nem betű, vagy szám, sok fajtája van. Az operátorokkal megadott kifejezéseknek vagy van visszatérési értékük, vagy nincs. Ha van, akkor fel lehet használni értékadásban, de lehet utasításként is használni, ha nincs, akkor csak utasításként lehet használni. Továbbá a több karakterből álló operátorokat nem szabad szóközökkel szétbontani, mert az hibát okoz.

Példa:

a=5+2; // a=7 lesz

5+2; // Kiértékeli, de nem csinál semmit vele. (A fordítóprogram figyelmeztetést ad és figyelmen kívül hagyja a sort.)

a++; // Növeli az a értékét eggyel.

 

A következő lista mutatja őket:

Matematikai operátorok:

+, -, *, /     Az alapműveletek jelei. Kétoperandusúak. Az operandusokat a két oldalára kell írni. Az osztás, egész számok esetén, a maradékos egészosztást jelenti.

+, -                     Előjelek, a plusz semmit sem csinál, a mínusz az ellentettjét adja.

%                             Osztási maradék. Pl.: 5 % 2 = 1; 7 % 5 = 2

++, --                Növelés, csökkentés. Egyoperandusú operátor. Az operátort írhatjuk elé vagy utána. Ha elé írjuk, akkor először növeli/csökkenti, majd utána értékeli ki, ha utána, akkor először kiértékeli, majd növeli/csökkenti a változót.

Példa:

a=2;

b=a++; // b=2; a=3 lesz.

 

a=2;

b=++a; // b=3; a=3 lesz.

 

a=2;

a++; // a=3 lesz, önálló utasításként is használható

Hozzárendelő operátok:

=    *=   /=   %=   +=   -=

<<=  >>=  &=   ^=   |=

Az = jel a hagyományos egyenlőség operátor, amivel az értékadást végezzük. A többit így kell értelmezni:

a op= b;           //(Ezt jelenti: a=a op b)

Pl.: a+=4;         //(Ugyanaz, mint a=a+4)

Ezeken a példákon keresztül remélhetőleg érthető, hogy mit is csinálnak ezek.

Relációs jelek

>    <    ==   >=   <=   !=

Azt hiszem ezek a jelek nem szorulnak különösebb magyarázatra. Ha a reláció igaz, a kifejezés értéke nem nulla, ha hamis nulla.

Logikai operátorok

&&          Logikai „és” operátor. Kétoperandusú. Érteke akkor igaz, ha mindkét oldalán álló kifejezés igaz.

||          Logikai „vagy” operátor. Kétoperandusú. Értéke akkor igaz, ha legalább az egyik oldalán álló kifejezés igaz.

!             Logikai „tagadás” operátor. Egyoperandusú. Érteke igaz, ha a tőle jobbra lévő operandus hamis, és fordítva.

A C-ben az igaz érték azt jelenti, hogy „nem nulla”, míg a hamis azt jelenti, hogy nulla.

Bitszintű operátorok

&             Bitszintű „és” operátor. Kétoperandusú. 1-re állítja, azokat a biteket, amelyek mind a két számban megegyeznek (36 & 6 = 4 [100100 & 110 = 000100]).

|             Bitszintű „vagy” operátor. Kétoperandusú. 1-re állítja azokat a biteket, amelyek legalább az egyik számban megegyeznek (23 | 64 = 87 [10111 | 1000000 = 1010111]).

^             Bitszintű „kizáró vagy” operátor. Kétoperandusú. 1-re állítja azokat a biteket, amelyek csak az egyik számban egyeznek meg (64 ^ 89 = 25 [1000000 ^ 1011001 = 0011001]).

~             Bitszintű „tagadás” operátor. Egyoperandusú. 1-re állítja azokat a biteket, amik 0-k, és fordítva (~38 = 217 [~00100110 = 11011001]). Ennél az operátornál számít, hogy milyen típusú adatnál végezzük el. 32 bites long típusú értéknél mind a 32 bitet invertálja, 16 bites short értéknél a 16 bitet fordítja át, 8 bites csak a nyolcat. Tehát ahány bit, annyi különböző érték, ezt mindig tartsuk szem előtt.

<<          Bitszintű „balratolás” operátor. Kétoperandusú. A baloldali érték bitjeit annyi helyi értékkel tolja balra, amennyi a jobboldali értékben meg van adva, az üres helyekre nullákat hoz be (6 << 3 = 48 [00000110 << 00000011 = 00110000]). Egy gyors módszere a kettő hatványaival való szorzásnak.

>>          Bitszintű „jobbratolás” operátor. Ugyanaz, mint előbb, csak ez jobbra tolja a biteket (33 >> 3 = 4 [100001 >> 11 = 000100]). Egy gyors módszere a kettő hatványaival való egészosztásnak.

Pointer operátorok

&             Változó címének lekérése, a visszatérési érték pointer. Egyoperandusú. (g = &f)

*             Hozzáférést biztosít a pointer által mutatott memóriaterülethez. Egyoperandusú. (*g = 5)

Feltételes operátor

?             Ez egy háromoperandusú operátor. Használata:

<feltétel> ? <érték1> : <érték2>

Ha a feltétel igaz (nem nulla), akkor az értéke az érték1, ha hamis (nulla), akkor az érték2.

Példa:

a=a>30 ? 30:a;    // Egyenértékű ezzel: if (a>30) a=30;

a=a<0 ? 0:a;      // Egyenértékű ezzel: if (a<0) a=0;

A példában az a változó értékét nem engedjük kilépni a [0;30] tartományból.

C++ specifikus operátorok

::                          Hozzáférési operátor. Akkor használják, ha egy alprogramban és azon kívül is deklarálunk egy azonos nevű változót, vagy egy objektum metódusát az objektumon kívül kívánjuk kifejteni.

1. Példa:

int i;        // Kint deklarált változó

...

void Eljarasom()

{

int i;  // A bent deklarált változó. Itt csak ezt érhetjük el. A külsőt közönséges módon nem.

::i=7;  // Így már tudunk értéket adni a külső változónak is.

i=8;    // Ezzel a belső változó állíthatjuk be.

}

...

2. Példa: objektumok

#define SCALE 1.4

...

typedef struct STAR

{

float x,y;

void Kirajzol();      // Objektumkezelő eljárás

} STAR;

...

float x,y;                  // Főprogramban is van egy x meg y is.

...

void STAR::Kirajzol()       // A STAR objektumhoz tartozó Kirajzol eljárást fejtjük itt ki.

{

float x;

float y;              // Az eljárás belső változói

x=STAR::x*SCALE;

y=STAR::y*SCALE;      // A STAR objektumhoz való x-re és y-ra van szükség.

putpixel(x,y,WHITE);

::x=x;

::y=y;                // A főprogram x-ére, és y-ába töltjük át az értéket.

}

...

.*, ->*     Ha egy struktúrában pointer mező van, akkor használhatjuk ezeket az operátorokat a címen lévő érték lekérésére sokkal kényelmesebb ez:

rekord.*mező

Mint ez:

*(rekord.mező)

sizeof operátor

Szimbólumok méretét adja meg. Ezt tulajdonképpen egy fordítóprogram makró, csak a számot fogja odaérteni.

Használata:

sizeof(<adattípus>) vagy

sizeof <szimbólum>

Pl.:

sizeof(float) (= 4)

...

double l;

...

sizeof l (= 8)

Egyéb operátorok

Vannak operátorok, amelyekről vagy nem tudom (még), hogy mire jók, vagy egyszerűen nem találkoztam még olyan problémával, amit nélkülük nem lehetett volna megoldani. Itt vannak:

const_cast         ???

delete             Lefoglalt objektumot töröl a memóriából.

dynamic_cast                  ???

new                Memóriát foglal le a heap-ben.

reinterpret_cast        ???

static_cast        ???

typeid             Valami futási idejű típuslekérdezés-féle dolog ez. (???)

,                  Vessző operátor???

Precedencia

Akár a matematikában, a C nyelvben van olyan, hogy műveleti sorrend. A következő tábla mutatja őket:

A fentebb lévő műveletek lesznek elvégezve előbb, az egy sorban lévő műveletek azonos előnyt élveznek.

Kategória

Operátor

Mit csinál

1. Legmagasabb

()

[]

->

::

.

Függvényhívás

Tömbelemlekérés

C++ indirekt mezőkiválasztó

C++ hozzáférés operátor

C++ direkt mezőkiválasztó

2. Egyoperandusú

!

~

+

-

++

--

&

*

sizeof

new

delete

Logikai tagadás

Bitszintű tagadás

Plusz

Mínusz

Növelés

Csökkentés

Címlekérés

Címhivatkozás

Méret

Memóriafoglalás

Memória-felszabadítás

3. Mező hozzáférés

.*

->*

Mezőpointer-címhivatkozás

4. Multiplikatív

*

/

%

Szorzás

Osztás

Maradék

5. Additív

+

-

Összeadás

Kivonás

6. Eltolás

<<

>>

Balra tolás

Jobbra tolás

7. Relációk

<

<=

>

>=

Kisebb, mint

Kisebb, egyenlő

Nagyobb, mint

Nagyobb, egyenlő

8. Egyenlőség

==

!=

Egyenlő

Nem egyenlő

9.

&

Bitszintű „és”

10.

^

Bitszintű „kizáró vagy”

11.

|

Bitszintű „vagy”

12.

&&

Logikai „és”

13.

||

Logikai „vagy”

14. Feltételes

? :

(a ? x : y azt jelenti, hogy „ha a akkor x, különben y”)

15. Hozzárendelő

=

*=

/=

%=

+=

-=

&=

^=

|=

<<=

>>=

Hozzárendelés

[a op= b, azt jelenti, hogy a=a op b]

16. Vessző

,

Kiértékelés

 

Formátum jelek

Ha valamilyen printf-et használsz, akkor szövegbe rakhatsz számokat, szövegeket változókból egyszerűen a szövegbe be kell írni %-jel után a formátumkódot, majd a karakterlánc után vesszővel elválasztva sorban az adatokat kell felsorolni, és kész. A következőkben megtudjuk, hogy hogyan kell ezeket a kódokat használni.

Az általános formája a kódoknak ez:

% [jelzők] [hossz] [.pontosság] [F|N|h|l|L] típus_karakter

Jelzők értékei (opcionális):

-              Balra igazítja a szöveget a megadott mezőben.

+             Mindig kirakja az előjelet a számok elé, akár plusz, akár mínusz.

(semmi)  A negatív számok elé tesz egy mínusz jelet, a pozitívak elé semmit.

#             Jelzi, hogy alternatív megadást használsz. (???)

Ezek szabadon kombinálhatók, akármilyen sorrendben.

Hossz értéke: itt adhatod, meg, hogy mekkora mezőbe írja be az értéket, ebben a mezőben jobbra igazítja az értéket. Itt is lehet variálni:

n             Legalább n db karaktert kiír, a mezőben jobbra igazítva.

0n           Legalább n db karakter kiír, az üres mezőket nullákkal tölti ki.

*             Jelzi, hogy értéktől függő a mezőhossz, a megadott változó adja meg a mezőhosszt.

Pontosság értékei:

(semmi)  Alapértelmezés

.0             Valós számok esetén azt jelenti, hogy nem kérünk tizedeseket

.n            n db betű, vagy tizedes lesz kiírva, ha több tizedes van vagy hosszabb a szöveg, akkor levágja a maradékot.

.               Egy változó adja meg, hogy milyen lesz a pontosság.

Méret határozó:

5 db van belőle:

N:            közeli pointer (2 bájt)

F:            távoli pointer (4 bájt)

h:            short int (2 bájt)

l:              long (4 bájt)

L:            long double valós szám (10 bájt)

Típus karakter:

Ezt mindenképpen meg kell adni.

Ezek azok:

d, i          Jelzi, hogy előjeles, egész számot akarunk kiíratni, 10-es számrendszerben.

o             Jelzi, hogy előjel nélküli, 8-as számrendszerbeli, egész számot akarunk kiíratni.

u             Jelzi, hogy előjel nélküli, 10-es számrendszerbeli, egész számot akarunk kiíratni.

x, X         Jelzi, hogy előjelnélküli, hexadecimális, egész számot akarunk kiíratni (kis ill. nagy betűket használva a 9 feletti számjegyekhez).

f              Jelzi, hogy valós számot készülünk kiíratni, közönséges formában ([-]ddddd.dddd)

e, E         Jelzi, hogy valós számot akarunk kiíratni, exponenciális formában ([-]d.dddddde[+/-]ddd, kis vagy nagy
E betűkkel)

g, G         Jelzi, hogy számot írunk ki. Az érték határozza meg, hogy hogyan. Egy egész szám esetén egész számot, valós szám esetén valósat, ha szám túl nagy, akkor exponenciális formát alkalmaz. (Exponenciális formában kis vagy nagy E betűt alkalmazva.)

c              Jelzi, hogy egy egyszerű karaktert akarunk kiíratni, egy bájtos érték adja meg karakter ASCII kódját.

s              Jelzi, hogy a változó egy szövegre mutató pointer.

%            Jelzi, hogy csak egy egyszerű %-jelet akarunk csak kiíratni.

n             Jelzi, hogy a változó, amit írtunk egy egész számra mutató pointer.

p             Jelzi, hogy a változó, amit írtunk egy pointer, az abban tárolt memóriacímet írja ki XXXX:YYYY formátumban. Vagy csak YYYY formátumban near pointer esetén.

Ezek közül a formátumjelzők közül csak keveset használunk, itt van, hogy általában mire miket szoktak:

%g          Szám, mindegy, hogy milyen, de ez elég lassú (viszonylag).

%d          Egész számok esetén.

%-0.3f    Három tizedesjegyre kerekített valós szám. %-0.3Lf long double esetén

%c          Karakter esetén

%s          Szöveg esetén

Tehát lényegében, csak elég típus karaktert használni.

Példa:

. . .

float f;

int i;

long l;

char str[300];

f=3.14;

l=345;

i=3465;

sprintf(str,"Calmarius");

printf("f=%-0.3f\n",f);

printf("l=%d\n",l);

printf("i=%d\n",i);

printf("str=%s\n",str);

printf("30%% kamat\n");

. . .

Pointerek

A C nyelvben alapvető fontosságú a pointer adattípus. Ez nem tárol mást, csak egy memóriacímet, és mérete csak 4 bájt. Az általa tárolt memóriacím segítségével tudunk közvetlenül a memóriában babrálni. Általában direkt memória-elérésre nem szokták használni, hanem egy változó memóriacímét eltárolják benne, így a pointerünkön keresztül irkálhatjuk annak az értékét. Ez akkor hasznos, amikor több változóval végezzük el ugyanazt a kódrészletet, nem kétszer írjuk be az egészet, hanem egyszer az egyik változóra mutatunk rá, majd a másikra.

Ha pointert használunk, mindig tudnunk kell, hogy milyen típusra mutat, ha értéket akarunk rajta keresztül módosítani, innét fogja tudni a fordítóprogram, hogy hibát írjon ki, vagy sem.

A programodban így adhatod meg a pointereket:

<típusnév> *<változó>;

Pl.:

float *x;

char *string;

Az első egy float típusú adatra, míg a másik egy char típusú adatra mutat: ilyen értékeket tudunk rajta keresztül módosítani.

A következő programrészlet bemutatja ennek egyik lehetséges használatát: változó értékének írása anélkül, hogy direkt írnánk bele.

. . .

int a,b,*p;

p=&a;

*p=3;

p=&b;

*p=6;

printf("a= %d\tb=%d\n",a,b);

. . .

Ez a programrészlet azt teszi, hogy definiálja a változókat, pointereket, majd p felveszi az a változó memóriacímét, majd a memóriacímen keresztül ír bele, majd ugyanezt csinálja b-vel is. Végén kiírja, hogy:

a=3        b=6

 

A pointerek esetén, látjuk, két műveleti jel van (több is, de azt nem sűrűn kell használni). A & műveleti jelet referencia-operátornak hívják, ez azt csinálja, hogy egy változó memóriacímét adja vissza, és ezt értékként átadhatjuk egy pointernek. A másik operátor a *. Ezt arra használják, hogy egy pointeren keresztül értéket lehessen írni. Egy pointer elé *-ot írva úgy lehet használni, mint egy változót, értéket lehet rajta keresztül írni. Amúgy az ilyen kifejezést úgy hívják, hogy lvalue (left value), azért mert általában értékadások bal oldalán állnak (egy változó is lvalue), egy lvalue-n valójában csak két művelet értelmezhető: az értékadás, és a címlekérés.

Tehát lényegében ilyen egyszerűek a pointerek. Később meglátjuk, hogy nagyon sok helyen kellenek majd.

Még fontos lenne beszélnünk arról, hogy van egy pointer érték, amit bármelyik pointernek át lehet adni, ez a NULL. Ez azt jelenti, hogy a pointerünk a „semmibe” mutat, ami nem igaz, mutat valahová, de ha egy NULL pointeren keresztül szeretnénk írni valamit, annak kiszámíthatatlan következményei lesznek.

Továbbá van lehetőségünk, olyan pointert megadni, amely nincs típushoz kötve, ekkor egy bizonyos void nevű adattípusra mutató pointert kell megadnunk, az ilyen pointeren keresztül nem lehet közvetlenül értéket adni semminek sem, csak memóriablokkok mozgatására alkalmas.

FONTOS: A * operátor segítségével a memóriában oda írhatunk, ahova a pointerben eltárolt memóriacím mutat, mielőtt ezt a műveletet elvégeznénk, mindig ÉRVÉNYES memóriacím-értéket kell átadni a pointernek, különben, ki tudja, hol fogunk a memóriába írni, és ebből nagy problémák lehetnek. Pl. a program olyan utasításnál akad ki, aminél ez teljesen abszurd (egy ártatlan értékadásnál, például), és lehet, hogy a hiba kiváltó oka akár több ezer sorral arrébb van. Windows alatt a lefoglalt memória meg van „címkézve”, így ha ott rossz helyre hivatkozunk, akkor „Access violation” hibát kapunk, a legtöbb esetben. De akár kifagyhat az egész Windows is.

Dinamikus memóriakezelés

A pointerek alkalmazhatók akkor is, ha a program közben szeretnénk lefoglalni memóriát adataink számára, ill. törölni a szükségtelen adatainkat onnan.

A C++ nyelvben elég egyszerű a dinamikus memóriakezelés. A new és delete operátorok valók erre. Lássunk egy kis példát erre:

...

long *var;

var=new long; // Lefoglalunk 4 bájtot (ennyit foglal egy long változó)

*var=32; // Eltárolunk egy 32-est a memóriaterületre.

... // Csinálunk még valamit...

delete var; // Ha már nem kell a memóriaterület, akkor felszabadítjuk más adatok számára.

...

 

Egyszerűen a new operátor után meg kell adni, hogy milyen típusú adatot tárol a pointer, és az pont ennek megfelően foglal le a memóriából annyi bájtot.

A delete operátor pedig annyi bájtot szabadít fel, amennyit előzőleg a new operátorral lefoglaltunk.

A new és delete operátor használható tömbök létrehozására is:

...

long *var;

var=new long[32]; // Lefoglalunk 32 long típusú adatot (128 bájt)

var[2]=3;

var[31]=3223;

delete[] var; // Kitöröljük a tömböt

...

 

Megjegyzés: Néhány fordítóprogram megköveteli, hogy delete utáni szögletes zárójelbe beírjuk, hogy mennyi elemet szabadítunk fel, azaz kéri a 32-t.

A new és delete operátorokkal létrehozhatunk egy többindexű tömböt, de az elég macerás. Tudnunk kell, hogy a tömb neve változóként, indexmegadás nélkül, a tömb első elemére mutató pointert jelenti, ezért tudtuk az előző példában egy pointerből tömböt csinálni: pointer is használható tömbként és tömb neve is használható pointerként. A többindexű tömböt úgy kell elképzelni, mint tömbökből álló tömböt, mikor memóriát foglalunk le ezt kell szem előtt tartanunk:

...
void main()
{
	int i,j,m,n,o;
	m=20;
	n=30;
	o=40;
	/*20×30×40-es tömb*/
	int ***Tomb;
	Tomb=new int**[m]; // Az első index létrehozása
	for (i=0;i<m;i++)
	{
		Tomb[i]=new int*[n]; // A második
		for (j=0;j<n;j++)
		{
			Tomb[i][j]=new int[o]; // A harmadik
		}
	}
	
	// Valamit csinálunk a tömbbel
	
	//Azután töröljük
	for (i=0;i<m;i++)
	{
		for (j=0;j<n;j++)
		{
			delete[] Tomb[i][j]; // Először töröljük a harmadik indexet
		}
		delete[] Tomb[i]; // Azután a másodikat.
	}
	delete[] Tomb; // Végül az egész tömböt
}
...

Néhány végigolvasás után remélem érthető lesz.

Némelyik mágikus C++ fordító képes ezt az egész tortúrát egy piszok egyszerű művelettel elvégezni, de ez még nem szabványos:

...

int ***Tomb;

Tomb=new int[20][30][40];

...

delete[][][] Tomb;

...

 

A new és delete alkalmas objektumok dinamikus létrehozására és törlésére ilyenkor az objektumok konstruktorát is el kell indítani. Az objektumokról lentebb van írva. A new operátor végrehajtja az objektum konstruktorát, míg a delete végrehajtja annak destruktorát.

...

typedef class OBJEKTUM

{

int x,y;

OBJEKTUM(int,int); // Objektum konstruktora

} OBJEKTUM;

...

...

void Valami()

{

OBJEKTUM *Objektum;

Objektum=new OBJEKTUM(3,2); // Itt el kell indítani a konstruktort, különben kiabál a fordítóprogram.

...

delete Objektum; // Objektum törlése. Destruktora végrehajtódik.

}

 

C nyelvben nincs new és delete. Ott a malloc és free eljárásokat kell használni.

...

long *valami;

valami=(long*)malloc(sizeof(long));

/* sizeof-fal kiszámoljuk, hogy 4 bájt kell. Majd ezt az adatot átpasszoljuk a malloc-nak. Azonban ez a függvény egy void* pointerrel tér vissza, ezért át kell konvertálni long* típusra, ezután már használható is.*/

*valami=234;

free(valami); // Ez a free legalább úgy működik, mint a delete...

...

 

C-ben ez az egyetlen módszer a dinamikus memória-lefoglalásra.

Tömböt a következőképpen foglalhatunk le:

...

int *Tomb;

Tomb=(int*)malloc(sizeof(int)*Elemszam);

...

free(tomb);

...

 

Tehát megadjuk a méretet, majd kasztolunk.

Többindexű tömb létrehozásakor ugyanazt a tortúrát kell végigcsinálni, mint fent a new és deleténél:

...
void main()
{
	int i,j,m,n,o;
	m=20;
	n=30;
	o=40;
	/*20×30×40-es tömb*/
	int ***Tomb;
	Tomb=(int***)malloc(sizeof(int**)*m); // Az első index létrehozása
	for (i=0;i<m;i++)
	{
		Tomb[i]=(int**)malloc(sizeof(int*)*n); // A második
		for (j=0;j<n;j++)
		{
			Tomb[i][j]=(int*)malloc(sizeof(int)*o); // A harmadik
		}
	}
	
	// Valamit csinálunk a tömbbel
	
	//Azután töröljük
	for (i=0;i<m;i++)
	{
		for (j=0;j<n;j++)
		{
			free(Tomb[i][j]); // Először töröljük a harmadik indexet
		}
		free(Tomb[i]); // Azután a másodikat.
	}
	free(Tomb); // Végül az egész tömböt
}
...

Referenciák

Fontos: A referencia C++ specifikus dolog!

A C++ nyelvben lehetőség van arra, hogy ne csak pointerrel mutassunk egy változóra, hanem lehessen referenciával is.

A & jellel lehet megadni őket.

Lássunk erre egy példát:

void ValamiProc()

{

int i;

int &a=i; // A referenciát mindig inicializálni kell, különben fikázik a fordítóprogram

int *iaddr;

a=4; // Ugyanaz, mint az i=4;

iaddr=&a; // Ez az i címét fogja visszaadni.

}

 

Tulajdonképpen egy változóra hivatkozunk egy más néven. Adunk neki egy "aliast".

Nem kell feltétlenül egy változót átadni a referenciának, lehet konkrét értéket is:

...

int &ref=6; // Létrehoz egy ideiglenes változót és átad neki a 6-os értéket.

...

 

Természetesen használhatók a referenciák függvény paramétereiként is:

void Func(int &n)

{

n=3*n;

}

...

n=12;

Func(n); // A függvényhívás után n==36.

...

 

Tulajdonképpen itt úgy viselkedik a referencia, mint a Pascal nyelvben a VAR kulcsszó.

Megjegyzés: összetettebb típusok (pl. pointer) esetén mindig a típusmegadás végére kell a & jelet tenni, különben elkezd pampogni a fordítóprogram, hogy nem lehet "pointer-to-reference" típus, csak "reference-to-pointer".

...

int *&jo;

int &*rossz;

...

 

 

Tömbök

A tömbök olyan adatszerkezetek, amelyben több azonos típusú elemet lehet tárolni. A megadása C-ben:

<típusnév> <változó>[kiterjedés1][kiterjedés2][…][kiterjedásn]

Általában egy, vagy két kiterjedést (dimenziót) adunk meg, az egydimenziós tömbben egy számsort tárolhatunk, a kétdimenziós tömbben általában egy táblázatot, a háromdimenziósban nem tudom mit. Persze ez értelmezés kérdése, hogy milyen tömböt használunk.

Példák:

int Szamsor[30];

int Tabla[32][32];

int BlockOut[5][5][12];

A számsor változóban egy 30 elemű számsort tárolhatunk el. A tábla nevű változóban egy 32X32-es táblát lehet tárolni, a BlockOut nevű változóban 12db 5X5-ös táblát tárolhatunk el például.

Tömbök elemei

A C nyelv a tömböket 0-tól kezdi el indexelni, pl. számsor nevű változóban az első elem indexe 0, míg az utolsóé 29 így 30 db elemet tárolhatunk el benne. A tábla változó bal felső sarkában lévő mező, a 0;0-s, míg a jobb alsó a 31;31-es. Stb.

Hivatkozás elemekre

Például a számsor 3. elemére így hivatkozunk:

Szamsor[2]

Azért kettő, mert az első elemnek 0 az indexe.

Például a táblánk 3. oszlopának 31. sorára így hivatkozhatunk:

Tabla[2][30]

Persze értelmezés kérdése csak, hogy melyik indexet nevezzük ki oszlopszámlálónak, ill. melyiket sorszámlálónak, de akkor tartsuk is be azt a szabályt, nem variálhatunk, hogy egyszer egyik egyszer másik, mert abból káosz lesz.

A C nem veszi hibának, ha kifutunk a tömbből, pl. el fogja fogadni a Szamsor[45] elemet is, de ennek kiszámíthatatlan következményei lehetnek, mert lehet, hogy ott más adatok vannak eltárolva, az már nem a tömbhöz tartozik.

A következő példarészletben feltöltünk egy 10 elemű tömböt:

. . .

int Tomb[10];

Tomb[0]=3;                            // 1. elem

Tomb[1]=5;                            // 2. elem

Tomb[2]=6;                            // 3. elem

Tomb[3]=7;                            // . . .

Tomb[4]=8;

Tomb[5]=3;

Tomb[6]=5;

Tomb[7]=63;

Tomb[8]=33;

Tomb[9]=63;

. . .

Szövegek

A C egy olyan programnyelv, amiben nincs külön szöveges típus, csak számokat és pointereket ismer. De persze tudunk szövegeket eltárolni, méghozzá nem másban, mint egy tömbben, ha ennek a tömbnek az elemei char típusúak, akkor egy ebből álló tömb funkcionálhat szövegként. Szöveget még mindig nem lehet csakúgy eltárolni benne. Az STDIO.H sprintf függvényét lehet arra használni, hogy eltárolhassunk szövegeket. Íme egy példaprogram erre:

#include "stdio.h"

#include "conio.h"

 

char Szoveg[40];

 

void main()

{

sprintf(Szoveg,"Ez egy tárolt szöveg már!\n");

printf("%s",Szoveg);

getch();

}

Az sprintf úgy működik, mint a sima printf, csak ez a képernyő helyett egy karaktertömbbe ír. Azt csinálja, hogy sorban elkezdi a szöveg karaktereit beírni a tömbbe így:

Szoveg[0]=’E’;    // 69

Szoveg[1]=’z’;    // 122

Szoveg[2]=’ ’;

Szoveg[25]=’\n’   // 10

Szoveg[26]=’\0’   // 0

(Megjegyzés: a C-ben lehetőségünk van karakterkonstansok megadására, ezeket aposztrófok közé kell írni, valójában az értékük nem egy karakter, hanem egy szám. Pl. ’A’ a 65-ös számot jelenti az ’a’ a 97-et stb.)

Ugye emlékezünk még arra, hogy a szövegek utolsó karaktere után mindig kell tenni egy NUL karaktert, hogy jelezzük a szöveg végét. Ezzel mindig számolni kell.

Tehát lényegében ilyen egyszerű az egész.

Ezután a programunk egy közönséges printf-fel kiíratja a szövegünket, majd vár egy karakterre és kilép.

Kezdőértéket is adhatunk rögtön a szövegeinknek a megadásukkor, így:

. . .
char Text[30]="Kezdőértékadás!";
. . .

Ilyenkor tehetünk olyat is, hogy nem adunk meg kiterjedést:

. . .
char Szov[]="Auto-méretezés";
. . .

Ilyenkor automatikusan határozza meg a karaktertömb méretét, ami a szöveg karaktereinek a száma, plusz 1, mert a NUL karaktert nem hagyja le. Esetünkben 15 lesz.

Adatbekérés

Jó lenne, ha nem csak irkálnánk, mint a teletext, hanem a gép várna tőlünk felhasználói beavatkozást is, pl. bekérni egy változó értéket, vagy valamit.

Erre van egy utasítás a C-ben. A CONIO.H-ban lévő scanf utasítás.

Segítségével számot, vagy szöveget olvashatunk be. Lássunk rá egy példát:

#include "stdio.h"

#include "conio.h"

 

int i;

char str[40];

float flt;

void main()

{

clrscr();

printf("Kérek egy egész számot:");

scanf("%d",&i);

printf("Írj be egy szöveget!");

scanf("%s",str);

printf("Kérek egy valós számot!");

scanf("%f",&flt);

getch();

}

Ez a scanf nagyon hasonlít a printf-hez, csak be kell írni, hogy milyen típusú adatot várunk, és a gép olyat fog várni a billentyűzetről, majd egy enter lenyomását. Nekünk csak a változónk címét kell elpostázni neki, a & jel használatával, és mehet is. Ez az utasítás az értéket el fogja tárolni a változóban.

Vegyük észre, hogy az str-nek nem kértük le a címét. Ez azért van, mert tömb. A tömb elemeire, mint tudjuk a [ és ] jelekkel lehet hivatkozni, a nevének, csak úgy magában az a jelentése, hogy a tömbre mutató pointer, így nem kell ott & jellel vacakolni.

De mi van akkor, ha nem megfelelő értéket adunk, szándékosan ki akarjuk akasztani a programot? A program nem fog kiakadni. Azt teszi, hogy addig írja a változóba az értékeket, míg odaillő karaktert talál. Tehát ha egész számot olvasunk be, a gép addig olvassa a számokat, amíg számjegyeket lát, ha eljut egy elválasztó karakterig (szóköz, tab, enter), vagy más karaktert talál, akkor megáll, és annyit tesz a változóba, amennyit addig beolvasott. Valós szám esetén, pl. a második tizedespont érzékelésénél, vagy akkor akad ki, mihelyt valami hülyeséget írsz. A szövegek esetén nem áll meg. Addig olvas, amíg egy elválasztó karaktert nem talál.

Feltételes elágaztatás

Ha nem akarunk mindig sorban egymás után végrehajtani, érzéketlenül, mindent, mint egy italautomata, akkor lehetőséget kell adnunk a programunknak, hogy döntsön. Erre való a C nyelv if utasítása.

Így néz ki:

if (kifejezés)

{

utasítások1

}

else

{

utasítások2

)

Ez megnézi, hogy mennyi az értéke a kifejezésnek, ha nem nulla, akkor végrehajtja az utasítások1-et, ha nulla, akkor az utasítások2-t.

Hogy megértsd miről is van szó, nézd meg a következő kódrészletet:

...

if (a<b)

{

printf("A kisebb, mint B\n");

}

else

{

printf("A nem kisebb, mint B\n");

}

...

Az a<b-nek milyen értéke van? Ez egy logikai kifejezés, értéke vagy IGAZ, vagy HAMIS, de C nyelv nem ismer ilyet, számára a HAMIS azt jelenti, hogy nulla, az IGAZ meg azt, hogy nem nulla. Tehát, ha a tényleg kisebb, mint b, akkor értéke igaz, azaz nem nulla, ha meg nem, akkor az értéke hamis lesz, amit nullával jelöl.

Nem mindig van szükség else részre, pl. ha azt akarjuk, hogy hamis érték esetén ne történjen semmi. Lássunk erre is egy példát:

...

if (Diszkriminans<0)

{

printf("Az egyenletnek nincs valós megoldása!\n");

getch();

exit(-1);               // Programból való azonnali kilépésre utasít

}

...

Az if utasításnak egy másodfokú egyenletet megoldó programban van haszna:

#include "conio.h"

#include "stdio.h"

#include "math.h"

 

float a,b,c,d,x1,x2;

 

void main()

{

printf("a=");

scanf("%f",&a);

printf("b=");

scanf("%f",&b);

printf("c=");

scanf("%f",&c);

if (a==0)

{

printf("Az egyenlet elsőfokú!\n");

}

else

{

d=b*b-4*a*c;

if (d<0)

{

printf("Az egyenletnek nincs valós megoldása!\n");

}

else

{

x1=(-b+sqrt(d))/(2*a);

x2=(-b-sqrt(d))/(2*a);

printf("A megoldások:\n");

printf("x1=%-0.3f\tx2=%-0.3f",x1,x2);

}

}

getch();

}

Pár új dolog lehet ebben a forráskódban, mint pl. a beszúrt MATH.H, ami a matematikai függvények használatához való, mint ott az az sqrt függvény, ami a gyökvonást jelenti. Meg ott fent az az a==0 kifejezés is furcsának tűnhet, de a C nyelvben ez jelenti az egyenlőség relációt, elég gyakori hiba, hogy csak szimpla egyenlőségjelet írnak a programozók, ezt elég nehéz észrevenni, de a fordítóprogram kiír egy figyelmeztetést, ha ilyet tapasztal.

(Megjegyzés: ha az if-nél csak egy utasítást írunk, akkor nem muszáj kapocszárójelet írni, de inkább írjunk, mint nem.)

Elágazás többfelé

A C-ben nemcsak kétfelé, hanem többfelé elágaztathatjuk a programunkat, erre való a switch utasítás:

switch (változó)

{

case érték:

utasítások;

case érték:

utasítások;

...

}

 

Ez az utasítás a megadott változó értéke szerint irányítja a végrehajtást. Attól a case címkétől kezdi az utasítások végrehajtását, amelyiknek az értéke megegyezik a változó értékével. Ha egyik case címkénél sincs megfelelő érték, akkor kilép ebből az utasításból, és a következővel folytatja a futtatást. Speciális case címke a default, erre a címkére akkor kerül vezérlés, ha nincs másik case címke, amire lehetne ugrani, tehát ebben az esetben biztos, hogy lesz végrehajtva utasítás a switch-en belül. Egy case címke csak egyszer szerepelhet, ez érthető is, mert különben nem lenne egyértelmű, hogy hova ugorjon a program. A switch-ből való kiugrásra használatos a break utasítás, nagyon ritka, hogy nem használják, akkor használatos, ha egy értékez csak egy utasításcsoportot kell végrehajtani. A következő példa jól szemlélteti a switch használatát:

...

switch (c)

{

case ’+’: c=a+b; break;

case ’-’: c=a-b; break;

case ’*’: c=a*b; break;

case ’/’: c=a/b; break;

default: printf("Érvénytelen művelet.\n");

}

...

 

Az utolsó utasítás után nem írtam break-et, mert utána úgyis vége van.

Számlálós ciklus

Gyakran utasításokat kell ismételünk, erre találták ki a számlálós ciklust, ezzel egy meghatározott számban hajthatunk végre egy utasítást.

Így kell beírni:

for(<kezdő_utasítás>;<feltétel>;<folyamat_utasítás>)

{

utasítások

}

Ez elég nyersen hangzik, de ha mutatók egy példát, akkor szebben fog:

for (i=0;i<30;i++)               //++ operátor növeli a változó értékét 1-gyel.

{

printf("%d\n",i);

}

 

Először i felveszi a 0 értéket, majd megvizsgálja a feltételt, ami igaz, tehát belép a ciklusba, végrehajtja az utasítást, majd az egészet kezdi elölről, de mostantól kezdve már mindig a jobb oldalon lévő utasítást hajtja végre, ami növeli az i értékét, megvizsgálja a feltételt, ha igaz, akkor végrehajtja az utasítást, ismét növel…

Tehát a baloldali utasítást csak a legelején hajtja végre, utána mindig csak a jobboldalit, ciklusonként, az utasítások után ellenőrzi a feltételt, ha igaz, akkor belép a ciklusba, ha hamis, akkor nem és tovább lép a következő utasításra.

(Megjegyzés: ha a for utasítás csak 1 utasítást ismétel, akkor a kapcsos zárójelek elhagyhatók, de jobb, ha mindig ott vannak.)

Elöltesztelő ciklus

Valamikor nem egy meghatározott számban, hanem egy feltételtől függően kell utasításokat ismételnünk, erre is van utasítás:

while (feltétel)

{

utasítások

}

Ez annyit tesz, hogy először megvizsgálja, hogy a feltétel igaz-e, ha igen, akkor végrehajtja az utasítást, majd ismét megnézi a feltételt, ha igaz…

Elöltesztelő lévén lehet, hogy az utasítás egyszer sem fog végrehajtódni.

Egy példaprogram az alkalmazásra: tízes számrendszerből kettesre átváltó program.

#include "stdio.h"

#include "conio.h"

 

void main()

{

long i,n,c;

char Bits[30];

clrscr();

printf("Kérek egy egész számot: ");

scanf("%d",&n);

c=0;

while (n!=0)

{

Bits[c]=n % 2;

n-=Bits[c];

n/=2;

c++;

}

printf("Binárisan: ");

for (i=c-1;i>=0;i--)

{

if (Bits[i]) printf("1"); else printf("0");

}

getch();

}

Pár új dolog akad ebben a kódban, menjünk sorba. A != operátor azt jelenti, hogy „nem egyenlő”. A % operátor az osztási maradékot adja (5 % 3 = 2). A -= operátor azt jelenti, hogy „csökkentés”, a változó értékét csökkenti a jobb oldali értékkel, a baloldali változó értékét csökkenti a jobb oldali kifejezéssel. A /= operátor hasonlóan így viselkedik, csak ez oszt (egész számok esetén az osztásnál csak az egészrészt adja vissza értékül). A ++-t már ismerjük, a hozzá hasonló -- csökkent eggyel. Korábban már arról is volt szó, hogy az if-nél, el lehet hagyni a kapocszárójeleket, ha csak egy utasítást hajtunk végre. Tehát remélem érthető a kód.

(Megjegyzés: a while utasításban, ha csak egy utasítást ismétlünk, a kapocszárójel elhagyható.)

Hátultesztelő ciklus

A feltételes ciklus egy másik fajtája a hátultesztelő ciklus. Ezt akkor használják, ha egy utasítást legalább egyszer biztos, hogy végre kell hajtani.

Megadása:

do

{

utasítások

}

while (feltétel);

Ez azt végzi, hogy először végrehajtja az utasítást, majd ellenőrzi a feltételt, ha az igaz, akkor ismét lefuttatja a ciklust, és így tovább.

Ez jó, pl. egy jelszó bekérésére:

#include "stdio.h"

#include "conio.h"

#include "string.h"

 

void main()

{

char Password[]="CALMARIUS-ALPHA";

char pw[30];

do

{

printf("Kérem a jelszót: ");

gets(pw);

}

while (strcmp(Password,pw));

printf("jelszó elfogadva!\n");

getch();

}

Azt csinálja, hogy nem engedi tovább a programot addig, amíg be nem írod a jó jelszót.

Ebben a kódrészletben is van néhány újdonság. A STRING.H-ban van deklarálva az strcmp függvény, ami igaz értéket ad, ha a paraméterének megadott két szöveg különbözik, hamis, ha egyezik. Ott fentebb az a gets függvény pedig, a scanf-fel ellentétben, egy egész sort beolvas és nem áll meg addig, míg egy enterig el nem jut.

(Megjegyzés: a do ciklusra is vonatkozik az, ha csak egyetlen utasítást ismétel, elhagyható a kapocszárójel, de jobb, ha mindig ott van.)

Egy trükk:

Ha már programoztál pascal-ban, vagy más nyelven, tudhatod, hogy a hátultesztelő ciklusokból akkor ugrunk ki, ha a ciklusfeltétel igaz. A C-ben más a helyzet, ott akkor ugorhatunk ki, ha a megadott feltétel hamis, ez zavaró lehet, ha más nyelvekről állunk át C-re, de ha azt akarjuk, hogy igaz érték esetén lépjen ki, arra is van módszer:

#define until(x) while(!(x))

 

do

{

...

} until (...);

Így a hagyományos hátultesztelő ciklust kapjuk, és a fordító be is fogja ezt venni. Nem ír ki rá hibát.

Rendezések

A programozásban nagyon gyakori, hogy sorba kell rendeznünk az adatokat. Ekkor írnunk kell egy rendező algoritmust, ami elvégzi a rendezést.

Én itt két algoritmust említek, a legelterjedtebbet, és leggyorsabbat (szerintem a leggyorsabbat).

Buborékrendezés

Van egy rendezetlen sorozatunk, és azt kell rendeznünk, a módszer az, hogy az egymás utáni elemeket nézzük meg, majd felcseréljük, őket, ha nem felelnek meg a sorrendnek. Addig megyünk végig az egész sorozaton, míg az egész sorozat rendezett nem lesz.

Íme a program:

#include "stdio.h"

#include "conio.h"

 

void main()

{

int Elemek[10];

int i,s;

char voltcsere;

clrscr();

for (i=0;i<10;i++)

{

printf("%d:",i);

scanf("%d",&(Elemek[i]));

}

do

{

voltcsere=0;

for (i=0;i<9;i++)

{

if (Elemek[i]>Elemek[i+1])

{

s=Elemek[i];

Elemek[i]=Elemek[i+1];

Elemek[i+1]=s;

voltcsere=1;

}

}

}

while (voltcsere);

for (i=0;i<10;i++)

{

printf("%d,",Elemek[i]);

}

getch();

}

Ez a programocska bekéri a tömb 10 elemét (látható, hogy hogyan kell egy elem címét átpostázni az eljárásnak), majd rendezi azt és kiírja. Jól látható a csere eljárása is, illetve a rendezés menete. Láthatjuk, hogy a rendező ciklus csak 9-ig megy, mert nem akarunk a tömbből kifutni, ez a végignézegetés egészen addig megy, amíg van mit cserélni, ha már nincs, akkor rendezett a sorozat, és nem kell többet ekkor lépünk ki a ciklusból. A program végén kiírjuk az eredményeket.

Lehet, hogy egy rendezést többször is meg kell ismételni, vagy a cserét, így elég kényelmetlen lenne újra meg újra beírni az egészet, erre találták ki az alprogramot. Az ismételendő részeket egyszerűen beleírjuk, majd elindítjuk, mielőtt mutatnám a következő rendezést, egy pár dologgal meg kell még ismerkedni.

Alprogramok

Az alprogramok ugyanúgy néznek ki, mint main nevű főprogram, általános formája:

<típusnév> <függvénynév>(<paraméterek>)

{

utasítások

}

A <típusnév> a függvény visszatérési értékének a típusát határozza meg. A <függvénynév> a függvénynek a neve. A <paraméterek> a függvény bemenő paraméterei ilyen formában: <típus> <név>,<típus> <név>,<típus> <név> …. Itt nem lehet halmozni a neveket, úgy, mint a változómegadásnál. A függvényértéket a return utasítással adjuk meg.

Nézzünk egy példát:

float Negyzet(float x)

{

return x*x;

}

Ez egyetlen return utasítást tartalmaz. A return utasítás azt csinálja, hogy beállítja a visszatérési értéket, majd kilép az alprogramból.

Van egy olyan adattípus, ami, szó szerint, semmit nem tartalmaz, ezt úgy hívják, hogy void (ismeretlen). Ezt akkor használjuk, ha nem akarunk semmilyen visszatérési értéket, csak végrehajtunk néhány utasítást és vége.

Nem csak return-nal lehet kilépni, ha egyszerűen vége van eljárásnak, akkor vége van.

Az alprogramon belül megadhatunk változókat, meg az alprogramon kívülről is. Amit ott bent adsz meg, azt csak ott bent lehet használni, amit kint adsz meg azt mindenhol. A bent megadott változónak lehet azonos neve, mint egy külsőnek, ilyenkor az a benti változót jelenti, nem a külsőt (helyi változó).

Na elég ebből nézzük meg, hogy hogyan néz ki az előbbi buborékos rendezés függvényeket használva:

#include "stdio.h"

#include "conio.h"

 

int Elemek[10];

int i,s;

 

void Csere(int *a,int *b)

{

int s;

s=*a;

*a=*b;

*b=s;

}

 

void Rendezes(int *Szamsor,int n)

{

int i;           // Ez a bent deklarált i változó NEM egyezik meg a külsővel. A külső változó nem érhető el, így.

char voltcsere;

int s;

do

{

voltcsere=0;

for (i=0;i<n-1;i++)

{

if (Szamsor[i]>Szamsor[i+1])

{

Csere(&(Szamsor[i]),&(Szamsor[i+1]));

voltcsere=1;

}

}

}

while (voltcsere);

}

 

void main()

{

clrscr();

for (i=0;i<10;i++)

{

printf("%d:",i);

scanf("%d",&(Elemek[i]));

}

Rendezes(Elemek,10);        // Elindítjuk az alprogramot, ami rendezi az elemeket.

for (i=0;i<10;i++)

{

printf("%d,",Elemek[i]);

}

getch();

}

Két alprogramot vezettünk be itt. A rendezést kitettük egy külön alprogramba, meg a cserét is. Érdemes megfigyelni, hogy ha egy változónak az értékét szándékozzunk változtatni, akkor mindig a címét (egy pointert) kell átadni az alprogramnak, ha a változót adjuk át, akkor csak az értéket fogja megkapni. Az értékkel mit tud kezdeni? Az csak egy érték, egy szám, semmi információt nem tartalmaz a módosítandó változóról. Ha a címet adjuk át neki, akkor már fogja tudni, hogy hol van a módosítandó változó a memóriában, és tudja azt módosítani. A cserében két változót, a rendezésben a számsort tudjuk módosítani ily módon.

Amikor egy alprogramot elindítunk, akkor olyan paramétereket kell átküldeni neki, amilyet kér, különben hibaüzenetet kapunk. Tehát, ha int-re mutató pointert kért, akkor azt adjuk át neki, ha float-ra mutatót, akkor azt, stb.

Még néhány kérdés nyitott. Egy void típusú eljárásból hogyan ugorhatunk ki, ha a return után egy visszatérési értéket kell írni? A válasz az, hogy egyszerűen nem írsz utána semmit sem, hanem egy return és pontosvessző és kész. Egy másik kérdés: mi van, ha egy nem void típusú függvény esetén nem írok return-t, hogy értékkel térjen vissza az alprogram? Ekkor kapsz egy figyelmeztetést, hogy a függvénynek vissza kell térnie egy értékkel, a programod simán lefut, de a visszatérési érték nem lesz definiálva, akármi lehet, ilyen esetben ne használd visszatérésre a függvényt. Egy harmadik: hogyan férhetek hozzá a visszatérési értékhez? Egyszerűen, behelyettesíted a függvényhívást egy kifejezésbe, és már használhatod is. Például:

c=sqrt(Negyzet(a)+Negyzet(b));

Ez egy Pitagorasz-tétel, ugyebár. De lehet a függvényeket úgy is el lehet indítani, hogy nincs szükségünk visszatérési értékre. Ekkor úgy írjuk be, mint egy utasítást:

Negyzet(4);

De ennek sok értelme nincs a négyzet függvény esetében, ilyet akkor szoktak használni, ha a visszatérési érték pl. egy hibakód, és ha az minket perpillanat nem érdekel. Ha a függvényünk visszatérési típusa void, akkor csak utasításként hívhatjuk meg. (A semmit hogy írnád be egy kifejezésbe?).

Előfordulhat olyan eset is, hogy pointert kell átadni paraméterként, de még sem akarjuk azt, hogy a változó értéke megváltozhasson, pl. szövegek átadásánál. Ilyenkor írhatunk a paraméter megadása elé egy const módosítót.

Példa:

void SzovegKiir(const char *Szoveg)

{

printf("%s",Szoveg);

}

Ennek a programkódra semmi hatása nincs, de ha a fordítód módosítást észlel az adott paraméterben, akkor hibát fog kiírni.

Egy alprogramnak lehetnek alapértelmezett paraméterei is. Ezeket a paraméterlistában meg lehet adni, ez azt jelenti, hogy a paraméterlista végéről el lehet hagyni ezeket paramétereket, és akkor egy alapértelmezett értékkel fognak lefutni, nézzük a következő példát, ami a kívánt helyre, vagy a kurzorpozícióra ír:

void SzovegKiir2(const char *Szoveg,int px=wherex(),int py=wherey())

{

gotoxy(px,py);

printf("%s",Szoveg);

}

...

SzovegKiir2("ALFA");                       // Ez megfelel ennek: SzovegKiir2("ALFA",wherex(),wherey());

SzovegKiir2("BÉTA",30,20);                 // 30. oszlop 20. sorába írunk.

...

Tehát, amikor az alprogramot indítjuk, akkor a végéről lehagyott paraméterek a deklarációban megadottak szerint kapnak értéket.

Rekurzió

Ez egy elég kemény dió, ez tömörem azt jelenti, hogy egy alprogram önmagát indítja el. Olyan, mintha a DOS-promptból indítanánk egy másik COMMAND.COM-ot. Vegyünk rögtön egy példát a rekurzióra, a faktoriális függvényt:

n! = n* (n-1)!, ha 1!=1.

Azaz minden n faktor megegyezik n és (n-1) faktor szorzatával, de ahhoz, hogy ezt értelmezni lehessen, a matematikában meg kell adnunk egy határt a rekurziónak, hogy 1!=1, különben végtelen lenne a rekurziónk, és ez nem jó.

Na vezessük is ezt le, pl. 5 faktorral:

5!=5*4!=5*4*3!=5*4*3*2!=5*4*3*2*1!=5*4*3*2*1=120

Tehát a rekurzió véges számban véget ér, ha adunk neki egy határt, és azt el is éri, mint pl. itt az 1!-nál. Így ez működni fog C nyelven is, nézzük a következő függvényt:

float fakt(int n)

{

if (n==1) return 1;

return n*fakt(n-1);

}

Ez azt csinálja, ha n=1, akkor azt mondja, hogy 1 a visszatérési érték és kilép, különben n*(n-1)!. Mivel a return azonnal kilép a függvényből, ezért az if-hez nem kell külön else rész.

Ezt komolyan írjuk be egy programba, és írassuk ki:

printf("7!: %g",fakt(7));

A programnak 5040-et kell kiírnia, működnie kell.

Az a gyorsrendező eljárás, amiről korábban írtam, szintén rekurziót használ, és így néz ki:

#include "stdio.h"

#include "conio.h"

 

int Elemek[10];

int i,s;

 

void Csere(int *a,int *b)

{

int s;

s=*a;

*a=*b;

*b=s;

}

 

void QuickSorter(int a,int f,int *Szamsor)

{

int i;

int j;

int s;

if (a>f) return;

i=a;

j=f;

s=Szamsor[(a+f)/2];

do

{

while (Szamsor[i]<s)

{

i++;

}

while (s<Szamsor[j])

{

j--;

}

if (i<=j)

{

Csere(&(Szamsor[i]),&(Szamsor[j]));

i++;

j--;

}

}

while (i<=j);

if (a<j) QuickSorter(a,j,Szamsor);

if (1<f) QuickSorter(i,f,Szamsor);

}

 

void main()

{

clrscr();

for (i=0;i<10;i++)

{

printf("%d:",i);

scanf("%d",&(Elemek[i]));

}

QuickSorter(0,9,Elemek);          // Első és utolsó elemet kell megadni, meg a tömböt.

for (i=0;i<10;i++)

{

printf("%d,",Elemek[i]);

}

getch();

}

Ez nagyon hatékony akkor, ha nagy méretű sorozatokat rendezünk. Azt hiszem ebben a programban nincs új elem, amiről eddig nem szóltunk volna, lépjünk tovább.

A rekurziót szokták furcsa ábrák rajzolására is használni. A következő programot egyszerűen vágólapozd át egy fájlba, majd fordítsd le. A C++ fordítóprogram könyvtárában biztos találsz egy BGI könyvtárat, amiben remélhetőleg van egy EGAVGA.BGI fájl. Azt másold át abba a könyvtárba, ahol a programod EXE fájlja van, majd futtasd le a programot, a forráskód itt van:

#include "graphics.h"

#include "conio.h"

#include "stdio.h"

#include "math.h"

#include "dos.h"

#define PI 3.1415926535897932384626433832795 // A PI a fordító számára jelentse ezt: 3.14......

#define RAD(x) (x)*PI/180                    // A RAD(x) meg ezt: (x)*PI/180

 

typedef struct TURTLE

{

float x,y;

float angle;

void Right(float);

void Left(float);

void Fward(float);

} TURTLE;                                    // Ez az objektum, majd később foglalkozunk vele.

 

void TURTLE::Right(float fok)       // JOBBRA

{

angle+=fok;

}

 

void TURTLE::Left(float fok)       // BALRA

{

Right(-fok);

}

 

void TURTLE::Fward(float d)       // ELŐRE

{

float px,py,c;

px=x;

py=y;

x=cos(RAD(angle))*d+px;

y=sin(RAD(angle))*d+py;

line(px,py,x,y);

}

 

TURTLE t;

 

void Hilbert(float x,int rec);    // Tudatjuk a fordítóval, lesz ilyen függvény.

void Hilbert2(float x,int rec);   // Ilyen is.

 

void Hilbert(float x,int rec)

{

if (rec>0)

{

t.Left(90);

Hilbert2(-x,rec-1);

t.Left(90);

t.Fward(x);

Hilbert(x,rec-1);

t.Left(90);

t.Fward(x);

t.Left(90);

Hilbert(x,rec-1);

t.Fward(x);

t.Left(90);

Hilbert2(-x,rec-1);

t.Left(90);

}

else

{

t.Left(180);

}

}

 

void Hilbert2(float x,int rec)

{

if (rec>0)

{

t.Right(90);

Hilbert(-x,rec-1);

t.Right(90);

t.Fward(x);

Hilbert2(x,rec-1);

t.Right(90);

t.Fward(x);

t.Right(90);

Hilbert2(x,rec-1);

t.Fward(x);

t.Right(90);

Hilbert(-x,rec-1);

t.Right(90);

}

else

{

t.Left(180);

}

}

 

void main()

{

int gd=VGA,gm=VGAHI;

initgraph(&gd,&gm,"");

t.x=0;

t.y=479;

t.angle=-90;

Hilbert(4,6);       // Első paraméter a vonalhossz, a második a rekurziós szint

getch();

}

Nem előírás, hogy elemezni tudjuk ezt a programot, de hosszas nézelődés, és gyönyörködés után rájövünk, hogy mi mit csinál benne. Ez a kód teknőcgrafikát használ egy Hilbert-görbe megrajzolásához. A legfontosabb, amit tudnunk kell a prototípushasználat. Ha csak tudatni akarjuk a fordítóprogrammal, hogy lesz egy valamilyen függvényünk a későbbiekben, akkor írjuk be az első sorát, és zárjuk le egy pontosvesszővel, így a fordító tudni fogja, hogy az csak egy előjegyzés egy későbbi kifejtésre. Ebben a programban keresztrekurzió van, egyik a másikat indítja, és fordítva. (Ez amúgy elég ritka.)

Pointerek és tömbök kapcsolata

Nézzük meg azt a fenti kódrészletet a rendező eljárásban:

void Rendezes(int *Szamsor,int n)

{

int i;           /* Ez a bent deklarált i változó NEM egyezik meg a külsővel. A külső változó nem érhető el, így.*/

char voltcsere;

int s;

do

{

voltcsere=0;

for (i=0;i<n-1;i++)

{

if (Szamsor[i]>Szamsor[i+1])

{

Csere(&(Szamsor[i]),&(Szamsor[i+1]));

voltcsere=1;

}

}

}

while (voltcsere);

}

Hogyan lesz, abból a számsor pointerből tömb? Az egy pointer, és egy int típusú értékre mutat. De mi mégis tömbként használjuk, hogy lehet ez? Egyszerűen úgy hogy lehet! Oda-vissza szabály van: ha a tömb neve önmagában egy pointer, akkor a pointereknél is használhatjuk [ és ] operátorokat tömbelemek lekérésére. Ehhez bele kell mennünk az elméletbe egy kicsit. Egy tömb elemei egymás után vannak a memóriában. Ott van az első elem, ahova a pointer mutat (a tömb neve). Az int adattípus 2 bájtos, tehát a következő elem 2 bájttal arrébb van, a harmadik 4 bájttal van az alapcím után, és így tovább. Tehát, ha van egy int-re mutató pointerünk, legyen t, akkor a t[0] azt a számot jelenti, amire közvetlenül a pointer mutat, ugyanazt jelenti, mint a *t. Ha azt írjuk, hogy t[1], akkor a 2 bájttal arrébb lévő, int típusú adatot érjük el, és így tovább, a t[30] 60 bájttal arrébb lévő adatot jelenti. Persze nem csak int tömbünk lehet, hanem long double, vagy bármi más típusú adatunk is, ilyenkor az eltolás elemenként nem két bájt lesz, hanem a típusnak megfelelő: long esetén 4, long double esetén 10 bájt.

Ha pointert deklarálunk, és azt szándékozzuk tömbként használni, akkor először memóriát kell, neki lefoglalni, hogy tudjunk hova irkálni. Ha ezt nem tesszük meg, akkor a pointer, ki tudja, hova, fog mutatni, és ennek kiszámíthatatlan következményei lehetnek (pl. lefagyás, kék halál, stb.). Amúgy a tömböknél automatikusan lefoglalódik a megfelelő mennyiségű memória.

Típusmegadás

Sokszor, főleg bonyolultabb adattípusnál az áttekinthetőség növelése értelmében, definiálhatunk saját adattípusokat, ha úgy tetszik.

Megadása egyszerű:

typedef <Típusnév> <újtípus>;

Ez annyiban különbözik a változómegadástól, hogy az eredmény egy új típus lesz.

Példa:

typedef unsigned short WORD;

typedef unsigned char BYTE;

typedef unsigned long DWORD;

DWORD dw;

BYTE b;

Szerintem érthető.

Struktúrák

Persze a lehetőségek tárháza nem merül ki a számoknál és tömböknél, vannak űrlapszerű adatstruktúrák is, vagy „közismertebb” nevén rekordok a leggyakrabban használt rekordfajta a struct.

Megadása:

struct <rekordnév>

{

<Típus1> <mező11>,<mező21>,...

<Típus2> <mező12>,<mező22>,...

} <rekordváltozó>;

A rekord mezőjére úgy lehet hivatkozni, hogy:

<rekordváltozó>.<mező>

Lássunk egy példát is:

...

typedef struct tagJATEKOS

{

float x,y;

int Elete;

int LoSebesseg,LoSuruseg,Erosseg;

} JATEKOS;

...

JATEKOS Player;

...

void Indit()

{

Player.x=320;

Player.y=300;

Player.Elete=3;

Player.LoSebesseg=4;

Player.LoSuruseg=4;

Player.Erosseg=1;

}

...

Ez végül is egy játékost tároló adatszerkezet, és a játék indításáért felelős kód. Ezen a példán keresztül könnyen látható, hogy hogyan kell használni a struct-okat.

A példában észrevehettük, hogy két neve is van a struktúrának, az egyik a hivatalos neve, amit meg a typedef-ben az az új adattípus neve. Lényegében nem muszáj új adattípust deklarálni, ezért megadhattuk volna a változóinkat így is, a struktúra nevét használva:

struct tagJATEKOS Player;

Ezt is elfogadja a fordítóprogram, de ez a hivatalos név el is hagyható, ekkor a fentebb említett forma nem alkalmazható, legfeljebb csak a definiált adattípus, amit a typedef-fel hoztuk létre.

A struktúrák nevei önmagukban nem jelentik a struktúrára mutató pointert, hanem az egész struktúrát jelölik, ilyenkor teljes struktúrákat adhatunk át értékként:

JATEKOS P1,P2;

...

P1=P2;            // Teljes struktúrákat is lehet értékadásban továbbadni.

...

Egy struktúrára mutató pointeren keresztül is hivatkozhatunk a struktúra mezőjére a -> operátorral:

JATEKOS *P1;

int ls;

...

ls=P1->LoSuruseg;       // Megfelel ennek: *(P1).LoSuruseg

...

Ha a struktúránk mezője pointer, és azon keresztül akarunk értékre hivatkozni, akkor ez is lehetséges a .*, vagy ->* operátorokkal.

typedef struct FILEOBJ

{

FILE *f;                // Fájlleíróra mutat

char Nev[30];           // Max 30 betűs név.

} FILEOBJ;

...

FILEOBJ K,*K2;

FILE f,g;

...

f=K.*f;                 // Lekérjük a struktúrát (Pointer címén lévő értéket)

g=K->*f;                // Struktúrapointeren keresztül is megy.

...

...

 

Uniók

A struktúrák egy másik fajtája az unió. Ezt akkor használjuk, ha több, egymást kizáró mezője van egy struktúrának.

Az uniók összes mezeje ugyanazon a memóriaterületen van, így ha az egyikbe beleírsz, az összes másik felülíródik. Tehát az uniónak csak az egyik mezőjében tárolhatsz adatot, és azt használhatod egy időben.

A következő példa mutatja, hogy mikor is jó uniót használni:

...

typedef struct GOMBINFO

{

char State;

} GOMBINFO;

 

typedef struct EDITINFO

{

int CursorPos;

} EDITINFO;

 

typedef struct WNDINFO

{

MENU Menu;

int x,y,width,height;

CTLPROC CtlProc;

} WNDINFO;

 

typedef union INFO

{

GOMBINFO GInf;

EDITINFO EInf;

WNDINFO WInf;

} INFO

 

typedef struct CONTROL

{

int Tipus;

int CId;

char CtlText[255];

INFO Info;

} CONTROL;

...

Tegyük fel, hogy a control struktúrában a típus mező meghatározza, hogy milyen típusú adatot tárolunk a struktúrában. És az Info unióba így pakoljuk be az adatokat, ha a típus gombot jelöl, akkor GInf mezőt, ha szerkesztő mezőt, akkor az EInf mezőt, stb. Lényeg az, hogy akármi is van, mindig csak egy mezőt fogunk használni az unióban. Miért tároljunk minden mezőnek külön tárhelyet? Használja ugyanazt a tárhelyet az összes mező! Erre jók az uniók.

Az uniók mezőire ugyanúgy hivatkozunk, mint a struktúrákéra.

Csinálhatjuk azt is, hogy a változóinkat rakjuk egy helyre. Ekkor anonim uniókat kell használni:

static union

{

int a,b,c;

};

Ha így deklaráljuk ezt, akkor az a, b és c változók egy helyre fognak kerülni a memóriában, így egyszerre csak az egyikben tárolhatsz adatot, úgy, mint a többi unióban.

Típuskonverzió (kasztolás)

Amikor az értékadás műveletét végezzük, akkor ügyelnünk kell arra, hogy megfelelő értéket adjuk át a változóknak, különben hibát kapunk. Szám típusú változóknak akármilyen számot átadhatunk, legfeljebb elvesztik a törtrészüket, vagy értékes számjegyeket veszítenek el, de ez hibát nem okoz, maximum a fordítóprogram kidob egy figyelmeztetést, hogy lehet, hogy ott gubanc lesz. Tehát számok esetén megpróbál konvertálni. De valamikor nem tud konvertálni, pl. pointerek esetén. Nem tud float-ra mutató pointerből int-re mutató pointerre, vagy long-ból pointerre konvertálni, ilyenkor segíteni kell rajta. Ha a kifejezés elé zárójelbe odaírod a típus nevét, akkor el fogja neked hinni, hogy az az az adattípus, amelyet kért, és elhallgat.

A lényege ennek az egésznek az, hogy több adattípus van, amely ugyanannyi bájtot foglal, csupán értelmezés kérdése, hogy ezeket a bájtokat miként értelmezzük. A 4 bájtot értelmezhetjük egy long vagy float típusú számként vagy egy pointerként, ami ráadásul mutathat int-re, char-ra, long-ra, akármire. De mégsem másolgathatjuk egymás között ezeket az adatokat szabadon át (pl. pointerbe nem írhatunk long típusú értéket). Ennek a problémának a megoldására van a kasztolás, hogy megadhassuk a fordítóprogramnak, hogy minként értelmezze azokat a bájtokat.

DOS programozásnál gyakran előfordul a kasztolás, amikor futás közben memóriát foglalsz le:

int *p;

...

p=(int*)malloc(1000);

...

A malloc függvény visszatérési értéke egy void-ra (típusnélküli) mutató pointer, és az nem ugyanaz, mint az int-re mutató társa, ezért erősködik a compiler, hogy ez nem jó, de mi tudjuk, hogy az, és átkasztoljuk azt a mutatót int típusúra, így már működni fog a kódunk. Ez a pointerek közti átkasztolás. Az adat változatlan marad, ugyanarra a címre fog mutatni ugyanúgy, tehát ez az átkasztolás jó.

Valamikor számot kell pointerré, vagy fordítva. Amikor Windows programot írunk, és az ablakkezelőnek küldünk parancsokat, akkor nagyon gyakran szoktunk átkasztolni:

...

char ButtonText[40]="(alapértelmezés)";

...

SendMessage(ComboBox,CB_ADDSTRING,0,(LPARAM)ButtonText);

...

A SendMessage utasítás 4. paramétere egy long típusú értéket vár (ott ezt LPARAM néven is használják), de mi szöveget akarunk áterőszakolni, ami, ugye egy pointer és nem long. Ezért átkasztoljuk long-gá, végül is teljesen mindegy, hogy miként küldjük át azt a négy bájtot (ne felejtsük el, hogy a tömb neve egy 4 bájtos pointer). A kasztolás eredményeként az adat sohasem változik meg, csak az értelmezése. (Kivétel ez alól a számok konvertálása, ha pl. egy long típusú értéket kasztolunk át short-ra, akkor a tartomány szűkülése miatt értékes számjegyek veszhetnek el.)

Óvatosan bánjunk a számok pointerré való átkasztolásakor, ha nem a megfelelő számot kasztolunk át pointerré, és azt használjuk, akkor ki tudja, hogy hol fogunk piszkálni a memóriában, és ez kiszámíthatatlan következményekkel járhat (DOS alatt lefagyás, Windows alatt legtöbbször „access violation” hiba, vagy akár kék halál). Gyakori, hogy egy ilyen, kasztolási baklövésünket észre sem vesszük, és a program sem mutat tüneteket először, aztán meg, egy módosítás után, hirtelen olyan utasításoknál kezdenek hibák történni, ahol az teljesen abszurd (pl. egy printf utasítástól szarik be). Ugyanez van a tömböknél való indextúllépéseknél is (pl. a 30 elemű tömbnek a 42. elemét akarjuk módosítani). Tehát nagyon ügyeljünk arra, hogy a memóriában ne „legeltessük a gépünket” rossz helyen, mert nem örülnek neki…

Fájlok kezelése

Háromféle fájl létezik: bináris fájl, adatfájl és textfájl. A bináris fájlt bájtonként lehet olvasni, az adatfájlt karakterenként, a text fájlból úgy lehet beolvasni az adatokat, mint a scanf esetében. A gyakorlatban vagy bináris, vagy textfájlt használnak.

Az STDIO.H-ban van megadva az az adattípus, amellyel a fájlokat kezelni lehet, a neve FILE.

A programjainkban egy erre mutató pointert kell majd használni.

Megnyitás

A fájlt valahogy meg is kell nyitni. Erre van az fopen utasítás, így van deklarálva:

FILE *fopen(const char *fajlnev, const char *mod);

Paraméterek:

fajlnev:                  a fájl neve, amit meg akarunk nyitni.

mod:                             a megnyitás módja, ez is egy szöveg. Az egyik ezek közül:

r                                Megnyitja a fájlt csak olvasásra. A fájlkurzor a fájl legelején lesz.

w                                Létrehozza a fájlt írásra, ha már létezik, akkor felülírja.

a                                Megnyitja a fájlt írásra úgy, hogy a fájlkurzort a végére teszi, ha nem létezik a fájl, létrehozza azt.

r+                             Megnyitja a fájlt olvasásra/írásra, a fájlkurzor a fájl elején lesz.

w+                             Létrehoz egy új fájlt olvasásra/írásra, ha a fájl létezik, felülírja azt.

a+                             Megnyitja a fájlt olvasásra/írásra, úgy, hogy a fájlmutató a fájl végén lesz. Létrehozza a fájlt, ha nem létezik.

                                   További két betű utánaírható, amivel a fájl típusát meghatározhatjuk, az egyik ezek közül:

b                                A fájl bináris.

t                                A fájl egy textfájl.

Egyik sem            A fájl egy adatfájl.

Megjegyzés: a b és t mód használható egy bármely másikkal (pl. rb vagy w+t stb.), de a kettő együtt nem.

Visszatérési érték:

Egy FILE struktúrára mutató pointer, amivel a fájlunkat kezelhetjük, fontos, hogy ne módosítsuk ezt a struktúrát. Hiba esetén a visszatérési érték NULL lesz.

Fájlmutató

A fájlmutató olyan, mint egy kurzor a szövegszerkesztőben. Megnyitáskor általában a fájl legelején, vagy a legvégén van. Minden egyes fájlművelet után előrébb megy annyi bájttal, ahányat kiolvastunk, vagy beírtunk. Ha olvasási művelet során a kurzor eléri a fájl végét, akkor hiba történik, ha írás közben éri el, akkor bővíti a fájlt. Ha íráskor a fájlmutató a fájlban van, akkor felülírja az ott lévő adatot.

Fájlmutató beállítása:

A fájlmutatót tetszés szerinti pozícióra állíthatjuk be a következő utasítás használatával:

int fseek(FILE *f, long mennyit, int honnet);

Paraméterek:

f:                                Fájlleíróra mutató pointer, amit beállítunk.

mennyit:                Egy egész szám, megmutatja, hogy mennyivel kell előrébb mozdítani a kurzort, a honnet paraméterben megadott ponthoz képest.

honnet:                     Megadja, hogy az előző paraméterben megadott érték honnét lesz számolva. A következő értékek közül az egyik:

SEEK_SET:           Az elejétől számol.

SEEK_CUR:           A pillanatnyi pozíciótól számol.

SEEK_END:           A fájl utolsó bájtjától számol.

Visszatérési értékek:

Ha a művelet sikeres, akkor nullával tér vissza, ha nem, akkor egy nem nulla értékkel.

Olvasás/írás

A fájl meg van nyitva, most már csak írni/olvasni kell a fájlt, erre van két, nagyon hasonló eljárás:

size_t fread(void *ptr, size_t meret, size_t db, FILE *f);

size_t fwrite(const void *ptr, size_t meret, size_t db, FILE *f);

Ahol:

typedef unsigned int size_t;

Paraméterek:

ptr:                             Arra a memória területre mutat, ahová beolvasni, vagy ahonnan fájlba írni akarunk adatokat.

meret:                       Mekkora méretű adatrekordokat akarunk kiolvasni.

db:                               Hány darabot.

f                                   fájlleíróra mutató pointer.

Visszatérési érték:

A visszatérési érték sikeres adatforgalom esetén átküldött rekordok száma (nem bájtok). Hiba esetén (fájl vége) kevesebb.

Megjegyzések:

A fájlból db*meret bájt lesz kiolvasva.

Bezárás:

Ha végeztünk, a fájlt be kell zárnunk, ez egy egyszerű utasítás:

int fclose(FILE *f);

Ez hiba esetén az EOF-fal, siker esetén nullával tér vissza.

Fájlkezelési példa:

...

FILE *f;

char PalyaElemek[400];

PLAYER Jatekosok[8];

...

f=fopen("mysave.sav","rb");

fread(PalyaElemek,sizeof(char),400,f);

fread(Jatekosok,sizeof(PLAYER),8,f);

fclose(f);

Textfájlok:

Ha az fopen-ben a módnál megadod a t módosítót, akkor textfájlt kapsz, ezt úgy olvashatod/írhatod, mint a képernyőt, a fscanf és a fprintf segítségével.

A két függvény deklarációja:

int fscanf(FILE *f, const char *formatum[,cim, ...]);

int fprintf(FILE *f, const char *formatum[, argumentum, ...]);

Ezek teljesen megegyeznek, mint az f betű nélküli társuk, csak ezek fájlból olvasnak, illetve írnak fájlba.

Visszatérési értékük az eltárolt/elmentett bájtok száma. Hiba esetén az EOF értéke.

Objektumokról általában

Rögtön az elején: az objektumokat csak a C++ nyelvben lehet használni!

Ha csinálsz egy struct-ot, akkor általában meg kell írnod a hozzá való eljárásokat. Azonban lehetőséged van, hogy a struktúrához hozzácsatold mezőként az azt kezelő eljárásokat (amiket metódusoknak is hívnak). Úgy, hogy benne deklarálod. De deklarálhatod rajta kívül is, ekkor használni kell a :: operátort, hogy azonosítsd az alprogramot. Itt van egy példa mindkettőre:

typedef struct CSILLAG

{

float px,py;

void Kirajzol()

{

putpixel(px,py,WHITE);

}

} CSILLAG;

 

Így is lehet:

 

typedef struct CSILLAG

{

float px,py;

void Kirajzol();

} CSILLAG;

...

void CSILLAG::Kirajzol()              // Jelentjük a fordítónak, hogy ez az alprogram a CSILLAG objektumhoz tartozik

{

putpixel(px,py,WHITE);

}

Láthatjuk, hogy az objektumhoz tartozó eljárás úgy fér hozzá a saját mezőihez, mint közönséges változókhoz, ez meg van engedve nekik. A saját hatáskörébe tartoznak, tehát használhatja nyugodtan azokat.

Tehát egy objektum nem más, mint az adat és az azt kezelő kód együttese.

A következő néhány témakör az objektumokkal foglalkozik.

Az objektumokban lévő alprogramot metódusnak is szokták hívni.

A mezőket és metódusokat együttesen tagoknak szokták hívni.

Objektumok publikus és privát tagjai

A struktúráknak alapértelmezetten van egy hozzáférési beállításuk, az alapértelmezés az, hogy minden elemhez bárhol, bármikor hozzá lehet férni. De nem csak struktúrák vannak, hanem vannak osztályok is, ezek egy kaptafára mennek a struktúrákkal, azzal a különbséggel, hogy egy szóban különböznek:

class <név>

{

<meződeklarációk>

} <változók>;

Ha osztályt adsz meg, akkor annak minden mezője és metódusa privát lesz. Csak az azt kezelő eljárások férhetnek hozzá az adatokhoz. Lássunk egy példát:

class tagCSILLAG

{

float x,y;

void Beallit(int x,int y)

{

tagCSILLAG::x=x;

tagCSILLAG::y=y;

}

void Megjelenit()

{

putpixel(x,y,WHITE);                // Itt lehet használni

}

} Univerzum[1000];

...

void main()

{

...

Univerzum[34].x=56;                      //Ezt nem lehet, nincs hozzáférés az x-hez.

Univerzum[45].Beallit(344,566);          //Ehhez sem?

...

}

Ezzel csak az a baj, hogy semmihez sem férhetünk hozzá benne. Abszolút biztonságos… De azért jó lenne, ha a beállító és a megjelenítő eljáráshoz hozzá tudnánk férni. Erre van lehetőségünk, csak egy szót kell beírnunk:

class tagCSILLAG

{

float x,y;

public:

void Beallit(int x,int y)

{

tagCSILLAG::x=x;

tagCSILLAG::y=y;

}

void Megjelenit()

{

putpixel(x,y,WHITE);

}

} Univerzum[1000];

...

void main()

{

...

Univerzum[34].x=56;                      //Ezt még mindig nem lehet, nincs hozzáférés az x-hez.

Univerzum[45].Beallit(344,566);          //Most már hozzáférhetünk

...

}

 

Azzal a public szócskával jelöljük, hogy minden, ami utána van az publikus lesz az osztályon belül, azok minden alprogram számára elérhetők lesznek.

Tehát már tudjuk mit jelent az, hogy privát és publikus mező.

Egyébként van lehetőség a struktúrákban privát mezőket létrehozni, oda a private: kifejezést kell egyszerűen beszúrni.

Objektumok származtatása

Előfordulhat, hogy két majdnem tök egyforma objektumod van, és szinte teljesen ugyanazt a dolgot csinálják, ekkor ahelyett, hogy kétszer deklarálnánk ugyanazt az objektumot, lehetőségük van arra, hogy a „fejlettebb” objektumot származtassuk az eredetiből. Lássuk a példát: egy 2D-s pontobjektumot felfejlesztünk 3D-sre.

Ez már nagyon objektum-orientált dolog, én is nehezen nyeltem le.

A lényeg az, hogy a származtatott objektumba csak azokat a tagokat kell beletenni, amit a meglévőhöz hozzáadunk.

#define SX 320

#define SY 240

#define ALAPTAV 1000

 

typedef struct

{

float x,y;

void Megjelenit()                        // 2D-s pont kirajzolása

{

putpixel(x,y,WHITE);

}

} PONT2D;                                      // Eredeti 2D-s pontobjektum

 

typedef struct : PONT2D

{

float z;

void Megjelenit()                        // 3D-s pont kirajzolása

{

float k,xk,yk;

k=ALAPTAV/z;

xk=x*k;

yk=y*k;

xk+=SX;

yk+=SY;

putpixel(xk,yk,WHITE);

}

} PONT3D;                                      // Ez a továbbfejlesztett objektum

Két struktúránk van, mind a kettőnek az összes tagja publikus. A PONT3D nevű struktúra örökli az összes mezőjét a PONT2D-nek (kettőspont után jelöltük az öröklött objektumot), így benne lesz az összes abban lévő tag a PONT3D-ben is, így x és az y valamint a megjelenítő eljárás is, de azt felülírtuk egy másikkal, így a PONT3D-ből indított megjelenítő eljárás ahhoz fog tartozni, a PONT2D-ből indított a 2D-shez. De ez a felülírás csak látszólagos, ha nagyon azt akarod, hogy a PONT3D-ből a 2D-s pontkirajzoló eljárást érd el, akkor azt így teheted meg:

P3d.PONT2D::Megjelenit();                      // A p3d egy PONT3D típusú változó

Persze van lehetőségünk privátan, és publikusan is örökölni egy objektumot, ha privátan öröklünk, akkor az örökölt objektum mezői és metódusai privátak lesznek az objektumnak, így csak az férhet hozzájuk, külső alprogram nem. Ehhez csak egy szót kell hozzáírni a kódhoz:

typedef struct : private PONT2D...

Vagy ezt:

typedef struct : public PONT2D...

Vagy ezt:

typedef struct : protected PONT2D... // Lásd alább

Struct-ok esetén az alapértelmezés a publikus, class-ok esetén a private.

Tehát:

Public esetén az örökölt objektum tagjait úgy örököljük, ahogy vannak: a publikus publikus marad, a védett védett, a privát pedig privát lesz.

Protected esetén minden publikus tag védett lesz, a többi hozzáférése nem változik.

Private esetén minden egyes tag és metódus privát lesz az objektumon keresztül.

Objektumok védett tagjai

Lehetőségünk van arra, hogy az objektum tagjai örökölhetők legyenek, de ne férhessen hozzájuk semmi külső eljárás vagy függvény. Ezek a védett tagok, ezeket úgy különbözetjük meg, hogy az objektumban protected: előjegyzés után következnek. Végül is ennyi az egész.

Objektumok virtuális metódusai

Lehetnek egy objektumnak virtuális metódusai is, ez akkor jó, ha van egy előre definiált objektumod, de te szeretnél egy jobbat, ezért upgrade-eled; magyarán származtatsz belőle egy másik objektumot, ami többet tud, mint az előző. Eddig még minden érthető. Azonban szeretnéd, ha ez az objektum egy kicsit másképp is működne, tehát felülírod az egyik metódusát, de amikor elindítod a programot, rájössz, hogy a metódus nem is működik.

Lássunk egy példácskát:

typedef class GLRENDERINGCONTEXT

{

      protected:

            virtual void Drawing() {}

      public:

            HGLRC hglrc;

            HWND Window;

            TIMER *FPSDraw;

            TIMER *Sec1;

            void Initialize() {}

            GLRENDERINGCONTEXT(PIXELFORMATDESCRIPTOR &pfd,HWND Window);

            ~GLRENDERINGCONTEXT();

            void DrawProc();

} GLRENDERINGCONTEXT;

...

void GLRENDERINGCONTEXT::DrawProc()

{

      if (Sec1->Ellapsed())

      {

            DFPS=DFPSC;

            DFPSC=0;

            Sec1->Set(1000);

      }

      if (FPSDraw->Ellapsed())

      {

            DFPSC++;

            Drawing();

            FPSDraw->Set(FPSDRAW);

      }

}

...

typedef class MYRENDERER : public GLRENDERINGCONTEXT

{

      public:

            MYRENDERER(PIXELFORMATDESCRIPTOR &pfd,HWND Win);

            ~MYRENDERER();

            void Initialize();

            void Drawing();

} MYRENDERER;

...

void ExampleProc()

{

      GLRENDERINGCONTEXT *Original;

      MYRENDERER *MyRender;

      ...

      Original->DrawProc();

/* GLRENDERINGCONTEXT::DrawProc-ot indítja el, azon belül a GLRENDERINGCONTEXT Drawing metódusát*/

      MyRender->DrawProc();

/*A GLRENDERINGCONTEXT DrawProc-át indítja el, azonban MYRENDERER Drawing eljárását fogja használni. Ha nem lenne a Drawing virtuális, akkor továbbra is a GLRENDERINGCONTEXT Drawing eljárását hívná.*/

      ...

}

 

Van két objektumunk.

Az alap objektumnak van egy DrawProc() eljárása, ami hivatkozik egy Drawing() metódusra. Ez érthető is.

Ha azonban egy belőle származtatott objektumban definiálnánk egy másik tökéletesen ugyanolyan paraméterezésű és visszatérési típusú Drawing() eljárást, akkor is az a származtatotthoz fog tartozni, és nem az alap objektumhoz, így ha a DrawProc-ot hívjuk a származtatott objektumból, akkor az az alapobjektum Drawing-ját fogja elindítani, és nem a származtatottét.

Ha azonban az alap osztályban a Drawing-ot virtuálisnak állítjuk be, akkor bármely az objektumból származtatott másik objektumban deklarált Drawing eljárás nem a származtatott objektumé lesz, hanem úgy viselkedik, mintha felül írtuk volna az eredetit. Magyarán a származtatottból elindított DrawProc (amit ne felejtsünk el, hogy az alap objektumban van) a származtatott objektum Drawing-ját fogja elindítani.

Fontos: A nem tökéletesen ugyanolyan paraméterezésű és visszatérési típusú a felüldefiniált virtuális metódus, akkor virtuálismetódus-mechanizmus nem fog működni.

A gyakorlatban gyakran találkozunk virtuális metódusokkal. Például akkor, amikor ObjectWindows platformon programozunk: ott az egyik ablakkezelő objektumból származtatni kell egy másikat, és felül kell írni az ablakkezelő eljárásokat, hogy az objektum úgy táncoljon, ahogy mi fütyülünk.

Ha az előbbi nem volt érthető, akkor egy kicsit rövidebb példaproggi:

typedef struct A

{

    void NonVirtual() {printf("A:NonVirtual\n");}

    virtual void Virtual() {printf("A:Virtual\n");}

    void CallEach() {NonVirtual_A(); Virtual_A();}

} A;

 

typedef struct B : A

{

    void Virtual() {printf("B:Virtual\n");}

    void NonVirtual() {printf("B:NonVirtual\n");}

} B;

 

void main()

{

    A *a;

    a=new A; // Belerakunk egy A-ra mutató pointert

    a->CallEach(); printf("\n");

    delete a;

    a=new B; // Belerakunk egy B-re mutató pointert. A B virtuális metódusai fognak elindulni.

    a->CallEach();

}

 

Ezt írha ki:

A:NonVirtual

A:Virtual

 

A:NonVirtual

B:Virtual

 

Tehát a virtuális metódusok újradefiniálása egy származtatott objektumban egyenértékű a metódus felülírásával.

A géped onnan tudja egy szál pointer-ből megállapítani, hogy az milyen típusú objektumra mutat, hogy tárol arra vonatkozó adatokat is. Csak olyan objektumokról tárol ilyen infót, amelyeknek van legalább egy virtuális metódusa (vagy olyan objektumból van származtatva). Ilyen infót tárol minden virtuális metódusról. A virtuális metódust tartalmazó objektumokat polimorf objektumoknak is nevezik.

Megjegyzés: a felülírt virtuális metódus is virtuálisnak értendő, akár odatesszük a virtual kulcsszót, akár nem.

Konstruktorok

A konstruktorokkal tudjuk inicializálni az objektumainkat. A konstruktorok automatikusan deklarálva is vannak, és automatikusan végre is hajtódnak. A konstruktorok olyanok, mint a metódusok, de nem térhetnek vissza értékkel, és a nevük megegyezik az objektum nevével. Nézzünk egy példát erre.

typedef class tagA

{

int x,y;

tagA()                               // Ez a konstruktor

{

x=0;

y=0;

}

} A;

 

A a;                             // Már deklarálákor végrehajtja a konstruktort, és az x és y tagot nullára állítja.

 

A konstruktorok mindig automatikusan hajtódnak végre, nem indíthatod őket el közönséges alprogramként. Többféle konstruktor van. Ez itt az alapértelmezett konstruktor, ami akkor hajtódik végre, mikor deklarálod a változót.

Érdemes megemlíteni, hogy a származtatott objektumok létrehozásakor először az alaposztály konstruktora, majd a származtatott konstruktora hajtódik végre.

Paraméteres konstruktorok:

A konstruktornak lehetnek paraméterei is:

typedef class tagA

{

int x,y;

tagA(int a,int b)                    // Ez a konstruktor

{

x=a;

y=b;

}

} A;

 

A a(3,6);                        // Deklaráláskor meg kell adni a kezdő paramétereket.

 

Láthatjuk, hogy paraméteres konstruktor esetén meg kell adni a kezdő paramétereket annak, különben hibát fog kiírni, magyarán definiálni kell a mező értékét a konstruktoron keresztül. Ezt elkerülhetjük, ha alapértelmezett paramétereket használunk:

typedef class tagA

{

int x,y;

tagA(int a=0,int b=0)      // Ez a konstruktor

{

x=a;

y=b;

}

} A;

 

A a;                             // Nem kell megadni a kezdő értékeket. Megfelel ennek: A a(0,0);

A b(3,5);                        // De meg lehet

 

Most nem kell feltétlenül megadni a kezdőértékeket. A fenti kezdőértékezés azt jelenti, hogy ha az adott paramétert nem adjuk meg, akkor azt nullának veszi, vagy bármilyen más számnak, ha azt írjuk oda. Magyarán ilyenkor elhagyhatók a paraméterek.

Többszörös konstruktor

typedef class tagX

{

double dpart;

int ipart;

tagX(double d)             // a valós részt eltároló konstruktor

{

dpart=d;

}

tagX(int i)                // az egész részt eltároló konstruktor

{

ipart=i;

}

} X;

...

X x((double)3.14);               // Ez a valós részt tároló konstruktort hívja meg. Konvertálás szükséges.

X x2(5);                         // Ez az egészrészt tároló konstruktort indítja el. Ez biztos, hogy egész szám.

...

 

A géped a változómegadásnál azt a konstruktort fogja elindítani, amelyiknek a paraméter megfelel. A fenti a double-öst. A lenti az int-est. A valósnál muszáj konvertálni, mert a fordítóprogram csak így tudja eldönteni, hogy melyik konstruktort indítsa el, a 3.14 konstans mehet az egészeshez is, meg a valósoshoz is.

Konstruktorok és az inicializáció

Lehet értékadással is inicializálni az objektumokat:

...

typedef class Y

{

int ipart;

float fpart;

char strpart[50];

Y(int i)                   // Ez a konstruktor az ipart-ot állítja be.

{

ipart=i;

}

Y(float f)                 // Ez meg a fpart-ot.

{

fpart=f;

}

Y(char *str)               // Ez az strpart-ot

{

strcpy(strpart,str);

}

} Y;

 

...

Y yvar=1;                        // Megegyezik yvar(1)-gyel. Y(int) constructor-t hívja.

Y yvar2=(float)3.14;             // Megegyezik yvar2(3.14)-gyel. Y(float) konstruktort indítja el.

Y yvar3="Corrin";                // Megegyezik yver3("Corrin")-nal. Y(const char*) konstruktor indítja el.

...

Ennyit a konstruktorokról. Több is van, ehhez nézd meg a súgót. Szerintem még ennek sem fogod a felét se használni, hacsaknem OOP fan vagy.

Ja, és a konstruktorok nem lehetnek virtuálisak.

Destruktorok

Egy kaptafára mennek a konstruktorokkal. Csak eléjük kell vágni egy tilde jelet, továbbá egy paramétere sem lehet. A feladata az lenne, hogy felszabadítsa az esetlegesen lefoglalt memóriát, mielőtt kitörlődik az azt kezelő objektum a memóriából.

A gyakorlati haszna az, hogy az objektum megcsináljon pár dolgot, mielőtt törlődik (megírja a végrendeletét és kiadjon egy halálsikolyt J ). Például egy űrhajó, amikor felrobban, létrehoz egy robbanáskezelő objektumot, és kiadja a bumm hangeffektet.

Íme egy példácska:

typedef class tagA

{

int x,y;

~tagA()                              // Ez a destruktor

{

...                            // Azt írsz ide, amit akarsz...

}

} A;

 

A destruktorok szintén automatikusan hívódnak meg, akkor, mikor az objektumok kitörlődnek a memóriából. A destruktor lehet virtuális. Persze ha exit-tel vagy abort-tal lépsz ki a programodból, akkor nem fog elindulni.

Amikor egy származtatott objektumot törlünk a memóriában, akkor először annak a destruktora fog végrehajtódni.

Virtuális destruktorokról bővebben

Legyen egy A objektumunk. Származtassunk belőle egy B objektumot. Legyen a egy A-ra mutató pointer típusú, míg b egy B-re mutató pointer típusú változó. Tároljunk el az a változóban egy A-ra mutató pointert (a=new A), majd töröljük az objektumot (delete a). Az A objektum destruktora fog végrehajtódni, eddig jó. Most b-be egy B-re mutatót (b=new B), majd töröljük ezt is (delete a). Először a B destruktora hajtódik végre, majd az A-é; ez még mindig jó. Most tároljunk el az a-ban egy B-re mutató pointert (a=new B), majd töröljük ezt is (delete a). Azt tapasztaljuk, hogy csak az A destruktora hajtódik végre, holott B-re mutató pointert tároltunk. Ez helyes működés, hisz az a típusa A*, ezért a fordító úgy látta jónak, hogy csak az A-ra vonatkozó destruktort törli. Ha a destruktort virtuálisnak deklaráljuk, akkor figyelembe veszi azt, hogy milyen típusú objektum van a pointer alatt, így az előbbi esetben, amennyiben az A destruktora virtuálisnak lett deklarálva, akkor megfelelően fog lefutni: először a B, majd az a destruktora hajtódik végre.

Hasznos tanácsok kezdőknek és haladóknak