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á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
Algoritmusok
A C/C++ nyelv előre
definiált alprogramjai
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!
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
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 2bá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 (később lesz róla szó):
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. 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.
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","r");
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.
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. Ú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.
#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...
Struct-ok
esetén az alapértelmezés a publikus, class-ok esetén a private.
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
Nem
tudom, mi a fene ez, mert nem preferálom az
objektum-orientált programozást ilyen szinten. De néhány dolgot tudok róla
írni. Először is akkor van értelme ennek az egésznek, ha egy objektumnak van
leszármazottja (kezd bonyolódni), továbbá pointer mutat egy objektumra (még
bonyolultabb).
Nézzünk
meg egy forráskódot:
#include "stdio.h"
#include "conio.h"
#include "graphics.h"
typedef struct
{
float x,y;
void Func()
{
printf("Func
A\n");
}
} A;
typedef struct : A
{
float z;
void Func()
{
printf("Func
B\n");
}
} B;
B b;
A a;
A* pa;
void main()
{
clrscr();
a.Func();
b.Func();
pa=&a;
pa->Func();
pa=&b;
pa->Func();
getch();
}
Először
is, mit csinál? Két objektumunk van: A és B. B az A leszármazottja, tehát csapot-papot örököl belőle. Egyelőre
ennyi. Menjünk tovább, 'a' és 'b' változóval hivatkozunk az objektumokra.
Először elindítjuk 'a' és 'b' változók függvényét, rendre kiírja, hogy 'FUNC A', meg azt is, hogy 'FUNC B'. Ez rendjén van. Most jön a
csavar. A 'pa' pointernek először az 'a' változó értékét adjuk át, és
elindítjuk a funkciót, kiírja, hogy 'FUNC A'. Ez még
mindig oké. A csavarodás most jön. Egyrészt hogyan lehet egy A-ra mutató
pointerbe a B objektum címét átadni? Úgy, hogy lehet, de a B-ből csak azok
mennek át a pointerbe, amik az A-hoz tartoznak: x, y változok valamint a Func
nevű metódus. Ha megint hivatkozunk rá, akkor a B-nek az A-hoz tartozó
adataival hajtunk végre az A-hoz tartozó Func-ot, tehát ismét azt írja ki, hogy
'FUNC A'.
De
most csináljuk azt, hogy az A-ban lévő alprogramot
virtuálisra állítjuk így:
typedef struct
{
float x,y;
virtual void Func()
{
printf("Func
A\n");
}
} A;
Ha
most lefuttatjuk a kódot, akkor a pointeres résznél, a másodiknál most azt
fogja kiírni, hogy 'FUNC B', tehát nem csak az adatok mennek át, hanem egy
információ is arról, hogy melyik alprogramot is kell lefuttatni, ezt valami
virtuális metódus táblának hívják. Fogalmam sincs, hogy ennek az egésznek mikor
is van haszna. De ha majd lesz, akkor írok róla…
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.
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.
Í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.
Ennyit
erről. Több is van, de a C++ súgójában, majd kiírom, ha nagyon kell valakinek.
Sosem használtam még destruktort, amúgy. Csak akkor szokott kelleni, ha
memóriát kell felszabadítani, mielőtt kitörölnéd az objektumot.
A
következő lista az STDIO.H-ban szereplő alprogramokat
mutatja be. Részletes leírásért nézd meg a súgót.
_fsopen Megnyit
egy fájlt megosztással.
_fstrncpy Adott
számú bájtot másol az egyik karakterláncból, levág, ha kell.
_pclose Lezár
egy parancsértelmező csatornát.
_popen Létrehoz
egy parancsértelmező csatornát.
_strerror Egy
saját hibaüzenetet tárol el.
clearerr Törli a hiba állapotot a fájlleíróról.
fclose Lezár egy fájlleírót.
fcloseall Lezár minden fájlleírót.
fdopen Fájlleírót csinál
egy kezelőváltozóból.
feof Igazat ad, ha a fájl végére értünk.
ferror Igazat ad, ha a fájlleíróban hiba
történt.
fflush A fájlbufferben tárolt adatokat kiírja a
fájlba.
fgetc Beolvas egy karaktert a fájlból.
fgetchar Beolvas egy karaktert a standard
bementről.
fgetpos A fájlmutató értékét adja vissza.
fgets Beolvas egy egész sort egy fájlból.
fileno Visszatérési értéke a fájlleíró kódja.
flushall Az összes fájlleíró tartalmát kiírja a
fájlba, vagy eltárolja a memóriában.
fopen Megnyit egy fájlt.
fprintf Egy textfájlba ír valamit.
fputc Kiír egy karakter a
fájlba.
fputchar Kiír egy karaktert a standard kimenetre.
fputs Kiír egy egész sort a standard kimenetre.
fread Adatokat olvas a fájlból.
freopen Egy másik fájlhoz rendeli át a
fájlleírót.
fscanf Adatokat olvas be egy textfájlból.
fseek Beállítja a fájlmutatót.
fsetpos Beállítja a fájlmutatót.
ftell Lekéri a fájlmutató értékét.
fwrite Adatokat ír ki a fájlba.
getc Egy karaktert olvas be a fájlból.
getchar Egy karaktert olvas be a standard
bemenetről.
gets Egy egész sort olvas be a standard
bemenetről.
getw Egy egész számot olvas be a standard
bemenetről.
perror Egy rendszer-hibaüzenetet ír ki a
standard kimenetre.
printf Szöveget ír ki a standard kimenetre.
putc Egy karaktert ír ki a fájlba.
putchar Egy karaktert ír ki a standard kimenetre.
puts Egy karakterláncot ír ki a standard
kimenetre.
putw Egy egész számot ír ki egy fájlba.
remove Fájlt töröl
rename Fájlt nevez át.
rewind A fájlmutatót a fájl elejére helyezi.
rmtmp Eltávolítja az
ideiglenes fájlokat.
scanf Adatokat olvas be a
standard bemenetről.
setbuf Bufferelést rendel hozzá
egy fájlleíróhoz.
setvbuf Bufferelést rendel hozzá egy
fájlleíróhoz.
spawnl Programot indít el.
spawnle Programot indít el.
spawnlp Programot indít el.
spawnlpe Programot indít el.
spawnv Programot indít el.
spawnve Programot indít el.
spawnvp Programot indít el.
spawnvpe Programot indít el.
sprintf Szöveges változóba ír
adatokat.
sscanf Szöveges változóból olvas
adatokat.
strerror Hibakód alapján hibaüzenetet ír ki.
strncpy Adott számú bájtot másol az
egyik karakterláncból, levág, ha kell.
tempnam Egyedi fájlnevet hoz létre.
tmpfile Ideiglenes fájlt hoz
létre.
tmpnam (=tempnam)
ungetc Backspace hatás.
unlink Fájlt töröl.
vfprintf (Paraméterlistás változata a 'v'
nélküli párjának)
vfscanf (Paraméterlistás változata a
'v' nélküli párjának)
vprintf (Paraméterlistás változata a
'v' nélküli párjának)
vscanf (Paraméterlistás változata
a 'v' nélküli párjának)
vsprintf (Paraméterlistás változata a 'v'
nélküli párjának)
vsscanf (Paraméterlistás
változata a 'v' nélküli párjának)
_setcursortype A szöveges kurzor megjelenését állítja be.
cgets Szöveget
olvas be a konzolról.
clreol A sorvégéig törli az egész sort.
clrscr Letörli
a képernyőt.
cprintf Formázott
szöveget ír ki a képernyőre.
cputs Szöveget
ír ki a képernyőre.
cscanf Adatokat
olvas be a konzolról.
delline Egy
sort töröl a szöveges ablakból.
getch Egy
karaktert olvas be a billentyűzetről, de nem írja ki a képernyőre.
getche Egy
karakter olvas be a billentyűzetről, és kiírja a képernyőre.
getpass Egy
jelszót olvas be.
gettext Szöveget
tárol el a képernyőről a memóriába.
gettextinfo Infót
ad a szöveges képernyőről.
gotoxy Beállítja
a kurzorpozíciót.
highvideo Fényes
színek használata.
inp Egy
bájtot olvas egy hardverportról.
inpw Két
bájtot olvas egy hardverportról.
insline Egy
üres sort szúr be a szöveges képernyőre.
kbhit Igaz
értéket ad, ha van billentyű leütve.
lowvideo Gyenge
fényű színek használata.
movetext Szöveget
másol egy téglalap alakú területről egy másikra.
normvideo Normál
fényű karakterek használata.
outp Egy
bájtot küld egy hardverportra.
outpw Két
bájtot küld egy hardverportra.
putch Egy
karaktert ír ki a képernyőre.
puttext Szöveget
ír ki a memóriából a képernyőre.
textattr Beállítja
a szöveg jellemzőit.
textbackground Beállítja
a szöveg háttérszínét.
textcolor Beállítja
a szöveg színét.
textmode Szöveges
módba állítja a képernyőt.
ungetch Visszarak
egy karaktert az input-bufferbe.
wherex A szöveges kurzor X koordinátájával tér vissza.
wherey A
szöveges kurzor Y koordinátájával tér vissza.
window Egy
téglalap alakú területet jelöl ki a szöveges képernyőn, és arra fognak
vonatkozni a szöveges utasítások.