Kihagyás

2. gyakorlat

Témák:

  • A fordító (compiler) és a fordítás
    • Makrók
  • Standard könyvtár
    • printf
    • scanf
  • Típusok

A fordító (compiler) és a fordítás

A fordítás művelete

A fordító a C nyelven írt szöveges állományból az alábbi 3 részműveleten keresztül lészít gépi kódot:

  1. előfeldolgozás (preprocessing)
  2. fordítás (compiling)
  3. összefűzés (linking)

Makrók

  • Az előfeldolgozást makrókkal befolyásolhatjuk
  • A makró azonosítóját (nevét) csupa nagybetűvel szokás írni, hogy megkülönböztessük a C kódtól

#include <header> vagy #include "header"

  • Ez a makró másolja be a fejlécállomány tartalmát a kódunkba
  • <> jelölés esetén a fordító installálásakor elhelyezett helyen keres
  • "" jelölés esetén az aktuális munkakönyvtárunkban keres

#define AZONOSÍTÓ érték vagy kifejezés

  • Egy konstans értéket vagy kifejezést tudunk definiálni vele
  • Ezt a preprocesszor az AZONOSÍTÓ helyére másolja be a kódon belül

Példa:

#include <stdio.h>
#define HT "Hello there\n"

int main()
{
    printf(HT);
    return 0;
}

#undef

  • Az AZONOSÍTÓ nevű makrót törli, így az a kód további részében nem lesz elérhető

Példa:

#include <stdio.h>
#define HT "Hello there\n"

#undef HT

int main()
{
    printf(HT);
    return 0;
}

Hibát kapunk: nem találja a HT nevű makrót

#define AZONOSÍTÓ(argumentumok) utasítás

  • Így meg tudunk adni utasításokat is
  • Matamatikai műveletek esetén javasolt a zárójelek használata, hogy elkerüljük a helytelen számolásokat
  • Az utsításmakrók egymásba ágyazhatóak (egyikbe meg lehet hívni a másikat)
#include <stdio.h>
#define SZUM(x,y) x + y

int main()
{
    printf("a + b = %d\n", SZUM(1,2));
    return 0;
}

if-ek

#ifdef AZONOSÍTÓ
  • Megnézi, hogy az AZONOSÍTÓ létezik-e már vagy sem
    • Ha igen: a makró törzsében lévő rész bekerül a kódba
    • Ha nem: akkor figyelmen kívül hagyja
#ifdef AZONOSÍTÓ
    valami
#else
    (valami)
#endif
#include <stdio.h>
#define PRINT

int main()
{
    #ifdef PRINT
        printf("Hello world!\n");
    #endif

    return 0;
}

Kiírja: Hello world!

#ifndef AZONOSÍTÓ
  • Megnézi, hogy az AZONOSÍTÓ létezik-e már vagy sem
  • Viszont ebben az esetben akkor kerül be a makró törzsében lévő rész, ha NINCS definiálva az AZONOSÍTÓ
#include <stdio.h>
#define PRINT

int main()
{
    #ifndef PRINT
        printf("Hello world!\n");
    #else
        printf("Goodbye world!\n");
    #endif

    return 0;
}

Kiírja: Goodbye world!

#elif
  • A #elif-fel további kifejezéseket adhatunk meg
#if KIFEJEZÉS
#elif MÁSIKKIFEJEZÉS1
#elif MÁSIKKIFEJEZÉS2
...
#else
#endif

Standard könyvtár

printf

int printf (const char * format, ...);
  • Függvény, amely kiírja a megadott konstans karakterláncot a standard outputra (terminal)
  • Visszatérési értéke a kiírt karakterek száma
  • Tetszőleges számú argumentumot is adhatunk neki
  • A format argumentumban további speciális karaktereket is megadhatunk, melyeket a % karakterrel jelzünk, folytatva előre meghatározott karakterrel*

* A fontosabbak:

  • %d - egész szám (pl.: int, short)
  • %f - lebegőpontos szám (pl.: float, double)
  • %c - karakter (pl.: char)
  • %s - karakterlánc (pl.: char tömb)

Még néhány:

  • %ld - (pl.: long)
  • %u - (pl.: unsigned)

Példák:

#include <stdio.h>

int main()
{
    printf("Hello World in %d!\n", 2022);
    printf("Hello %f part of World!\n", 0.001);
    printf("%cello World!\n", 'H');
    printf("Hello %s", "World!\n");

    // Tetszőlege számú adható meg
    printf("Hello %s in %d!%c", "World", 2022, '\n');

    return 0;
}

scanf

