Informatika2-2015/Eloadas 11 C-4 Uj tipusok

A MathWikiből
A lap korábbi változatát látod, amilyen Csirke (vitalap | szerkesztései) 2015. április 27., 10:05-kor történt szerkesztése után volt.

Tartalomjegyzék

Tömbök és függvények

Még az előző előadás mutató/tömb témájával kapcsolatban ez kimaradt akkor.

Függvény paraméterként kapás

Mint mondtam, a tömb változóknak nem lehet változtatni az "értékét", csak a "tartalmát", tehát eleve nem futtathatnék ilyesmit:

int v1[10];
int v2[10];
v1 = v2;

Tehát ha ilyesmit nem csinálhatunk, akkor a függvény paraméterként kapás, amit múlt órán láttunk, az hogy hogy működik? Úgy, hogy ha függvény paraméternek tömböt írunk, akkor azt a C teljesen úgy kezeli, mintha mutatót írtunk volna. Tehát ez a 3 mind ekvivalens:

void f1(int param[]) { ... }
void f2(int param[10]) { ... }
void f3(int *param) { ... }

Tehát összefoglalóan, változó definiálásnál a szögletes zárójelnek kétféle jelentése lehet:

  • Ha a változó egy függvény paraméter, akkor a szögletes zárójel az nem jelent mást mint a csillag, azt jelenti, hogy az adott változó mutató.
  • Ha a változó nem függvény paraméter, hanem egy tetszőleges blokkon belül definiált változó, akkor az egy új változó, ami vagy akkora amit a szögletes zárójelben megadtunk, vagy akkora amennyi értéket beletettünk kezdetben. Addig létezik ez a tömb, amig a blokk végéig érünk.

Ennek egyik következménye, hogy a tömbök "mutable"-ként viselkednek, amikor átadjuk őket függvény paraméterként, de ez csak azért van, mert ilyenkor gyakorlatilag mutatót adunk át.

Tehát akkor tömb visszaadása

Eddig nem beszéltünk arról, hogy hogy nézne ki az, ha egy tömböt akarnánk visszaadni egy függvényből. A szintaxis eleve nem működik, mert hogy nézne az ki? "int f[]() {..."? vagy "int f()[] {..."? Ez egyik sem működik, és ez nem véletlen.

Ehelyett egyelőre azt a módszert alkalmazhatjuk, ha azt akarjuk, hogy egy függvény egy tömböt adjon vissza, hogy a függvénynek odaadunk egy tömböt, és abba beírja a megfelelő értékeket a függvény. Ilyenkor érdemes a tömb hosszát is odaadni a függvénynek, hogy ne menjen túl. És ilyenkor az a szokás, hogy az a visszatérési érték, hogy hány elemét sikerült a tömbnek ténylegesen feltölteni. Pl.

// Feltolti a tomb-ot az n-nel kisebb paros
// szamokkal
int parosok(int n, int tomb[], int tombhossz) {
  int i;
  int hely = 0;
  for(i = 0; i < n; i += 2) {
    tomb[hely] = i;
    ++hely;
    if(hely >= tombhossz){
      return hely;
    }
  }
  return hely;
}
 
#define TOMB_MERET 20
 
int main()
{
  int szamok[TOMB_MERET];
  int siker;
  siker = parosok(30, szamok, TOMB_MERET);
 
  // ...
 
  return 0;
}

Típusok definiálása

Az eddigi típusok amiket tanultunk, azok az egész és valós számok, és az ezekből álló tömbök, meg az ezekre mutató mutatók. A mai előadás hátralevő részében végignézünk minden más típust ami még fontos a C-ben és eddig nem említettük. Két dolgot még kihagyunk (a uniont és a függvény mutatót), mert azokat akkor lenne haszna elmondani ha bőven több mint 6 előadás lenne C-ből, de azok már sokkal ritkábban kellenek. Ezekkel a típusokkal együtt amik most jönnek, a C kódok túlnyomó többségét már meg tudjátok érteni.

const

konstans változók

A const kulcsszó a C nyelvben egy típus módosító, amit a típus neve elé lehet tenni. Ha egy változó const típusú, az azt jelenti hogy nem változtathatjuk meg az értékét sohasem. Mivel nem változtathatjuk meg az értékét, ezért ha const-nak deklarálunk egy változót, akkor általában egyből abban a sorban értéket is adunk neki, mert később már nem lehet:

const int n = 10;
// Ezt nem tudom csinalni:
n = 20;

Ha egy függvényparaméterre mondjuk hogy const, akkor az persze a függvény hívásakor kap értéket, és a függvényen belül nem változtatható meg:

void kiir(const int k) {
  printf("%d\n", k);
  // Itt nem valtoztathatnam meg k-t, nem lehetne pl.
  // --k;
}
 
