C++

Ebben a cikkben megvizsgáljuk a C++ hatását a társadalom különböző aspektusaira. Felbukkanásától napjainkig a C++ alapvető szerepet játszott abban, ahogyan interakcióba lépünk, kommunikálunk és megértjük a minket körülvevő világot. A történelem során a C++ vita és elemzés tárgya volt, és befolyása olyan változatos területeken is érezhető volt, mint a politika, a technológia, a művészetek és a populáris kultúra. Interdiszciplináris megközelítésen keresztül alaposan megvizsgáljuk, hogy a C++ hogyan alakította át tapasztalatainkat és perspektíváinkat, és milyen következményekkel jár a jövőre nézve.

C++

Paradigmatöbbelvű: generikus, objektumorientált, imperatív
Jellemző kiterjesztés.h, .hh, .hpp, .hxx, .h++, .cc, .cpp, .cxx, .c++
Megjelent1983
TervezőBjarne Stroustrup
FejlesztőBjarne Stroustrup
Utolsó kiadás2020, ISO/IEC 14882:2020
Típusosságstatikus típusosság, erősen típusos, nem típus biztos, nominatív
FordítóprogramGNU Compiler Collection, Microsoft Visual C++, Borland C++ Builder
DialektusokISO/IEC C++ 1998, ISO/IEC C++ 2003, ISO/IEC C++ 2011, ISO/IEC C++ 2014, ISO/IEC C++ 2017, ISO/IEC C++ 2020
MegvalósításokC++ Builder, clang, Comeau C/C++, GCC, Intel C++ Compiler, Microsoft Visual C++, Sun Studio
Hatással volt ráC, Simula, BCPL, CLU, Algol
Befolyásolt nyelvekC#, Java, PHP, Python
Weboldal

A C++ (ejtsd: cé plusz plusz) egy általános célú, magas szintű programozási nyelv. Támogatja a procedurális, az objektumorientált és a generikus programozást, valamint az adatabsztrakciót. Napjainkban szinte minden operációs rendszer alá létezik C++ fordító. A nyelv a C programozási nyelv hatékonyságának megőrzése mellett törekszik a könnyebben megírható, karbantartható és újrahasznosítható kód írására, ez azonban sok kompromisszummal jár, erre utal, hogy általánosan elterjedt a mid-level minősítése is, bár szigorú értelemben véve egyértelműen magas szintű.

Története

Bjarne Stroustrup

Bjarne Stroustrup kezdte el a C++ programozási nyelv fejlesztését a C programozási nyelv kiterjesztéseként, más nyelvekből véve át megoldásokat (Simula67, Algol68), ötleteket (ADA). A nyelv első, nem kísérleti körülmények közt való használatára 1983-ban került sor, 1987-ben pedig nyilvánvalóvá vált, hogy a C++ szabványosítása elkerülhetetlen. Ez a folyamat 1991 júniusában kezdődött el, amikor az ISO szabványosítási kezdeményezés részévé vált. A C++ programozási nyelv szabványát 1998-ban hagyták jóvá ISO/IEC 14882:1998 néven, az aktuális, 2017-es változat kódjelzése ISO/IEC 14882:2017.[1]

Érdekesség

Mire a nyelvet szabványosították, már rengeteg C++ nyelvű kód készült, került használatba. Mivel a szabvány fejállományok némileg eltértek az eddigiektől, a bizottság érdekes megoldást választott a kompatibilitás megtartására:

  • A régi C++ fejállományok (pl. „iostream.h”) továbbra is használhatóak (bár hivatalosan nem támogatottak), de tartalmuk nincs benne a standard névtérben.
  • Az új, hivatalos fejállományok („iostream”) szinte megegyeznek a régiekkel, de tartalmuk a standard névtérben szerepel.
  • A szabvány C fejállományok (pl.: „stdio.h”) továbbra is támogatottak, de tartalmuk a globális névtérben van.
  • A C könyvtárak átvétele szintén a .h eltávolításával történt, beszúrva egy c-t a nevük elé (pl. „stdio.h”-ból „cstdio” lett). Tartalmuk a standard névtérben szerepel.[2]

Fordítók, fejlesztőeszközök

Windows operációs rendszeren tanuláshoz megfelelő – és ingyenes – eszköz a Code::Blocks. Haladó szinten kényelmes választás a Visual C++ Express Edition, amely ingyen letölthető a Microsoft oldaláról, de több helyen bevallottan eltér a szabványtól.

Linux/UNIX alatt megszokottabb a konzolból való fordítás (ez a lehetőség Windowsnál is megvan). Erre az említett rendszerekben általában a GNU Compiler Collection g++ programját használjuk, illetve grafikus fejlesztőeszközként rendelkezésünkre áll a KDevelop is, illetve a fentebb már említett Code::Blocks is elérhető Linux alatt.

A legtöbb fordító – ha nem adjuk meg külön – néhány esetben eltér a szabványtól, így optimalizáltabb kódot hozhatnak létre. Természetesen minden esetben lehetőség van a szabvány szerinti fordításra.

A név eredete

Nevét Rick Mascitti találta ki. A C++ név kifejezi, hogy a nyelv a C kibővítése: az inkrementáló operátorra utal a ++.[3]

A C++ alapelemei

Jelkészlet

A kis- és nagybetűs angol ABC, általános írásjelek és a matematikai operátorok, jelek.

Azonosítók

A nyelv bizonyos összetevőire (változók, konstansok, függvények stb.) névvel hivatkozunk. A legtöbb fordító csak az első 32 karaktert veszi figyelembe a nevekben. A név első karaktere betű vagy aláhúzásjel (_) lehet, ettől kezdődően már számok is szerepelhetnek benne. Lehetőleg saját névként ne adjunk meg aláhúzásjellel (_) kezdődő nevet, mert ezek a fordító számára vannak fenntartva (pl. __DATE__, __cplusplus, _MSC_VER). A C++ különbséget tesz a kis- és nagybetűk között (case-sensitive). Az alma név nem ugyanaz, mint az Alma név.

Kulcsszavak

Ezek a kifejezések a nyelv részei, önmagukban nem használhatóak névként.

asm delete goto reinterpret_cast try
auto do if return typedef
bool double inline short typeid
break dynamic_cast int signed typename
case else long sizeof union
catch enum mutable static unsigned
char explicit namespace static_cast using
class export new struct virtual
const extern operator switch void
const_cast false private template volatile
continue float protected this wchar_t
decltype for public throw while
default friend register true

Megjegyzések

A megjegyzések olyan karaktersorozatok, amelyeket a program dokumentálása érdekében használunk. A fordító nem veszi figyelembe a programban elhelyezett megjegyzéseket.

// egysoros megjegyzés

/* Itt kezdődik a többsoros megjegyzés
és itt a vége */

Operátorok

A C++-ban nem vezethetünk be új operátorokat, de majdnem mindegyiket túlterhelhetjük. Van infix, prefix és postfix jelölésű operátor is. Általában meghatározott számú operandusuk lehet (egy, kettő vagy három), kivéve a függvényhívás operátor (operator()), amelynek bármennyi operandusa lehet.

Precedencia

Az elsőbbségi (precedencia-) szabályok a kifejezések kiértékelésének helyes sorrendjét írják elő. A kiértékelés során először mindig a magasabb precedenciájú kifejezés értékelődik először.

Az operátorok összefoglalása
Precedencia Operátor Rövid leírás Kiértékelés iránya
1 :: Hatókör-operátor nincs
2 ()

