Informatika2-2015/Eloadas 9 C-2 C tombok

A MathWikiből
A lap korábbi változatát látod, amilyen Csirke (vitalap | szerkesztései) 2015. április 8., 09:25-kor történt szerkesztése után volt.
(eltér) ←Régebbi változat | Aktuális változat (eltér) | Újabb változat→ (eltér)

Tartalomjegyzék

C tömbök

A C-ben a tömbök töltik be körülbelül ugyanazt a szerepet, mint python-ban a listák. Ez az alapvető mód, amivel több dolgot egymás után rendezhetünk. Azonban a tömbök egy jelentősen egyszerűbb konstrukció mint a listák. Ennek egyik része a típusosság következménye, egy tömbben mindennek egyforma típusúnak kell lennie, hisz előre meg kell mondanom az elemek típusát. De vannak ezen túli különbségek is, amik még megnehezíthetik az életünket.

Tömbök definiálása

Alapvetően kétféleképpen lehet tömböket definiálni a C-ben. Az egyik módszer az, ha a definíciónál egyből megmondod az értékeket is amik a tömbben vannak. Pl.:

int primek[] = {2, 3, 5, 7, 11, 13, 17, 19};
printf("%d\n", primek[0]);  // Ez a 2-t fogja kiirni

A másik módszer az, hogy a definíciónál csak a méretét mondom meg, pl.:

int primek[8];
primek[0] = 2;
// Ez nem mukodik: primek = {2, 3, 5, 7, 11, 13, 17, 19};
// Csak definicional
printf("%d\n", primek[0]);  // Ez a 2-t fogja kiirni

Miután definiáltam a tömböt, a következő dolgok igazak rá:

  • A tömb mérete fix, nem változhat a futás közben. Sőt, a hivatalos szabvány szerint nem is lehet oda a méret helyére változót írni, csak egy fix számot, esetleg egyszerű számítást mint "4*10". (A gcc viszont elfogad egy változót oda.)
  • Ezek a tömb típusok is mind különböző típusok (általában úgy írják hogy pl. "int[]" ha int tömbről van szó), tehát nem "kompatibilisek" más típusokkal csak az explicit megengedett esetekben.
  • A tömb tagjait a python-hoz hasonló szögletes zárójeles módszerrel lehet elérni, és itt is 0-tól (n-1)-ig vannak számozva. Itt nem működnek az olyan negatív indexek, se az ilyen hosszabb részek kiválasztása kettőspontos kifejezésekkel. Csak egy darab egész szám lehet az index amit a szögletes zárójelbe írunk.
  • A tömb hosszának meghatározására nincs egyszerű és mindig működő módszer. Általában ha nem egyértelmű (mert pl. mindig 3 dimenzió), akkor egy külön egész szám változóban tároljuk el, amit a tömbbel együtt odaadunk pl. függvényeknek meg ilyesmi.

Ez a szögletes zárójel egy módosító az alaptípushoz. Egy sorban több változót is lehet definiálni, és a hozzátartozó módosítók különbözőek is lehetnek, így pl. ez:

int tomb_a[10], szam, tomb_b[10];

Az két tömböt definiál, és egy sima int-et. De általánosságban azt ajánlom hogy egy sorban csak egy változót definiáljunk, vagy legfeljebb azonos típusú összetartozó dolgokat, de ilyen különböző típusokat ne vegyítsünk.

Ellenőrzés és "undefined behaviour"

Fontos különbség a python és a C alapvető filozófiái között, hogy mit csinál a program, ha hiba történik. Nézzük a következő, egyszerű, C kódot:

int a[] = {10, 11};
int b[] = {20, 21};
 
printf("%d\n", a[-1]);

Mit fog kiírni ez a kód? Először is, az biztos, hogy nem fog egy szép (vagy csúnya) hibaüzenetet dobni, hogy nem lehet -1-dik tagot kérni. Ahol én teszteltem, ott az alapbeállításokkal, ez azt írja ki, hogy 21, mert a memóriában éppen a b tömb adatai vannak az a mellett, ezért ha megyek egyet az a tömb 0. elemétől visszafelé, ott a b tömb utolsó elemét találom. A C szabvány szerint ez "undefined behaviour". Ezzel a kifejezéssel még fogunk találkozni máskor is, úgyhogy kifejtem hosszabban mit jelent ez a C sztenderd esetében.