int scanf (const char * format, ...);
  • Függvény, amely adatot kér be a standard inputról (terminal) a felhasznlótól
  • Visszatérési értéke a beolvasott karakterek számával egyezik meg (ha a beolvasás sikeres)
  • A % jeles speciális karakterek itt is használhatóak, meghatározhatjuk vele a beolvasott adat típusát
  • Meg kell adni a változót, ahova a beolvasott adatokat szeretnénk letárolni
    • Valójában nem a változót adjuk át neki, hanem annak a memóriacímét egy pointerrel (pl. &a)
    • Pointer (azaz mutató): memóriacímre mutat

Példa:

#include <stdio.h>

int main()
{
    int a;
    printf("Add meg az 'a' értékét: ");
    scanf("%d", &a);
    printf("a = %d\n", a);

    return 0;
}

Típusok, adatok, utasítások

Definíció és deklaráció

  • Deklaráció
    • Amikor jelezzük a fordító felé, hogy ilyen változó, konstans vagy függvény létezik, használva lesz
  • Definíció
    • Amikor értéket adunk a változónak, konstansnak
    • Amikor függvény esetében implementáljuk a függvény törzsét (kapcsos zárójelek közötti rész)
  • Változók deklarálásakor érdemes egyben értéket is adni (azaz definiálni)
    • Ha nem definiáljuk a változót, akkor a fordító 0-t ad neki értékként
    • Konstansok értékét kötelezően a deklaráláskor tudjuk csak megadni
// Deklaráció
int a;

// Definíció
a = 19;

Típusok

  • egész típusok
    • char: 1 byte
    • short: 2 byte
    • int: 4 byte
    • long: 8 byte
    • long long: 8 byte
  • lebegőpontos típusok
    • float: 4 byte
    • double: 8 byte
  • void
  • összetett típusok:
    • array (tömb)
    • struct
    • union

Megjegyzések

  • A típusok méretei architektúra függők, a megadott értékek csak általában igazak
    • A sizeof() függvénnyel le tudjuk kérni a foglalt memória méretét
    • A sizeof() long-ot ad vissza, ezért %ld-vel jelöljük
  • A C nyelvben nincs logikai változó (bool)!
    • A logikai értékeket int-ben (esetleg char-ban) tároljuk
  • A void szigorú értelemben nem típus
    • Akkor használjuk, amikor egy függvénynek nincs visszatérési értéke
    • A pointereknél lesz még szerepe

Típusmódosítók

  • unsigned
    • Nem negatív értéket tárol
    • Jelölése: %u
    • Alapértelmezetten int-re használjuk, ezért nem kell kiíírni, hogy unsigned int, elég csak az unsigned-ot kiírni
  • const
    • Konstans, azaz a változó értéke nem változtatható
    • Egyszerre deklaráljuk és definiáljuk
    • Redundáns többször használva egy deklarációban
    • Ez a módosító balra hat (kivéve, ha első kulcsszó a deklarációban)
      • pl.: int const * p esetén a const az int-re hat
      • pl.: const int == int const (ezek megegyeznek)
  • static

    1) Ha a globális változó előtt használjuk, akkor az nem érhető el más .c fileból 2) Egy függvény lokális változója előtt használva az egyszer inicializálódik (ha a fv-t többször is meghívjuk akkor az egyszer lesz deklarálva és definiálva) - Kevésbé használatosak: - volatile - register

Overflow

Túlcsordulás (azaz overflow): Amikor túllépjük a megadott típusnak megfelelő maximális vagy minimális értéket. Ilyenkor a változó értéke a minimális vagy maximális értékre változik.

Példa: itt a b-nél overflow történik (mert nem tárolhat negatív számot)

#include <stdio.h>

int main()
{
    int a = 0;
    unsigned int b = 0;
    printf("a = %d, b = %u\n", a, b);

    a = a - 1;
    b = b - 1;
    printf("a = %d, b = %u\n", a, b);

    return 0;
}

Összetett típusok

--> Tömbök

  • Azonos típusú változók gyűjteménye
  • A memórában egy összefüggő területet foglal el
  • Inicializátorlista: a {} közötti értékadást
  • A tömbök indexelése is 0-tól indul
    • Az utolsó tömbindexünk a méret – 1 értékével egyezik meg
    • Ha negatív vagy túl nagy tömbindexet adunk meg, akkor memóriaszemetet kapunk vissza

Példák:

#include <stdio.h>

int main()
{
    // deklarálás, a fordító többnyire feltölti 0-val
    // általánosan: típus változó[méret];
    char a[5];

    // a fordító kitalálja a tömb méretét
    // általánosan: típus változó[] = { érték1, … értékN};
    short b[] = {1, 2, 3, 4, 5};

    // kevesebb érték esetén 0 a többi (ált.)
    // általánosan: típus változó[méret] = { érték1, … értékN};
    int c[5] = {1, 2, 3, 4, 5};

    // egy adott elem elérése
    // általánosan: tömb[index]
    printf("c[0] = %d\n", c[0]);

    return 0;
}