->
.
++
--
Függvényhívás
Tömbindexelés
Mutatón keresztüli tag-elérés
Objektumon keresztüli tag-elérés
Posztfix növelés
Posztfix csökkentés
Balról jobbra
3 !
~
++
--
-
+
*
&
(típus)
sizeof
Logikai tagadás
Bitenkénti negálás
Prefix növelés
Prefix csökkentés
Negatív előjel
Pozitív előjel
Dereferálás
Címképzés
Konverzió típusra
Méret lekérdezése
Jobbról balra
4 ->*
.*
Tagkiválasztás mutatón
Tagkiválasztás objektumon
Balról jobbra
5, 6 *
/
 %
+
-
Szorzás
Osztás
Maradékszámítás
Összeadás
Kivonás
Balról jobbra
7 <<
>>
Bitenkénti eltolás balra
Bitenkénti eltolás jobbra
Balról jobbra
8, 9 <
<=
>
>=
!=
==
Kisebb
Kisebb-egyenlő
Nagyobb
Nagyobb-egyenlő
Nem egyenlő
Egyenlő
Balról jobbra
10 & Bitenkénti ÉS Balról jobbra
11 ^ Bitenkénti kizáró VAGY Balról jobbra
12 | Bitenkénti megengedő VAGY Balról jobbra
13 && Logikai ÉS Balról jobbra
14 || Logikai megengedő VAGY Balról jobbra
15 ?: feltételes (if-then-else) operátor Jobbról balra
16 =
+=
-=
*=
/=
 %=
&=
^=
|=
<<=
>>=
Értékadás
Összeadás és értékadás
Kivonás és értékadás
Szorzás és értékadás
Osztás és értékadás
Maradékképzés és értékadás
Bitenkénti ÉS és értékadás
Bitenkénti kizáró VAGY és értékadás
Bitenkénti megengedő VAGY és értékadás
Eltolás balra és értékadás
Eltolás jobbra és értékadás
Jobbról balra
17 , Szekvenciaoperátor Balról jobbra

Operátorok túlterhelése

A közönséges függvényekhez hasonlóan a legtöbb operátort is túl lehet terhelni, ami a felhasználói típusok kényelmesebb, szabványosabb használatát teszi lehetővé, aritmetikai operátorokkal (+, +=), és az std::cout-hoz való << operátorral.

struct Complex {
  //Barátfüggvény deklarációja, hogy hozzáférjen a tagokhoz
  friend std::ostream& operator<<(std::ostream& stream, const Complex& z);

  //Konstruktor, taginicializációs listával
  Complex(double a, double b): re(a), im(b) { }

  Complex& operator+=(const Complex& rhs) {//hozzáadó operátor tagfüggvény...
    re += rhs.re; im += rhs.im;
    return *this;
  }

private:
  double re, im;
};

Complex operator+(const Complex& lhs, const Complex& rhs) {//összeadó operátor viszont globális
  return Complex(lhs) += rhs;
}

//az ostream definíciójához nem férünk hozzá, de
//operator<<(ostream&, const complex&)-t definiálhatunk
std::ostream& operator<<(std::ostream& stream, const Complex& z)
{
  return (stream << '(' << z.re << ", " << z.im << ')');
}

//Ezután az operátort egyszerűen használhatjuk:
Complex c(1.0, 4.6);
std::cout << c; //A kimeneten megjelenik: (1.0, 4.6)

Típusok, változók, konstansok

A C++ -ban minden felhasznált névről meg kell mondanunk, hogy mi az, amit képvisel, tudatnunk kell a fordítóprogrammal a típusát. Megkülönböztetünk egyszerű és összetett típusokat. Egyszerű típusok az egész típusok (előjeles és előjel nélküli), a lebegőpontos típusok, a karaktertípusok, a bool és a void. Összetett típusok az alaptípusok felhasználásával felépített tömb-, mutató- stb. típusok és a felhasználói típusok (pl. osztály).

Az alapvető adattípusok mérete fordító- és platformfüggő, de a következők adottak:

  • 1 == sizeof(char) <= sizeof(short) <= sizeof(int) <= sizeof(long)
  • 1 <= sizeof(bool) <= sizeof(long)
  • sizeof(char) <= sizeof(wchar_t) <= sizeof(long)
  • sizeof(float) <= sizeof(double) <= sizeof(long double)
  • sizeof(N) == sizeof(signed N) == sizeof(unsigned N) (ahol N lehet char, short, int vagy long)

Az itt megadott méretek a „megszokott” 32 bites asztali környezetben általánosak, de nem garantáltak.

Adattípus Értékkészlet Méret (bájt) Pontosság
bool false, true 1
char -128..127 1
signed char -128..127 1
unsigned char 0..255 1
wchar_t 0..65535 2
int -2147483648..2147483647 4
unsigned int 0..4294967295 4
short -32768..32767 2
unsigned short 0..65535 2
long -2147483648..2147483647 4
unsigned long 0..4294967295 4
long long -9223372036854775808..9223372036854775807 8
unsigned long long 0..18446744073709551615 8
float 3.4E-38..3.8E+38 4 6
double 1.7E-308..1.7E+308 8 15
long double 3.4E-4932..3.4E+4932 12 18

A memóriában létrehozott tárolókat névvel látjuk el, amelynek segítségével hivatkozhatunk rájuk. Ezeket a tárolókat változóknak nevezzük.

Konstansoknak azokat a változókat nevezzük, amelyeknek pontosan egyszer, a definícióban adhatunk értéket (ekkor kötelező), és ezt a típusnév elé vagy mögé írt const típusminősítővel jelezzük:

const int x; //Hiba!
const int y = 10; //Jó
y = 10; //Hiba!

A felsorolt típus (enum)

Az „enum” olyan adattípust jelöl, melynek lehetséges értékei egy konstanshalmazból kerülnek ki.

enum Animals {bear, wolf, rabbit};

A fordító balról jobbra haladva nullával kezdve egész értékeket feleltet meg a felsorolt konstansoknak. Ha egy konstansnak egész értéket adunk, akkor a következő elemek ettől a kiindulási értéktől kapnak értéket.

enum Animals {bear = 10, wolf, rabbit};
//wolf és rabbit értéke rendre 11 és 12

A felsorolásban azonos értékek is szerepelhetnek. A deklarációban megadott név típusnévként is felhasználható:

enum Animals {bear, wolf, rabbit};
Animals x = bear;

Típuskonverziók

Előfordulhat, hogy valamely kétoperandusú operátor különböző típusú operandusokkal rendelkezik. Ekkor, beépített típusok esetén, hogy a művelet elvégezhető legyen, a fordítónak azonos típusra kell alakítania az operandusokat. Megkülönböztetünk implicit és explicit típuskonverziót.

Implicit típuskonverzió: Ez a fajta konverzió a programozó beavatkozása nélkül automatikusan megy végbe, a C++ definíciójában szereplő szabályok alapján. A „szűkebb” típus adatvesztés nélkül konvertálódik a „szélesebb” típusra, ha szükséges.

int x = 10;
float f = 1.23;
x + f; //A kifejezés float típusú lesz, 10.00 + 1.23 alakban értékelődik ki

Ebben az esetben adatvesztés léphet fel:

float f = 1.23;
int x = f; //x értéke 1 lesz, a törtrész elvész

Explicit típuskonverzió

Feltétel nélküli típusátalakítás:

(típusnév)kifejezés (pl.: (char *)c)

Ideiglenes érték létrehozása:

típusnév(kifejezés) (pl.: int(x))


Explicit konverziót a programozó előírhat a típuskonverziós operátorok használatával is.