A szabvány meghatározza hogy a helyes programoknak hogy kell viselkedniük, nem csak hogy mit kell kiírniuk, de a hatékonyságra és egyéb hasonló dolgokra is ad feltételeket. Azonban, ha a program nem helyes, ha a programozó hibát követ el, akkor sok helyen a szabvány azt mondja, hogy ebben az esetben a lefordított program "undefined behaviour"-t tanusít. Ez elméletben a szabvány szerint azt jelenti, hogy ha ilyent csinálunk, akkor a szabvány felteszi a kezét, hogy akkor ő nem felelős azért, hogy mi fog történni, akár az egész merevlemezünket is formázhatja a program.

Ezt persze nem úgy kell érteni, hogy azt leírni hogy "a[-1]" veszélyes a gépünk egészségére. Azért nincs meghatározva ilyen esetben a viselkedés, hogy a különböző fordítók a lehető legnagyobb hatékonyságot érhessék el abban az esetben, ha helyes a program. Általában a fordítók sok esetet, ami a szabvány szerint "undefined behaviour", jobban definiálnak a dokumentációjukban, hiszen ők már tudják, hogy az ő fordítójuknál ott mi fog történni. Pl. lehet hogy a gcc dokumentációjában le van írva, hogy "ha az egészeket tároló tömbből olyan index-el olvasunk ki adatot, ami nem értelmes, akkor is vissza kapunk egy számot, aminek az értéke bármi lehet". (Bár ebben az esetben ez nem a teljes igazság, de ilyesmi.)

Ezzel ellentétben ugye a python kiírja hogy IndexError ezmegaz. De mit nyerünk ezzel az "undefined behaviour"-rel, fejfájásokon túl? Hatékonyságot! Ha a python-on leírom azt, hogy "l[2]", akkor a processzoron sok utasítás fog lefutni, ami először leellenőrzi hogy a 2-nek van-e értelme az adott listánál, ha nincs, akkor a hibát jelzi, ha van, akkor kiolvassa a megfelelő értéket. A C-ben egyetlen utasítás lesz, ami a megfelelő memóriacímet kiolvassa. Manapság azok az egyszerű ellenőrzések amiket a python csinál, a legtöbb esetben beleférnek az időbe, úgyhogy a legtöbb alkalmazásnál megéri ezt a kis hatékonyságot nagyobb biztonságra cserélni, de általában azokra a célokra használják a C-t ahol minden kis hatékonyságnak örülnek.

A fix méret kezelése

Mivel a tömb mérete a szabvány szerint még változó sem lehet, csak teljesen fix érték, ezért bajban vagyunk ha nem tudjuk előre hogy mekkora tömbre lesz szükségünk, mert pl. a felhasználó által megadott adatoktól függ. Erre több megoldás is van, a szép megoldás majd az lesz, amit később tanulunk, hogy a memóriát kezeljük a programunkban. Addig is egyszerűbb, és sokszor teljesen megfelelő megoldás, ha azt mondjuk, hogy az adott program/függvény maximum 1000 elemű tömb kezelésére alkalmas.

// Dokumentaljuk is le, hogy max. 1000-el mukodik
int fun(int n) {
  if(n > 1000) {
    return -1;  // Ezzel a hibat jelezzuk
  }
  int tomb[1000];
  // A tenyleges szamolasok...
  // Amik a tomb-nek csak az elso n elemet hasznaljak
}

Ez ugyan pazarlásnak tűnhet, és ha nagyon hatékony kell hogy legyen a programunk, akkor nem is jó ötlet, de általában jó. (És ha hatékonyak akarunk lenni, akkor még mindig át tudjuk írni később.) És végezzünk egy gyors számolást hogy mennyire pazarlunk ezzel (a C-nek egy előnye hogy az ilyesmi pontosan kiszámolható):

A gépünkben manapság mondjuk legalább 1 GB (GigaByte) memória van. Egy int típusú egész szám pontosan 4 byte memóriát foglal C-ben. Tehát ez az ezer elemű tömb még mindig csak 4 KB, a rendelkezésre álló memória 250000-ed része. (Ezzel ellentétben a firefox/chrome könnyen többszáz MB memóriát el tudnak vinni.) Amíg az így fixen lefoglalt memória 1 MB alatt van, még messze nem kell aggódni a hatékonyság miatt, ha amúgy úgy biztosra mehetek hogy az elég lesz.