--> Többdimenziós tömbök (mátrixok)

  • Mindenben megegyeznek az egydimenziós tömbökkel, csak az indexelésük különbözik
  • Gyakorlatilag egy tömbben tömböt definiálunk

Példák:

#include <stdio.h>

int main()
{
    // Deklarálás
    // általánosan: típus tömb[méret1][méret2]…[méretD];
    int a[2][3];

    // Értékadás
    // általánosan: tömb[méret1][méret2]…[méretD] = {{{}, {}, .. {}}, {}};
    unsigned m[3][3] = {{11, 12, 13}, {21, 22, 23}, {31, 32, 33}};

    // egy adott elem elérése
    // általánosan: tömb[index1][index2]…[indexD]
    printf("m[1][2] = %u\n", m[1][2]);

    return 0;
}

--> Struktúrák

  • Tetszőleges, már létező típus lehet benne
  • Akár másik struct is lehet benne

Struct szintax általánosan:

struct StructNév
{
    típus1 adattag1;
    típus2 adattag2;
    ...
    típusN adattagN;
};

Példa:

#include <stdio.h>

struct S
{
    char karakter;
    int szam;
};

int main()
{
    // 1)

    // az s1 az S egy struktúra változója
    // általánosan: struct StructNév structVáltozó;
    struct S s1;

    // a . operátorral tudjuk elérni a már definiált változókat
    // általánosan: structVáltozó.adattagX
    s1.karakter = 'A';
    s1.szam = 5;

    // 2)

    // az s2 is az S egy struktúra változója
    // definíció / értékadás
    // általánosan: structVáltozó = {érték1, érték2, … értékN};
    struct S s2 = {'B', 4};

    return 0;
}

--> Uniók

  • Ritkán használt (nem fogjuk használni)
  • Önhivatkozhat
  • Mérete: a legnagyobb adattag mérete
  • Az összes adattagjának egy memóriát foglal, aminek mérete megegyezik a legnagyobb méretű adattagéval

Általános szintax:

union UnionNév
{
    típus1 adattag1;
    típus2 adattag2;
    ...
    típusN adattagN;
};

--> Typedef

  • Ezzel a kulcsszóval egy már létező típusra tudunk hivatkozni új névvel : alias

Általános szintax:

typedef type aliastype;

Pl.:

typedef struct S Struct;

Műveletek utasítások

Matematikai műveletek

  • Szokásos aritmetikai műveletek: +, -, *, / (sima osztás azaz div), % (az osztás maradékát adja vissza azaz mod)
  • Egyszerűsítések az egy változón történő műveletek esetében: +=, -=, *=, /=

Példa:

#include <stdio.h>

int main()
{
    int a = 10;
    a += 2;     // ugyanez: a = a + 2;

    return 0;
}
  • Inkrementálás: ++ és dekrementálás: --
    • 1-gyel növelik, illetve csökkentik az egész szám értékét
    • Prefix: ++a, --a
      • a változó értéke előbb inkementálódik, dekrementálódik, majd a beágyazó műveletek hajtódnak végre
    • Possix: a++, a--
      • a beágyazó műveletek hajtódnak végre, és azok után lesz csak a változó értéke módosítva

Példa:

#include <stdio.h>

int main()
{
    int a = 42;
    printf("a = %d\n", a);
    // terminal: a = 42

    int b = a++;
    printf("a = %d, b = %d\n", a, b);
    // terminal: a = 43, b = 42

    int c = ++a;
    printf("a = %d, b = %d, c = %d\n", a, b, c);
    // terminal: a = 44, b = 42, c = 44

    return 0;
}

Bitműveletek

  • Valójában minden művelet bitműveletre van visszavezetve
  • A változók értékei 0 és 1 formában vannak tárolva a memóriában
  • Így ezek között a legtermészetesebb műveletek a bitműveletek
  • Integer típusokon hívhatjuk meg őket
Jele Művelet
& bitenkéni ÉS (AND)
^ bitenkéni kizáró VAGY (XOR)
\| bitenkéni VAGY (OR)
~ bitenkénti negálás (NOT)
>> és << bit léptetés (shift)

Pl.: Az egész számok összeadása például egy bitenkénti ÉS művelet.

Példa:

#include <stdio.h>

int main()
{
    // negálás
    int a = 0;
    a = ~a;
    printf("a = %d\n", a);
    // terminal: a = -1

    // léptetés
    int b = 2;
    b = b << 1;
    printf("b = %d\n", b);
    // terminal: b = 4      (a 2 hatványait kapjuk vissza)

    return 0;
}

Típuskonverzió

  • A beépített típusok között átjárhatóság van
  • Kasztolás (casting)
#include <stdio.h>

int main()
{
    // kasztolás általánosan: (újtípus)változó

    int i = 42;
    float f = (float)i;

    return 0;
}