Dinamikus típusátalakítás:

dynamic_cast<típus>(ptr)

Futásidejű konverziókat végezhetünk vele. A típus osztályra mutató pointer, referencia, vagy void* típusú kifejezés, míg a ptr pointer vagy referencia típusú kifejezés lehet (a típusnak megfelelően). Ha a konverzió nem végezhető el, akkor pointerek esetén 0 lesz az eredmény, referenciáknál pedig bad_cast kivételt vált ki.

Fordítási idejű átalakítások:

static_cast <típus>(arg)

Visszafordítható típuskonverzió egymásba konvertálható típusok között. A típusnak és arg típusának fordítási időben ismertnek kell lennie.

reinterpret_cast<típus>(arg)

Úgy, mint a (típus)arg kifejezés, csak a reinterpret_cast nem használható const vagy volatile minősítés megszüntetésére.

Konstans típusátalakítás:

const_cast<típus>(arg)

Akkor használjuk, ha fel akarjuk oldani a const vagy volatile típusmódosítók hatását egy adott mutatón vagy referencián. A típus és arg típusának azonosnak kell lennie, kivéve a fent említett típusmódosítókat.

Pointerek

Amikor egy változót definiálunk, a memóriában létrejön egy megfelelő méretű tároló, amelybe bemásolódik a kezdőérték.

int * p;

A fenti példában egy int típusú változó címének tárolására használható tároló jön létre. A címet a címképző operátorral (&) érhetjük el.

int x = 7;
p = &x;

Most az x név és a *p (a p által mutatott tároló) érték ugyanarra a memóriaterületre hivatkozik.

* p = x + 6;

A kifejezés hatására x értéke 13 lesz.

A referencia a pointernek egy olyan változata, amelyet kötelező inicializálni, és az értékét később nem lehet megváltoztatni, valamint használatának szintaktikája is különbözik:

int x = 10;
int &r = x; /* 'r' most 'x'-re mutat */
r = r + 1;  /* ugyanaz, mint x = x + 1 */
&r;         /* ugyanaz, mint &x */

Elterjedt nézet, hogy a referenciák minőségileg különböznek a pointerektől, ezért nem tartalmazhatnak NULL-értéket, illetve érvénytelen címet. Ez nem egészen igaz, tekintsük az alábbi példákat:

    int &ref1= *(int *)0;
    int &ref2= *new int; delete &ref2;
    int &ref3= *(int *)malloc (sizeof (int)); free (&ref3);

Itt ref1 tartalma nulla, ref2 és ref3 tartalma érvénytelen (már felszabadított) memóriacím.

Névterek

A fordító a programban használt neveket különböző névterekben (namespace) tárolja. Egy névtérben lévő neveknek egyedieknek kell lenniük, azonban a különböző névterekben azonos néven is szerepelhetnek, azaz a névterek a láthatósági szabályokat teszik könnyebben alkalmazhatóvá. Egy névtérben logikailag összefüggő változókat, függvényeket, típusokat tárolunk. Egy osztály/struktúra egyben a nevével azonos nevű névteret is definiál.

// Az Állat névtéren belül szerepel a Farkas és Medve osztályok definíciója
namespace Allat{
 class Farkas{...};
 class Medve{...};
}

Egy névtér elemeire háromféleképpen hivatkozhatunk:

  • Globális névfeloldással (using direktíva), ekkor a névtér összes eleme használható; használata körültekintést igényel, mivel egész névterek importálásakor könnyen névütközés lehet:
#include <iostream>
using namespace std; //A standard (std) névtér globális használata

int main(int argc, char *argv)
{
 cout << "Globális névfeloldás" << endl; //std::cout << "Globális névfeloldás" << std::endl; helyett
 return 0;
}
  • Explicit névfeloldással megadva a kívánt névteret, közvetlenül a feloldani kívánt elem esetében:
#include <iostream>

int main(int argc, char *argv)
{
 std::cout << "Explicit névfeloldás" << std::endl;
 return  0;
}
  • A kívánt elem nevének feloldásával (using deklaráció):
# include <iostream>
using std::cout; //A standard (std) névtérbeli cout globális használata

int main(int argc, char *argv)
{
 cout << "Csak a cout lett feloldva" << std::endl;
 return 0;
}

Létezik az úgynevezett névtelen névtér, amit arra használhatunk, hogy ne szemeteljük tele a globális névteret, megvédjük magunkat a többértelműségektől.

namespace
{
 //amit el akarunk keríteni
}

A C++ programok szerkezete

Hello World!

Az úgynevezett „Helló, világ!” programot először Brian Kernigham és Dennis Richie alkalmazta A C programozási nyelv című könyvükben példaprogramként. Mindössze annyit csinál, hogy a képernyőre írja az üdvözletet.

#include <iostream>

int main() {
    std::cout << "Helló, világ!";
    return 0;
}

