Kihagyás

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
#include <iostream>

int main() {
  std::cout << "Hello world!" << std::endl;
  return 0;
}

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 nem boolean-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
#include <iostream>

using namespace std;

int main() {
  cout << "Hello World!" << endl;
  return 0;
}

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:

  1. Előfeldolgozás (preprocessing)

  2. Fordítás (compilation)

  3. Ö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
/*
Fontos C++ elemek
*/

/home/user/peldak/pelda.cpp:

1
2
#include <secret/fontos.h>
int main() {/*KOD*/ }

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
#include <iostream>

#define PRINT_MY_NAME std::cout << "My name is John Doe" << std::endl;
#define MY_FAV_NUM 42

int main() {
  PRINT_MY_NAME
  std::cout << "My fav number is: " << MY_FAV_NUM << std::endl;
}

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
#include <iostream>

#define DEBUG
#define VERSION 1

int main() {
#ifdef DEBUG
  std::cout << "My debug line" << std::endl;
#else
  std::cout << "My non debug line" << std::endl;
#endif

#if VERSION == 2
  std::cout << "Rossz verzio" << std::endl;
  return 1;
#endif
}

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
#ifndef _KEDVENCEK_H_
#define _KEDVENCEK_H_
/*
Masolando tartalom akar tobb include-al.
*/
#endif

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
#include <iostream>

int main() {
#ifdef SECRET
  std::cout << "My password is: " << MY_PWD << std::endl;
#endif
}

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
#include <iostream>

#ifndef BIRO_TEST
int main() {

  /*Kiadott kodreszlet. */
  std::cout << "Biro, make my output wrong, please!";
}
#endif

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
#include <iostream>

#define SQUARE(x) x * x

int main() {
  std::cout << SQUARE(3) << " " << SQUARE(1 + 2) << std::endl;
}

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
#include <iostream>

using namespace std;

int main(int argc, char* argv[]) {
  int kor = 0;
  float magassag = 0.0;
  char nev[10];

  cout << "Adja meg a korat (evek szama) es magassagat szokozzel elvalasztva: ";
  cin >> kor >> magassag;

  cout << "Adja meg a nevet: ";
  cin >> nev;

  cout << "Nev: " << nev << " kor: " << kor << " ev, magassag: " << magassag << " cm" << endl;
  return 0;
}

Sorvéget az endl segítségével írhatunk.

A fájl letöltése

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
#include <iostream>
#include <string>

using namespace std;

int main() {
  char nev[10];
  cout << "Adja meg a nevet: ";
  cin >> nev;
  cout << "A nev: " << nev << endl;
}

Az eredmény (ha a program lefut):

1
2
Adja meg a nevet: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Nev: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

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:

A string osztály fontosabb metódusai, amelyeket a leggyakrabban használunk:

Létrehozás, azaz a konstruktorok

1
2
3
4
string s1;           // ures string
string s2("alma");   // string, amely az "alma" szoveget tartalmazza
string s3 = "korte"; // string, amely az "korte" szoveget tartalmazza
string s4(s2);       // string, amely az "alma" szoveget tartalmazza, mert az s2 "alma"

Beolvasás, kiírás

1
2
3
string str;
cin >> str;
cout << "A beolvasott szoveg: " << str << endl;

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
string s1, s2;
cin >> s1 >> s2;
if (s1 == s2)
    cout << "A ket string azonos" << endl;
else
    cout << "A ket string nem azonos" << endl;

A string-eket az == operátor segítéségével össze lehet hasonlítani.

Összefűzés

1
2
3
4
string s1("Hello"), s2("World"), s3;
s3 = s1 + " " + s2 + "!";
cout << s3 << endl;
// kimenet: Hello World!

Méret, üres-e, tartalom törlése

1
2
3
4
5
6
7
8
string s("alma");
// size() es length() ugyanaz, length olvasmanyosabb
cout << "meret: " << s.size() << " " << s.length() << endl; // meret: 4 4
if (s.empty()) // van benne karakter -> false
    cout << "s ures" << endl;
s.clear();
if (s.empty())
    cout << "s ures" << endl; // most kiirja, hogy "s ures"

i-edik elem

1
2
3
4
5
string s("alma");
cout << s[2] << endl; // m
s[0] = 'l';
s[1] = 'a';
cout << s << endl;    // lama

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
int i = 5;
string s1 = to_string(i);
string s2 = to_string(3.14);
cout << s1 << " " << s2 << endl; // 5 3.140000

string-et számmá konvertálni az stoi függvénnyel lehet.

1
2
3
string s1("12345");
int i = stoi(s1);
cout << i << endl; // 12345

Konverziók a különböző típusokra:

  • int: stoi
  • long: stol
  • long long: stoll
  • unsigned long: stoul
  • unsigned long long: stoull
  • float: stof
  • double: stod
  • long 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
string s("123 alma");
cout << stoi(s) << endl; // 123
  • 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
try {
  string s1("999999999999999999999999999999");
  cout << stoi(s1) << endl;
}
  catch (const out_of_range& e_out) {
  cout << "out_of_range exception" << endl;
}
  • ha nem lehet konvertálni, akkor std::invalid_argument kivétel dobódik
1
2
3
4
5
6
7
try {
  string s1("alma");
  cout << stoi(s1) << endl;
}
  catch (const invalid_argument& e_inv) {
  cout << "invalid_argument exception" << endl;
}

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
#include <iostream>
#include <cassert>
using namespace std;

int main() {

  int even_num = 3;

  // asserts ami ellenőrzi, hogy az even_num páros-e (ahogy azt a neve alapján elvárjuk tőle)
  assert((even_num % 2 == 0));

  return 0;
}

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
#include <iostream>
using namespace std;

int main() {

  int even_num = 3;

  #define DEBUG
  #ifdef DEBUG
  cout << "LOG: [" << __FILE__ << ": " << __LINE__ << "]: az even_num valtozo erteke: " << even_num << endl;
  #endif

  return 0;
}

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.


Utolsó frissítés: 2025-09-05
Létrehozva: 2024-06-20