void main() {
  int n = 10;
  // Itt kap a "k" erteket:
  kiir(n);
}

konstans mutatók

Mutató esetén kétféleképpen érthetem hogy egy változó legyen konstans:

  • vagy azt akarom, hogy a mutatón keresztül elérhető változó értékét ne módosíthassam,
  • vagy azt, hogy ne módosíthassam azt, hogy melyik változóra mutat a mutató.

Attól függően hogy melyiket szeretném csinálni, máshova kell tennem a változó deklarálásában a const-ot:

int a = 5;
// Normalis mutato:
int *m1 = &a;
// Mutato, amin keresztul nem lehet valtoztatni
// a masik valtozo erteket:
const int *m2 = &a;
// Mutato, aminel nem lehet valtoztatni
// hogy hova mutat:
int * const m3 = &a;
// Mutato aminel semmit sem lehet valtoztatni:
const int * const m4 = &a;

Úgy lehet megjegyezni, hogy melyiknek mi a hatása, hogy amikor az egész számot nem lehet változtatni (amire mutat), akkor az int mellett van a const, amikor a mutatót magát, tehát azt hogy hova mutat, akkor a csillag mellett van a const.

Karakterláncoknál sokszor nem szabad változtatni a karakterlánc tartalmát, ha egy karakterláncot nem egy saját tömbben tárolunk, csak leírjuk idézőjelek közé (és pl. így adjuk át függvényparaméternek), akkor az olyan karakterlánc, aminek a tartalmát nem szabad változtatni. Ezért az ilyen karakterláncokat általában "const char *" típusként szokták a függvények paraméterként kapni.

char elsobetu(const char* szoveg) {
  return szoveg[0];
}
 
char c = elsobetu("Mondat!");

A beépített könyvtárak függvényei is, amik nem módosítják a karakterláncokat, így mindig "const char *" típusú paramétereket várnak, így annak odaadható olyan karakterlánc is amit tömbben tárolunk, és olyan is amit csak odaírunk.

typedef

Egy kulcsszó a C nyelvben, amit eddig nem tanultunk, a typedef. Ennek az a célja, hogy ha egyfajta típust sokszor használunk, rövidebb nevet is adhatunk neki. Úgy néz ki, formátumilag, hogy leírjuk hogy "typedef", majd a sor hátralevő része pontosan ugyanolyan mintha egy változót deklarálnánk, de amit névnek megadunk, az egy új típusnak a neve. Pl.:

typedef unsigned long long int ull_int;
 
int main()
{
  ull_int i;
  for(pont = 0; i < 5000000000ll; ++i) {
    printf("%d ", i % 100);
  }
}

Ezt nem csak az alap típusokhoz, hanem a különböző módosított típusokhoz is lehet használni:

typedef int vektor[3];
typedef int *p_int;
 
int main()
{
  vektor pont;
  pont[0] = 4;
  pont[1] = 5;
  pont[2] = 6;
  p_int mutato = &pont[0];
}

Ekkor a 3 elemű tömböt elneveztem egy új néven, vektor-nak, és innentől kezdve a vektor ugyanazt jelenti, mint ha azt írnám hogy 3 elemű tömb legyen. De attól még hogy így elneveztem, nem tud semmivel többet mint a 3 elemű tömb tudna: nem lehet egyben megváltoztatni az értékét, nem működnek rá az összehasonlító operátorok, ha függvényparaméterként átadom, továbbra is csak mutatóként adódik át, nincsen egyszerűbb mód a kiiratására, semmi ilyesmi.

vektor pont1, pont2;
// Ezek mind nem mukodnek
pont1 = pont2;
if(pont1 < pont2) {
  printf("%v", pont1);
}

Annak ellenére hogy ez ilyen korlátozottan hat a dolgokra, azért hasznos lehet

  • mert rövidebb név lehet, amit könnyebb leírni
  • ha több különböző változónak mindenképp ugyanaz a típusa, de a típus lehet hogy megváltozik majd a program fejlődése folyamán, így elég lehet csak a typedef-et átírni.

enum, switch/case

enum típusok megadása

Az enum nem csak egy típus a C-ben, hanem egy típus család. Az enum-al olyan típusokat lehet csinálni, amiknek azt szeretnénk, hogy az értéke bizonyos fix lehetőségek közül legyen kiválasztva. Nézzünk egy példát:

// Ebben a sorban a tipust definialjuk, mint typedef:
enum izek {CSOKIS, VANILIAS, EPRES, MALNAS};
// Ezek ilyen tipusu valtozok:
enum izek fagyi = CSOKIS;
enum izek szekrenyben[] = {CSOKIS, EPRES, EPRES, EPRES, VANILIAS};
 
if(fagyi == EPRES) {
  printf("Hmm de finom!\n");
}