A kettőskereszttel (#) jelzett sorok az előfordítónak (precompiler) szóló utasítások. Az include utasítás behelyettesíti a hívás helyére a megadott fájl tartalmát. Az iostream fejállomány tartalmazza a megfelelő utasításokat a kiíratáshoz. A main egy függvény, ez a program belépési pontja. Minden C++ nyelven írt programnak tartalmaznia kell. Paraméterei közül az argc a parancssori paraméterek számát adja meg, míg az argv egy nullpointerrel terminált, karaktermutatókat tartalmazó tömb, amelyben a paraméterek vannak C-stílusú stringként. A C++-ban a tömböket nullától indexeljük, az argv nulladik eleme a futtatható állomány neve, első eleme pedig az első paraméter.

./program elso masodik (vagy program.exe elso masodik)
argv == "program"
argv == "elso"
argv == "masodik"

A két kapcsos zárójel ({}) közti részt blokknak nevezzük. A cout a C++ standard kimenete, az std:: pedig arra szolgál, hogy a fordító a standard névtérben keresse a cout definícióját. A :: az ún. hatókör operátor. A return visszaadja a vezérlést az őt hívó függvénynek, jelen esetben ez a program futásának befejezését jelenti, ezért az operációs rendszernek. A return mögé írt szám a visszatérési érték, a 0 általában azt jelzi, hogy a program rendben lefutott. A main()-ben ez nem kötelező; ha elhagyjuk, akkor automatikusan 0-t ad vissza.

A program futásának eredménye:

./program
Hello World!

Standard IO

A C++ megkülönböztet standard inputot, outputot, illetve errort. A standard output (cout), amire ír, ez alapértelmezés szerint a képernyő. A standard input (cin) a bejövő adatokat fogadja, alapesetben a billentyűzetet. A standard error (cerr) az az eszköz, ahová a hibaüzenetek érkeznek, alapértelmezetten szintén a képernyő.

std::cout << "Standard kimenet!";
char ch; std::cin >> ch; // A standard bemenetről beolvasunk a ch változóba
std::cerr << "Standard error!";

Az előfordító

A C++ megörökölte a C előfordítóját. Az előfordító a tényleges fordítás előtt fut le. Feladatai közé tartozik a forráskód megfelelő átalakítása, szimbólumok és makrók definiálása, illetve felhasználható feltételes fordításra is. Az előfordító által definiált szimbólumok és makrók típus nélküli értékekkel dolgoznak, ezért míg a C – gyengén típusos nyelv lévén – erősen épít az előfordítóra, a C++ nyelvben gyakorta (fordítási és futási) hibák forrásai lehetnek.

Vezérlési szerkezetek

Szekvencia

A szekvencia az egymás után végrehajtott utasításokat jelenti:

utasítás1;
utasítás2;
...

Elágazás (szelekció)

Amikor a program a futásakor egy elágazáshoz ér, megvizsgál egy feltételt, és ha igaz, akkor végrehajtja a megfelelő utasítást.

if(feltétel) utasítás;
else utasítás;
//az utasítások lehetnek blokkok({}) is, az else ág elhagyható

A feltétel több szempontból való megvizsgálására a C++ a switch szerkezetet adja, de ez korlátozott: csak integrális (int, char és enum) típust lehet vizsgálni.

void Eldontendo(char c) {
  switch(c) {
    case 'i':
    case 'I': cout << "Igen\n"; break;           //break; nélkül "átesne" a következő case-be is
    case 'n':                                    //itt viszont szándékosan nincs break;
    case 'N': cout << "Nem\n"; break;
    default: cout << "I/i vagy N/n!\n"; break;  //nem kötelező, ha semelyik sem igaz, ide ugrik
  }
}

Ha mégis bonyolultabb többszörös feltételt kell megvizsgálnunk, akkor használhatjuk az else if szerkezetet, azaz az else ágban nyitott if -et.

if(feltétel_1) {
 utasítás_1;
}
else if(feltétel_2) {
 utasítás_2;
}
else if(...) {

}
.
.
.
else utasítás;

Egy elágazásban pontosan egy if és legfeljebb egy else ág lehet.

A feltételek kiértékelése balról jobbra történik a logikai operátorok asszociativitásának megfelelően, és csak addig megy, amíg a maradék kifejezéstől függetlenül biztosan igaz vagy hamis lesz az eredmény (lusta vagy rövid záras kiértékelés):

bool l = false;
bool k = true;
if(l && k)
{
 utasítás;
}

A fenti példában a logikai és operátort használtuk, ez a feltétel akkor lesz igaz, ha l és k is igaz. Mivel l hamis, ezért a program csak l-t fogja vizsgálni. Ez fontos lehet, ha mellékhatással jár a feltétel megvizsgálása.

Ciklus (iteráció)

Ciklust akkor használunk, ha ugyanazt a feladatot többször kell elvégezni egymás után. Háromféle ciklus áll rendelkezésre:

Számláló ciklus (for)

for(int i = 0; i < 10; ++i) {
 std::cout << "i = " << i << std::endl;
} // A {} egyetlen utasításnál elhagyható.

A ciklusfeltételben felveszünk egy ciklusváltozót, kezdőértéket adunk neki, és megadjuk, meddig menjen a számláló. A ++ a C++ ún. inkrementáló operátora, megvan a párja (--, dekrementáló) is. Két alakja létezik:

  • változó++ → postfixes alak, csak a kifejezés kiértékelése után növeli az értékét és létrehoz egy átmeneti változót.
  • ++változó → prefixes alak, nincs átmeneti változó, azonnal növeli az értékét, általában ezt használjuk.

Elöltesztelő ciklus:

while(feltétel) {
 utasítás;
} // A {} egyetlen utasításnál elhagyható.

Az elöltesztelő ciklus előbb vizsgálja a ciklusfeltételt, aztán hajtja végre az utasításokat. Ez azt jelenti, hogy nem biztos, hogy akár egyszer is lefut.

Hátultesztelő ciklus:

do { // A {} egyetlen utasításnál elhagyható.
    utasítás;
} while(feltétel);

Ez a ciklus biztos, hogy egyszer legalább lefut.

A C++-ban sok nyelvvel ellentétben a for ciklus szinte egy az egyben megfeleltethető while ciklusnak.

for(inicializál; tesztel; inkrementál) {
    programrész;
}

// Ekvivalens ezzel
inicializál;
while(tesztel) {
    programrész;
    // Apró különbség: Ha a 'programrész' 'countinue' utasítást tartalmaz,
    // akkor 'while' esetén az 'inkrementál' nem fut le.
    inkrementál;
}

// Ekvivalens ezzel is
inicializál;
if(tesztel)
    do {
        programrész;
    } while(inkrementál, tesztel);  // Ha a tesztel-nek nincs mellékhatása
Break, continue

Speciális vezérlőszavak a break és a continue: a break kilép a legbelső ciklusból (nincs többszörös break), a continue pedig a ciklus végére ugrik, azaz a feltételvizsgálathoz, átugorva a ciklusmag hátralévő részét.

A goto

A goto mint utasítás speciális a vezérlők között, a vezérlés szerkezetének felborítását végzi, egyszerű ugróutasítás.

Szerkezete: goto címke;, ahol a címkét címke: alakban vezetjük be. Túlzott használata átláthatatlanná teheti a kódot, néha mégis alkalmazni kell, például ha többszörösen beágyazott ciklusból kell kilépni.

Adatszerkezetek

Tömb

A C stílusú tömb azonos típusú adatok halmaza, amelyek a memóriában folytonosan helyezkednek el. Csak alapértelmezett konstruktorral rendelkező (minden beépített típus ilyen) típusokból lehet tömböt létrehozni. A tömb elemeire a tömb nevével és az indexelő operátorral () hivatkozhatunk:

int t; //10 elemű statikus tömb, más néven vektor
for(int i = 0; i < 10; ++i)
{
    t = i; //az i-edik index értéke legyen i
}

Elemi típusok foglalása esetén a tárolók kezdeti értéke nem definiált (legtöbbször memóriaszemét, bizonyos futási környezetekben lehet csupa nulla). Osztályok esetén minden elem konstruktora külön meghívódik.

A C++-ban a tömböket nullától indexeljük. A C++-fordító nem ellenőrzi a tömbindexeket, ezért hibás indexeléssel is lefordul a program, de futás közben ez több problémát is okozhat. Egyrészt felülírhatjuk a memóriában előtte vagy utána lévő adatainkat, másrészt akár olyan memóriaterületre próbálhatunk meg írni (vagy onnan olvasni), amely nem a mi programunkhoz tartozik, ekkor az operációs rendszer – amennyiben a hardver érzékeli, ill. támogatja és saját maga is elég fejlett ehhez – megszakítja a programunk futását és értesíti a felhasználót. Ennek formája operációs rendszerenként és felhasználói felületenként változó (Segmentation fault, General protection fault, Access violation…).

int t; // 10 elemű tömb létrehozása
// tömb feltöltése
std::cout << t;
//lefordul, de hibás, mivel nulla az első elem indexe, t a legmagasabb hivatkozható elem

Létrehozhatunk többdimenziós tömböt is:

int t...;

A leggyakrabban használt a kétdimenziós tömb, azaz a mátrix:

int sizeN, sizeM;
sizeN = sizeM = 5;
int t; //5*5 mátrix

for(int i = 0;i < sizeN;++i)
{
 for(int j = 0;j < sizeM;++j)
 {
  t = i + j; // az i -edik sor j -edik oszlopa legyen i+j
 }
}

A tömb neve kifejezésekben a nulladik elemre mutató pointerként érvényesül. Gyakran nem tudjuk előre a tömbök méretét, sokszor csak futásidőben derül ki, ekkor dinamikus memóriakezelést kell használnunk. Az egy értékre, illetve a több értékre mutató pointerek között nincsen szintaktikai különbség, a programozónak kell tudnia a programlogika alapján, hogy mikor melyikkel van dolga.

int meret;
std::cin >> meret; // Beolvasás felhasználótól
int *t = new int; // Foglalás
for(int i=0; i<meret; ++i)
{
  std::cin >> t; // Beolvasás a tömb i-edik elemébe
}
// Tetszőleges feladat megoldása itt, pl rendezés, átlag számítás, stb...
delete t; // A dinamikusan foglalt memória felszabadítása

A tömbök és pointerek "mechanikai" azonosságát jól demonstrálják az alábbi sorok, melyek mind ugyanazt jelentik:

t = 9;
* (t+3) = 9; // +3: 3 elemnyivel arrébb; *: mutató feloldása, hogy megkapjuk a "rekeszt"
* (&(t) + 1) = 9; // &: A második "rekesz" címe; +1: 1 elemnyivel arrébb; *: mint előbb

Illetve

t = 9;
* (t+0) = 9;
* t = 9;

C++-ban tömbök helyett gyakran az STL részeként rendelkezésre álló std::vector sablont szokták alkalmazni. Lásd: Tárolók (STL).

Struktúrák

A programozás során gyakran találkozhatunk olyan esetekkel, amikor több különböző adatot egy egységként kell kezelnünk, például egy könyvnek van szerzője, címe, kiadója, stb. A C++ nyelvben a struktúra (struct) különböző típusú adatok együttese:

//a Book struktúra definíciója
struct Book
{
 std::string title;
 std::string writer;
 int year;
};

A string típus karaktersorozatot jelöl, és a standard névtérben helyezkedik el. A struktúrák adattagjaira a pont operátor (.) segítségével hivatkozhatunk és adhatunk nekik értéket:

//Book struktúra megadása tagonként
Book b; //típusként kezelhetjük
b.title = "A C++ programozási nyelv";
b.writer = "Bjarne Stroustrup";
b.year = 2005;

//Book kezdőértékadása
Book c = {"A C++ programozási nyelv", "Bjarne Stroustrup", 2005};

//c azonos tartalmú b-vel

Struktúrákat a C nyelvben is lehet használni. A C++-ban a struktúrákat kibővítették, hogy alkalmasak legyenek az objektumorientált programozás megvalósítására:

  • Rendelkezhetnek konstruktorral és destruktorral
  • Lehetnek tagfüggvényeik
  • Szabályozható az adattagok elérése

A struktúra minden tagjának láthatósága alapértelmezetten nyilvános (public), ettől eltekintve lényegében ekvivalens a class szerkezettel, ahol az alapértelmezett elérés zárt (private).

Tömb struktúrán belül
struct Minta {int x};
struct A {int x; int t};
struct B {int x; int *t};
// ...
A a;
B b;
// ...
a.t = 9;
b.t = 9;

Az A struktúra „fizikailag” tartalmazza a 10 darab int típusú értéket, a sizeof(A) ennek megfelelően 10 intnyivel nagyobb, mint a sizeof(Minta). A B struktúra csak egy pointert tartalmaz, amely „mögé” tetszőleges számú elemnek lefoglalhatunk memóriát, ekkor a sizeof(B) csak egy pointer méretével több, mint a sizeof(Minta). A tömb elemeire hivatkozáskor a két eset között nincsen szintaktikai különbség, de a struktúra másolásakor óvatosnak kell lennünk.

Uniók, bitmezők

A C nyelv kidolgozásakor a takarékos memóriahasználat érdekében több megoldást is beépítettek a nyelvbe:

Az uniók (union) lényege, hogy ugyanazt a memóriaterületet több változó közösen használja (persze nem egyidejűleg). Leggyakrabban gépfüggő adatkonverziók megvalósítására használják.

A bitmezők az egy bájtnál kisebb helyfoglalású változókat egyetlen bájton tárolják. Gyakorlati hasznuk főleg a hardverek vezérlésénél van.

Osztályok

A C++ az objektumorientált programozás megvalósításához egyrészt kibővíti a struktúrákat, másrészt bevezeti a class típust. Mindkettő alkalmas osztály definiálására. Egy osztály (class) adattagjának háromféle elérhetősége lehet:

  • public, nyilvános, mindenki számára elérhető
  • private, privát, csak az osztályon belülről, illetve barátosztályokból és -függvényekből lehet elérni
  • protected, védett, a származtatott osztályok számára közvetlen elérhetőséget biztosít. A private tagok a leszármazottakból csak az ősosztály tagfüggvényeiből (metódusok) elérhetőek.
//Egy egyszerű osztály
class SimpleBook {
 public:
  SimpleBook(std::string param_cim) {cim = param_cim;}
 private:
  std:string cim;
};

A programozó által definiált híján minden osztály és struktúra rendelkezik alapértelmezett konstruktorral (típusnév()), másolókonstruktorral (típusnév( típusnév&)) és értékadó operátorral (operator=( típusnév&)), ami egyszerű adatszerkezet esetén általában megfelelő és hatékony.

Függvények

A függvény (function) a program olyan névvel ellátott egysége, amely a program bármely pontjából hívható. Általában olyan utasítássorozatokat tartalmaz, amelyekre gyakran van szükség, de ismételt megírásuk fárasztó vagy helyigényes. A hagyományos C++-program sok kis méretű, könnyen karbantartható függvényből épül fel.

Eljárás, függvény

A Pascal nyelvben megkülönböztettünk függvényeket (function) és eljárásokat (procedure) aszerint, hogy volt-e visszatérési érték vagy sem. A C++-ban azonos módon definiálhatjuk a kettőt:

//Eljárás, nincs visszatérési érték
void func_1(){ std::cout << "Hello World!" << std:endl; }
//Függvény, string típusu visszatérési érték
std::string func_2(){ return "Hello World!"; }

A void a C++ általános típusa, a függvény neve elé írt típus a visszatérési értéket jelöli. A fenti példában tájékoztattuk a fordítót, hogy nem lesz visszatérési érték. A függvény neve után írt zárójelekben lehetnek a függvény paraméterei (ha vannak neki)

//Kiírja a megadott karaktersorozatot
void func(std::string msg){ std::cout << msg << std::endl; }

Definíció, deklaráció

A függvényhívás előtt meg kell adnunk a függvény deklarációját, ami jelzi a fordítónak, hogy egyáltalán létezik a függvény. A definíció nem feltétlenül esik egybe a deklarációval, a programon belül bárhol elhelyezkedhet. A deklarációban szerepelnie kell a visszatérési értéknek, a függvény nevének és paraméterlistájának. Ezeket együttesen (név és paraméterlista) aláírásnak (szignatúra) vagy a függvény prototípusának nevezzük.

#include <iostream>

//deklaráció
void func(std::string msg);

std::string func_2();

int main(int argc, char *argv) {
 //hívás
 func("Hello");
 std::cout << func_2();
 return 0;
}

//definíció
void func(std::string msg) {std::cout << msg << std::endl;}
std::string func_2() {return "Hello World!";}

A főprogram a második függvény által visszaadott karaktersort átadta a cout << operátorának, így a szöveg megjelenik a szabványos kimeneten.

Paraméterátadás

A C++-ban kétféle paraméterátadási mód van:

  • Érték szerinti, az átadott típusból másolat készül a memóriában, az eredeti értéket nem módosítja a függvény.
  • Cím szerinti, paraméterként az átadott típus referenciája szerepel, a függvény módosíthatja a paramétereket.
//a és b összegével tér vissza
int sum_1(int a, int b) {return a + b;}
//az eredményt c-ben tárolja
void sum_2(int a, int b, int &c) {c = a + b;}

A második változat harmadik paramétere nem c értéke, hanem a memóriában elfoglalt címe.

A C++-ban minden paraméterátadás érték szerint történik, de referenciák vagy mutatók átadásával azonos hatás érhető el, mint a referencia alapú nyelvekben.

Kis objektumok esetén (pl. egy int) az érték szerinti átadás általában hatékonyabb, de ha a függvénybeli módosításokat nem akarjuk elveszíteni, muszáj referenciaként vagy mutatóval átadni. Fordítva: nagy objektumok másolása általában költséges, így ha tehetjük, kerüljük, de ha nem adhatunk const referenciát valamiért, és nem akarjuk, hogy módosíthassa a függvény az objektumunkat, akkor kénytelenek vagyunk érték szerint átadni. Az ekkor (nagy objektumok másolásakor) fellépő költségek minimalizálásra több technika is született, ilyen például a Copy on Write (CoW, csak akkor másolunk ténylegesen, ha muszáj).

class A
{
 public:
  A(){...}
  A(const A & a){...}
  ~A(){...}
  ...
 private:
  std::string s, k;
};

A func_val(A a){ return a; }
A func_ref(A & a){ return a; }

A x;

func_val(x);
func_ref(x);

A példában az első függvényhíváskor meghívódik az A osztály másoló konstruktora, hogy inicializálja a-t x-szel, majd a függvény által visszaadott objektumot is inicializálja, végül meghívódik a destruktor a-ra és a visszaadott objektumra. Eközben az A osztály tagjainak is meghívódik a konstruktoruk és a destruktoruk.

A második függvényhívás az objektum címét adja át, így csak a visszaadott példányt kell inicializálni.

Alapértelmezett paraméterek

A függvénydefinícióban bizonyos paraméterekhez alapértelmezett értéket rendelhetünk. Ezt az értéket a program akkor használja, ha az adott argumentum nem szerepel a listában. Alapértelmezett értékkel rendelkező paraméter után csak ugyanilyen paraméterek szerepelhetnek a formális paraméterlistában.

void sayHello(std::string msg = "Hello"){ std::cout << msg << std::endl; }
//A függvény hívása
sayHello();
sayHello("Hello");
//A kettő ugyanazt jelenti

Inline függvények

Inline függvény esetén a függvényhívás helyére a függvény kódja helyettesítődik be fordítási időben. Ezáltal megspórolható a függvényhívás költsége, viszont növekszik a tárgykód. Általában kis méretű, nem bonyolult függvények esetén használható hatékonyan.

inline void sayHello() {std::cout << "Hello" << std::endl;}

Az inline definíció csak javaslat a fordítónak, amelyet nem muszáj figyelembe vennie. A legtöbb fordító a ciklust vagy rekurzív függvényhívást tartalmazó függvény esetén elutasítja az inline direktívát.

Lokális változók

A függvények belsejében (illetve a programban lévő blokkokon belül) deklarált változókat lokális változóknak nevezzük. Ez a gyakorlatban azt jelenti, hogy a láthatóságuk és élettartamuk a függvényen (blokkon) belülre korlátozódik. A lokális változók a függvényhívás végén automatikusan megsemmisülnek és kívülről nem hivatkozhatóak.

//Két változó értékének cseréje
void swap(int &a, int &b) {
 int tmp = a; //nem dinamikusan (statikusan) lefoglalt változó
 a = b;
 b = tmp;
}
tmp = 10; //Hiba, tmp nem hivatkozható a hatókörén (a függvény blokkján) kívül

A dinamikus objektumokra mutató pointerek szintén megsemmisülnek a hatókörükből kikerülve, de az objektum maga nem.

int * createArray(int n)
{
 int * v = new int ;
 return v; //A függvény egy n elemű tömbre mutató pointerrel tér vissza
}

int * t = createArray(10);
t = 12; //Működik, most t mutat a tömbre
v = 2; //Hiba, v már nem létezik

Ha nem gondoskodunk a blokkon belül létrehozott dinamikus objektum külső elérhetőségéről, az érvényes hivatkozás nélkül a memóriában marad, azaz memóriaszivárgás keletkezik.

void func()
{
 int * v = new int ;
}

v = 12;
/*Hiba, a tömbre mutató pointer már nem létezik,
 és más sem mutat rá -> memóriaszivárgás*/

Túlterhelés

A túlterhelés leveszi a programozó válláról a sokféle név megjegyzésének terhét, mivel azonos névvel létezhet több függvény is.

Akkor beszélünk túlterhelésről, ha azonos látókörben (scope) több azonos nevű függvény van deklarálva, különböző szignatúrával. Egyébként, ha egy külső látókörben van a másik név, elfedésről van szó.

Túlterhelt függvény hívásakor a fordító kiválasztja a látható függvények közül a legjobban illeszkedőt, szignatúra alapján. A feloldási szabályok meglehetősen bonyolultak, így óvatosan kell túlterhelt nevet bevezetni.

struct Point {
    Point(int X, int Y): x(X), y(Y) {}
    int x,y;
};

double VectorLength(int, int);
double VectorLength(Point);

int main()
{
    cout << VectorLength(3, 4) << endl;
    cout << VectorLength(Point(3, 4)) << endl;  //feltehetőleg mindkét függvény 5.0-t fog visszaadni
}

Kivételkezelés

Kivételnek (exception) azt a hibás állapotot vagy váratlan eseményt nevezzük, amely megszakítja a program rendes futását. A kivételkezelés lehetővé teszi, hogy a vezérlés ahhoz a programrészhez kerüljön, amely képes az adott kivétel kezelésére. A throw utasítás segítségével kivételt „dobhatunk”, amelyet a program „elkaphat” (catch). A C++ megszakításos modell alapján kezeli a kivételeket, azaz a kivételt kiváltó programrész futása megszakad. Három elem szükséges a kivételkezelés megvalósításához:

  • A kivételt kiváltható programrész kijelölése (try)
  • A kivétel dobása (throw)
  • A kivétel elkapása (catch)

A C++-ban kevés beépített kivétel található, de bármely típus dobható, így magunknak is definiálhatunk, ha szükséges. A kezelőig (a megfelelő típust elkapó catch a hívási sorban valahol feljebb) tartó minden automatikus változó megsemmisül destruktorhívással.

Ha kivételkezelés közben újabb kivétel keletkezik (például egy felszámolt változó destruktora dob – ez nagyon veszélyes) a futás eredménye definiálatlan, de általában katasztrofális. Érdekesség, hogy a C++ a statikus területen mindig fenntart akkora helyet, hogy a memóriafoglaló bad_alloc kivételét ki tudja váltani.

void f(){ throw 1; } //ez a függvény mindenképpen dob kivételt
try{
  f(); //kivétel dobódik
}catch(int){ //elkapjuk
  std::cerr << "Exception!\n";
}

Függvények esetében megadhatjuk, hogy milyen kivételeket továbbíthat:

void f() throw(); //nem dobhat kivételt
void g() throw(int); //int típusú kivételek
try{
  g();
}catch(int){
  //int típusú kivételek
}
...
  //más kivételek elkapása
...
}catch(...){ //minden kivétel elkapása, ez mindig az utolsó helyen áll

}

