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:
Mi a különbség C és a C++ között?
Várakozás egy billentyű leütésére
Pointerek és tömbök kapcsolata
Objektumok publikus és privát
tagjai
Objektumok virtuális metódusai
Virtuális destruktorokról bővebben
Algoritmusok
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ó.
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.
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.
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");
}
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.
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ű.
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ű.
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);
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 |
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");
.
. .
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.
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 } ...
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;
...
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;
. . .
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.
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.
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.)
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.
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.)
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ó.)
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.
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.
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 "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.
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 "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 "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.
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ő.
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.
...
...
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.
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…
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.
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.
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.
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.
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.
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
A programozást is az A betűnél kell kezdeni. Nem érdemes rögtön a C/C++ nyelvekkel kezdeni!
Definiálj (#define ) szimbolikus konstansokat az állandó értékek (például a limitek) tárolására, ne írd mindig mindenhova oda a pontos értékeket, írd a szimbolikus konstanst mindenhova. Ennek előnye az, hogy nem kell mindenhol átírni az értéket, ha úgy gondolod, hogy meg kell azt változtatni. Csak a konstansnál írod át azt az egy sort!
Mindig használj for ciklust, ha a ciklusnak bármi számlálásos jellege van. A nem kezelt eseteket egyszerűen hagyd ki a continue, míg a ciklus megszakítására használj break utasítást!
Ha több egymásba ágyazott ciklusból akarsz kiugrani, akkor használj goto utasítást, és ugorj vele a ciklus utáni első utasításra! Bizonyos C-hez hasonló nyelvekben, mint a PHP, már lehet a break és a continue utasításokkal több ciklusszinttel ugrálgatni.
A végtelen ciklus szervezéséhez használd a for(;;) {} konfigurációt!
Ha egy függvénynek egy esetet nem kell kezelni, akkor több egymásba ágyazott if utasítás helyett if () return; konfigurációt használd a függvény futásának a befejezésére!
Minden több utasításból álló részt tegyél külön egy alprogramba! Tedd ugyanezt a különböző feladatokat végrehajtó utasításokkal is.
A kis programokat nyugodtan lehet hagyományos módon, míg a nagyon nagy programokat érdemes objektum-orientáltan megírni.
Ha objektum-orientált programot írsz, akkor minden objektumhoz csinálj külön header fájlt, és egy azonos nevű cpp fájlt, így áttekinthető lesz a projekted!
Ha a programod futási hibával áll le, mert rossz helyen babrálja a memóriát, és még a fejlesztőkörnyezeted se tudja megmondani, hogy melyik sorban történt a hiba (és jobb esetben assembly nyelvű halandzsát lök eléd, vagy azt se), akkor használd a fejlesztőkörnyezeted Call stack műveletét (ha van, mákod van), ez segíthet, hogy melyik alprogramban következett be a hiba, és lerövidíti a hibakeresési időt.
Sose ments pointert fájlba! (Bizony nekem időnként sikerül, és van is nagy gubanc miatta...)
Ha te is olyan programot írsz, amelyikhez folyamatosan újabb és újabb funkciókat pakolsz, és így változik a mentett állomány szerkezete is, akkor ahhoz, hogy a programod a régebbi verziójú fájlokkal is kompatibilis legyen, van néhány tippem:
Minden struct fájlba mentése előtt annak méretét írd be először a fájlba.
Az új elemeket csak a struct végére teheted.
C++ nyelvben mindig adj meg a struct-oknak is alapértelmezett konstruktort, hogy a struktúra létrehozásakor a mezőnek legyenek alapértelmezett értékei.
A fájlból való beolvasáskor először beolvasod a méretet, és annyi bájtot olvasol be a struktúrába, amennyit a fájl mentésekor kiírtál.
De van más lehetőség is kompatibilis fájlok létrehozására:
Mentheted a fájlt inicializációs fájl formátumban (mint amilyenek a *.ini fájlok).
Mentheted XML formátumban.
Mentheted RIFF formátumban is (ilyenek a WAV és MP3 fájlok is.)
Ezekben az a közös, hogy a felépítésük olyan, mint a registry-é: kulcs-érték párokként vannak tárolva az értékek. A fájlok olvasása sokáig tarthat, de bolondbiztosak.