Informatika2-2013/Eloadas

A MathWikiből

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:  m \leftrightarrow n
    • 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  m \leftarrow n és  n \leftarrow r és menjünk vissza E1 lépésre.

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á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);
}
Személyes eszközök