Dinamikus memóriakezelés

A dinamikus memóriahasználat alapgondolata, hogy adataink számára akkor foglalunk helyet, amikor szükség van rá, ha pedig feleslegessé válnak, azonnal felszabadítjuk a memóriaterületet. A C++-ban különösen fontos szerepe van, mivel nincs „szemétgyűjtő”, mint a Javában vagy C#-ban. A felszabadítatlan memória „memóriaszivárgáshoz” (memory leak) vezethet.

A new és delete operátorok

A C++-ban lehetőség van használni a C nyelv malloc (és egyéb változatai) és free utasítását, de ajánlott az újakat alkalmazni. A new operátor az operandusban megadott típusnak megfelelő méretű területet foglal a memóriában, és arra mutató pointert ad vissza. A delete felszabadítja a new által lefoglalt területet:

int *v = new int; //helyfoglalás egy int-nek
delete v; //felszabadítjuk

Nemcsak egy elemnyi terület elfoglalására van lehetőség, hanem több egymás után elhelyezkedő elem számára is foglalhatunk területet. Ezt az adatstruktúrát dinamikus tömbnek nevezzük:

int *v = new int ; //helyfoglalás 10 db int-nek
delete v; //felszabadítjuk

Többdimenziós tömböt is foglalhatunk dinamikusan, ekkor a tömb elemei egy-egy pointerre mutatnak:

