C++ alapok
Első program¶
Ahhoz, hogy tudjunk fordítani, szükségünk lesz egy fordítandó forráskódra is.
Készítsünk egy fájlt elso.cpp
néven!
1 2 3 4 5 6 |
|
Fordítás¶
Ha már eddig kész vagyunk, a programot le kell fordítani, hiszen ezt a kódot Pythonnal ellenben a gép nem tudja értelmezni.
A fordítás során a g++
programot használjuk:
g++ -o elso -Wall elso.cpp
A kapcsolókról:
-o
kapcsolóval adhatjuk meg a kimeneti fájl nevét-Wall
kapcsolóval további fordítóprogram figyelmeztetést (warningot) kapcsolhatunk be- Ilyen például az, hogy egy
boolean
értéket egy nemboolean
-nek megfelelő egésszel hasonlítunk össze - vagy mondjuk a kiírt, de üresen hagyott
if
/else
blokkokról is kaphatunk warningot. elso.cpp
a fordítandó forrás fájl neve
A lefordított, linkelt alkalmazást ezután már futtathatjuk elso.exe
vagy ./elso
utasítással.
A fordító -o
paraméter nélkül a.exe
vagy a.out
nevű futtatható állományt készít.
Kis kényelem¶
Hogy ne kelljen mindenhova kiírni az std::
előtagot, a program elejére beírhatjuk a using namespace std;
sort. (Ennek a jelentéséről majd később beszélünk.)
1 2 3 4 5 6 7 8 |
|
A felkommentelt fájl letöltése
Fordítás fázisai¶
Amikor egy C++ forrásfájlt lefordítunk, valójában több lépésen keresztül jutunk el a kész futtatható programig. Ezek a lépések:
-
Előfeldolgozás (preprocessing)
-
Fordítás (compilation)
-
Összekapcsolás (linking)
Előfeldolgozás (Preprocessing)¶
A C++ programokban gyakran vannak olyan elemek melyek mögöttes tartalmat hordoznak. Ezek olyan részek amik valamilyen rövidítést vagy fealdatot látnak el. Ezek nem C++ utasítások, így a fordítóprogram nem tudná értelmezni azokat, de mégis, C++ forráskód lesz belőlük. Ezt az átalakítást látja el az Előfeldolgozó vagy Preprocessor.
Mik helyettesíthetők be?
Minden olyan sor ami a #
jellel kezdődik, a preprocesszornak szól.
INCLUDE
Ahogy azt már a fenti kódrészletekben is láttuk, vannak olyan sorok, melyek #include
ként kezdődnek.
Az include során olyan elemeket adunk a forráskódhoz, melyeket valakik előre elkészítettek nekünk.
Mintha mások C++-ban elkészített munkáját a mi kódunkba illesztenénk annak érdekében, hogy azt felhasználjuk.
Ez elképzelhető úgy, mintha valaki kimásolná azt a kódot és a saját forráskódjába beillesztené.
Ezt meg is tudjuk nézni, hogy valóban így történik-e.
A fordítás során a -E
kapcsoló segítségével a fordító nem binárist (futtatható) fog gyártani, hanem csakis a preprocesszált eredményt fogja nekünk megmutatni.
g++ -E -o elso.preproc elso.cpp
Ekkor azt láthatjuk, hogy rengeted minden került a rövidke programunk elé.
Ez mind az, amit átmásolt a preprocesszor.
Ekkor kérdés lehet, hogy hol van ez az információ letárolva, milyen fájlt másol oda és az a fájl merre található.
Az, hogy az include során hol kell keresni a fájlokat több dologtól függ.
Ha az includenak <>
jelek között adjuk meg a fájlt amit be szeretnénk másolni, akkor a standard könyvtárak között fogja keresni, amiknek megvan a megfelelő helye az operációs rendszeren belül.
Ha a ""
jelek között adjuk meg a fájlt, pl. #include "szuperfunkciok.h"
, akkor a feldolgozott fájlhoz nézve relatív útvonalat használja a preprocesszor.
Így ez is érvényes útvonal lehet: #include "../../../fontos/szukseges.h"
.
A különféle jelek mellett a preprocesszornak azt is meg lehet mondani, hogy hol keresse a fájlokat.
Alapból a standard könyvtárakat használja, de a -I
kapcsoló segítségével mi magunk is adhatunk meg olyan könyvtárakat amikben keres.
Példa:
/home/user/project/secret/fontos.h:
1 2 3 |
|
/home/user/peldak/pelda.cpp:
1 2 |
|
Ez a program most nem fordulna le, hiszen a standard könyvtárak között nem található secret/fontos.h
, de ha megadjuk a -I
kapcsoló segítéségvel a /home/user/project
mappát, akkor ott már megtalálja.
g++ -I /home/user/project -o pelda pelda.cpp
A példában az is látszódik, hogy hiába csak a project mappáig van megadva a könyvtár ahol a fájlokat kell keresni, az include során mappát is megadhatunk. Természetesen erre nem lenne szükség, ha a secret mappa is része lenne a keresési könyvtáraknak.
Mivel ez a bemásolás is csak egy szöveges rész, így ez is tartalmazhat include elemeket, így azokat is fel kell oldani.
A probléma akkor van, ha egy fájl olyan fájlt húz be, akár indirekt módon is, ami az eredeti fájlra hivatkozik.
Ekkor a behelyettesítés során a preprocesszor egy végtelen körbe kerül.
Ennek a megoldása az, hogy számontartjuk, hogy mely fájlok kerültek már be, és ha egy fájl már bemásolásra került, azt nem másoljuk be újra.
Erre két megoldás létezik:
1. #pragma once
: Ha ezt belerakjuk a fájl elejére, akkor a preprocesszor tudni fogja, hogy ezt csak egyszer kell bemásolni. A probléma, hogy habár a legtöbb és elterjedt fordító támogatja, nem része a C++ szabványnak.
2. Include guardok / védőfeltételek használata: Ekkor saját magunk tudunk változókat definiálni és feltételes másolást garantálni. Ehhez azonban kettő dologra van szükségünk: Változókra és feltétel vizsgálatra. Ezek elvezetnek a makrókig, így nézzük is meg azokat majd visszatérünk az include guardokhoz.
MAKRÓK
A makrók is a preprocesszorhoz köthetők, így ezek is behelyettesítést látnak el.
A különbség annyi, hogy ezeknek a nevét és értékét mi adhatjuk meg.
Ahhoz, hogy létre tudjunk hozni egy makrót, definiálnunk kell:
#define SAJAT_MAKROM
Látható, hogy #
jellel kezdődik, tehát a preprocesszornak szól, illetve, hogy definíció.
Innentől kezdve létezik a SAJAT_MAKROM
nevű makró.
Ez csak annyit jelent, hogy létezik, de érték nincsen mögötte.
Ha értéket is akarunk adni neki, azt a következő képpen adhatjuk meg:
#define SAJAT_MAKROM std::cout << "Ez a sajat makrom viselkedese";
A makrónak bármilyen értéket adhatunk.
Lehet ez a fentebb látott módon egy egész hosszú kifejezés, de lehet egy string #define STRING_MAKRO "Ez a stringem"
vagy egy szám is #define SZAM_MAKRO 12542
.
A Makrókat használatára példa:
1 2 3 4 5 6 7 8 9 |
|
Látható, hogy a PRINT_MY_NAME
használatakor nem kell ;
jelet rakni, hiszen az nem egy C++ utasítás, hanem csak egy behelyettesítendő rész.
A behelyettesítendő részben pedig már szerepel a lezáró.
Mivel ezek a makrók a preprocesszor számára ismeretesek, tudja, hogy melyek léteznek és tudja, hogy mire kell helyettesíteni, így ezt kis is használhatjuk arra, hogy a forráskódunk máshogy forduljon attól függően, hogy milyen értékei vannak a makróknak.
Preprocesszor direktívák
Ezek azok az elemek melyek #
jellel kezdődnek, tehát a preprocesszornak szólnak, de annak valamilyen utasítást vagy kifejezést jelentenek.
Ilyen például a feltétel.
Tudjuk vizsgálni, hogy egy makró definiálva van-e: #ifdef
.
Ennek a segítségével például debug kiíratásokat tudunk a kódba rejteni.
Példa:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Ezekkel a direktívákkal irányíthatjuk, hogy a makrók függvénéyben mi kerüljön bele a forráskódunkba. Ezzel vissza is térhetünk az include problémájához. Ha include guardokat használunk, akkor szükség van egy változóra, mellyel számontartjuk, hogy egy fájl már bemásolásra került-e vagy sem illetve egy tesztelésre.
Ez teljesen megoldható makrók és direktívák használatával. Ha láttuk már a fájlt, akkor definiálunk egy makrót, és ha a makró definiálva van, akkor megszakítjuk a láncot, azaz nem másolunk semmit. Példa:
kedvencek.h:
1 2 3 4 5 6 |
|
Mi történik itt?
Ha először include-oljuk a kedvencek.h fájlt, akkor a makró még nem került definiálásra, így a feltétel ifndef
(ha nem lett még definiálva) igaz lesz, vagyis az igaz ágban lévő részt bemásoljuk.
Mit tartalmaz az igaz ág?
A hasznos tartlamat illetve a makró definiálását.
Így ha bármi uton is valami újra be akarja include-olni a kedvencek.h fájlt, ezáltal egy végtelen kört okozva, a feltétel már látni fogja a makrót és megszakítja a láncot.
Hogyan szakad meg a lánc?
Egyszerűen a másolandó fájl tartalom egy üres szöveg lesz, hiszen az if-en kívül nem található tartalom.
Az üres szöveg pedig biztosan nem vezet több include-ra.
Preprocesszor direktívák és makrók 2
Valójában az eddig látott, #include
, #pragma
, #if
, #ifdef
, #ifndef
stb elemek mind preprocesszor direktívák.
Mivel sok direktíva tesztelésre hazsnálható jogos a kérdés, hogy mindig a forráskódban kell-e megadni a vizsgálandó makrók értékét? Megtehetjük, hogy fordításkor adunk makróknak értéket.
1 2 3 4 5 6 7 |
|
Ez a program fordítás után nem ír ki semmit, hiszen nincsen difinálva a SECRET makró.
Ezt azonban megváltoztathatjuk, ha másként fordítunk.
A -D
kapcsoló segítéségvel megadhatunk makrókat.
Ekkor is lehetőség van csak definiálásra és érték megadására is:
g++ -DSECRET -DMY_PWD=Alma
Látható, hogy több makró definiálásakor több -D
kapcsolót adunk meg és érték adásához csak egyenlőségjelre van szükség.
Fontos, hogy nincsen szóköz!
Ezt a funkcionalitást a ZH tesztelés során is kihasználjuk. Minden programban ahol van kiinduló main függvény, a függvény preprocesszor direktívák között szerepel. Ezek azt tesztelik, hogy a biro makro definiálva van-e. Ha definiálva van, akkor biro környezetben fut és az a tartalom eltávoolításra kerül annak érdekében, hogy biztosan ne legyen zaj a biro tesztelésben.
1 2 3 4 5 6 7 8 9 |
|
Mivel a biro a -DTEST_BIRO=1
kapcsolóval fordít, így ennek nem lesz hatása a biro kimenetre.
A probléma az lehet, ha valaki ilyen részbe írja a megodlását, akkor a biro azt sem fogja látni, hiszen a preprocesszor kidobja.
Gyakran találkozhatunk olyan makrókkal is, melyek paramétert várnak, mintha csak függvények lennének. Ezek igen hasznosak tudnak lenni, de óvatosan kell kezelni ezeket, hiszen a preprocesszor nem végez kiértékellést, csak behelyettesítést!
1 2 3 4 5 6 7 |
|
Gondolhatnánk, hogy a makró ugyanazt jelenti mindkét esetben, hiszen 33 = 9 és 1+2 = 3 az is 33, így 9.
Azonban ha szigorúan követjük, hogy a preprocesszor nem értékel ki semmit csak behelyettesít, a második esetben a valós jelentés a következő lesz:
1 + 2 * 1 + 2
ami pedig 5.
Természetesen megfelelő zárójelezéssel ez orvosolható #define SQUARE(x) (x) * (x)
, azonban nagyon jól mutatja, hogy a makrók mint függvények használata során nagyon elővigyázatosan kell eljárjunk!
Fordítás (Compilation)¶
Ebben a fázisban a preprocesszor már elvégezte a dolgát, nincsen semmilyen direktíva, azok alapoján a megfelelő kódrészlet került bemásolásra vagy éppen kivágásra, és a makrók is lecserélődtek a megfelelő tartalmukra. Ekkor a fordító legyártja az object kódot. Ez olyan kód, ami már a gép számára értelmezhető, azonban még mindig nem futtatható. Ez scak egy nagy utasítás halmaz, azonban hogy futtatható programot kapjunk, ahhoz még több másik utasítás is kell.
Ezeket a további utasításokat a fordító a linkelési fázisban köti a programunkhoz. Mi értelme van, ha ezt a fordító úgysi megcsinálja, miért kell object kódot generálni? Sokszor nem akarunk teljes futtatható programot írni, mi csak egy funkcionalitást akarunk elkészíteni, pl. cicás képeket elforgató függvényt írunk. Ha azt akarjuk, hogy mások ezt a függvényt fel tudják használni a saját programjuk során, akkor nem kell egy teljes programot írnunk, csak ezt a pár funkcionalitást kell gép számára értelmezhető utasításokká alakítanunk és majd egy másik programozó csak annyit mond: "Csináld azt ami ott található a leírásban", utalva a mi object kódunkra.
Természetesen, hogy egy programozó tudja, hogy hogyan hívható meg az object kódban lévő függvény, tudnia kell, hogy annak mi a neve, milyen paramétereket vár stb. Erre megoldás ha a függvények (interfészek) definíciója és deklarációja külön van választva. Erről részletesebben itt olvashatsz.
C++ input/output¶
C++-ban az input/output műveletek streamek (folyamok) segítségével vannak megvalósítva
- cout: az alapértelmezett kimenet
- cerr: az alapértelmezett hibakimenet
- cin: az alapértelmezett bemenet
A streamekbe (folyamokba) a <<
operátor segítségével írhatunk, és a >>
operátor segítségével olvashatunk belőlük.
Ezek használatához C++-ban használatos iostream
header fájlt kell include-olni. Ezzel az új, stream-eken alapuló műveleteket adjuk hozzá programunkhoz.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Sorvéget az endl
segítségével írhatunk.
C++ string¶
Mi történik az alábbi programmal akkor, ha a fix méretű karakter tömbbe hosszabb méretű karakterláncot adunk meg?
1 2 3 4 5 6 7 8 9 10 11 |
|
Az eredmény (ha a program lefut):
1 2 |
|
Azonban ha túl hosszú a bemenet, akkor a program hibával megállhat. A megoldás az, hogy az inputnak megfelelő méretű területet foglalunk, és oda írjuk be a felhasználó által megadott nevet. A C++-ban a "szövegeknek" van egy hatékonyabb, kevésbé körülményes megvalósítása is, a string
osztály (osztályokról később lesz szó, most csak azt mutatjuk be, hogyan lehet használni a string
-et). A string
objektumok dinamikusan változtatják a méretüket a tartalmazott karakterek számától függően. A félév folyamán stringek alatt ezt a reprezentációt értjük, nem pedig a char*
-ot.
A string objektumok használatához, string műveletekhez szükségünk lesz a string
header include-olására is (és nem string.h
). Ezt követően kényelmesen használhatjuk a szövegeket, ahogy már Javaban is megszoktuk. Itt csak a fontosabb használati eseteket emeljük ki, bővebb leírásokhoz linkek:
- http://www.cplusplus.com/reference/string/string/
- https://en.cppreference.com/w/cpp/string/basic_string
A string
osztály fontosabb metódusai, amelyeket a leggyakrabban használunk:
Létrehozás, azaz a konstruktorok¶
1 2 3 4 |
|
Beolvasás, kiírás¶
1 2 3 |
|
A scanf
nem használható a string
beolvasására, de a cin
segítségével be tudjuk olvasni, és a cout
segítségével ki tudjuk írni.
Összehasonlítás¶
1 2 3 4 5 6 |
|
A string
-eket az ==
operátor segítéségével össze lehet hasonlítani.
Összefűzés¶
1 2 3 4 |
|
Méret, üres-e, tartalom törlése¶
1 2 3 4 5 6 7 8 |
|
i-edik elem¶
1 2 3 4 5 |
|
Ha olyan indexre hivatkozunk, amelyik érvénytelen, azaz a string hosszánál nagyobb, akkor nem definiált viselkedés lesz az eredménye.
Konverzió szám és string
között¶
Számból string
-gé a to_string
segítségével konvertálhatunk.
1 2 3 4 |
|
string
-et számmá konvertálni az stoi
függvénnyel lehet.
1 2 3 |
|
Konverziók a különböző típusokra:
int
: stoilong
: stollong long
: stollunsigned long
: stoulunsigned long long
: stoullfloat
: stofdouble
: stodlong double
: stold
Mi történik,
- ha a szöveg elején van szám, de van mögötte ,,egyéb'', akkor a számot átkonvertálja
1 2 |
|
- ha nem fér bele a tartományba, akkor
std::out_of_range
kivétel dobódik (kivételkezelés, const és referencia később)
1 2 3 4 5 6 7 |
|
- ha nem lehet konvertálni, akkor
std::invalid_argument
kivétel dobódik
1 2 3 4 5 6 7 |
|
Hibakeresés¶
Debugger használata¶
Az IDE eszközök általában tartalmaznak debuggert is, ami lehetővé teszi, hogy a program végrehajtását utasításról utasításra lépve kövessük végig úgy, hogy közben a változók állapotát, értékét folyamatosan monitorozni tudjuk. Fontos, hogy ahhoz, hogy a debugger megfelelően tudjon működni, a kódot úgy kell fordítani, hogy abba belekerüljenek a debugger számára szükséges információk, illetve a kódot ne optimalizálja a fordító, amellyel esetlegesen kihagy belőle utasításokat, amely miatt egy kicsit nehezebben követhető lehet, hogy a program miért úgy fut, ahogy.
Assertek¶
C++-ban lehetőség van a assert
preprocesszor makró által arra, hogy bizonyos feltételeket ellenőrizzünk a program futtatása során, amelyek ha nem teljesülnek, a program végrehajtása megszakad. Ennek használatához importálnunk kell a cassert
headert.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Kiiratás¶
Sok esetben persze a legegyszerűbb és leggyorsabb megoldás, ha a programunkban kiirató utasításokat használunk egy-egy változó értékének lekérésére és ellenőrzésére. Annak érdekében, hogy ne vesszünk el esetlegesen a sok kiíratásban, illetve adott esetben ezeket a saját debugolásra használt üzeneteket könnyen ki tudjuk kapcsolni, érdemes a feltételes fordítás lehetőségét alkalmaznunk, illetve a megfelelő makrókkal jelezhetjük azt is, hogy az adott kiiratás a kódunk melyik részén történt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
A #define DEBUG
sor hozzáadásával, vagy a -DDEBUG
fordítási kapcsolóval tudjuk elérni, hogy a programba bekerüljün a megfelelő kiirató utasítás futtatása. Ezek elhagyásával a program a kiiratás nélkül fut le.
Létrehozva: 2024-06-20