Tehát ebben az esetben a változók típusa "enum izek". Az első sor mondja meg hogy az "enum izek" nevű enum típus milyen értékeket vehet fel, majd a második sorban definiálok egy "enum izek" típusú változót, a harmadik sorban pedig egy ilyen típusú változókat tartalmaző tömböt.

Az enum változókat a C egész számokként tárolja, és egész számokként kezeli, ezért ezt megengedi a fordító:

fagyi = 2;

De ha már egy változó enum típusú, akkor én azt ajánlom, mindig csak a megnevezett értékeket tegyük bele, és azokkal hasonlítsunk össze.

switch/case

A switch egy feltételt megadó parancs, ami kicsit az if-re hasonlít, de bizonyos konkrét esetben használható. Arra használható, ha egy változónak több különféle konkrét értéke lehet, amik esetében mást kell csinálni. Tehát ha ilyen if sorozatunk van:

if(a == 0) {
  // valami
} else if(a == 1) {
  // valami mas
} else if(a == 2) {
  // harmadik fele dolog
} else {
  // kulonben ez legyen
}

Akkor ezt lehet lecserélni a direkt erre kitalált struktúrával:

switch(a) {
case 0:
  // valami
  break;
case 1:
  // valami mas
  break;
case 2:
  // harmadik fele dolog
  break;
default:
  // kulonben ez legyen
  break;
}

Ezt enumokra főleg szokták használni:

switch(fagyi) {
case CSOKIS:
  printf("Az edessegek kiralya!\n");
  break;
case VANILIAS:
  printf("Jo, de kicsit uncsi.\n");
  break;
case MALNAS:
  printf("Sok kis kemeny mag, bleh.\n");
  break;
}

Azért lehet ezt enumra különösen jól használni, mert

  • Ennél úgyis csak bizonyos véges sok érték lehet, és általában egyenlőséget ellenőrzünk vele, nem olyant hogy nagyobb/kisebb valaminél, amit switch-el nem lehet.
  • Mivel tudhatjuk a konkrét értékeket amiket felvehet a változó, tud szólni a fordító ha valamelyiket elfelejtjük kezelni. Pl. a fenti kódnál szólna hogy az EPRES esetet kifelejtettük (csak warninggal, tehát azért lefordulna és futna a program).

Még szeretném külön felhívni a figyelmet a break-ekre mindegyik case végén, azt könnyű elfelejteni amikor switch kódot írunk, de ha kihagyjuk akkor a következő case is egyben végrehajtódik.

struktúrák

A struktúrák C-ben a megfelelői a python objektumoknak, de (ahogy megszokhattuk), jelentősen egyszerűbbek. Lényegében csak arra jó, hogy egy adag változót összecsoportosíthatunk egy blokkba, egy változóba. Az enum-hoz hasonlóan kell definiálni, tehát először megmondjuk hogy az adott fajta struktúra miből is áll, majd után csinálhatunk ilyen változókat. A kulcssó a struct, itt egy egyszerű példa:

// Az adott struct altipus definialasa:
struct ZHeredmeny {
  int pontszam, jegy;
  char neptun[7];
};
 
// Hasznalat:
struct ZHeredmeny eredmeny1, eredmeny2;
eredmeny1.pontszam = 23;
strcpy(eredmeny1.neptun, "KOD123");
eredmeny2 = eredmeny1;
eredmeny2.neptun[0] = 'D';

Tehát mit tud pontosan egy struct?

  • A tagváltozók a "." (pont) operátorral érhetőek el, mint python-ban. Ezeket C-nél inkább a struktúra mezőinek szokták hívni, nem tagváltozóknak.
  • Kezdeti érték adásnál meg lehet adni a mezők tartalmát kapcsos-zárójelben (mint egy tömböt). Ugyanazt a sorrendet kell használni mint az adott struktúra típus megadásánál:
struct ZHeredmeny eredmeny3 = {20, 4, "LZRKTD"};
  • Lehet lemásolni egyik struktúra értékét egy másikba. Ilyenkor minden mező lemásolódik, beleértve a struktúrán belül tárolt tömböket.

Miket nem tud, amit esetleg elvárhatnánk?

  • Az összehasonlító operátorok nem működnek rá, még az egyenlőséget ellenőrző "==" sem. Ha ilyesmi kell, akkor külön függvényt kell írni rá, pl:
int ZHeredmeny_egyenlo(struct ZHeredmeny a, struct ZHeredmeny b) {
  if(a.pontszam == b.pontszam && a.jegy == b.jegy &&
     strcmp(a.neptun, b.neptun) == 0) {
    return 1;
  } else {
    return 0;
  }
}
  • Nincsen külön szintaxis a struktúrához tartozó függvényekre, nincsenek metódusok. A struktúrához köthető függvényeknek a nevéből derülhet ki hogy oda tartoznak, mint az előző példa.
Személyes eszközök