int **v = new int * ; //10 db int-re mutató pointer
for(int i = 0;i < 10;++i) {
 v = new int ; // 10x10-es mátrix, v minden eleme 10 db int-re mutat
 for(int j = 0;j < 10;++j) {
  v = i + j;
 }
}

//felszabadítjuk a mátrixot, minden egyes mutatót külön
for(int i = 0;i < 10;++i){ delete v; }
delete v; //végül magát v -t is

Fontos, hogy minden new-hoz a megfelelő delete-et használjuk:

int *t = new int;
delete t; //definiálatlan, a legjobb esetben lefoglalva marad a memória
int *v = new int;
delete v; //szintén definiálatlan.

Az malloc-kal szemben a new nem csak területet foglal, de automatikusan meghívja a megfelelő konstruktorokat is.

Főleg nagy méretű adatok esetén előfordulhat, hogy nincs elég memória. Ekkor a C++ std::bad_alloc kivételt dob.

try{
 int *t = new int; //40000 byte memóriát foglalunk
}catch(std::bad_alloc){
 //Hibakezelés
}

Objektumorientált C++

Adattagok és tagfüggvények

Az adattagokat a változókhoz hasonlóan deklaráljuk, de az adattagok nem tartalmazhatnak inicializációs listát. Ha egy osztályon belül egy másik osztályt akarunk adattagként használni, akkor előzőleg szerepelnie kell a másik osztály teljes deklarációjának.

