1 / 51 C alapok nem kiadasi verzio sok helyen nem pontos informaciok talalhatoak!
2 / 51 IDE (Integrált fejlesztő környezet): Microsoft Visual Studio 2005 Új Project létrehozása: File -> New -> Project Itt a Project Types közül válasszuk ki a Vicual C++-t, azon belül a Win32-t, majd jobb oldalt a Win32 Console Application-t. Adjunk nevet a projectnek, majd OK. A feljött ablakban nyomjunk Next-t. Az Application type legyen Console Application. Az Additional options-nál pedig legyen bepipálva az Empty project box. A Solution Explorer-ben a Source Files- könyvtárra kattintsunk jobb gombbal, majd Add -> New Item. A felugró ablakban válasszuk ki a Visual C++ / C++ file (.cpp) –t. Adjunk meg egy filenevet, majd OK. A project nevére (NE a felette található Solution névre) kattintsunk jobb gombbal, majd Properties. Itt a Configuration Options -> C/C++ ->Advanced –nél a Compile As-t állitsuk át Compile as C Code (/TC) –re. Erre azért van szükség, mert a Visual Studio alapvetően C++-t fordít, így viszont C kódot. A két nyelv között alapvető különbségek vannak Írjuk meg a helyes programot. Futtatás: CTRL+F5 GCC: gcc -ansi -Wall -pedantic -o
forras.c -ansi: ANSI szabványos C kód ellelnőrzés -Wall: több hibára figyelmeztetés (Warning all) -pedantic: egyéb megszorítások kikényszerítése Futtatás: ./kimeneti_filename
3 / 51 Egy C állomány felépítése: [library_hivatkozások] [prototípusok] [main] [definíciók] (A prototípusokról és a definíciókról később, a függvényeknél lesz szó.) A legegyszerűbb C program: main(){ }
Ez nem csinál semmit. Minden C program fügvényekből és változókból áll. A függvények utasításokat tartalmaznak, amelyek azt írják le, hogy a program mit csinál. Ez a program egy main nevű függvényből áll. A függvényeknek tetszőleges nevet adhatunk, azonban a main speciális: ez jelenti a főprogramot. A főprogram végrehajtása a main elején kezdődik, az utasításokat a {}-ek között lehet elhelyezni. #include <stdio.h> main(){ printf("Hello World!"); }
A program kiírja, hogy Hello World! A #-el kezdődő utasítások az előfordítónak szólnak. #include: azt írja le, hogy a fordítás során az stdio könyvtárra vonatkozó információkat töltse be. stdio: standard input/output rövidítése, és az alapvető be/kimenetet kezelő utasítások, függvények stb. találhatóak meg benne. Ilyen pl. a printf(), ami a képernyőre ír ki tetszőleges karaktersorozatot. A ()-ek közötti "Hello World!" –t aktuális paraméternek hívjuk, és azt mondjuk, hogy a printf() függvényt az adott aktuális paraméterrel hívtuk meg. (Az aktuális paraméter szinonímája az argumentum). Bővebb magyarázat:függvények. A printf() nem hajt végre soremelést. Ezt akár így is írhattuk volna: #include <stdio.h> main(){ printf("Hello "); printf("World!"); }
A soremelésre a \n ún. escape jelsorozatot (escape szekvencia) használhatjuk. Erre azért van szükség, mert az ASCII kódtáblában vannak karakterek, amelyekhez nem tartozik karakterkép. Ezek az ún. vezérlőkarakterek (a 32-es kód alatt). Az escape szekvenciák segítségével ezeket a vezérlőkaraktereket adhatjuk meg. Bővebb lista: a függelékben. main(){ printf("Hello World!\n"); }
4 / 51 Literálok A konstans (állandó) szinonímája. A literál egy öndefiniáló eszköz. Két komponense van: típus és érték, ezeket a felírási módja határozza meg. Egész literál: előjellel vagy számjeggyel kezdődik, és több számjegyből állhat. Pl: -122 egy egész literál. Módosító betűk: a szám után álló betű(k): L vagy l: hosszú egész U vagy u: unsigned, azaz előjel nélküli Pl.: 111u: előjel nélküli egész, 55Lu, előjel nélküli hosszú egész Az egész számok felírása történhet oktális és hexadecimális formában is. A 0-val kezdődő számok oktális, a 0x-el kezdődő számok hexadecimális alakúak. Pl.: 031: oktálisan 31, decimálisan 25 (ezért keverik a programozók a helloweent és a karácsonyt: oct 31 = dec 25 :) ) 0x1F: hexadecimálisan 1F, decimálisan 31 Ezek után is lehet azokat a jelzőket írni, amelyek az egész számoknál lehetségesek. Pl.: 0XFALU egy szabályos hexadecimális alakú egész szám Lebegőpontos literál (valós szám): van benne pont, e betű, vagy mindkettő. Pl.: 12.2, 12e1, 12.2E+3, 1e3 Értéke: az e betű előtt álló részt szorozzuk 10 e betű után álló hatványával. pl.: 12e-1 = 12*10-1 Módosító betűk: F vagy f: float, egyszeres pontosságú lebegőpontos szám L vagy l: long double, háromszoros pontosságú lebegőpontos szám Módosító nélkül: double, kétszeres pontosságú lebegőpontos szám Pl: 11.2f egy float típusú szám Karakter literál: 'a' azaz aposztrófok között egy karakter. String literál (karakterlánc): "szoveg", azaz dupla aposztófok közé zárt karakter vagy karaktersorozat. Lehet üres string is, ekkor csupán: ""-t kell írni. String literálok esetén, ha nem fér ki egy sorban, a \ karakter segítségével új sorban folytathatjuk. Pl.: "valami \ amerika"
megegyezik a "valami amerika"
string literállal. A stringek és a karakterek között lényeges a különbség: 'a' "a"
:1 byte :2 byte, 'a' és '\0'
5 / 51 '' ""
:HIBÁS : nincs üres karakter! :1 byte, a lezáró \0
(A \0 karakter: lásd a stringeknél) Változók, típusok, deklarációk: A változónak 4 komponense van: név, attribútumok, cím, érték. A típust és az attribútumot szokták egymás szinonimájaként is emlegetni, azonban helytelenül. Az attribútum a változó futás közbeni viselkedését határozza meg. Az attribútum rögzíti a változó számára lefoglalandó memória mennyiségét, a benne található információ belső ábrázolási módját, hatáskörét, láthatóságát, élettartamát stb. A típus csak az első kettőt határozza meg. A legfontosabb attribútum a típus. Egész típusok kulcsszavai: int, short, long, char Megengedett a short int és a long int is. int: egész, short: rövid egész, long: hosszú egész típus. Lehetőség van előjel nélküli egészek használatára is, az unsigned kulcsszóval. Pl.: unsigned int. Az előjeles egész kulcsszava: signed. Az int mérete általában a gépi alapszó méretével egyezik meg (általában 4 byte, esetleg 2 byte), a short általában 2 byte-os, a long általában 4 byte-os (32 bites rendszeren). A char pontosan 1 byte-os. Az egész típusok méretére nincs konkrét megkötés (a char típus kivételével), de a következő egyenlőtlenség feltétlenül teljesül: short ≤ int ≤ long. Lebegőpontos típusok kulcsszavai: float, double, long double. float: lebegőpontos szám, double: duplapontos lebegőpontos szám, long double: kiterjesztett pontosságú lebegőpontos szám. A float általában 4 byte-os, de hasonlóan az egész típusokhoz, a méretek implementációfüggőek: float ≤ double ≤ long double Karakter: char. A kezelése speciális. A C 1 byte-os egész típusként kezeli, azaz műveletek is végezhetőek vele (+,-,* stb.). Mivel egész típusú, ezért lehet előjeles vagy előjel nélküli. A karakter belső ábrázolása: értéke mindig a karakter ASCII kódja. Speciális a void típus: nincs tartománya, művelete, reprezentációja. A típusnélküliséget jelöli. Logikai típus nincs a C nyelvben, az int típus helyettesíti. A 0 érték jelenti a hamis értéket, minden 0-tól különböző érték az igaz értéket jelöli. C-ben minden változót a használata előtt deklarálni kell. Ez általában a függvények elején, az első végrehajtható utasítás előtt történik. Deklarálni a következő képpen lehet: tipus valtozo [= kezdeti_ertek] [, valtozo [= kezdeti_ertek]]...;
ekkor egy tipus típusú, valtozo nevű változót hoztunk létre. Egy deklarációs utasításban több változót is deklarálhatunk, ekkor a típusaik azonosak lesznek. Kezdeti értéket is adhatunk a változónak, kifejezéssel. Pl.:
double valtozo, meg_egy_valtozo; int valtozo = 3 + 2; long int valtozo2 = 5L; long int b = valtozo + 3 + valtozo2;
6 / 51 Deklarálni blokk elején lehet, mindíg az első végrehajtható utasítás előtt. Blokk az, ami {}-ek között van. Tehát először minden változót deklarálni kell, és csak azután használhatjuk fel a programunkban. Fontos megjegyezni, hogy a változók, ha mi nem adunk explicite kezdőértéket, akkor deklarációkor nem kapnak kezdeti értéket, az érték valamilyen véletlen bitkombináció lesz. Nem okoz fordítási hibát, azonban a program futása során felléphetnek egyéb problémák. Nevesített konstans 3 komponense van: név, típus, érték. Szintén deklarációval hozzuk létre. Értéke a deklarációnál eldől, és nem változtatható meg futás közben. Szerepe, hogy bizonyos gyakran előforduló értékeket névvel lássunk el, és csak egy helyen kelljen értéket változtatni, ha ez szükséges. Pl.: egy személy neve általában állandó, ez többször is megjelenhet, azonban ha valamikor változtatni kell, akkor csak egy helyen kell, nem szükséges a forrás teljes szövegében manuálisan keresni. Létrehozása 3-féle módon történhet: Konstans létrehozása előfordítónak szóló utasítással: #define nev ertek
ahol a nev a változó neve, ertek az ertéke. Pl.:
#define ANYU 46
Ez tulajdonképpen makrózást végez. Ahol a forrás szövegében szerepel a nev, ott az előfordító a nev helyére az ertek-et illeszti be. Konstans létrehozása deklarációs utasításban: const tipus nev = kifejezes [,nev = kifejezes]...;
ahol a kifejezés típusa és értéke fordítási időben eldönthető. Ez valójában nem nevesített konstanst hoz létre, hanem a fordító fogja ellenőrizni, hogy a változó kap-e valahol új értéket (szerepel értékadás bal oldalán), és fordítási hibával leáll, ha valahol talál olyan értékadást, amelynek bal oldalán konstans szerepel. Pl.:
const double valami = 3.0;
Konstans létrehozása felsorolásos típussal (enumerátor, enum konstans, felsorolásos állandó) enum [enum_név] {név [=konstans_kifejezés] [, név [=konstans_kifejezés]]... }[változólista];
A felsorolásos állandó egész (int) értékek listájából áll. Az enum_név a felsorolásos állandó neve. A listában szereplő nevek implicit módon 0-tól kezdve, egyel növekedve kapnak értéket. Azonban megadható kifejezéssel a névhez tartozó érték, és ekkor az azt követő név eggyel nagyobb értéket kap. Több névnek ugyanaz lehet az értéke, azonban két különböző nevű enumerációban nem lehet azonos név.
7 / 51 Pl.: enum napok { hetfo = 1, kedd, szerda, csutortok, pentek, szombat, vasarnap }; enum napok isten_megpihent = vasarnap;
Ciklusok while-ciklus Feladat: írassuk ki az első 10 számot, és négyzetét: #include <stdio.h> main(){ int i; i = 0; while( i <= 10 ){ printf("i=%d", i); printf(" i^2=%d\n", i*i); i++; }
}
Működése: i-nek adtunk egy értéket: 0-t. A while ciklus fejében levő feltétel kiértékelődik. Ha a feltétel igaz, végrehajtódik a ciklusmag, egyébként pedig a ciklus után folytatódik a program. A ciklusmagban található printf() kiírja az i változó aktuális értékét, mellé pedig a négyzetét (tehát nem csak változót tudunk kiíratni, hanem kifejezést is). Az i++ az i értékét növeli eggyel (inkrementálja), ugyanaz, mintha a következőt írtuk volna: i = i + 1;
Kétféle inkrementálás van: a prefix (++i) és a postfix (i++). Különbség a prefix és a postfix használat között: int i,j; i = 0; j = ++i; printf("%d", j); i = 0; j = i++; printf("%d", j);
Az első esetben az i értéke növekszik 1-el, és ez lesz j értéke. A második esetben i értékét kapja j, majd utána növekszik az i változó értéke. Tehát ha elől van a ++, akkor előbb cselekszik, utána gondolkodik, ha hátul, akkor előbb gondolkodik, aztán cselekszik. A printf() segítségével egyszerre több dolgot is ki lehet íratni: #include <stdio.h> main(){
8 / 51 int i; i=0; while( i <= 10 ){ printf("i=%d i i^2=%d\n", i, i*i); }
i++;
}
A printf() formátumozó stringje: az első paramétere mindig egy string, ez a formátumozó string. Lehet több paramétere is. A formátumozó stringben tudjuk megadni, hogy mit írunk ki és milyen típusú változót íratunk ki. Itt a %d azt jelenti, hogy egy egész típusú változót írunk. Konverziós karakterek: %c - karaktert írat ki %d - egész számot ír ki %ld - hosszú egész számot ír ki %f – float vagy double típust írat ki %L - long double típust írat ki %s - stringet írat ki %p - mutatót írat ki (lsd a mutatóknál) Másik példa: #include <stdio.h> main(){ int vmi, geza; float i; char j; vmi = 12; geza = 23; i = 0; j = 'a'; printf("%d geza=%d %f %c", vmi, geza, i, j); }
Ekkor összerendeli a konverziós karaktereket és a változókat. Fontos, hogy mindig pontosan adjuk meg a formátumkaraktereket a megfelelő típusú változókhoz. Ha több változó van, mint formátumkarakter, nincs semmi gond, csupán a felesleges változókat nem írja ki. Ha több a formátumkarakterek száma, mint a változóké, valamilyen szemetet ír ki, esetleg lefagy a program, még rosszabb esetben elpárolog a sör. Pl.: cseréljük ki a printf-t a következőkre: printf("%d %d %f", vmi, geza, i, j); printf("%d %d %f %c", vmi, geza, i);
A négyzetes kiírató program megírható a következőképpen is: #include <stdio.h>
9 / 51
main(){ int i for( i = 0 ; i <= 10 ; i++ ) printf("%d %d\n", i, i*i); }
a ciklusmag nincs körülvéve { }-el. Ez azért van, mert ha csak egy utasítást írunk a ciklus után, akkor nem szükséges kitenni. Ki lehet egyébként. Fontos: a for ciklus nem előírt lépésszámú ciklus! Csupán a while ciklus általánosítása. A két ciklus tulajdonképpen átírható egymásba: for(A;B;C){ D }
Equivalens ezzel: A; while(B){ D C; }
ahol D több utasítás is lehet. Tipikus hiba for ciklus esetén: for( i = 0 ; i <= 10 ; i++ ); printf("%d %d\n", i, i*i);
nem szabad a for ciklus feje után ;-t tenni (hacsak nem ez a cél) ebben az esetben csak egyszer íródik ki a két szám, ugyanis a for ciklus magja így az üres utasítás. Ugyanez vonatkozik a while ciklusra is, csak ott valószínűleg végtelen ciklus kapunk. Feladat: kérjünk be egy számot, és döntsül el, hogy páros vagy páratlan: #include <stdio.h> main(){ int i; printf("Kerem a szamot: "); scanf("%d", &i);
}
if( i%2 == 0 ) printf("paros\n"); else printf("paratlan");
a scanf() formátumstringje hasonlóképpen működik, mint a printf()-é. Azonban a változók felsorolásakor a változók neve elé & operátort (ún. címe operátor) kell tenni! Ne felejtsük el, ez tipikus kezdő hiba.
10 / 51 Még egy különbség: double típus esetén nem %f –t használunk, hanem %lf szükséges: i%2: a % az egészosztás maradékát adja meg (moduloképzés). Pl.: 3%2 azt jelenti, hogy a 3at elosztva 2-vel 1 lesz a maradék. Jelen esetben azt jelenti, hogy az i osztható-e maradék nélkül 2-vel (azaz páros-e). if-else: A kétirányú elágazás megvalósítása. Ha igaz az if utáni feltétel, akkor az if ág-ban levő utasítások hajtódnak végre, egyébként az else ágbeliek. Az else ág opcionális, nem szükséges kitenni. Ha nincs else ág, és nem teljesül a feltétel, akkor ez egy üres utasítás. Itt is érvényes, hogy egy utasítás esetén nem kell { }, 2 vagy több esetén viszont szükséges. pl.: if( i%2 == 0 ){ printf("ez a szam "); printf("paros\n"); } else printf("paratlan");
ugyanez vonatkozik az else-re is. Feladat: kérjünk be egy zh pontszámot, és írjuk ki, mennyire sikerült teljesíteni: 40 pont alatt sikertelen, 40-50 pont között javithat, 50 és felette megvan az aláírás #include <stdio.h> main(){ int i; printf("Kerem a pontszamot: \n"); scanf("%d", &i); if( i < 40 ) printf("sikertelen"); if( i >= 40 && i <50 ) printf("javithat az alairasert\n"); else printf("sikeres alairas\n"); }
Csellengő else probléma: A program egy számról döntse el, hogy páros-e vagy páratlan. Ha páros, Határozza meg, hogy nagyobb-e egy másik számnál. #include <stdio.h> main(){ int n = 5, i = 6; long eredmeny = 1; if( n%2 == 0 ) if( n > i ) printf("paros, n nagyobb\n"); else
11 / 51 printf("paratlan\n"); }
printf("valami");
Ekkor a program csak a "valami"-t írja ki. A C az else ágat a hozzá legközelebbi else néküli if-hez társítja, és nem nézi, hogyan van tagolva a forrás szövege. Helyesen: #include <stdio.h> main(){ int n = 5, i = 6; long eredmeny = 1; if( n%2 == 0 ){ if( n > i ) printf("paros, n nagyobb\n"); } else printf("paratlan\n"); }
printf("valami");
Egyébként pedig így néz ki az if (ahogy a C is értelmezi): if( n%2 == 0 ) if( n > i ) printf("paros, n nagyobb\n"); else printf("paros, n kisebb\n");
else-if Az else-if szerkezet a többszörös elágazások egy általános lehetősége. A gép sorban kiértékeli a feltételeket, és ha ezek közül talál egy igazat, akkor az ott található utasításokat végrehajtja. Az utolsó else ág opcionális, azt jelöli, hogy ha egyik feltétel sem teljesült, akkor milyen tevékenységet hajtsunk végre. Feladat: Írjuk át a zh pontos feladatot else-if-re #include <stdio.h> main(){ int i; printf("Kerem a pontszamot: \n"); scanf("%d", &i); if( i < 40 ) printf("sikertelen"); else if( i >= 40 && i <50 ) printf("javithat az alairasert\n"); else
12 / 51
}
printf("sikeres alairas\n");
Írjunk faktoriális megoldó programot. N faktoriálisa: 1 * 2 * 3 * … * N-1 * N #include <stdio.h> main(){ int n, i = 1; long eredmeny = 1; printf("Kerek egy szamot: "); scanf("%d", &n); for(; i <=n; i++) eredmeny *= i; printf("%d faktorialisa: %d\n", n, eredmeny); }
A for és a while ciklus tetszőleges komponense elhagyható. Itt a for ciklus első komponense hiányzik, értéket deklarációkor kapott. Változót felhasználhatunk úgy is, hogy nem adunk értéket neki. Ekkor a tartalma véletlenszerű, a memóriában levő valamilyen bitsorozat értéke. Nem okoz fordítási hibát, de futási hibát eredményezhet. Határozzuk meg 0-100-ig a páros számok harmadik hatványát: #include <stdio.h> main(){ int i; for( i = 0; i <= 100; i = i + 2) printf("%d %d\n", i, i * i * i); }
Tehát a for ciklusban (és a while ciklusban is) nemcsak egyesével lehet a ciklusváltozót léptetni, hanem tetszőleges lépésközzel. A ciklusokon belül pedig a ciklusváltozónak lehetséges új értéket is adni, pl.: #include <stdio.h> main(){ int i; for( i = 0; i <= 100; i = i + 2){ i = i + 1; printf("%d %d\n", i, i * i * i); } }
Határozzuk meg, hogy egy szám prímszám-e: Nézzük végig, hogy van-e osztója. Ha van az 1-en és önmagán kívül, akkor nem prím.
13 / 51
#include <stdio.h> main(){ int n = 5, i = 6; int prim = 1; scanf("%d", &n); for( i=2;i
break;
} if( prim ) printf("prim"); else printf("nem prim"); printf("\n\n"); /* ez tomorebb kodot eredmenyez valamint egyes esetekben gyorsabban hajtodik vegre, mint az if */ }
( prim ) ? printf("prim\n") : printf("nem prim\n");
Amire figyelni kell: i nem mehet nullától: osztási hiba. 1 ne legyen, mert akkor nem veszi prímnek, valamint ne érje el a szám értékét sem, hasonló okok miatt. Új eszközök: Megjegyzés: /* megjegyzés */. Ezt a fordító nem veszi figyelembe, nem lesz eleme a kódnak. Ez a programozónak, illetve a forrás szövegét olvasónak szól. Informálhat a felhasznált algoritmusról, a program működéséről, az alkalmazott megoldásról. Bárhol állhat, ahol szóköz állhat és több soron keresztűl folytatódhat. Egy jó program sok megjegyzést tartalmaz. break utasítás: a tartalmazó ciklust leállítja. continue utasítás: folytatja a ciklust, az utána
következő utasításokat nem hajtja végre. Azaz
a következő iterációba lép. Pl.: for(i = 2 ; i < n ; i++){/*n-et nem erjuk el,mert akkor nem veszi primnek*/ if( n % i == 0 ){ prim = 0; } else
break; continue;
printf("itt vagyok\n"); }
Egyszer sem fogja kiírni, hogy itt vagyok.
14 / 51 A continue Különbsége az egyes ciklusok esetén: i = 2; while( i < n )/* n-et nem erjuk el, mert akkor nem veszi primnek */{ if( n % i == 0 ){ prim = 0; } else{ }
break; printf("vegtelen ciklus\n"); continue;
printf("itt vagyok\n"); ++i; }
A kettőnél nagyobb számok esetén ez egy végtelen ciklust eredményez, ugyanis az i-t nem fogja növelni, és így mindig kisebb lesz a megadott számnál. Azaz a ciklus feltétele mindig igaz lesz. Fogalmak: végtelen ciklus: az ismétlődés soha nem áll le. Végtelen ciklusból szabályosan kilépni a break utasítással lehet. Üres ciklus: a ciklusmag nem kerül végrehajtásra, a ciklus feltétele hamis. A ? : operátor hasonló az if-hez, if( feltetel ) utasitas1 else utasitas2
átírható a ( feltetel ) ? utasitas1 : utasitas2 ;
alakba. Pontosvesszőt nem lehagyni! Program: kérjünk be egy számot, és írjuk ki fordítva. #include <stdio.h> main(){ int szam; int eredmeny; scanf("%d", &szam); while(szam){ eredmeny = szam % 10; printf("%d", eredmeny);
15 / 51
}
szam /= 10;
puts(""); }
Működése: a legutolsó számjegyhez úgy férünk hozzá, hogy vesszük a tízzel vett modulóját (azaz az utolsó számjegyet). Kiírjuk, majd a számot újra elosztjuk tízzel. Mivel \n nincs a printf()-ben, ezért egymás mellé írja ki a számjegyeket. Eszközök: /= valtozo /= kifejezes
equivalens a valtozo = valtozo / kifejezes
alakkal. Hasonlóan van +,-,*,% stb is, lsd a precedenciatáblázatot. A while(szam) ugyanaz, mint a while(szam != 0). Magyarázat: a C logikai érték kezelése. A szám nulla lesz az utolsó osztás esetén. Kisebb lesz, mint 10 (0 és 0.9 közé esik). Lsd később. Ezzel az a gond, hogy a nullára nem működik, ugyanis hamis lesz a feltétel. Megoldás lehet, hogy előtte megvizsgáljuk, és ha nulla, kiírjuk. Másik megoldás: do-while ciklus: do{ eredmeny = szam % 10; printf("%d", eredmeny); szam /= 10; } while(szam);
Belép a ciklusba, végrehajtja az utasításokat, majd megvizsgálja a ciklus feltételét, Ha igaz, újra végrehajtja a ciklusmagot. Nem lehet üres ciklus, mivel egyszer mindenképp lefut a ciklusmag. Program: kérjünk be egy számot, és írjuk ki szövegesen. Lehet sok if-else if -et használni, ami nem túl szép. switch-case: switch(egesz_kifejezes){ case konstans_kifejezés : utasítás(ok) case konstans_kifejezés : utasítás(ok) ... default : utasítás(ok)
} A kifejezés típusának egészre konvertálhatónak kell lennie. A case ágak értékei nem
16 / 51 lehetnek azonosak. A default ág bárhol állhat. Az utasításokat nem kötelező megadni (azaz lehet üres case-ág is). Ugyanígy a default ág is opcionális. Működése: Kiértékelődik a kifejezés, majd értéke a felírás sorrendjében hasonlításra kerül a case ágak értékeivel. Ha van egyezés, akkor végrehajtódik az adott ágban megadott tevékenység, majd a program a következő ágakban megadott tevékenységeket is végrehajtja, ha még van ág. Ha nincs egyezés, de van default ág, akkor az ott megadott tevékenység hajtódik végre, és végrehajtja az összes többi ág utasításait is. Ha nem talált megfelelő ágat, akkor ez egy üres utasítás. Ahhoz, hogy csak egy adott ágat hajtsunk végre, külön utasítást kell elhelyezni. #include <stdio.h> main(){ int szam = 5, osztas = 6; int eredmeny; scanf("%d", &szam); switch(szam){ case 4 : case 1 :printf("egy"); case 2 :printf("ketto"); case 3 : printf("harom"); default : printf("sok"); } }
Ez nem működő megoldás. Helyesen: break-el le kell zárni az ágakat. switch(szam){ case 4 : case 1 : printf("egy"); break; case 2 : printf("ketto"); break; case 3 : printf("harom"); break; default : printf("sok"); }
Mint látható, ha több utasítást akarunk elhelyezni egy case ágban, itt nem szükséges a {}-ek használata. Kapcsolat a karakterek és számok között: A karakter belső ábrázolása: értéke a karakter ASCII kódja. A %c konverzió fogja "értelmezni", és az adott ASCII kódhoz tartozó karaktert fogja kiírni. Feladat: készítsünk ASCII kódtáblát.
17 / 51 #include <stdio.h> main(){ int i;
}
for(i = 0; i <= 255; i++) printf("ASCII kod: %d \t karakter: %c\n", i, i);
A csipogás nem hiba miatt van, az egyik kód a speakert csipogtatja. A 10 esetén nem véletlenül van új sor, az az enter kódja (soremelés, hexában: 0x0A). Feladat: bekérünk két számot, egy műveleti jelet, és írjuk ki a művelet eredményét. #include <stdio.h> main(){ float a,b; char c; scanf("%f %c %f", &a, &c, &b); switch(c){ case '+' : printf("%f", a + b); break; case '-' : printf("%f", a - b); break; case '*' : printf("%f", a * b); break; case '/' : printf("%f", a / b); break; default: printf("nem definialt muvelet\n"); } }
Hatáskör Vegyük a következő programot: #include <stdio.h> main(){ int i; i = 3; if( i == 3){ int i; i = 2; } }
printf("%d", i);
18 / 51
A program végén a kiíratás eredményeképpen 3-t kapunk, és nem 2-t. A két deklarált i változónak nem sok köze van egymáshoz. Mint már korábban volt róla szó, deklarálni minden blokk elején lehet. A változók akkor jönnek létre, ha arra a blokkra kerül a vezérlés, amelyben deklarálták, és megszűnnek, ha a blokkot elhagyja a vezérlés. Az if-en belüli változó teljesen független a main-beli változótól, és az if blokkja után már nem is létezik, csupán a nevükben van egyezés. Ezt nevezzük a változó hatáskörének. Egy változó hatáskörének meghatározása pedig a hatáskörkezelés. A hatáskör fogalma névhez és deklarációhoz kapcsolódik. Mindkét i változó lokális a saját blokkjára nézve. Lokális változó hatásköre az a blokk, amelyben definiálták, élettartama pedig az az idő, amíg a vezérlés a saját blokkjában van (ekkor rendelkezik címkomponenssel, ekkor foglal helyet a memóriában). Változó hatásköre az a programrész, ahol érvényesen hivatkozni lehet rá. Lokális változó hatásköre az a blokk, és annak minden beágyazott blokkja, amelyben definiálták. Ha ugyanolyan névvel deklarálok egy másik nevet két, egymásba ágyazott blokkban, akkor a két név másik változót nevez meg. Ez a lyuk a hatáskörben esete: a külső változó számára lyuk keletkezik. Ha nem deklarálom újra, akkor elérhető, és ekkor a belső blokk számára nem lokális, hanem globális változó. Globális változó: ha egy blokkban nincs deklarálva, de érvényesen hivatkozható. Kifejezések, kifejezések kiértékelése, operátorok Kifejezések Operátorokból és operandusokból, esetleg kerek zárójelekből épülnek fel. Két komponensük van: típus és érték. Mindkettő a kiértékelés során a legutolsó operátor által kerül meghatározásra. A legegyszerűbb kifejezés egyetlen operandusból áll. Kiértékelés : az a folyamat, amelynek során az operandusok és operátorok által reprezentált értékeket és az operandusok egy sorrendjét figyelembe véve a konkrét érték előáll. A ()-ek között álló kifejezést mindig hamarabb kell kiértékelni, mint a többit. pl: 2*(3+4) esetén előbb 3+4, majd ezt szorozzuk 2-vel. Az infix kifejezések kiértékelése nem egyértelmű. Pl.: 2*3+4 helyesen: (2*3)+4 és nem 2*(3+4). A nyelvek az operátorokhoz megadnak egy ún. precedenciatáblázatot. Felülről lefelé haladva egyre kevésbé „fontosak”, „erősek” az adott operátorok (azaz precedencia szerint csökkenő sorban vannak), valamint megadják a kötési irányt, amely azt írja le, hogy az azonos erősségű operátorok közül melyiket kell hamarabb kiértékelni (merről-merre haladunk). Az azonos precedenciájú operátorok egy sorban találhatóak. A C precedencia táblázata: Operátor () * * + >> <
[]
Asszociativitás .
& / -
+ %
>
<=
-> !
-
<< >=
~
++
--
SIZEOF
(típus)
→ ← → → → →
19 / 51 == != & ^ | && || ?: = += -= ,
*=
/=
%=
>>=
<<=
&=
^=
|=
→ → → → → → → ← →
A ++ és -- esetén a post nagyobb precedenciájú, mint a pre. Operátorok és jelentésük ()
: függvényoperátor. Lsd a függvényeknél.
[]
: tömb operátor
.
: statikus minősítés. Lsd.: struktúrák
->:
dinamikus minősítés. Lsd.: mutatók
: indirekciós (mutató, pointer) operátor. Lsd pointerek. A mutató típusú operandusa által hivatkozott értéket adja. *
&
: címe operátor. Lsd pointerek. Az operandusának a címkomponensét adja
+ -
: előjelek
!
: logikai negáló operátor. Aritmetikai és mutató típusokon működik.
~
: egyes komplemens képzés. Egész típusokon alkalmazható. : aritmetikai műveletek
+ * - /
: léptető operátorok. A baloldali operandust lépteti a jobboldali által meghatározott számú bittel jobbra illetve balra.. Ha balra léptet, akkor 0-kat hoz be jobbról. Ha jobbra léptet, akkor az előjelbitet lépteti végig bal oldalon. Egész típusú operandusokon működik. <<
>>
: inkrementáló és dekrementáló operátor. A dekrementáló operátor hasonló az inkrementálóhoz, de az 1-el való csökkentést valósítja meg. Aritmetikai típusokon működik. ++ --
: operandusának (amely tetszőleges objektum lehet) a belső ábrázolásához szükséges byte-ok számát adja meg. Pl.: sizeof()
int valtozo; printf("%d", sizeof(valtozo));
megadja a valtozo memóriabeli méretét byte-okban. A valtozo helyere írhattunk volna int-et is. Érdekessége, hogy konstans kifejezést is meglehet adni, amelyet a fordító értékel ki. Azonban ekkor az értéket nem határozza meg, csak a típust, és az ehhez szükséges memória méretét.
20 / 51 : kasztolás, casting, explicit típuskényszerítés. Prefix operátor, operandusát az adott típusra konvertálja. (típus)
< > <= >=
: a szokásos matematikai értelemben vett hasonlító operátorok
== !=
: hasonlító operátorok, operandusainak értéke megegyezik-e, vagy nem egyezik-e meg.
& ^ |
: egész típusú operandusok esetén használható. Bitenkénti AND, XOR, OR művelet
: egész típusú operandusok esetén használható. Rövidzár AND, OR művelet, int 0 és 1 értékek esetén működik. && ||
: feltételes kifejezés. 3 operandusú operátor, már volt róla szó, azonban általánosabb, mint az ott leírtak. Ha igaz a ? előtt található kifejezés, akkor a teljes kifejezés értéke a ? és : között található kifejezés értéke, egyébként pedig a : utáni kifejezés értéke. ?:
Pl.: két változó közül válasszuk ki a nagyobb értéket: #include <stdio.h> main(){ int a,b; int i; a = 5; b = 9; }
i = (a > b) ? a : b;
Itt ha az a a nagyobb értékű, akkor az a értékét, ha nem, akkor a b értékét kapja meg az i változó. Határozzuk meg egy változóról, hogy igaz-e: #include <stdio.h> main(){ int i = 1; printf("i %s", (i) ? "igaz" : "hamis"); }
értékadó vagy más néven hozzárendelés operátorok, már volt róluk szó. Fontos azonban megjegyezni, hogy a nyelvben nincs értékadó utasítás! Helyette értékadó kifejezés van. A hozzárendelés a következő alakú: =, +=, -=, *=, /=, %=, >>=, <<=, &=, ^=, |=
objektum = kifejezés
Az értékadó operátor bal oldalán olyan objektumnak kell állnia, amely értéket képes felvenni. Ezt (módosítható) balértéknek hívjuk. Jobb oldalán meghatározható értékű kifejezésnek kell állni (jobbérték). A hozzárendelés hatására a kifejezés értéke felülírja az objektum értékét (esetleg konverzió történhet). Ekkor az értékadó kifejezés értéke az objektum új értéke, típusa az objektum típusa. Az objektum bármi lehet, kivéve tömböt, függvényt, nem lehet const, ha struktúra vagy union, akkor nem lehet egyetlen tagja sem const minősítésű. ,
: vessző operátor. A balról-jobbra kiértékelést kényszeríti ki. Előbb a baloldali, majd a
21 / 51 jobboldali operandusát értékeli ki. Fontos fogalom a konstans kifejezés: értéke és típusa fordítási időben eldönthető, a fordító értékeli ki. Operandusai általában literálok és nevesített konstansok. Logikai tagadás: ! operátorral történik. #include <stdio.h> main(){ int vmi;
}
vmi = 12; vmi = !vmi; printf("%d", vmi); vmi = !vmi; printf("%d", vmi);
Tagadás esetén bámilyen értékből 0 lesz, 0-ból viszont mindig 1. Logikai AND, OR műveletek: Feladat: írjuk ki, hogy egy év szökőév-e: Szökőév akkor, ha osztható 4-el, de nem osztható százzal, kivéve, ha osztható 400-al. #include <stdio.h> main(){ int i; printf("Kerem az evszamot: "); scanf("%d", &i);
}
if( i%4 == 0 && i%100 != 0 || i%400 == 0 ) printf("szokoev"); else printf("nem szokoev");
A rövidzár operátor a következőt jelenti: logikai értékek esetén nem szükséges minden operandust kiértékelni. Pl.: AND operátor esetén, ha az egyik operandus hamis, akkor a művelet eredménye a másik operandustól függetlenül hamis lesz. Hasonlóan az OR művelet, ha az egyik operandus igaz, akkor a másik operandustól függetlenül igaz lesz a művelet eredménye. Léptető (shift) operátorok: Határozzuk meg, hogy hány bites az int típus az adott számítógépen: #include <stdio.h> main(){ unsigned int i = 1; unsigned short bitek = 0;
22 / 51 do{
++bitek; i = i<<1; } while( i ); printf("%u\n", bitek); }
Az előjel nélküli i változónak a kezdőértéke 1 (kettes számrendszerben: 00000001). Ezt az 1 db 1-es értékű bitet „toljuk” végig a változón belül, egyesével, közben számoljuk az eltolások számát. Bitenkénti műveletek Vegyük egy int típusú változó alsó 5 bitjét, majd kapcsoljuk be a 10. bitjét: main(){ int i = 0x0425; int alsobitek; alsobitek = i & 0x001F; i = i | 0x0200; }
Explicit típuskényszerítés (kasztolás, casting) pl.: int a = 8; double b; b = (double) a;
Ezzel az a értékét double típusra kényszerítettük. Ezt nevezik explicit típuskényszerítésnek. Implicit típuskényszerítés: ha a kényszerítés automatikusan jön létre. Pl.: b = a;
esetén a értéke automatikusan konvertálódik double típusra. Kifejezés típusát az operandusai és operátorai együtt határozzák meg. Pl. int op int esetén int lesz a kifejezés típusa, int op double esetén double (ahol op egy tetszőleges aritmetikai operátor). Aritmetikai típuskonverziók 1. Ha az egyik operandus long double, akkor a másik long double-á alakul 2. Ha az egyik operandus double, akkor a másik is double-á alakul 3. Ha az egyik operandus float, akkor a másik is float-á alakul 4. A char és a short típusok int-é konvertálódnak, ha megőrzi értékét, egyébként unsigned int lesz 5. Ha az egyik operandus unsigned long int, a másik is unsigned long int-é alakul
23 / 51 6. Ha az egyik operandus long int, a másik unsigned int, és a long int ábrázolható az unsigned int értékével, a közös típus long int lesz, egyébként unsigned long int 7. Ha az egyik operandus long int, a másik long int típusúvá alakul 8. Ha az egyik operandus unsigned int típusú, a másik unsigned int-é alakul 9. Egyébként mindkét operandus int típusú
Általánosságban elmondható, hogy ha van egy szélesebb operandus a kifejezésben, akkor a kifejezés típusa a szélesebb operandus típusa lesz. pl.: int a, b; double c; a = 1; b = 2; c = a / b;
ha c értékét kiíratjuk, akkor 0-t kapunk. Ugyanis bár az a / b művelet eredménye 0.5, a C nem kerekít, hanem levágja a tizedespont utáni részt, és az eredmény 0 lesz. A helyes megoldás az, hogy az egyik operandust lebegőpontos számmá alakítjuk: double c; c = (double) a / b;
Ugyanezek literálokra is érvényesek. Azonban literálok esetén fontos a felírási mód. pl.: double c; c = 1 / b;
ez szintén 0-t ad eredményül. helyesen: c = 1. / b; Kerekítsünk egy számot. Egy szám kerekített értéke: ha < x.5, akkor x, ha >=x.5, akkor x+1 pl. 0.5 felfele kerekítve 1. Ezt úgy érjük el, hogy hozzáadunk 0.5-öt, majd csonkítjuk. Ha a szám tizedesrésze >= 0.5nél, akkor a szám egész része így 1-el növekszik, amit ha csonkítunk, megkapjuk a helyes eredményt. ha a tizedesrész kisebb, mint 0.5, és hozzáadunk 0.5-öt, és csonkítjuk, akkor szintén megkapjuk a várt eredményt. Tehát: double x = 3.2, y; y = (int)(x + 0.5);
És megkapjuk a 3-at. Ha pl.: x = 3.75, akkor 4-et kapunk.
24 / 51 , operátor: a balról-jobbra kiértékelési irányt kényszeríti ki. Két operandusa van, egy bal és egy jobb oldali kifejezés. Először a bal oldali, majd a jobb oldali kifejezés hajtódik végre. A kifejezés értéke az utoljára végrehajtott kifejezés értéke. pl.: int x=5, y=3, z=2, w=6; x=5, y+=3,z=0,w++;
ekkor x=5 y=6 z=2 w=7 lesz az egyes operandusok értéke, a kifejezés értéke 6. Fontos: a deklarációs utasításban a , nem operátor, hanem a változók felsorolását jelző karakter! Kérdés: mi lesz x=(5, y+=3,z=0,w++); után x értéke, és miért?
int a=5,b=7,c=5; (a==c && a++!=b-- || --b>c);
a kifejezés értéke 1 lesz, típusa int. Struktúrák: A rekord adatszerkezet megjelenése a nyelvben. Egy vagy több, azonos vagy különböző típusú változók összessége. Tulajdonképpen változókat gyűjtünk össze, és egyben tudjuk kezelni. Pl.: egy személy azonosítható a nevével, születési idejével és helyével, valamint az anyja nevével. Deklarációja: struct [struktúraazonosító] { típus azonosító; [típus azonosító;]...} [változó]...;
Itt a struktúraazonosítót struktúracímkének, a struktúrában felsorolt neveket a struktúra tagjainak nevezzük. A struktúraazonosító és a változó(k) megadása opcionális. Ha nincs változólista, akkor nem foglal helyet, és ha címkézett volt a struktúra, akkor a címke a későbbi definíciókban a struktúra konkrét előfordulása helyett használható. pl.: struct descartes { int x; int y; };
a Descartes-féle koordináta rendszer koordinátáit reprezentálja. Ezután a struct descartes együtt egy változó típus. Változó deklarációja descartes típussal: struct descartes a,b,c;
A fordító éppen annyi helyet foglal le a struktúra számára, hogy benne a struktúra minden
25 / 51 tagja elférjen. A struktúratagok a deklaráció sorrendjében, egyre növekvő memóriacímeken foglalnak helyet. Lehetőség van kezdeti érték adására is: struct descartes a = { 15, 20 };
Fontos, hogy a típus definiálásakor ; választja el a tagokat (ugyanis az deklaráció), míg kezdeti érték adásakor szimplán egy , ! Egy struktúrának ugyanúgy adhatunk egy másik struktúrát értékként, azonban azonos típusúaknak kell lenniük: struct descartes a,b = { 15, 20 }; a = b;
ekkor a tagjainak értéke ugyanaz lesz, mint b-nek. Struktúra adattagjainak elérése a . operátorral történik (statikus minősítés operátor). Pl.: adjunk az a struktúra egyik tagjának értéket: a.x = 2;
Struktúrák nem hasonlíthatóak össze (==)! Az összehasonlítást tagonként kell elvégezni. Unionok Hasonlít a struktúrához, azonban a tagok között 0 a címeltolás. Ugyanazon a memóriaterületen több, különböző típusú adatot tárolhatunk. Pl.: egy változóhoz hozzáférhetünk int, valamint float típusként is. Deklarációja: union azonosito { tipus valtozo; [, tipus valtozo]...} [valtozo] [, valtozo]...; #include <stdio.h> main(){ union unio { int i; float f; char c[4]; }; union unio u; u.i = 1869180533;
}
printf("Egesz:\t%d\n", u.i); printf("Valos:\t%f\n", u.f); printf("Karakter:\t%c \tszam: printf("Karakter:\t%c \tszam: printf("Karakter:\t%c \tszam: printf("Karakter:\t%c \tszam:
%d\n", %d\n", %d\n", %d\n",
u.c[0], u.c[1], u.c[2], u.c[3],
u.c[0]); u.c[1]); u.c[2]); u.c[3]);
26 / 51 A használt számítógépen az int 4 byte-os, ezért a 4 byte-os float-ot fel lehetett használni. A négyelemű karaktertömb pedig pontosan byte-onként éri el a másik két 4 byte-os változót. Nem szükséges minden változó méretének megegyeznie, ekkor a fordító a legnagyobb méretű változónak foglal helyet, így minden tagja el fog benne férni. Azonban a változók ekkor is azonos kezdőcímen helyezkednek el. Pl.: a következő union az ábrán látható módon értelmezhető: union unio { float f; short s; char c; };
ahol LSB a legalacsonyabb helyiértékű bit, MSB a legmagasabb helyiértékű bit a használt számítógépen. Figyelem!!! Ez egy erősen implementációfüggő és hardware függő eszköz!!! Tömbök: Olyan összetett adattípus, amely azonos típusú elemekből áll és a memóriában folytonosan helyezkedik el. Deklaráció: tipus azonosito[elemszam1]{[elemszam2]...}
Az azonosito lesz a tömb neve, [] –ek között az egyes dimenziókhoz tartozó elemszámot adjuk meg, ennek konstans kifejezésnek kell lennie. Az indexelés 0-tól elemszám-1-ig tart. Legalább 1 dimenziót meg kell adni. Egy tömbelemre hivatkozás: név[indexkifejezés1]{[indexkifejezés2]...}
Az index kifejezés tetszőleges egész típusú kifejezés. Az index érvényességét a fordítóprogram nem ellenőrzi, azaz a hibás indexelés csak futás közben derül ki. Feladat: kérjünk be 10 számot, és írjuk ki. #include <stdio.h> main(){ int i,t[10]; for( i = 0; i < 10; i++ ) scanf("%d", &t[i]); for( i = 0; i < 10; i++ )
27 / 51 printf("%d\n", t[i]);
}
Bővítsük ki a kódot úgy, hogy írja ki a tömbben levő elemek összegét, és átlagát. #include <stdio.h> main(){ int i,t[10], osszeg; osszeg = 0; for( i = 0; i < 10; i++ ) scanf("%d", &t[i]); for( i = 0; i < 10; i++ ){ osszeg += t[i]; } }
printf("osszeg= %d, atlag= %f\n", osszeg, osszeg/10.);
Tömb deklarációjakor is lehet kezdőértéket adni: int t[10] = { 10, 20, 30 };
ekkor a tömb első 3 eleme rendre 10, 20 és 30, a maradék 7 elem mind 0 értéket kap. Az int t[] = { 10, 20, 30 };
tömbdeklarációkor egy 3 elemű tömböt deklarálunk, azaz a kezdeti érték adásakor a kapcsos zárójelek közötti elemek darabszáma határozza meg a tömb méretét, ha azt nem adtuk meg. Struktúratömbök struct descartes tomb[22];
Ez egy 22 elemű, descartes struktúrákat tároló tömböt hoz létre. Egy tömbelem egy struktúratagjának elérése: tomb[0].x
Többdimenziós tömbök Elhelyezkedése a memóriában általában sorfolytonos (lsd. adatszerkezetek), azaz a jobb oldali index „fut gyorsabban”. Pl egy matrix[2][3] tömb elemeire a hivatkozás (tulajdonképpen 2x3-as matrix): matrix[0][0], matrix[0][1], matrix[0][2], matrix[1][0],matrix[1][1], matrix[1][2]
sorrendben történik. Ez valójában nem többdimenziós tömb. A C azt mondja, hogy tömbök tömbjeiből épül fel, azaz ennek az 1 dimenziós tömbnek az elemei 1 dimenziós tömbök.
28 / 51 Kezdőértékadás: int t[2][3]={{1,2,3},{4,5,6}};
azaz felsoroljuk a tömböket, és azok elemeit. Lehetséges az is, hogy nem adunk meg elemszámot: int t[][3]={{1,2,3},{1,2}};
Az utolsó indexnek azonban mindig ismertnek kell lennie, csak az első index lehet határozatlan! Magasabb dimenzióra hasonlóan működnek az itt leírtak. A többdimenziós tömb elemeit egyszerű felsorolással is megadhatjuk: int t[2][3]={ 1, 2, 3, 4, 5, 6 };
Ennek az az oka, hogy a tömb a memóriában folytonosan (méghozzá sorfolytonosan) helyezkedik el. Megjegyzés: a más nyelvek által használt tomb[i,j] hivatkozás itt is szabályos, azonban nem az i,j-edik elemet érjük el, hanem a sorfolytonosan tárolt tömb j-edik elemét (lsd C kifejezések). Többdimenziós tömböket általában matrixok reprezentációjára használunk. Feladat: határozzuk meg, hogy egy kvadtratikus mátrix szimmetrikus-e? Kvadratikus a mátrix, ha a sor és oszlopszáma megegyezik (azaz négyzetes alakú). Szimmetrikus, ha a főátlóra szimmetrikus, azaz a főátló alatt és felett ugyanazok az elemek vannak. Kérjük be a dimenziót, majd olvassuk be a mátrixot, az elemeit sorfolytonosan megadva: #include <stdio.h> main(){ int i,j,n; int t[50][50]; int symm = 1; printf("A matrix merete: "); scanf("%d", &n); printf("A matrix elemei:\n"); for( i = 0; i < n; i++ ) for( j = 0; j < n; j++ ) scanf("%d", &t[i][j]); /* szimmetria ellenorzes */ for( i = 0 ; i < n ; i++ ){ for( j = 0 ; j < n ; j++ ){ if( j==i ) continue; if( t[i][j] != t[j][i] ){
/* a foatlot felesleges ellenorizni */
29 / 51 symm = 0; break; } } if( !symm ) break;
/* ha mar tudjuk, hogy nem szimmetrikus, ne folytatodjon a program */
} }
( symm ) ? printf("Szimmetrikus\n") : printf("Nem szimmetrikus\n");
Feladat: Adott egy n X n -es egészeket tartalmazó mátrix. (n, n < 200) Írjuk a kimenetre növekvő sorrendben azoknak az oszlopoknak az indexét (a sorszámozás 0-ról indul), amelyekben több a számok összege, mint a mátrix transzponáltjának ugyanilyen indexű oszlopában. A bemenet első sora tartalmazza n-et, a többi sor pedig a mátrix elemeit n sorba és n oszlopba rendezve. #include <stdio.h> main(){ int i,j,n,t[200][200]; int sum1,sum2; scanf("%d", &n); for( i = 0 ; i < n ; i++ ) for( j = 0 ; j < n ; j++ ) scanf("%d", &t[i][j]); for( i = 0 ; i < n ; i++ ){ sum1 = sum2 = 0; for( j = 0 ; j < n ; j++ ) sum1+=t[i][j],sum2+=t[j][i]; /* a transzponaltra ilyen egyszeruen hivatkozhatunk*/ if( sum1 < sum2 ) printf("%d\n", i); }
}
Stringek (karaktertömbök, karakterláncok) A C nyelvben nincs külön típus stringekre, azokat karakterek tömbjeként értelmezi. A stringeket karaktertömbökben kell elhelyezni. A karakterlánc végét külön speciális, zérus értékű byte, a nullkarakter jelzi ('\0'). Pl.: a "string" karakterlánc a következőképpen kell letárolni egy karakter tömbben:
A karakterlánc első karaktere a tömb 0. eleme, a t az 1., a legutolsó karakter a t[5]-ben van, és az ezt követő t[6]-ban található a lezáró karakter. Tehát a string tárolására használt tömb mérete mindig 1-el nagyobb, mint a stringet alkotó karakterek száma, a lezáró karakter miatt. Fontos megjegyezni, hogy a lezáró karakterlánc tömbindexe pontosan megegyezik a
30 / 51 karakterlánc hosszával. A '\0' karakter nem egyezik meg a 0-val, mint karakterrel ('0'), de megegyezik a 0-val, mint egész számmal. #include <stdio.h> main(){ char str[] = "string"; printf("%s\n", str); }
Karakterenkénti kiíratás: #include <stdio.h> main(){ char str[] = "string"; int i;
}
for( i = 0; str[i] != '\0'; i++ ) printf("%c\n", str[i]);
Ekkor, amíg el nem éri a lezáró karaktert, kiírja az aktuális indexű karaktert. String bekérése: #include <stdio.h> main(){ char str[100]; printf("Kerem a nevedet: "); scanf("%s", str); printf("A neved: %s\n", str); }
Fontos megjegyezni, hogy stringek bekérése esetén nem kell a & címe operátort a változó elé tenni! Mivel a tömb csak 100 elemet, azaz 100 karaktert képes tárolni, ezért bemenetként maximum 99 karaktert tudunk megadni (bár ritka az az ember, akinek 100 karakter hosszú a neve). Ha 99 karakternél többet adunk meg, akkor a tömböt túlindexeljük, és futási hibával leáll a program. Pointerek (mutatók) A mutató adattípus létrejöttének okai: - előre nem ismert méretű tömbök létrehozása (eddig csak fix méretű tömbökről volt szó) - egyetlen adatelemre való többszörös hivatkozás - dinamikus függvényhivatkozás A mutatók memóriacímeket tárolnak. Minden típushoz tartozik egy mutatótípus, amely az adott típusú változó címeit képes eltárolni.
31 / 51
Egy változónak 4 komponense van: név, attribútumok, cím, érték. A mutató egy olyan speciális változó, amelynek típusa: egy adott típusú változóra mutató mutató, értéke: egy adott típusú változó címe. Deklarációja: típus *azonosító[, *azonosító]...;
a * deklarációkor nem a típushoz, hanem az azonosítóhoz tartozik, azaz csupán egy jelöléstípus. Értelmezés: int *bela;
bela egy int * típusú változó, azaz olyan mutató típusú változó, ami egy int típusú változóra mutat. Érdemes a deklarációt jobbról balra olvasni, így könnyebb megérteni: bela egy (*) mutató, ami egy (int) egészre mutat. int *bela; *bela
azaz a hivatkozott változó egy int típusú változó.
Mutató típusú változó inicializálása: int sor, j; int *bela; bela = &sor;
A hivatkozott változónak ismertnek kell lennie! Azonban nem szükséges, hogy a hivatkozás előtt legyen értékadás. operátor : egy adott változó címét adja meg. operátor : egy adott memóriacímen található (meghatározott típusú) aktuális érték előállítása, az indirekció operátora. & *
Pl.: j = *bela;
Mutató mutathat egy másik mutatóra is. Nézzünk meg egy programot: #include <stdio.h> main(){ int i; int *j; int **k;
32 / 51
i = 5; printf("i = %d\n", i); j = &i; *j = 42; printf("i = %d\n", i); printf("*j = %d\n", *j); k = &j;
}
**k = 52; printf("i = %d\n", i);
Három változót deklaráltunk: egy egész, egy egészre mutató mutatót, valamint egy egészre mutató mutató mutatójára mutató mutatót. Az ilyen mondatok elkerülése miatt a mutató, mint eszköz helyett a pointert fogom használni. Első körben az i értéke 5 lett. Ez egy nagyon jó ötlet. Majd a j pointer értékéül i címét adtuk a & operátor segítségével. Ekkor azt mondjuk, hogy a j pointer az i változóra mutat, röviden i-re mutat. A mutatott változóhoz a * operátorral férhetünk hozzá, így az eredeti i változó értéke fog módosulni. A k változó már kétszeres indirekciót valósít meg, amelynek értéke a j pointer címe, egyszeres indirekcióval j-hez, kétszeres indirekcióval pedig i értékéhez férhetünk hozzá (lsd az ábrákat). Ha felvennénk még egy 3 csillagos pointert, akkor a jobb alsó ábra szerint lehetne értelmezni. Nem a konkrét memóriacím a fontos, amelyre mutatnak. A pointereket változókra pozícionálhatjuk, ezáltal műveleteket végezhetünk velük. A mutatott változót ugyanúgy felhasználhatjuk, mintha a sima változót használnánk. Pl.: ++*pi az i változót fogja egyel növelni. Azonban: *pi++ helytelen, ugyanis ez a mutatót növelné, és azután használja a * operátort. Helyesen: (*pi)++. Oka: a precedencia sorrend. Speciális mutató érték a NULL érték:: olyan mutatók értéke, amelyek nem mutatnak változóra. Általában az stdio.h header-ben definiálják a következő módon: #define NULL
((void *)0)
ritkábban: #define NULL
((char *)0)
itt a void * az általános mutatót jelöli (az ANSI szabvány előtt ezt a szerepet a char * töltötte be). Minden tárbeli cím tulajdonképpen egy egész szám, amely egyértelműen azonosítja az adott tárhelyet. Azonban a pointerek és az egész számok nem felcserélhetőek, kivéve a 0 értéket. A 0 konstansként összehasonlítható pointerekkel, valamint azokhoz hozzárendelhető. Pointer aritmetika A pointer értéke egy előjel nélküli egész, vele matematikai műveletek végezhetőek el.
33 / 51 Két pointer értéke összehasonlítható ( ==, !=) #include <stdio.h> main(){ int i,j; int *pi,*pj; i=6; j=6; pi = &i; pj = &j; if (pi == pj) printf("egyenloek\n"); else printf("nem egyenloek\n"); }
Értelmezése: a két pointer ugyanoda mutat-e? Pointer összehasonlítható a NULL értékkel és a 0 konstanssal is. Összeadás Pointerhez adható egy tetszőleges egész érték, amelynek jelentése: érték darabszámú, adott típusú objektummal való előre mutatás a memóriában (lsd. a tömbök és pointerek kapcsolatánál) Pointerből kivonható egy tetszőleges egész érték, amelynek jelentése: visszafelé mutatás a memóriában. Távolság Két pointer kivonható egymásból, ha azonos tömbben, azonos típusú objektumot címeznek, az eredmény pedig a két objektum közti címkülönbség (távolság). Tömbök és pointerek közötti kapcsolat A tömb és a pointer majdnem ugyanaz. Minden tömb azonosítója, mint kifejezés, megegyezik a tömb első elemére mutató pointerrel. Egy tömbelemre hivatkozás pedig megegyezik a neki megfelelő pointer aritmetikai kifejezéssel. Deklaráljunk egy 5 elemű, int típusú t tömböt és egy p, int-re mutató pointert, és tekintsük a következő ábrát:
34 / 51 A t egy pointer, értéke a tömb nulladik elemének a címe. A nulladik elemhez a következő módokon férhetünk hozzá: t[0] tömbhivatkozással *t pointerként *(t+0) pointeraritmetika segítségével Pointerhez tetszőleges egész szám adható. Itt az objektumok int méretűek, és ha a pointerhez hozzáadunk 1-et, akkor pontosan 1 int mérettel fog a pointer arrébb mutatni. Tehát a *(t+1) megegyezik a t[1] tömbhivatkozással, a *(t+2) a t[2]-vel stb. A t értékül adható egy másik pointernek. A p = &t[0];
utasítás megegyezik a p = t ;
utasítással (ezért is nem kellett stringek bekérésénél a & operátor, ugyanis a tömb értéke önmagában egy cím) A tömb elérés és a pointer aritmetika keverhető: *(t+1) megyegyezik a p[1]-el. Egy fontos megjegyzés: a p++ megengedett, a t++ viszont nem, ugyanis a t egy lefoglalt memóriaterület első elemére mutató konstans pointer. A p viszont kaphat új értéket, mert a pointer egy változó, a tömb neve viszont nem az. String bejárása pointeraritmetika segítségével: #include <stdio.h> main(){ char t[]="ez egy string"; char *p; p = t;
}
for( ; *p; ++p ) printf("%c\n", *p);
A fordító nem ellenőrzi, hogy egy tömbre hivatkozáskor érvényes indexet adtunk-e meg. Ha egy tömb nem valós indexű elemére hivatkozunk, akkor ez futási hibát eredményezhet. Azonban egy tömb utolsó utáni elemére szabályosan hivatkozhatunk (ez néhány algoritmus implementálásához szükséges) Struktúrákra és unionokra mutató pointerek Deklaráljunk egy struktúrát, valamint egy rá mutató pointert: struct list{ int data; int index; };
35 / 51
struct list l; struct list *pl;
a struktúra egy elemére a l.data módon hivatkozhatunk. A következő, struktúra tagjaira való hivatkozások egyenértékűek. (*pl).data; pl->data;
Az első esetben a struktúrára mutató pointer segítségével férünk a struktúra adattagjához. A második esetben szintén pointer segítségével érjük el az adattagot, azonban a dinamikus minősítés operátor segítségével. A -> operátort használhatjuk struktúrára vagy unionra mutató pointer mellett, a kétféle elérés equivalens, azonban így egyszerűbb és érthetőbb a kód. A pointer használatát indokolja még a dinamikus adatszerkezetek használata (dinamikus tömb), adatszerkezetek szétszórt reprezentációja, függvények esetén a cím szerinti paraméterátadás. Függvények Az alprogramok a proceduláris absztrakció megjelenése a programozási nyelvekben. Akkor alkalmazható, ha a program különöző pontjain ugyanaz a programrész megismétlődik. Az ismétlődő programrészt csak egyszer kell megírni, és a program azon pontján, ahol ez a programrész szerepelt volna, csak hivatkozni kell rá. Formális paraméterekkel látjuk el, és általánosabban írjuk meg, mint ahogy az adott helyen szerepelt volna. A C nyelvben nincs eljárás, csak függvény. A függvény feladata: egy érték meghatározása, amelynek típusa meg kell, hogy egyezzen a függvény visszatérési típusával. A visszatérési értéket a függvény neve hordozza. Függvény létrehozása: Meg kell adni a függvény prototípusát (a specifikációt, amely általában a main() előtt található) a következő módon: tipus fuggveny_neve([fpl]);
ahol a tipus a visszatérési érték típusa, a fuggveny_neve egy azonosító, fpl pedig a formális paraméterek listája. Speciális típus a void típus: ez jelöli, hogy nem függvényről, hanem eljárás-szerű alprogramról van szó. Tehát C-ben nincs definíció szerint vett eljárás, csak void típusú függvény. A definíciós részben kell megadni, hogy a függvény mit csináljon (implementáció). Hozzunk létre egy függvényt, ami visszatér a paramétereinek az összegével: Legyen a következő prototípusunk: int osszead(int a, int b);
a hozzá tartozó definíció: int osszead(int a, int b){ return (a + b);
36 / 51 }
A prototípus és a definíció nem mondhat ellent egymásnak! A visszatérési érték típusa bármi lehet a tömbön és a függvényen kívül. Ha több paramétere van a függvénynek, akkor ,-el elválasztva sorolhatjuk fel a többit. A return utasítás jelöli, hogy vége a függvénynek, és mi legyen a visszatérési érték. A visszatérési értéket kifejezéssel adhatjuk meg, ennek megadása opcionális. A kifejezést néha zárójelbe teszik. Ha a return utasítás, vagy a return utasításhoz tartozó kifejezés hiányzik, a visszatérési érték definiálatlan, de nem okoz fordítási hibát. Ha nincs return utasítás, akkor a függvényt záró } zárójel elérése esetén térünk visza a hívás helyére. A kifejezéssel meghatározott visszatérési érték típusa meg kell, hogy egyezzen a visszatérési típussal, vagy arra konvertálhatónak kell lennie. A definíciónál NEM kell ; ! A prototípusból elhagyhatóak a változó nevei, elegendő a típust megadni: int osszead(int, int);
A C-ben a main is egy függvény, amely rendelkezik paraméterekkel. Figyeljük meg, hogy a main függvénynek nincs prototípusa. Szokásos egyébként az is, hogy a prototípus helyett csak a definíciót adják meg, még a main előtt. Egészítsük ki az osszead() függvényt egy főprogrammal: #include <stdio.h> int osszead(int a, int b); int main(int argc, char **argv){ int a,b,c; a = 3; b = 4; c = osszead( a, b ); printf("%d\n", c); return 0; } int osszead(int a, int b){ return a + b; }
Ekkor c értéke a és b összege lesz. A függvényhívás helyén a függvény paramétereit aktuális paramétereknek hívjuk, esetleg argumentumoknak. Formális paraméter listából összesen egy darab van (egyszer írtuk le) aktuális paraméter listából annyi, ahányszor meghívtuk a függvényt. Az aktuális paraméternek elegendő, ha van értéke, tehát kifejezés is lehet, nem csak változó. Híváskor a fordító típus és számbeli egyeztetést végez a formális és az aktuális paraméterek között, összerendeli őket, majd átadja az értéket. A C-ben csak érték szerinti paraméterátadás van. Az aktuális paraméterekről átmeneti másolat készül és ezek lokális változókként viselkednek a függvény törzsén belül.
37 / 51
A formális paraméterek saját memóriaterülettel rendelkeznek, azok a függvény saját változói. Függvény hívásakor a vezérlést a függvény törzsének első végrehajtható utasítása kapja meg. Fontos, hogy a hívás előtt ismert legyen a függvény prototípusa. A fordító így tisztában van a paraméterek típusával, számával, sorrendjével, a visszatérési érték típusával. Így a fordító végre tud hajtani implicit típuskonverziót az aktuális paraméter értékén. Ha nem ismert a prototípus, akkor pl.: az alapértelmezett visszatérési érték int, de mi double visszatérési típusú függvényt írtunk, akkor a visszatérési értéket int-é alakítja. Próbáljuk ki azt is, hogy ha az osszead() függvény egyik paraméterét double típussal adjuk meg és nem adunk meg prototípust, mi történik. Megoldás lehet még, hogy a hívás előtt adjuk meg a függvény prototípusát. Azonban ez is deklarációnak minősül, ezért az első végrehajtható utasítás előtt kell megadni. #include <stdio.h> int main(int argc, char **argv){ int a,b,c; int osszead(int a, int b); c = osszead( 3., 4. ); printf("%d\n", c); return 0; } int osszead(int a, int b){ return a + b; }
A main függvény visszatérési típusa int, és 0 értékkel tér vissza. Ez jelzi a hívó környezetnek (általában az operációs rendszernek), hogy rendben lefutott a program. A 0-tól eltérő érték általában a program abnormális befejezését jelzi. A paraméterlistában található két paraméter a parancssori argumentumok kezeléséhez szükséges, lsd később. A main függvény esetén több ok miatt voltak ezek elhagyhatóak: – ha egy függvénynek nem adunk visszatérési típust, az alapértelmezetten int lesz. – egy függvény esetén nem szükséges a return utasítás. A } elérésekor a függvény visszatér a hívás helyére, és a visszatérési érték nem definiált lesz. Hasonlóan a return utasítás után álló kifejezés elhagyásával szintén nem definiált értéket kapunk. – nem szükséges egy függvénynek paramétert adni. Azonban a ()-eket kötelező kitenni. Függvény nem definiálható egy másik függvény törzsében (egymásba ágyazott függvények esete)! A függvényben az a és b független a main-beli a-tól és b-től. A függvény paraméterei lokálisak a függvény blokkjára nézve, értékük pedig az aktuális paraméter értéke. Pl.: int osszead(int a, int b){ a = 10; return a + b; }
38 / 51
és ha a main-ban kiíratjuk az a-t, akkor láthatjuk, hogy nem változott meg az értéke. írjunk egy csere nevű eljárást (void függvényt), ami a két paraméterét felcseréli (adatszerkezeteken rendezőalgoritmusoknál szokás használni). #include <stdio.h> void csere(int a, int b); int main(int argc, char **argv){ int i,j; i=3; j=4; csere(i,j); printf("%d %d\n", i,j); return 0; } void csere(int a, int b){ int c; c = a; a = b; b = c; }
Mint látható, nem cserélte fel az értékeket, a már fenn említett dolog miatt. Megoldás: pointert kell átadni. Pointert akkor szokás átadni, ha a paramétert változtatni kell: #include <stdio.h> void csere(int *, int *); int main(int argc, char **argv){ int i,j; i=3; j=4; csere(&i,&j); printf("%d %d\n", i,j); }
return 0;
void csere(int *a, int *b){ int c; c = *a; *a = *b; *b = c; }
Megjegyzés: a cserét nem szokás függvényként implementálni: lassabb kódot eredményez. Deklaráljuk a következő függvényeket:
39 / 51
int fv1(int); int fv2(void); /* megegyezik az int fv2() -el */ void fv3(int); fv4(int *); void fv5(int *a);
A void az fpl-ben azt jelenti, hogy nincs paramétere. Ugyanaz, ha nem adunk meg semmit. Azonban a visszatérési típus-nál ha nem adunk meg semmit, akkor az int lesz!!! Fordításkor az fv4-nél a fordító ki is fogja írni, hogy implicite int lesz: a.c:6: warning: type defaults to `int' in declaration of `fv4' Nézzük meg az egyes függvények hatását: #include <stdio.h> int fv1(int); int fv2(void); /* megegyezik az int fv2() -el */ void fv3(int); int fv4(int *); void fv5(int *a); int main(int argc, char **argv){ int a,b; a=3;b=2; printf("a= %d, b= %d\n", a,b); a=fv1(b); printf("a= %d, b= %d\n", a,b); b=fv2; /* szintaktikai hiba: meg kell adni a () -et is !!! */ b=fv2(); printf("a= %d, b= %d\n", a,b); fv3(a); printf("a= %d, b= %d\n", a,b); b=fv4(&a); printf("a= %d, b= %d\n", a,b); fv5(&b); printf("a= %d, b= %d\n", a,b); return 0; } int fv1(int a){ return a+1; } int fv2(void){ int b; b=3; return b; } void fv3(int a){ a=2; } int fv4(int *a){ *a=5; return 2;
40 / 51 } void fv5(int *a){ int b; b=3; a=&b; }
Függvényhíváskor automatikus típuskonverzió történik a paraméterek átadásakor: char és short típusból int float típusból double
tömb-ből pointer lesz Régi stílusú függvénydeklaráció: tipus fvnév([paraméternevek]) tipus paraméter; tipus paraméter2; ... { implementáció } pl.: double fuggveny( a, b, c) double a; double b; int c; { return a + b + c; }
Megjegyzés: manapság ezt a stílust már nem használják, lehetőleg ne használjuk, és ne keverjük a két stílust, mert rejtélyes programhibákhoz vezethet. Függvény paraméterében lehet tömböt átadni: void kiir(char param[]){ printf("%d", param); }
Azonban írhatjuk ezt is tömb helyett: void kiir(char *param)
A [] jelölés a paraméterlistában azt jelenti, hogy a memóriában azonos típusú elemek folytonosan helyezkednek el (pl többdimenziós tömb paraméterátadáskor: dinamikus tömb nem feltétlenül foglal le egy összefüggő tárterületet). Tömböt függvényből visszaadni nem lehet! Lehetőség van struktúrát vagy uniont függvény paraméterében átadni, valamint struktúrát vagy uniont felhasználni függvény visszatérési értékeként. Pl.: descartes koordinátájú struktúra létrehozása fügvény segítségével:
41 / 51
struct descartes createdescartes(int a, int b){ struct descartes d; d.x = a; d.y = b; }
return d;
Két descartes koordinátájú pont összeadása: struct descartes adddescartes(struct descartes a, struct descartes b){ struct descartes d; d.x = a.x + b.x; d.y = a.y + b.y; }
return d;
Megjegyzés: kerüljük a nagyméretű struktúrák és unionok paraméterként történő átadását, valamint a velük való visszatérést a függvényeknél, ugyanis a nagyméretű adatokról készült másolás időigényes. Ha nagy méretű adatokkal dolgozunk, inkább egy rájuk mutató pointert adjunk át, illetve adjunk vissza. Rekurzió A függvény törzsében önmagára hivatkozik akár közvetlenül, akár közvetve. Pl.: long fakt(long n){ if (n <= 0) return 1; else return n * fakt(n-1); }
Minden függvényhíváskor az aktuális paraméterek számára hely foglalódik a veremben, amely a függvény befejeztével felszabadul. Így minden függvényhíváskor saját lokális változókkal és paraméter másolatokkal rendelkezik. A függvény 5 értékű paraméterrel történő hívása:
Függvénypointerek A függvények azonosítói tulajdonképpen címek, tulajdonképpen kódok belépési pontjai, amelyek értéket adnak vissza. Ez a cím felhasználható. Függvénypointer deklarációja: long (*fvp)(long n);
ami nem egyenértékű a
42 / 51
long *fvp(long n);
deklarációval! Előbbi egy függvénypointer, utóbbi egy pointerrel visszatérő függvény. Ekkor értékéül adható egy függvény, pl: fvp = fakt;
hívása: (*fvp)( 5 );
esetleg: fvp( 5 );
Írható függvény pointerekből álló tömb is ( "függvények tömbje"): int (*fvp[10]) ();
Ekkor 10 darab függvénypointerből álló tömböt deklaráltunk. Parancssori argumentumok A main függvény hívásában két argumentum szerepel: • argc: az argumentumok száma, értéke legalább 1 • argv: karaktersorozatokat tartalmazó tömböt címző mutató. A paraméterek nevei eltérhetnek Az argv tömb első eleme mindig a futó program elérési útjával ellátott neve, a többi eleme a paraméterek, stringként, utolsó eleme NULL. Pl.: echo Hello World! argc=3 argv[0]="echo" argv[1]="Hello" argv[2]="World!" argv[argc]=NULL Az argumentumok kilistázása: int main(int argc, char *argv[]){ int i; for( i = 0; i < argc; i++ ) printf("%s\n", argv[i]); return 0; }
Program befejezése: Történhet a már ismert return utasítással. A 0 érték jelöli a program normális befejezését, a
43 / 51 0-tól különböző érték pedig valamilyen hibát közöl. Az stdlib.h header-ben definiáltak a következő eszközök: EXIT_SUCCESS: EXIT_FAILURE:
nevesített konstans, sikeres kilépést jelöl nevesített konstans, valamilyen hibát jelez
void exit(int status); void abort();
Tetszőleges szinten a program befejezését eredményezi. A status a kilépési állapotot jelöli, valamilyen egész érték, a hívó program kapja meg, mint információt az indított program kilépési állapotáról. Az exit() függvény kiüríti a kimeneti puffereket, lezárja a fileokat, és meghívja a kilépési függvényeket. Az abort függvény megjeleníti az Abnormal program termination
üzenetet a hibakimeneten, nem zárja le a puffereket és az állományokat, valamint SIGABRT jelzést generál. Dinamikus memóriakezelés Eddig csak statikus tömbökkel foglalkoztunk, amelynek a mérete fix volt, már deklarációkor eldőlt. Azonban szükség lehet egy tömb méretének megváltoztatására. Az is lehet, hogy nem tudjuk előre a tömb méretét, és csak futási időben tudjuk ezt megadni. Tömböt nem tudunk függvény visszatérési értékeként megadni. A C a memóriát három részre osztja. Az elsődleges adatterületen helyezi el a fordító a konstansokat és a statikus objektumokat. A lokális objektumokat és a függvényparamétereket a verembe (stack) teszi. A harmadik memóriaterületet (heap, halom, kupac) futás közben éri el a C program, és változó méretű memória blokkok dinamikus allokálására való. Segítségével megvalósítható a dinamikus méretű tömbök és a szétszórt adatszerkezetek megvalósítása. Az stdlib.h header-ben található függvények segítségével lehet megvalósítani a dinamikus memóriakezelést. void *malloc(size_t meret) meret
méretű objektum számára foglal helyet, a lefoglalt terület inicializálatlan.
void *calloc(size_t nobj, size_t meret)
számú, egyenként meret méretű objektum számára foglal helyet. A lefoglalt terület 0 értékű byte-okkal van feltöltve. nobj
A malloc és calloc függvényeknél, ha a meret 0, akkor egy 0 méretű memóriablokkot foglal le, és érvényes mutatót ad vissza. void *realloc(void *p, size_t meret)
Az újrafoglalás függvénye. A p mutatóval címzett objektum méretét meret-re változtatja. Az objektum tartalma a régi és új méretek közül a kisebbig méretig változatlan marad. Ha az új méret nagyobb a réginél, akkor az új tárterület mutatójával tér vissza.
44 / 51 void free(void *p)
Felszabadítja a malloc, calloc, realloc függvények által lefoglalt, p pointerrel címzett területet. Ha a p pointer értéke NULL, a függvény nem csinál semmit. Ha a malloc, calloc, realloc függvények visszatérési értéke NULL, az hibát jelent, nem sikerült lefoglalni a kért tárterületet. realloc esetén ekkor nem változik az eredeti blokk. Ha nem a malloc, calloc, realloc fügvények által lefoglalt területet próbálunk felszabadítani, akkor ez befolyásolhatja a későbbi allokációs kéréseket, vagy programhibát okoz. A lefoglalt területek a program futásának befejeztével automatikusan felszabadulnak. Példa: dinamikus tömb létrehozása #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]){ int n, *tomb; int i; scanf("%d", &n); tomb = (int *) malloc( n * sizeof(int) ); for( i = 0; i < n; i++ ) scanf("%d", &tomb[i]); for( i = 0; i < n; i++ ) printf("%d", tomb[i]); free(tomb); return 0; }
Példa: kétdimenziós dinamikus tömb létrehozása #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]){ int sor, oszlop, **tomb; int i, j; sor = 2; oszlop = 3; tomb = (int **) malloc( sor * sizeof(int *) ); for( i = 0; i < sor; i++ ) tomb[i] = (int *) malloc( oszlop * sizeof(int) ); for( i = 0; i < sor; i++ ) for( j = 0; j < oszlop; j++ ) scanf("%d", &tomb[i][j]); for( i = 0; i < sor; i++ ) for( j = 0; j < oszlop; j++ )
45 / 51 printf("%d", tomb[i][j]); for( i = 0; i < sor; i++ ) free(tomb[i]); free(tomb); return 0; }
A memória felszabadításának sorrendje pont ellentétes a lefoglalásokkal. Ha nem szabadítjuk fel a már nem szükséges területeket, akkor feleslegesen a memóriában marad a lefoglalt terület, amely az ún. memóriaszivárgáshoz vezet. A felszabadítás mindig a programozó, illetve a lefoglalást végző függvény hívójának a felelőssége. Tömb, mint függvény visszatérési értéke Tömbbel függvény nem térhet vissza, mutatóval azonban igen. Tehát egy olyan pointerrel térünk vissza, amely egy dinamikus tömbre mutat. Példa: string másolása #include <stdio.h> #include <stdlib.h> char *strdup(char *string); int main(int argc, char *argv[]){ char string[] = "string"; char *ujstring; ujstring = strdup(string); printf("%s \n", ujstring); free(ujstring); return 0; } char *strdup(char *string){ char *tomb; int i; tomb = (char *) malloc( (strlen(string) + 1) * sizeof(char) ); for( i = 0; string[i]; i++ ) tomb[i] = string[i]; tomb[i] = '\0'; return tomb; }
Ehhez hasonló módon többdimenziós dinamikus tömbbel is visszatérhetünk. Vegyük észre, hogy a helyfoglalás az strdup függvényben történt, a felszabadítás viszont azon kívül. Struktúra pointerrel történő visszatérés
46 / 51
A függvény két struktúrára mutató pointert fog megkapni, és egy struktúrára mutató pointerrel fog visszatérni: struct descartes *adddescartes(struct descartes *a, struct descartes *b){ struct descartes *d; d = (struct descartes) malloc( sizeof(struct descartes) ); d->x = a->x + b->x; d->y = a->y + b->y; }
return d;
Hasonlóképpen lehet unionra mutató pointerrel is visszatérni. Miért nem így oldottuk meg a függvényt?: struct descartes *adddescartes(struct descartes *a, struct descartes *b){ struct descartes d; d.x = a.x + b.x; d.y = a.y + b.y; return &d; }
Azért, mert a d változó lokális változó, amely a függvény befejeztével megszűnik, és a visszaadott pointer értéke egy nem definiált memóriaterületre fog mutatni. Bár, ha nem végzünk memóriafoglalást a hívó környezetben, míg fel nem használjuk a viszakapott címen található értéket, akár helyes eredményt is kaphatunk, azonban nem érdemes ezt a szerencsére bízni. Többek közt ezen okból nem szabad tömböt megadni függvény visszatérési értékeként. Újrafoglalás, átméretezés: char *readline(){ char *ret; int i = 1; int c; ret = (char *) malloc( sizeof(char) ); while( ( c=fgetc(stdin) ) != EOF && c != '\n' ){ ret = (char *) realloc( ret, ( ++i )*sizeof(char) ); ret[ i-2 ] = c; } ret[ i-1 ] = '\0'; }
return ret;
Ez a függvény a standard bemenetről olvas be egy tetszőleges hosszú sor. Az újrafoglalás a while ciklusban történik, a realloc() függvény segítségével. Ha érvényes karaktert adunk meg, akkor 1-el növeli a lefoglalt dinamikus tömb méretét. Megjegyzés: az egyesével történő helyfoglalás lassítja a program futását. A realloc() ugyanis, ha a paraméterül megadott
47 / 51 memóriaterületet növeli, akkor először lefoglal egy új memóriaterületet, a régi tartalmát átmásolja majd felszabadítja, és visszatér az új memóriaterület címével. Helyette érdemes pl 100 karakternek elegendő helyet foglalni, és csak akkor újrafoglalni, ha már betelt az eddig lefoglalt memóriaterület. Az fgetc() és a EOF-t lsd a filekezelés fejezetben. Filekezelés File-okkal már eddig is dolgoztunk, csak nem tudtunk róla. A C ugyanis ismer egy implicit állománynak megfelelő fogalmat. Ez automatikusan létezik, nevük: standard bemenet, illetve standard kimenet. A standard bemenet általában a billenytűzethez (konzolhoz) köthető, a második általában a monitorhoz. Tehát amikor a scanf() függvénnyel kértünk be számokat, akkor tulajdonképpen egy file-ból (standard bemenet, billentyűzet) olvastunk adatot. A printf() szintén egy file-ba ír (standard kimenet, monitor). Próbáljuk ki az ún file átirányítást: futtassuk egy parancssorban (konzol, command line, terminal) pl a tömb elemeinek összegét meghatározó programot a következőképpen: hozzunk létre egy szöveges file-t, amelynek tartalma legyen ugyanaz, mintha a konzolon gépeltük volna be az adatokat (egy szám, majd enter). Majd futtassuk a programot a következőképpen: program < textfile Ekkor nem fog a konzolról adatot bekérni, hanem csak megjeleníti a kimeneten az eredményt. A textfile ekkor a billentyűzetet helyettesíti. Három file létezik alapértelmezetten: stdin: standard bemenet stdout: standard kimenet stderr: standard hibakimenet A C a file-okat ún. stream-ként, byte-ok sorozataként, karakterek sorozataként értelmezi. A programozó dolga, hogy ezt az adatfolyamot feldolgozza és értelmezze. Ha saját szövegfile-t akarunk kezelni, ahhoz a file-t deklarálni kell, majd megnyitni, feldolgozni az adatokat, végül lezárni. File-ok kezelésére nincs beépített mechanizmus, standard könyvtári függvényekkel állnak rendelkezésre. #include <stdio.h> int main(){ FILE *f; char t[100]; f = fopen("vmi.txt", "r"); if( f == NULL ){ puts("Nem lehet megnyitni a \"vmi.txt\"-t"); return 0; } fscanf( f, "%s", t ); fclose( f ); printf("%s", t);
48 / 51
return 0; }
Ez a kód beolvas egy sort a vmi.txt file-ból, majd kiírja a képernyőre. A főprogram első sora tartalmazza a deklarációt. A FILE struktúra az stdio.h-ban van definiálva. A fopen() függvény első paramétere egy string, a file neve (esetleg teljes elérési úttal), második paramétere szintén egy string, a megnyitási mód, amely legalább egy, de maximum három karakterből állhat. A módok a következőek lehetnek: r
read, csak olvasás (a file létezik)
w
write, csak írás
a
append, csak hozzáfűzés
r+
olvasás+írás (a file létezik)
w+
olvasás+írás
a+
olvasás+hozzáfűzés
A harmadik karakter határozza meg, hogy milyen állományt akarunk megnyitni: t: szöveges (text) b: bináris (binary) Ha nem adjuk meg a t vagy a b karaktert, akkor a text file lesz az alapértelmezés. Ha a file nem létezik vagy nem nyitható meg, a fopen() függvény visszatérési értéke NULL, egyébként a file-ra mutató pointer. Az eddig használt be, illetve kimenetet kezelő függvényeknek általában van egy f betűvel kezdődő változata is, itt pl az fscanf(), amelynek első paramétere, hogy melyik file-ból kívánunk olvasni. Hasonlóan van fprintf(), amely egy írásra vagy hozzáfűzésre megnyitott file-ba ír, fputs(), az előző fejezet végén az fgetc(), amely egy karaktert olvas be a paraméteréül kapott file-ból stb. Az fclose() függvény lezárja a paraméterül kapott file-t. Visszatérési értéke 0, ha sikeres volt a lezárás, egyébként EOF. Az EOF egy konstans, amely az stdio.h-ban van definiálva, és az állomány végét jelzi (End Of File). Standard könyvtári függvény a feof(), amely nem nulla értékkel tér vissza, ha a paraméteréül kapott file elérte az állomány végét. Pl. a következő program a standard bemenetet átmásolja a standard kimenetre: #include <stdio.h> int main(){ int c;
49 / 51
while( !feof(stdin) ){ fgetc( stdin ); }
fputc( c, stdout );
return 0; }
Az fgetc() egy karaktert olvas a paraméteréül megadott file-ból, az fputc() egy paraméteréül kapott karaktert helyez a szintén paraméteréül kapott file-ban. Beolvasáskor szükséges az int méret, ugyanis az EOF-t nem tartalmazza az ASCII kódtábla, tehát egy char méretű változóban nem lehet tárolni értékvesztés nelkül. Megjegyzés: a printf("%s", t) megegyezik az fprintf(stdout, "%s", t) függvényhívással, hasonlóképpen a scanf( "%s", t ) megegyezik a fscanf( stdin, "%s", t )-val. Megjegyzés: elérési útra példák különböző platformokon: a teljes elérési út UN*X/Linux rendszereken: "/tmp/valami.txt" Microsoft platformon: "c:\\temp\\valami.txt" Azaz pl Windows alatt egy darab \ jel helyett kettőt kell használni, lásd escape karakterek. Ha nincs megadva teljes elérési út, akkor az aktuális könyvtárban fogja keresni a file-t.
50 / 51 Escape karakterek: \a \b \f \n \r \t \v \\ \? \’ \” \ooo \xhh
figyelmeztető jelzés (alarm) visszalépés (backspace) lapdobás (formfeed) új sor (new line) kocsivissza (carriage return) vízszintes tabulátor (horizontal tab) függőleges tabulátor (vertical tab) backslash (\) kérdőjel aposztróf idézőjel oktális szám hexadecimális szám
printf()
formátumozó karakterei (nem teljes lista):
d, i
decimális szám
o
előjel nélküli oktális szám
x, X
előjel nélküli hexadecimális szám, 0x nélkül
u
előjel nélküli decimális szám
c
karakter
s
string
f
float vagy double
e, E
lebegőpontos szám, exponenciális alakban
g, G
%e, vagy %E, vagy %f alakú kiírás, a számtól függően
p
pointer
n
a printf() hívásakor kiírt karakterek darabszáma kerül az argumentumba
%
%
+ (space) 0 mezőszélesség.pontosság ' '
Pl.: printf("%+010.3f\n", 6.234567f);
Balra igazítás Előjel kiírása Szám elé szóköz, ha nincs előjel 0-val tölti fel az üres helyeket a mezőben Legalább mezőszélesség-ben írja ki a számot, pontosság tizedesjegyig
51 / 51 előjellel együtt, vezető nullákkal egy 10 széles mezőbe írja ki a számot, 3 tizedesjegy pontossággal, a kimeneten a következő jelenik meg: +00006.235 scanf() formátumkarakterei (nem teljes lista): d
decimális szám
i
egész szám
o
oktális szám
u
előjel nélküli decimális szám
x
hexadecimális egész szám
c
karakter
s
string
%
%