Informatika2-2013/Eloadas
Tartalomjegyzék
|
A programozásról
Gyakran egy matematikai probléma megoldását, vagy a megoldás egy lépését számítógép segítségével határozhatjuk meg. A probléma megfogalmazása után először a megoldáshoz vezető algoritmust kell megadnunk. Ezután következik az algoritmus implementálása, beprogramozása a számítógépbe.
Algoritmusok
Az algoritmusnak nincs egységesen elfogadott definíciója, így az alábbi megfogalmazás sem definícióként értendő.
Algoritmus: (Cormen, T., Leiserson, C. E., Rivest, R. L., Stein, C. Introduction to Algorithms) Bármely jól meghatározott számítási eljárás, amelynek bemenete egy bizonyos érték vagy értékhalmaz, és amely létrehoz valamilyen értéket vagy értékhalmazt kimenetként.
Egy algoritmust sokféleképp jellemezhetünk. Knuth könyvében az alábbi, 5 fontos tulajdonságot emeli ki: (Knuth, D. E. - The Art of Computer Programming)
- Végesség: Az algoritmus véges sok lépés után befejeződik.
- Meghatározottság: Az algoritmus minden lépése pontosan definiált.
- Bemenet (Input): Az algoritmus igényelhet olyan értékhalmazt (adatokat), amiket elindítása előtt meg kell adnunk.
- Kimenet (Output): Az algoritmushoz tartozhat olyan kimeneti értékhalmaz, mely meghatározott kapcsolatban van a bemenettel.
- Elvégezhetőség: Elvárjuk, hogy az algoritmust végre lehessen hajtani
Számítási módszernek nevezünk egy olyan eljárást, mely a végességet leszámítva teljesíti a fenti feltételeket.
Egy példa algoritmusra - Euklideszi algoritmus:
- A feladat: Adott két pozitív egész szám, m és n, keresendő legnagyobb közös osztójuk.
- Az Euklideszi algoritmus lépései:
- E0 Ha m < n , akkor cseréljük meg a két számot:
- E1 Osszuk el m-et n-nel, legyen a maradék r.
- E2 Ha r = 0, akkor az algoritmus véget ért, az eredmény n.
- E3 Legyen
és
és menjünk vissza E1 lépésre.
- E0 Ha m < n , akkor cseréljük meg a két számot:
Programozás, programozási nyelvek
Célunk algoritmusok beprogramozása a számítógépbe. Ehhez először a számítógép fogalmát kell megértenünk. A mai számítógépek Neumann János 1946-ban kidolgozott elvei szerint működnek. Felépítésük az ún. Neumann és Harvard architektúrák felépítését követi. Leegyszerűsített felépítésük a következő:
- Processzor (CPU): beolvassa a memóriából az utasításokat és az adatokat, az utasítások alapján műveleteket végez, az eredményt visszaírja a memóriába; valamint vezérli a perifériákat - adatokat olvas belőlük, és ír ki
- Memória: általános tároló, mely utasításokat és adatokat tartalmaz
- Perifériák: háttértárolók, beviteli eszközök, monitor, ...
Egy algoritmust programozási nyelv használatával adjuk meg a számítógépnek.
- Egy programozási nyelvvel képesek vagyunk vezérelni, utasítások sorozatát előírni a számítógép processzorának.
- Az egyes nyelvek között compiler-ek fordítják át az utasításokat.
- Gépi kódnak nevezzük azon nyelvet, mellyel a számítógép processzora közvetlenül vezérelhető.
Egy algoritmus programozásakor a választott programozási nyelvvel képesnek kell lennünk:
- az algoritmus álltal használt adatok tárolására/kezelésére.
- az algoritmus utasítás sorozatának megadására.
Példák programozási nyelvekre: Assembly, C, C++, Java, Python, ...
Bevezetés a C programozási nyelvről
A C programozási nyelv egy általános célú nyelv, melyet eredetileg Dennis Ritchie fejlesztett az AT&T Bell laboratóriumában 1969-1973 között. A nyelv néhány tulajdonsága:
- Gépi kódra fordul
- Hordozható
- Hatékony programok írására alkalmas
- Egyszerű nyelv, tömör szintaktika
- Előfeldolgozó
- Szabványos könyvtári függvények
- Nagyon sok nyelv "alapja"
A nyelv lényeges részei
A nyelv lényeges részei, melyekről részletesebben lesz szó:
- Adatok (változók, konstansok)
- Adattípusok
- Operátorok
- Vezérlő struktúrák
- Paraméterek, argumentumok
- Pointerek és tömbök
- Függvények, függvény könyvtárak
- Kulcsszavak
- Láthatóság
- Struktúrák
- File I/O, argumentumok
A nyelv szintaktikai csoportosítása
- azonosítók
- kulcsszavak
- állandók
- karaktersorozatok
- operátorok
- szeparátorok
Kódolás, fordítás a gyakorlatban
Forráskód
A nyelv részletes elemzése előtt érdemes tanulmányozni a "Hello World!" program C-ben írt forráskódját.
#include <stdio.h> int main(void){ printf("Hello world!\n"); return 0; }
- A program első sora egy #include utasítás, hatására az előfeldolgozó erre a helyre bemásolja a megnevezett állomány tartalmát. Ez most az stdio.h állomány.
- Az első sorban a "<>" jelek arra utalnak, hogy az stdio.h állomány a fordító részére megadott ún. „include path” által definiált helyen van.
- A következő nem üres sor a main függvény definíciója. A main függvény egy speciális függvény a C programokban, amely a program indításakor legelőször hívódik meg.
- Az int megadja, hogy a „main” függvény egy egész szám típusú adatot ad vissza.
- A void azt jelenti, hogy a függvény nem vár paramétereket vagy adatokat az őt meghívó rutintól.
- A kapcsos zárójel a függvény törzsének kezdetét jelzi.
- A következő sor futtatja a printf függvényt. Az stdio.h tartalmazza a printf függvény meghívásának leírását (prototípusát, deklarációját).
- Ebben az esetben, a printf függvényt mindössze egyetlen paraméterrel hívjuk meg, mégpedig egy fix szöveggel: „Helló, világ!\n”, ahol \n új sort jelent.
- A printf függvény ugyan ad vissza értéket (a kiírt karakterek számát), de ezt most nem használjuk ki.
- A return utasítás jelenti a kilépést az aktuális függvényből (ami ebben az esetben a main), és megadja a hívónak visszaadandó értéket-Ez az esetünkben 0, ami a program hibátlan lefutását jelzi.
- Végül a záró kapcsos zárójellel jelezzük a „main” függvénytörzs végét.
Fordítás
- Egy egyszerű szövegszerkesztőben is megírható C forráskódot compiler-el fordíthatunk le gépi kódra. Az így kapott fájl már futtatható lesz.
- Itt most a linux-on, gcc-vel való fordítást mutatjuk be.
- A GCC a GNU Compiler Collection rövidítése. Kezdetben GNU C Compilert, tehát GNU C fordítót jelentett. Mára a GCC kiegészült egyéb nyelvek fordítóival is. Elsősorban linux és BSD rendszereken használják, de Windows-on is elérhető.
- A fordítás legegyszerűbb esetben, ha a forrásunk a hello.c file:
gcc hello.c
- Ekkor a futtatható file-unk az "a.out" lesz.
- Előfordulhat, hogy a megírt forráskódunk a nyelv szerint hibás részt tartalmaz. Ekkor a kód nem fordul le, a fordító pedig hibaüzeneteket küld a felhasználónak, például:
hello.c: In function ‘main’: hello.c:3:1: error: expected ‘,’ or ‘;’ before ‘}’ token hello.c:3:1: error: expected declaration or statement at end of input
Kapcsolók
- A fordítás folyamatát különböző kapcsolókkal befolyásolhatjuk.
- Sokféle akpcsoló létezik: gcc command options
- Néhány fontos kapcsoló:
- -o a létrejövő futtatható file nevét állíthatjuk be. Az alábbi példában a hello file lesz a futtatható file.
gcc hello.c -o hello
- -W a legfontosabb figyelmeztető üzeneteket (warning) bekapcsolja
- -Wall még néhány fontos figyelmeztető üzenetet bekapcsol
- -s felesleges részeket (pl. nyomkövetési információk és szimbólumok) eltávolítja kimenetből
- -g hibakereső információk hozzáadása a kimenethez
- -ggdb gdb (lsd. később) futtatásakor használt információk hozzáadása a kódhoz
- -O1 (vagy -O), -O2, -O3' különböző szintű optimalizálások bekapcsolása. Magasabb szám magasabb szintű optimalizálást jelent. Ez hosszabb fordítási idővel, nagyobb memóriahasználattal, azonban csökkentett futtatási idővel és kisebb kódmérettel jár.
- -Os kódméretre való optimalizálás
- további optimalizálási kapcsolók
Linking and Compiling
- Eddig fordításnak (compiling) neveztük azt a folyamatot, amikor a forráskódból a fordító (gcc) futtatható (gépi) kódot hozott létre. A fordítás folyamata azonban nem mindig ilyen "egyszerű". Ez történik, ha a programunk egyes részei különböző forrás file-okban találhatóak:
- helyesen inkább "build"-nek kéne nevezni azt a folyamatot, mely során a forráskódból futtatható file-t állítunk elő.
- compile folyamat során minden egyes forrásfájlból ún. object (.o) file-ok keletkeznek
- linking alatt pedig azt a folyamatot értjük, amikor az object file-okból egyetlen, végső futtatható file jön létre. A fordítónak az object file-okból kell összeraknia a végső futtatható file-t.
- A forráskódtól a futtatható file-ig tehát a compile és linking folyamatok vezetnek.
- Ezt azért érdemes tudni, mert ha egy program nem fordul, akkor ennek oka
- lehet a forráskódban lévő hiba miatt,
- de az is elképzelhető, hogy a fordító a "linking" során nem talál egy forrásfájlt, amire egy másik hivatkozik.
- Ne feledkezzünk meg a C preprocesszorról, ami a fordítás és linkelés előtt fut le. Célja, hogy a "compiler"-nek csak a tényleges fordítással kelljen foglalkoznia. Feladatai:
- optimalizálja a whitespace karaktereket
- végrehajtja a kódban "#"-gal jelölt preprocesszor utasításokat
- eltávolítja a megjegyzéseket a kódból
Hibakeresés (Debugging)
- Előfordulhat, hogy a megírt programunk hibás. Ennek oka lehet
- szintaktikai hiba: ekkor a nyelvi hibát vétünk, a kód nem fordul le, a fordító álltal küldött hibaüzenetekből tájékozódhatunk a hiba forrásáról.
- szemantikai hiba: ekkor a kód ugyan lefordul, de a futás nem úgy történik, ahogy azt elvárnánk (például végtelen cikulsba fordul a programunk, vagy összeomlik). Ennek forrásáról először a fordító álltal adott "warning" figyelmeztetések adhatnak információt. Ha ez sem segít, akkor hibakereső programok használatára van szükségünk.
- A gdb (GNU Project debugger) egy ilyen hibakereső program. Néhány hasznos tudnivaló a gdb-ről (fejlesztés alatt, mivel itt sok fogalom tisztázatlan még, ez itt csak egy terv):
- Használatához először fordítsuk le az adott kódot:
gcc hello.c -gdb -Wall -o hello
- Ezután hívjuk meg a gdb-t az adott kóddal:
gdb hello
- Később lesz szó argumentumokról. ezeket a következőképp adhatjuk meg:
set args argumentum1 argumentum2 ...
- Ha ezzel is megvagyunk, akkor futtathatjuk a kódot
run
- ha a kódunk összeomlott, akkor visszakérhetjük, hogy pontosan milyen hívás okozta ezt:
backtrace
...
Fejlesztői környezetek
- Fejlesztői környezetek célja egy program fejlesztési folyamatának, egy programozó munkájának a megkönnyítése.
- Egy fejlesztői környezet nagyon sokoldalú lehet. Az alábbiak csak példák arra, hogy milyen funkciókat tartalmazhat:
- forráskód szerkesztés,
- "syntax highlighting" (adott nyelv forráskódjának színezése),
- automatikus kiegészítés,
- forráskód automatikus formázása,
- hibakeresés segítése, egyszerűsítése,
- több forrásfájlból álló, bonyolult kód hatékony kezelése ...
- Példák fejlesztői környezetekre:
- Code::Blocks
- CodeLite
- Eclipse
- NetBeans
- Visual Studio ...
A nyelv (fontosabb) részei
Adatok (Változók, konstansok) és típusaik
Deklarálás, inicializálás, konstansok definiálása
- Változók és az állandók alkotják a programban feldolgozott alapvető adatobjektumokat.
- Ha egy adat értéke változhat a futtatás során, akkor változó, ha nem, akkor konstans.
- Egy változót deklarálunk, ha megadjuk annak típusát és azt a nevet, mellyel hivatkozunk rá a kódban:
int sum;
- Az előző példában az int a változó típusára utal, melyről a továbbiakban lesz szó.
- Egy változót inicializálunk, ha a deklarálás után megadjuk annak kezdeti értékét:
int sum = 0;
- Egy változót konstansnak definiálunk a következő módon:
const int valasz=42;
Röviden a változók láthatóságról
- Egy változó láthatóságán azt értjük, hogy a programkód mely részeiből érhető el, hol használható.
- Az egyszerű szabály az, hogy a változó azon a blokkon belül érhető el, ahol deklaráltuk.
- Blokknak számít egyrészt maga a teljes C forrásfájl, másrészt minden kapcsos zárójelek közötti rész egy-egy újabb blokk.
- A zárójeleken belül lokális változóink lesznek. Lokális változók csak az adott blokkon belül látszódnak.
- A zárójeleken kívül, a C forrásban deklarált változók globális változók. A globális változók a kód minden részéből láthatóak.
Típusok
- Most kitérünk az adatok lehetséges típusaira.
- A C nyelv alapvető adattípusai:
- char egyetlen bájt, a gépi karakterkészlet egy elemét tárolja,
- int egész szám,
- float egyszeres pontosságú lebegőpontos szám,
- double kétszeres pontosságú lebegőpontos szám.
- lebegőpontos számokról
- Minősítők:
- Egész számokhoz (int):
- short int általában 16 bites
- long int általában 32 bites
- char és bármilyen egész szám esetén:
- signed és unsigned minősítések
- A long double típus növelt pontosságú lebegőpontos számot jelöl.
- Az egyes egész és lebegőpontos számok mérete gépfüggő.
- Egész számokhoz (int):
Egész számok logikai értelmezése
- A C nyelvben az egész számoknak logikai jelentése van: ha a szám nem nulla, az IGAZ-at jelent, ha nulla, az HAMIS-at.
- A logikai műveletek eredménye egész számként használható. A logikai művelet eredménye 0, ha HAMIS és 1, ha IGAZ.
- C++-ban már létezik bool, azaz logikai igaz/hamis típus.
Vezérlési szerkezetek
Utasítások
- Mindent, aminek értéke van kifejezésnek hívunk.
- Tetszőleges kifejezés (pl. x = 0) utasítássá válik, ha egy pontosvesszőt írunk utána.
- A C nyelvben a pontosvessző az utasításlezáró jel (terminátor).
- A {} kapcsos zárójelekkel deklarációkat és utasításokat fogunk össze egyetlen összetett utasításba vagy blokkba, ami szintaktikailag egyenértékű egyetlen utasítással.
if/else szerkezet
if (kifejezés) 1. utasítás else 2. utasítás
- Ha az if utáni kifejezés igaz, akkor az 1. utasítást hajtjuk végre.
- Ha az if utáni kifejezés hamis, akkor az else utáni utasítást hajtjuk végre.
- Az else utasítás opcionális.
else if szerkezet
if (kifejezés) utasítás else if (kifejezés) utasítás else if (kifejezés) utasítás else if (kifejezés) utasítás . . . else utasítás
- Ha az if utáni kifejezés hamis, akkor egymás után sorra megvizsgáljuk az egyes else if utáni kifejezéseket, és csak ezután lépünk az else utáni utasításra.
- Ha egy else if utáni kifejezés igaz, akkor az utána következő utasításokat végrehajtjuk és kilépünk a vizsgáló láncból. Az ezutáni if else kifejezéseket tehát nem vizsgáljuk meg.
switch szerkezet
switch (kifejezés) { case állandó kifejezés: utasítások case állandó kifejezés: utasítások . . . default: utasítások }
- Az egyes, mindig case után szereplő állandó értékű kifejezések értékét hasonlítjük össze a switch után szereplő kifejezéssel.
- Az összehasonlítandó állandól egész típusú értékek kell legyenek.
- Az utolsó, default ág akkor hajtódik végre, ha egyetlen case ághoz tartozó feltétel sem teljesült.
- A default ág opcionális.
for ciklus
for (1. kifejezés; 2. kifejezés; 3. kifejezés) utasítás
- Szintaktikailag a for utasítás mindhárom komponense kifejezés.
- Leggyakrabban az 1. és 3. kifejezés értékadás vagy függvényhívás, és a 2. kifejezés egy relációs kifejezés.
for (ii=0; ii<100; ii++) utasítás
- A három komponens bármelyike hiányozhat, de az őket lezáró pontosvessző kiírása ekkor is kötelező.
- Ha az 1. vagy 3. kifejezés hiányzik, akkor azokat egyszerűen elhagyjuk a for utasítást követő zárójelből.
- Ha a 2. (vizsgáló) kifejezés is hiányzik, akkor azt a gép úgy tekinti, hogy az állandóan igaz, és ezért a program végtelen ciklusba fordul.
while ciklus
while (kifejezés) utasítás
- A program kiértékeli a while után szereplő kifejezést
- Ha értéke igaz, tehát nem nulla, akkor végrehajtja a while utáni utasítást, majd újra kiértékeli a kifejezést.
- Ha a kifejezés értéke hamis, akkor kilép a ciklusból.
do while ciklus
do utasítás while (kifejezés);
- Hasonló a while ciklushoz, csak hátul, a ciklusmag után teszteli a kifejezést.
- Ez esteben tehát a ciklusmag garantáltan egyszer le fog futni.
Ciklusokról általában
- A ciklusok ismétlődő (azonos vagy hasonló) tevékenységek megvalósítására szolgálnak.
- Megkülönböztetünk feltételes és számlálós típusokat.
- A feltételesen belül megkülönböztetünk elől és hátultesztelős ciklusokat.
Egyéb (ugró) utasítások
- a break utasítás hatására a legbelső ciklus vagy a teljes switch utasítás fejeződik be.
- a continue utasítás hatására azonnal (a ciklusmagból még hátralévő utasításokat figyelmen kívül hagyva) megkezdődik a következő iterációs lépés.
- a goto címkékkel a programkód adott részeire való ugrást tesz lehetővé.
for(...) for(...) { ... if (zavar) goto hiba; } ... hiba:
- a return parancs egy függvény (lsd. később) visszatérési értékét adja meg.
- Ne használjuk a goto utasítást, és amennyiben lehetséges, kerüljük a break és continue parancsokat.
- Szintén kerüljük a switch utasítást.
Operátorok
- Először gondoljuk végig a függvények fogalmát. A függvényeket részletesebben alább tárgyaljuk.
- Operátorokra helyes definíciót nehéz adni.
- Néhány állítás:
- Az operátorok szintakszisa eltér a függvényekétől
- Az operátorok mindig operandusokon végeznek el műveleteket, és valamilyen értéket adnak vissza.
- Az operátorok szintakszisa rögzített az adott nyelvben.
- Az operátorokat kifejezésekben használjuk.
- Az operátorokat tulajdonságaik alapján csoportosíthatjuk. Lényeges tulajdonságok:
- operandusok típusa és száma
- az operátor által visszaadott eredmény
- az operandusok kiértékelési sorrendje (pl. asszociativitás összeadás esetén)
- az egyes operátorok egymáshoz való viszonya (precedencia)
- történik-e, ha igyen milyen automatikus típuskonverzió az operátor meghívásakor?
- operandusok - operátor helyzete ( innfix, postfix, prefix)
- A alábbi táblázat összefoglalja a C nyelv operátorait (forrás ):
Precedencia | Operátor | Rövid leírás | Asszociativitás | Jelölés |
---|---|---|---|---|
1 | () [] -> . ++ -- |
Csoportosítás Tömb-elérés Mutatón keresztüli tag-elérés Objektumon keresztüli tag-elérés Posztfix növelés Posztfix csökkentés |
Bal | (a) a[] ptr->b() a.b() a++ a-- |
2 | ! ~ ++ -- - + * & (típus) sizeof |
Logikai tagadás Bitenkénti negálás Prefix növelés Prefix csökkentés Előjel - Előjel + Dereferálás Objektum címe Konverzió típusra Méret |
Jobb | !a ~a ++a --a -a +a *ptr &a (b)a sizeof(a) |
3 | * / % |
Szorzás Osztás Maradékszámítás |
Bal | Infix |
4 | + - |
Összeadás Kivonás |
Bal | Infix |
5 | << >> |
Bitenkénti eltolás balra Bitenkénti eltolás jobbra |
Bal | Infix |
6 | < <= > >= |
Kisebb Kisebb-egyenlő Nagyobb Nagyobb-egyenlő |
Bal | Infix |
7 | == != |
Egyenlő Nemegyenlő |
Bal | Infix |
8 | & | Bitenkénti ÉS | Bal | Infix |
9 | ^ | Bitenkénti kizáró VAGY | Bal | Infix |
10 | | | Bitenkénti megengedő VAGY | Bal | |
11 | && | Logikai ÉS | Bal | Infix |
12 | || | Logikai(megengedő) VAGY | Bal | Infix |
13 | ? : | if-then-else operátor | Jobb | logikai-kif ? kifejezés : kifejezés |
14 | = += -= *= /= %= &= ^= |= <<= >>= |
Értékadás Összeadás és értékadás Kivonás és értékadás Szorzás és értékadás Osztás és értékadás Maradékképzés és értékadás Bitenkénti ÉS és értékadás Bitenkénti kizáró VAGY és értékadás Bitenkénti megengedő VAGY és értékadás Eltolás balra és értékadás Eltolás jobbra és értékadás |
Jobb | Infix |
15 | , | Szekvencia operátor | Bal | a, b |
Kulcsszavak
A következő azonosítók kulcsszavak, melyek más célra nem használhatók:
auto double int struct break else long switch case enum register typedef char extern return union const float short unsigned continue for signed void default goto sizeof volatile do if static while
Memória lefoglalás, pointerek és tömbök
Bevezetés, Mutatók
- Először tekintsünk egy egyszerű modellt a számítógép memóriájára, illetve az ebben történő adattárolásra.
- A memóriát képzeljük el diszkrét adattároló egységeknek. Minden egységnek külön tároljuk a címét a memóriában.
- Az egységeket külön-külön, vagy tömbösítve tudjuk kezelni.
- Ezek alapján értlemezzük a nyelv absztrakt szintjén a tömböket és pointereket.
- Memóriacím egy egész szám, amely kijelöli a memória egy bájtját.
- Az & operátor hívása adja meg az adott változó címét a memóriában.
- Pointernek nevezzük azt a változót, amely egy másik változó memóriacímét tartalmazza.
- Pointer típusa: milyen típusú adatra vagy függvényre mutat a pointer.
- Ha * operátort (melynek neve indirekció) egy pointerre hattatjuk, akkor visszaadja az adott pointer által megcímzett értéket.
- Pointerek deklarációja:
int *p;
- NULL pointer: olyan, külön erre a célra fentartott értékű pointer, mely nem mutat semmilyen értékre:
int *p=NULL;
Tömbök
- Tömbök esetén "tömbösítve" foglaljuk le a változók helyét a memóriában.
- Példa deklarációra:
int a[10];
- Példa inicializálásra:
int t[10]={6,8,-2,6,123,-8,3,4,2,1};
- A tömb elemei 0-tól indexelődnek.
- Deklaráláskor tehát le kell rögzítenünk egy konstanssal a tömb méretét
- Fontos, hogy a méretet csak konstanssal adhatjuk meg. Néhány fordító támogatja a tömb méretének változóval való megadását, de ezt általánosan nem alkalmazhatjuk.
- Ha fordítási időben nem ismert, mekkora tömbre lesz szükség, használjuk dinamikus tömböt. (lsd. később)
- Mindig kötelező a tömbnek méretet adni.
- A tömb nem másolható az = operátorral, végig kell iterálnunk elemenként a két tömbön másoláskor:
- Sztringeket karaktertömbökként tárolunk.
- A sztring utolsó eleme mindig a "szöveg végét" jelölő "\0" karakter. Tehát a sztringben lévő karakterek számánál mindig egyel nagyobb a tároló karaktertömb mérete.
- az a és &a[0] jelölések ekvivalensek, és a tömb első elemére mutatnak. A következő kód végén a pa változó pedig az a tömb 6. (indexe 5) elemére mutat:
int *pa; pa = &a[0]; pa = &a; *(pa + 5);
- Lehetőségünk van többdimenziós tömbök deklarálására.
- Többdimenziós tömbök sorfolytonosan tárolódnak a memóriában, tehát a következő két inicializálás (mely egyben példa is) ekvivalens:
int t[3][4][2]={34,-5,3,20,12,5,-1,0,4,77,12,-3,-14,3,1,23,75,2,10,24,78,1,2,7};
int t[3][4][2]={{{34,-5},{3,20},{12,5},{-1,0}},{{4,77},{12,-3},{-14,3},{1,23}},{{75,2},{10,24},{78,1},{2,7}}};
Dinamikus memória foglalás
- Függvények:
- malloc megadott byte-ot foglal le a memóriában,
- realloc a már lefoglalt memória-tömb méretét változtatja meg,
- calloc memóriafoglalás és a lefoglalt memória byte-jainek 0-ra állítása egyben,
- free üríti a megadott memória-részt.
- Példa a malloc függvény használatára
- Példa egydimenziós, dinamikusan foglalt tömb lefoglalására és használatára:
... int m; scanf("%d",&M); int *vec = (int *) malloc(M * sizeof (int)); ... int i; for(i=0; i<M; i++){ vec[i]=1/(double)i; } ...
- Példa kétdimenziós, dinamikusan foglalt tömb lefoglalására és használatára:
... int M,N; scanf("%d",&M); scanf("%d",&N); int **matrix = (int **) malloc(M * sizeof (int *)); int i; for (i = 0; i < M; i++){ matrix[i] = (int *) malloc(N * sizeof (int)); } ... int j; for(i=0; i<M; i++){ for(j=0; j<N; j++){ matrix[i][j]=(i+1)*(j+1); } }
Függvények, függvény könyvtárak
Függvények
- Programozásban a függvény egy nagyobb program forráskódjának része, mely adott feladatot hajt végre, és többször felhasználható anélkül, hogy a program kódjának több példányban is tartalmaznia kellene.
- Függvények használatával
- a többször használt programrészeket csak egyszer kell lekódolni,
- valamint a bonyolult algoritmusok kis részekre bontva áttekinthetővé, kezelhetővé válnak.
- Egy függvény mindig bemenő paraméterek egy értékhalmazát kapja meg és futása után valamilyen visszatérési értéket ad vissza. Itt most a hangsúly az értéken van mindkét esetben.
- C nyelvben egy függvénynek mindig tetszőleges számú és típusú argumentuma, és egyetlen visszatérési értéke van.
- A C nyelvben maga a főprogram is függvénybe kerül, ez a main függvény.
- Az általános felépítés mindig a következő:
visszatérési-típus függvénynév (argumentumdeklarációk){ deklarációk és utasítások }
- A függvény függvényfejből és függvénytörzsből épül fel.
- A függvényfej megadja a függvény típusát (milyen típusú adatot ad vissza), a függvény nevét, valamint paramétereinek típusát és nevét.
int hatvanyozo(int alap, int kitevo)
- A függvénytörzs tartalmazza a függvényt felépítő utasítássorozatot.
{ int i; for( ... ) ... return ... ; }
- A függvénydefiníció maga a függvény, azaz a függvényfej és a függvénytörzs együtt.
int hatvanyozo(int alap, int kitevo){ int i; for( ... ) ... return ... ; }
- A függvény prototípusa a függvényfej pontosvesszővel lezárva, paraméternevek nélkül.
int hatvanyozo(int,int);
- A függvénydeklaráció a függvény prototípusa paraméterek nélkül. Csak a függvény típusát és nevét tartalmazza.
int hatvanyozo();
- A prototípus és deklaráció jelentősége, hogy egy C program csak olyan függvényt tud meghívni, amelynek deklarációja megelőzi a kódban a függvényhívást.
- A C szabvány azt ajánlja, hogy ne a deklaráció, hanem az annál több információval bíró prototípus előzze meg a hívás helyét.
- Nem kell deklaráció vagy prototípus abban az esetben, ha a hívott függvény definíciója megelőzi a hívás helyét.
- Az alábbi két példa mutatja be prototípus használatával és használata nélkül a programkód felépítését:
#include <stdio.h> ... int hatvanyozo(int,int); int main(){ int a,b,c; ... c=hatvanyozo(a,b); ... } int hatvanyozo(int alap, int kitevo){ ... }
#include <stdio.h> ... int hatvanyozo(int alap, int kitevo){ ... } int main(){ int a,b,c; ... c=hatvanyozo(a,b); ... }
Paraméterek, argumentumok
- Lehetőségünk van a programnak parancssor-argumentumokat vagy paramétereket átadni a végrehajtás megkezdésekor.
- Amikor a végrehajtás kezdetekor a rendszer a main-t hívja, akkor a hívásban két argumentum szerepel.
- Az argc megadja a parancssor-argumentumok számát, amellyel a programot hívtuk.
- Az argv egy karaktersorozatokat tartalmazó tömböt címző mutató. Ez tartalmazza a program hívásakor átadandó parancssor-argumentumokat (minden argumentum egy karaktersorozat).
- Megállapodás szerint az argv[0] az a név, amellyel a programot hívták, így argc legalább 1.
- Ha argc=1, akkor a program neve után nincs parancssor-argumentum.
- Például az alábbi függvényhívás esetén:
cprogram 1 10 file 20
- argc=5
- argv[0] a program neve, tehát cporgram
- argv[1]=1, argv[2]=10, argv[3]=file argv[4]=20
- Ügyeljünk arra, hogy a beadott '20' is karakterlánc, ha számként akarjuk a programban használni, akkor megfelelő függvénnyel át kell alakítanunk.
- Példakód:
int main(int argc, char *argv[]){ float x,y,z; int n; if(argc!=5){ printf("a program 4 parametert var, melyek rendre: x y z n"); return 1; } else{ x=atof(argv[1]); y=atof(argv[2]); z=atof(argv[3]); n=atoi(argv[4]); } } ..
File I/O
Megnyitás
- Fileba való olvasáshoz és íráshoz először egy ún FILE pointer deklarálására van szükségünk:
FILE *fp;
- Tekintsünk erre úgy, mint egy absztrakt adat-struktúrára mutató pointerre.
- Akár írni, akár olvasni szeretnénk, a file-t mindenképp meg kell nyitnunk:
fp=fopen("c:\\test.txt", "r");
- Ezt tehát az fopen() függvénnyel tehetjük meg.
FILE *fopen(const char *filename, const char *mode);
- a függvény második argumentumaként megadott karaktersorozat rögzíti, hogy milyen céllal nyitjuk meg a file-t:
- r - megnyitás olvasásra
- w - megynyitás írásra
- a - megnyitás és az eddigi file-hoz való írás
- r+,w+,a+ - megnyitás írásra és olvasásra. A 3 különböző jelölés 3 különböző módot takar (felülírás, ...)
Bezárás
- Ha végzünk a file-on való műveleteinkkel, akkor be kell zárnunk a file-t:
fclose(fp);
Műveletek
- C nyelvben ún. bináris és szövegfájlok feldolgozására és írására is van lehetőségünk.
- Itt a szövegfájlok feldolgozását mutatjuk be.
- A file-ba való írás a printf függvényhez hasonlóan történik, most az fprintf függvény használatával:
#include <stdio.h> int main() { int i, N; FILE *fp; // fájl mutató char fname[80]; // a fájl neve lesz itt printf("Mennyi számot generáljak?\n"); scanf("%d", &N); printf("A fájl neve ahova írjam:\n"); scanf("%s", fname); fp = fopen(fname, "w"); // itt nyitjuk meg a fájlt, írásra if (fp == NULL) { // hibakezelés printf("Nem sikerült megnyitni a fájlt: %s", fname); return 1; } for (i = 0; i < N; i++) { fprintf(fp, "%d\n", i); // az "fprintf()" fájlba ír } fclose(fp); // be is zárjuk a fájlt ! }
- File-ból való olvasás a scanf függvényhez hasonlóan történik az fscanf függvénnyel.
#include <stdio.h> int main(void){ FILE * proba; proba=fopen("probaFile","r"); int a=0; while(!feof(proba)){ fscanf(proba,"%d",&a); printf("%d\n",a); } }
Típusnév hozzárendelés
- Egyes (egyszerű és összetett) adattípúsokhoz azonosítókat rendelhetünk hozzá. Ez egyszerűen egy új "címke" hozzárendelése az adattípushoz.
Példakód:
typedef int* int_mutato; /* ezzel elneveztük "int_mutato"-nak az "int*" típust*/ int szam1 = 42; int* szam1_ptr = &szam1; int_mutato szam2_ptr = szam1_ptr; *szam2_ptr = 23;
- A példakód végére szam1 értéke 23 lesz.
Struktúrák
- Struktúra: Logikailag egy egységet alkotó, akár különböző típusú adatokból álló összetett adattípus.
- Példakód, mely egy "pont" struktúrát definiál:
struct pont { int x; int y; };
- Példakód, melyben a definíció után rögtön deklarálunk két pont típusú változót (d1 és d2):
struct datum { int ev; int ho; int nap; } d1, d2;
- A következő kód bemutatja a "." operátort, mellyel a struktúra elemeit érhetjük el:
struct Pont { int x; int y; }; int main(){ struct Pont p1; p1.x=7; p1.y=5; }
- A fenti példában látható volt, hogy p1 deklarációjánál a "struct Pont" kifejezéssel hivatkoztunk a definiált Pont struktúrára.
- Kényelmesebb lenne, ha a struct-ot a deklaráláskor elhagyhatnánk. Ehhez a struktúra definiálásakor a "struct Pont" kifejezést rögtön át kell neveznünk "Pont"-ra, így értelmezhető az alábbi kód:
typedef struct { int x; int y; } Pont;
- A fenti példában a struct{...} definiált struktúrát nevezzük el typedef használatával Pontnak.
Egy bonyolultabb példakód
A kód a Floyd-Warshall algoritmust valósítja meg.
#include<stdio.h> #include<stdlib.h> #include<math.h> const double INF=1.7976931348623158e+308; double **newMatrix(int rowNum, int colNum); void setMatrix(double ** matrix, int rowNum, int colNum); void readFromFile(char fileName[256], double ** matrix); void writeIntoFile(char fileName[256], double** matrix, int nodeNum); void floydWarshallCore(double ** adjacencyMatrix, int nodeNum); int main(int argc, char *argv[]){ int nodeNum=atoi(argv[3]); double ** adjacencyMatrix=newMatrix(nodeNum,nodeNum); setMatrix(adjacencyMatrix,nodeNum,nodeNum); readFromFile(argv[1],adjacencyMatrix); floydWarshallCore(adjacencyMatrix,nodeNum); writeIntoFile(argv[2],adjacencyMatrix,nodeNum); return 0; } double **newMatrix(int rowNum, int colNum){ double ** matrix = (double **) malloc(rowNum * sizeof (double*)); int ii=0; for (ii = 0; ii < rowNum; ii++){ matrix[ii] = (double *) malloc(colNum * sizeof (double)); } return matrix; } void setMatrix(double ** matrix, int colNum, int rowNum){ int ii,jj; for(ii=0; ii<rowNum; ii++){ for(jj=0; jj<colNum; jj++){ matrix[ii][jj]=INF; } } } void readFromFile(char fileName[256], double ** matrix){ FILE *filePointer; filePointer = fopen(fileName,"r"); while(!feof(filePointer)){ int colID,rowID; double weight; fscanf(filePointer,"%d",&rowID); fscanf(filePointer,"%d",&colID); fscanf(filePointer,"%lf",&weight); matrix[rowID][colID]=weight; } fclose(filePointer); } void floydWarshallCore(double ** adjacencyMatrix, int nodeNum){ int ii,jj,kk; for(kk=0; kk<nodeNum; kk++){ for(ii=0; ii<nodeNum; ii++){ for(jj=0; jj<nodeNum; jj++){ if(ii!=jj && jj!=kk && ii!=kk){ if(adjacencyMatrix[ii][kk]+adjacencyMatrix[kk][jj]< adjacencyMatrix[ii][jj]){ adjacencyMatrix[ii][jj]=adjacencyMatrix[ii][kk]+adjacencyMatrix[kk][jj]; } } } } } } void writeIntoFile(char fileName[256], double** matrix, int nodeNum){ FILE *filePointer; filePointer = fopen(fileName,"w"); int ii,jj; for(ii=0; ii<nodeNum; ii++){ for(jj=0; jj<nodeNum; jj++){ if(matrix[ii][jj]<INF){ fprintf(filePointer,"%d %d %lf\n",ii,jj,matrix[ii][jj]); } } } fclose(filePointer); }