Az adattagokon műveleteket végző tagfüggvényeket az osztály törzsében deklaráljuk. Ezen a függvény szignatúráját, vagy a helyben kifejtett implicit inline definícióját értjük (vagyis a függvény prototípusa előtt nem szerepel az inline kulcsszó). Az inline módosító megadásával az osztálydefiníción kívül definiált függvényeket is inline-ná tehetjük.

class MyClass {
 public:
  MyClass(int x) { value = x; }
  void printValue() { std::cout << value << std::endl; } //inline definíció
 private:
  int value;
};

Amennyiben csak a deklarációt tartalmazza az osztály, a függvény törzsét azon kívül kell definiálni. A definíció sorrendben a visszatérési értékből, az osztály nevéből, a hatókör operátorból, a függvény szignatúrájából és törzséből áll:

class MyClass {
 public:
  MyClass(int x) { value = x; }
  void doSomething();
 private:
  int value;
};

void MyClass::doSomething(){ /*do something*/ } //külső definíció

Egy osztály bármely tagfüggvénye hozzáfér az adattagokhoz, függetlenül annak elérésétől.

Statikus tagok

A static kulcsszóval bevezetett adattagokból és tagfüggvényekből osztályszinten egy darab létezik.

Konstruktorok

Az objektumok kezdeti értékadásaiért (inicializálás) speciális tagfüggvények, a konstruktorok felelnek. A konstruktor olyan tagfüggvény, amelynek neve megegyezik az osztályéval, és nem rendelkezik visszatérési típussal.

class MyClass {
 public:
  MyClass(const int & data) { x = data; } //Konstruktor
 private:
  int x;
};

MyClass* mc = new MyClass(10); //mc->x egyenlő 10-zel

A fordító minden olyan esetben, mikor egy objektum létrejön, meghívja a konstruktorát. Egy osztálynak bármennyi konstruktora lehet a szignatúrától függően. Alapértelmezés szerint minden osztály két konstruktorral rendelkezik, a paraméter nélküli (default) és a másoló (copy) konstruktorral. Ha saját konstruktort készítünk, attól fogva az alapértelmezett nem lesz elérhető. A konstruktorok egyaránt lehetnek public, private vagy protected elérésűek. A csak private konstruktorokat tartalmazó osztályt rejtett osztálynak nevezzük.

Ha az egyparaméteres konstruktorokkal rendelkező osztályok példányainak nem teljesen illeszkedő típust adunk kezdőértékül, akkor a fordító implicit típuskonverziót hajt végre. Ezt megtilthatjuk az explicit kulcsszó használatával:

class MyClass {
 public:
  explicit MyClass(const int & data){ x = data; } //csak int -et fogad el
 private:
  int x;
};

void f() {
  MyClass x;
  x = 3;          //hiba! explicit kulcsszó miatt nincs konverzió
  x = MyClass(3); // jó
}

Ha az objektum egy másik osztály példányát is tartalmazza, akkor a belső osztály konstruktorát a külső osztály konstruktorában hívjuk. Ezt a taginicializációs lista használatával oldhatjuk meg, amelyet a konstruktor szignatúrája után kettősponttal elválasztva adhatunk meg:

class MyClass {
 public:
  MyClass(const int & data) : x(data) { };
 private:
  int x;
};

Taginicializációs listát csak a konstruktorban adhatunk meg. Használata kötelező, ha az osztály referencia típusú vagy paraméterezett típussal rendelkező adattagot tartalmaz. A konstans tagok beállítására is ezt használjuk:

class MyClass {
 public:
  MyClass(const double & d_data, const int & i_data) : y(d_data), x(i_data) {};
 private:
  const double y;
  int x;
};

Destruktorok

Az objektumok által felhasznált memória mindaddig lefoglalt marad, míg fel nem szabadítjuk. Erre a célra a C++ biztosít számunkra egy speciális tagfüggvényt, a destruktort. Hasonlóan a konstruktorhoz, a destruktornak sincs visszatérési értéke. A destruktornak nem lehetnek paraméterei. A destruktor nevét az osztály nevéből és a hullám karakterből (tilde: ~) képezzük:

class MyClass {
 public:
  MyClass(int val){ t = new int(val); } //konstruktor
  ~MyClass(){ delete t; } //destruktor
 private:
  int * t;
};

Ha nem definiálunk destruktort az osztályunkban, a fordító automatikusan létrehozza. A destruktor minden olyan esetben meghívódik, amikor az objektum érvényessége megszűnik. Kivételt képeznek a dinamikusan (a new operátorral) létrehozott példányok, amelyek destruktorát csak a megfelelő delete operátor hívhatja meg. A destruktor közvetlenül is hívható.

Dinamikus tömbök esetén a konstruktorok az indexek növekvő sorrendjében hívódnak meg, a destruktorok éppen fordítva, de csak a delete operátor alkalmazásával. A statikus tömbök ugyanígy törlődnek, de automatikusan (tehát nem kell delete), amint kikerülnek a hatókörükből. A nem megfelelő delete használatával a legjobb esetben is csak a tömb első eleme semmisül meg.

Példányosítás

Egy osztály egy memóriában létrehozott példányát objektumnak nevezzük. Minden objektum rendelkezik a neki megfelelő osztály minden egyes adattagjával (természetesen az egyes példányok külön másolatokat birtokolnak, kivéve a statikus tagokat) és tagfüggvényével. Egy objektumot létrehozhatunk dinamikusan és statikusan is.