C karakterláncok

A C-ben több különböző típus is van az egész számok tárolására, attól függően hogy mennyi memóriát foglalnak. Nagyjából ezek vannak:

  • A char típus -128 és 127 közötti számokhoz, 1 byte-on
  • A short típus -32,768 és 32,767 közötti számokhoz, 2 byte-on
  • Az int típus -2,147,483,648 és 2,147,483,647 közötti számokhoz, 4 byte-on
  • A long típus, ugyanaz mint az int (A szabványban nem így van meghatározva, de jelenleg ez a helyzet.)
  • A long long típus -9*10^18 és 9*10^18 közötti számokhoz, 8 byte-on (bár ez hivatalosan még a '89-es C szabványban nincs benne, csak a '99-esben, ismerik a fordítók)

Még vannak mindegyikből unsigned változatok is, ha negatív számokra nincs szükségünk, pl. az unsigned int típus 0 és 4,294,967,295 közötti számokat tud tárolni. Még egy fontos típus amit néha láthatunk a size_t, ez csak egy másik név valamelyik már említett típusra (általában az unsigned int-re), a beépített könyvtárak függvényei ezt használják a dolgok méretének a megadására. További leírást találhatunk pl. az angol wikipédián.

Na de ezek közül az egész típusok közül most a char-al foglalkozunk kicsit. A C-ben ha szöveget akarunk tárolni, akkor azt char típusu változók segítségével tesszük. Egy char változóban egy angol betűt el lehet tárolni ASCII kódban kódolva. (Nem angol betűk tárolását a python-ban sem tárgyaltam, de ott azért viszonylag kis szenvedéssel megoldható. C-ben elég nehéz, nem ajánlom.) Így a szöveget el lehet tárolni egy char tömbben, pl.:

char szoveg[] = "Szoveg!\n";

Ez egy 9 elemu char tomb lesz, és szoveg[0] az 83, mert 83 a nagy S-nek az ASCII kódja. Tehát ha egy szöveget char tömbként akarunk nézni akkor arra használjuk a dupla idézőjelet. Ha egy betűt külön akarunk char-ként (és nem tömbként) nézni, ahhoz egyszeres idézőjel kell. Pl.:

char betu = 'S';
// vagy
if(szoveg[0] == 'S') {
  printf("S-el kezdodik!\n");
}

Még egy dolgot csinál pluszban a dupla idézőjel ami itt nem látszott, mégpedig azt, hogy a karakterlánc végére tesz egy 0-t. A 0-nak az ASCII táblában nincs jelentése (az van odaírva hogy "null"), hanem speciálisan arra szokták használni hogy jelezzék a végét a karakterláncnak. Így ha betünként akarunk feldolgozni egy karakterláncot, akkor addig kell menni amig egy 0 betűt találunk. Pl.:

int feldolgoz(char[] szoveg) {
  int i = 0;
  while(szoveg[i] != 0) {
    // Csinalunk valamit szoveg[i]-vel
    ++i;
  }
}

Vagy, mivel a for ciklus rugalmas, azzal is ugyanezt megcsinálhatjuk:

int feldolgoz(char[] szoveg) {
  int i;
  for(i = 0; szoveg[i] != 0; ++i) {
    // Csinalunk valamit szoveg[i]-vel
  }
}

Sőt, mivel a C-ben nincs külön igaz/hamis változó, hanem a 0 jelent hamisat, és minden ami nem 0 az igazat jelent, ezért még egyszerűbben így is írhatjuk:

int feldolgoz(char[] szoveg) {
  int i;
  for(i = 0; szoveg[i]; ++i) {
    // Csinalunk valamit szoveg[i]-vel
  }
}

Ezeket tudva érthetjük hogy a következő két tömbmegadás miért jelenti pontosan ugyanazt:

char sz1[] = "abc\n";
char sz2[] = {97, 98, 99, 12, 0};

A karakterláncok kezeléséhez lesz néhány függvény a beépített könyvtárakban majd, egyelőre ennyit elég tudni róluk.

Személyes eszközök