C++ alapok
Első program¶
Készítsük el első c++
programunkat! Ennek a programnak a forrását az elso.cpp
fájlba írjuk bele.
Az egyszerű programunk egy Hello world!
sort fog kiírni a képernyőre.
Ennek a forráskódja legye az alábbi kód:
elso.cpp | |
---|---|
1 2 3 4 5 6 7 8 9 10 |
|
Ahhoz, hogy ezt a programot végre tudjuk majd hajtani elpször le kell fordítanunk.
Fordítás (compile)¶
Pythonnal ellentétben (ahol közvetlenül lehet a forráskódot végrehajtani),
minden c++
programkódot először le kell fordítani (compile) futtatható binárissá.
A forráskód lefordításához használjuk a g++
programot:
# Linux esetén
$ g++ -o elso -Wall elso.cpp
# Windows esetén
$ g++ -o elso.exe -Wall elso.cpp
Mi az a $
jel itt?
A fenit két sor példában szereplő $
jel azt jelzi, hogy ezeket a parancsokat teriminálban kell kiadni, ahol a $
mutatja a promptot
és ezt a karaktert külön nem kell begépelni.
A kapcsolókról:
-o nev
kapcsolóval adhatjuk meg a kimeneti fájl nevét, ahol anev
tetszőleges lehet-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.
- Ilyen például az, hogy egy
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 operációs rendszertől függetlenül.
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 |
|
Fontos!
Ilyen using namespace ..;
elemeket csak .cpp
forrás fájlokban használjunk. Header fájlokban (.h
, .hpp
, .hxx
) célszerű kerülni!
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 vagy mi készítettünk, de egy másik fájlban van.
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 rengeteg 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:
#include <...>
forma használata esetén, a standard könyvtárak között fogja keresni, amiknek megvan a megfelelő helye az operációs rendszeren belül. Ezt a formát tipikusan a standard könyvtárak illetve egyéb mások által megírt fájlok include-olásához használjuk amik nem a mi projektünk szoros részei.#include "..."
forma használataok (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"
. Ezt a változatot pedig a program saját részét képző header-jeihez alkalmazzák.
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.
Vegyük példának az alábbi fontos.h
és pelda.cpp
fájlokat:
/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:
-
#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. -
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.
#define - 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 a define
preprocesszor kulcsszóval.
Makró definálás példa (a makró tartalma üres) | |
---|---|
1 |
|
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:
Makró definálás példa | |
---|---|
1 |
|
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ók azonosítójaira hasonló megkötések van mint változó nevekre, de nagyon sok esetben csupa nagybetűvel írják a makró neveket.
Ezzel jobban elkülönülnek normál azonosítótktól a forráskódban.
Vizsgáljuk meg az alábbi példát.
1 2 3 4 5 6 7 8 9 |
|
1 2 3 4 5 6 7 8 9 |
|
Érdemes megjegyezni, hogy a preprocesszor direktívák kiértékelése/behelyettesítése után azok a sorok ahol voltak ilyen elemek megmaradnak mint üres sorok. Ez látható a fenti példában is.
Ha megfigyeljük a PRINT_MY_NAME
használatakor nem kellett ;
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.
#if - Feltételes direktívák¶
További preprocesszor direktívák között vannak különféle feltételes kifejezések is.
Az egyik ilyen segítségével megvizsgálhatjuk, hogy egy makró definiálva van-e: #ifdef
.
Az #ifdef
a #if defined(..)
kifejezésnek a tömörebb változata és hasonlóan
más feltételes szerkezetekhez lehet #else
vagy #elif
-et használni (elif
az else if
-et jelenti itt).
Ennek a segítségével például debug kiíratásokat tudunk a kódba rejteni. Vizsgáljuk meg az alábbi kódrészletet:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
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 fordítás során a végső programunkba.
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.
Vegyük az alábbi kedvencek.h
példát.
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_KEDVENCEK_H_
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
_KEDVENCEK_H_
makró definiálását, pontosan azt amit vizsgáltunk, hogy létezik-e már. Valamint a további bármilyen egyéb hasznos program kódsorokat. - Hogyan szakad meg a lánc?
Mivel a
_KEDVENCEK_H_
makró definiálásra került az első esetben amikor a fájlt include-oltuk, a rákövetkező include-ok esetén az első sorban található#ifndef
hamis lesz majd. Így az igaz ág tartalmát kihagyja a preprocesszor. Ezzel konkrétan mintha egy üres fájlt include-olt volna a fordítás során a fordító. Az üres szöveg pedig biztosan nem vezet több include-ra.
Preprocesszor direktívák és makrók (folytatás)¶
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.
Makró értékek forrásfájlon kívül (secret.cpp) | |
---|---|
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ó (így az #ifdef
hamis lesz).
Ezt azonban megváltoztathatjuk, ha másként fordítunk.
A -D
fordítási 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:
1 |
|
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.
Példa `BIRO_TEST` használatára | |
---|---|
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 megoldását, akkor a biro azt sem fogja látni, hiszen a preprocesszor kidobja.
Paraméteres makrók¶
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 |
|
1 2 3 4 5 6 7 |
|
Gondolhatnánk, hogy a makró ugyanazt jelenti mindkét esetben, hiszen 3*3 = 9
és 1+2 = 3
az is 3*3
, í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árnunk!
Fordítás (Compilation)¶
Ebben a fázisban a preprocesszor már elvégezte a dolgát, nincsen semmilyen direktíva, azok alapjá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 (tipikusan .o
vagy .obj
fájlok).
Ez olyan kód, ami már a gép számára értelmezhető, azonban még mindig nem futtatható.
Ez csak 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 és a sztandard osztálykönyvtár részei (std
namespace):
- 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 |
|
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 7 8 9 |
|
A string
-eket az ==
operátor segítéségével össze lehet hasonlítani.
Összefűzés¶
1 2 3 4 5 6 7 8 |
|
Méret, üres-e, tartalom törlése¶
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Indexelés / 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:¶
Típus | Függvény név |
---|---|
int |
std::stoi |
long |
std::stol |
long long |
std::stoll |
unsigned long |
std::stoul |
unsigned long long |
std::stoull |
float |
std::stof |
double |
std::stod |
long double |
std::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 |
|
- ha nem lehet konvertálni, akkor
std::invalid_argument
kivétel dobódik
1 2 3 4 5 6 |
|
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 |
|
Kiíratá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: 2025-10-01