class MyClass{ ... };

MyClass my_static_object; //statikus definíció
MyClass * my_dynamic_object = new MyClass(); //dinamikus definíció

A két esetben a tagok elérése különbözik. Statikus definíció esetén a pont (.) operátort, míg dinamikus esetben a nyíl (->) operátort használjuk.

my_static_object.member_func();
my_dynamic_object->member_func();

Öröklődés

Az öröklődés az objektumorientált stílus meghatározó eleme, amely a kód-újrafelhasználást és a dinamikus típuskezelést teszi támogatottá. C++-ban az eszköztára az absztrakt metódusok, a (többszörös) öröklődés és a virtual kulcsszó.

Absztraktnak nevezünk egy osztályt, ha van legalább egy absztrakt tagfüggvénye (ezt a függvénydeklaráció után tett = 0-val jelezzük), ekkor az osztály nem példányosítható.

Az öröklés úgy hoz létre új objektumot, hogy az megtartja az őse minden adattagját és (az ősosztályban nem private) tagfüggvényét.

Segítségével nem kell ismernünk egy objektum pontos típusát, mutatókon és referenciákon keresztül dinamikus kötésű műveleteket hívhatunk meg (polimorfizmus – többalakúság).

Hatékonysági okok miatt C++-ban minden függvény statikus, ha nincs explicit megmondva a virtual kulcsszóval ott vagy a bázisosztályban.

Öröklés láthatósága

Az öröklődés lehet public, ez a bázisosztály osztály specializációja, protected illetve private, ekkor a származás nem lesz kívülről látható, azaz le kell mondanunk a dinamikus típusazonosításról, de egyébiránt teljes értékű öröklődés.

Névütközések

Ha az ős és a származtatott osztályban szerepel ugyanolyan néven függvény, akkor nincs túlterhelés, a származtatott elfedi azokat, a névtér-szabályok miatt. A nem virtuális függvények ellenben fordítási időben kötődnek, így ősosztályra mutató pointeren keresztül minden nem virtuális függvényhívás az ősosztálybelit fogja végrehajtani, függetlenül a mutatott objektum dinamikus típusától.

struct A {
  virtual void print1() {
    cout << "A";
  }

  void print2() {
    cout << "A";
  }

  virtual ~A() {}     //ökölszabály: bázisosztályban _mindig_ legyen virtuális a destruktor, a delete operátor így tudja megfelelően megsemmisíteni az objektumot
};

struct B: public A {  //public alapértelmezett: nem kötelező
  virtual void print1() { //virtual-t származtatottban nem kötelező kitenni
    cout << "B";
  }

  void print2() {
    cout << "B";
  }
};

int main() {
  A* p = new B;
  p->print1();     //"B"
  p->print2();     //"A"
  delete p;
}

Generikus C++

A generikus programozásról

A generikus programozás az alapvetően típusfüggetlen algoritmusok (pl. rendezések) és általános célú tároló szerkezetek (pl. listák) létrehozásán alapuló programozási stílus, a generikus programozási nyelv pedig az, ami ezt nyelvi eszközökkel támogatja.

A template kulcsszó

A C++ közvetlenül támogatja ezt a programozási stílust, a template kulcsszóval, mely osztályok és függvények elé egyaránt beszúrható. A formális paraméterek a template után <>-ben sorolandók fel, típusuk lehet konkrét típus (pl. int) vagy típusparaméter, ezt a typename kulcsszóval jelöljük.

//sablon-osztály
template <typename T, int size>
class MyBuff {
public:
  MyBuff() {}
  T GetItem(int);
private:
  T buf;
};

template<typename T, int size>
T MyBuff<T, size>::GetItem(int num) {
  return buf;
}

A tagfüggvények és az osztályon belül deklarált osztályok maguk is sablonok, kifejthetők az osztályon kívül is, ekkor jelezni kell a teljes sablonszignatúrát (pl. GetItem). Az osztálysablon nem implicit inline tagfüggvényeit minden olyan forrásállományba be kell építenünk, amelyből azokat hívjuk (bevett szokás az osztályt és tagfüggvényeit egyetlen fejállományban elhelyezni).

A sablondeklarációban typename helyett írható class is, a kettő között nincs különbség. Amikor a fordító számára nem egyértelmű, hogy típussal van dolga, akkor a typename/struct/class szóval jelezhetjük ezt:

template <class T>
void func() {
 typename T::iterator ti;
}

A fenti példában a T típus még nem jött létre, ezért tudatnunk kell a fordítóval a létezését.

Az osztály példányosításakor ki kell írni a paramétereket az osztály neve után.

void f() {
//...
  MyBuff<int, 10> IntBuff;
//...
}

Sablonfüggvény hívásakor a fordító a paraméterek típusából megállapítja T aktuális értékét, nem kell explicit kiírni.

//sablon-függvény
template<typename T>
void sort(vector<T>& v) {
//valamely rendező algoritmus
}
//...
void func(vector<int>& vekt) {
  sort(vekt);
}

Példányosulás

A sablonok fordítási időben példányosulnak, így a fordítónak ismernie kell a típusparaméterek típusát, a konkrét típusú paramétereknek konstansnak kell lenniük. A hibát legkésőbb szerkesztéskor jeleznie kell a fordítónak. Mivel csak a használt sablonok példányosulnak, ezért kódtakarékos megoldás lehet a generikus programozás, de akár összetettebb megoldások is elképzelhetők (jellemző példa a < operátor, melyet nem lehet minden típushoz értelmesen biztosítani, ezért a list::sort fordítási hibát okoz ilyenekből épített listára, de listát magát létre lehet hozni). Rossz tervezés esetén azonban nagyon elszaporodhatnak az egymástól alig különböző függvények kódjai. A fordító felismeri a MyBuff<int, 10> és MyBuff<int, 10+2-2> közötti azonosságot.

Típusazonosságok

Két sablon pontosan akkor azonos típusú, ha a sablon-paramétereik azonosak, egyéb esetekben teljesen különálló típusok. Ez magával vonja, hogy a sablonok teljesen függetlenek az osztályhierarchiától. A sablon nem terhelhető túl a paramétereire, de specializációt lehet adni, konkrét típusokra/értékekre.

Template metaprogramok

A sablonokkal fordítási idejű programokat lehet írni, és ez a nyelv Turing-teljes, azaz minden számítógéppel megoldható problémára alkalmazható.[4] Példaként tekintsük a faktoriális számítást!

template <int N>
struct Faktor {
    enum {value = N * Faktor<N - 1>::value};
};

template <>
struct Faktor<0> {
    enum {value = 1};
};

// Faktor<4>::value == 24
// Faktor<0>::value == 1
int main() {
    int x = Faktor<4>::value; // == 24
    int y = Faktor<0>::value; // == 1
}

Jegyzetek

  1. C++ története. . (Hozzáférés: 2009. május 19.)
  2. C++ szabványos fejállományok elnevezése. . (Hozzáférés: 2009. május 31.)
  3. C++ név eredete. . (Hozzáférés: 2009. május 19.)
  4. Template metaprogramozás Turing-teljes. . (Hozzáférés: 2010. január 22.)

Források

  • Bjarne Stroustrup: A C++ programozási nyelv
  • Scott Meyers: Hatékony C++
  • Stephen C. Dewhurst: C++ hibaelhárító
  • Tóth Bertalan, Lapteva Natalia: Programozzunk C++ nyelven

További információk

Magyarul

Angolul