C++

A Wikipédiából, a szabad enciklopédiából
C++
Paradigma többelvű: generikus, objektumorientált, imperatív
Jellemző kiterjesztés .h, .hh, .hpp, .hxx, .h++, .cc, .cpp, .cxx, .c++
Tervező Bjarne Stroustrup
Utolsó kiadás 2011, ISO/IEC 14882:2011
Típusosság statikus típusosság, erősen típusos, nem típus biztos, nominatív
Fordítóprogram GNU Compiler Collection, Microsoft Visual C++, Borland C++ Builder
Dialektusok ISO/IEC C++ 1998, ISO/IEC C++ 2003, ISO/IEC C++ 2011
Megvalósítások C++ 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 nyelvek C#, Java, PHP, Python

A C++ 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 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[szerkesztés | forrásszöveg szerkesztése]

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, 2003-as változat kódjelzése ISO/IEC 14882:2003.[1]

Érdekesség[szerkesztés | forrásszöveg szerkesztése]

Mire a nyelvet szabványosították, már rengeteg C++ nyelvű kód készült, került használatra. 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[szerkesztés | forrásszöveg szerkesztése]

Windows operációs rendszeren tanuláshoz megfelelő – és ingyenes – eszköz a Dev - C++, illetve 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 Windowszal is megvan). Erre az említett rendszerekben általában a GNU g++ programot 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[szerkesztés | forrásszöveg szerkesztése]

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[szerkesztés | forrásszöveg szerkesztése]

Jelkészlet[szerkesztés | forrásszöveg szerkesztése]

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

Azonosítók[szerkesztés | forrásszöveg szerkesztése]

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[szerkesztés | forrásszöveg szerkesztése]

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

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

Megjegyzések[szerkesztés | forrásszöveg szerkesztése]

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[szerkesztés | forrásszöveg szerkesztése]

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[szerkesztés | forrásszöveg szerkesztése]

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[szerkesztés | forrásszöveg szerkesztése]

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
Előjel -
Előjel +
Dereferálás
Objektum címe
Konverzió típusra
Méret
Jobbról balra
4 ->*
.*
Tagkiválasztás mutatón
Tagkiválasztás objektumon
Balról jobbra
5 *
/
 %
Szorzás
Osztás
Maradékszámítás
Balról jobbra
6 +
-
Összeadás
Kivonás
Balról jobbra
7 <<
>>
Bitenkénti eltolás balra
Bitenkénti eltolás jobbra
Balról jobbra
8 <
<=
>
>=
Kisebb
Kisebb-egyenlő
Nagyobb
Nagyobb-egyenlő
Balról jobbra
9 ==
 !=
Egyenlő
Nem 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 , Szekvencia operátor Balról jobbra

Operátorok túlterhelése[szerkesztés | forrásszöveg szerkeszté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[szerkesztés | forrásszöveg szerkesztése]

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 10 19

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)[szerkesztés | forrásszöveg szerkesztése]

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[szerkesztés | forrásszöveg szerkesztése]

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)

Temporális é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ípus"-nak megfelelően). Ha a konverzió nem elvégezhető, 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ípus" é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" azonos típusúak kell legyenek, kivéve a fent említett típusmódosítókat.

Pointerek[szerkesztés | forrásszöveg szerkesztése]

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 az "address of" 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 típus felhasználásával már létező változókra is hivatkozhatunk.

int x = 10;
int &r = x;

Ebben az esetben nem új változó jön létre, hanem a fordító egy második nevet ad az "x" változónak.

Névterek[szerkesztés | forrásszöveg szerkesztése]

A fordító a programban használt neveket különböző névtereken (namespace) tárolja. Egy névtérben lévő neveknek egyedieknek kell lenniük, azonban a különböző névtereken 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[szerkesztés | forrásszöveg szerkesztése]

Hello World![szerkesztés | forrásszöveg szerkesztése]

Az úgynevezett "Hello World" 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!\n";
    return 0;
}

A kettős kereszttel (#) 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ő IO utasításokat a kiiratá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[0] == "program"
argv[1] == "elso"
argv[2] == "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[szerkesztés | forrásszöveg szerkesztése]

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) ahonnan a bejövő adatokat fogadja, alapesetben a billentyűzet. 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ó[szerkesztés | forrásszöveg szerkesztése]

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[szerkesztés | forrásszöveg szerkesztése]

Szekvencia[szerkesztés | forrásszöveg szerkesztése]

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

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

Elágazás (szelekció)[szerkesztés | forrásszöveg szerkesztése]

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;  //ez 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ó)[szerkesztés | forrásszöveg szerkesztése]

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 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;
}

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
{
 
} 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
        inkrementál; // tartalmaz, akkor 'while' esetén az 'inkrementál' nem fut le.
    }
 
    // Ekvivalens ezzel is
    inicializál;
    if(tesztel)
    do
    {
         programrész;
    } while(inkrementál, tesztel);  // Ha a tesztel-nek nincs mellékhatása
Break, continue[szerkesztés | forrásszöveg szerkesztése]

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[szerkesztés | forrásszöveg szerkesztése]

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 alkalmazzuk, például ha többszörösen beágyazott ciklusból kell kilépni.

Adatszerkezetek[szerkesztés | forrásszöveg szerkesztése]

Tömb[szerkesztés | forrásszöveg szerkesztése]

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]; //10 elemű statikus tömb, más néven vektor
for(int i = 0;i < 10;++i)
{
 t[i] = 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]; // 10 elemű tömb létrehozása
// tömb feltöltése
std::cout << t[10];
//lefordul, de hibás, mivel nulla az első elem indexe, t[9] a legmagasabb hivatkozható elem

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

int t[méret1][meret2]...[meretN];

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

int sizeN, sizeM;
sizeN = sizeM = 5;
int t[sizeN][sizeM]; //5*5 mátrix
 
for(int i = 0;i < sizeN;++i)
{
 for(int j = 0;j < sizeM;++j)
 {
  t[i][j] = 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[meret]; // Foglalás
for(int i=0; i<meret; ++i)
{
  std::cin >> t[i]; // 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[3] = 9;
* (t+3) = 9; // +3: 3 elemnyivel arrébb; *: mutató feloldása, hogy megkapjuk a "rekeszt"
* (&(t[2]) + 1) = 9; // &: A második "rekesz" címe; +1: 1 elemnyivel arrébb; *: mint előbb

Illetve

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

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

Struktúrák[szerkesztés | forrásszöveg szerkesztése]

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) több 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[szerkesztés | forrásszöveg szerkesztése]
struct Minta { int x; };
struct A { int x; int t[10]; };
struct B { int x; int *t; };
// ...
A a;
B b;
// ...
a.t[3] = 9;
b.t[3] = 9;

Az "A" struktúra "fizikailag" tartalmazza a 10 darab int típusú értéket, a sizeof(A) ennek megfelelően 10 int-nyivel 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[szerkesztés | forrásszöveg szerkesztése]

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[szerkesztés | forrásszöveg szerkesztése]

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 lehet elérni, illetve barát osztályok és függvények
  • 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([const] típusnév&)) és értékadó operátorral (operator=([const] típusnév&)), ami egyszerű adatszerkezet esetén általában megfelelő és hatékony.

Függvények[szerkesztés | forrásszöveg szerkesztése]

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 kisméretű, könnyen karbantartható függvényből épül fel.

Eljárás, függvény[szerkesztés | forrásszöveg szerkesztése]

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ó[szerkesztés | forrásszöveg szerkesztése]

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 második függvény a karaktersort, amivel visszatért, átadta a cout << operátorának, így a szöveg megjelenik a szabványos kimeneten.

Paraméterátadás[szerkesztés | forrásszöveg szerkesztése]

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[szerkesztés | forrásszöveg szerkesztése]

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 listá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[szerkesztés | forrásszöveg szerkesztése]

Inline függvény esetén a függvényhívás helyére a függvény kódja helyettesítődik be, még 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[szerkesztés | forrásszöveg szerkesztése]

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 [n];
 return v; //A függvény egy n elemű tömbre mutató pointerrel tér vissza
}
 
int * t = createArray(10);
t[0] = 12; //Működik, most t mutat a tömbre
v[1] = 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 [10];
}
 
v[0] = 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[szerkesztés | forrásszöveg szerkesztése]

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.

//példakód:
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[szerkesztés | forrásszöveg szerkesztése]

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[szerkesztés | forrásszöveg szerkesztése]

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 JAVA-ban vagy C#-ban. A felszabadítatlan memória "memóriaszivárgáshoz" (memory leak) vezethet.

A new és delete operátorok[szerkesztés | forrásszöveg szerkesztése]

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 [10]; //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]; //10 db int-re mutató pointer
for(int i = 0;i < 10;++i)
{
 v[i] = new int [10]; // 10x10-es mátrix, v minden eleme 10 db int-re mutat
 for(int j = 0;j < 10;++j)
 {
  v[i][j] = i + j;
 }
}
 
//felszabadítjuk a mátrixot, minden egyes mutatót külön
for(int i = 0;i < 10;++i){ delete[] v[i]; }
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[10];
delete v; //szintén definiálatlan.

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

Főleg nagymé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[10000]; //40000 byte memóriát foglalunk
}catch(std::bad_alloc){
 //Hibakezelés
}

Objektumorientált C++[szerkesztés | forrásszöveg szerkesztése]

Adattagok és tagfüggvények[szerkesztés | forrásszöveg szerkesztése]

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[szerkesztés | forrásszöveg szerkesztése]

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

Konstruktorok[szerkesztés | forrásszöveg szerkesztése]

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 típussal.

class MyClass
{
 public:
  MyClass(const int & data) { x = data; } //Konstruktor
 private:
  int x;
};
 
MyClass* mc = new MyClass(10); //mc.x egyenlő 10 -el

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[szerkesztés | forrásszöveg szerkesztése]

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[szerkesztés | forrásszöveg szerkesztése]

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 (->) operátort használjuk.

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

Öröklődés[szerkesztés | forrásszöveg szerkesztése]

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, hogy virtual ott vagy a bázisosztályban.

Öröklés láthatósága[szerkesztés | forrásszöveg szerkesztése]

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[szerkesztés | forrásszöveg szerkesztése]

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++[szerkesztés | forrásszöveg szerkesztése]

A generikus programozásról[szerkesztés | forrásszöveg szerkesztése]

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ó[szerkesztés | forrásszöveg szerkesztése]

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[size];
};
 
template<typename T, int size>
T MyBuff<T, size>::GetItem(int num)
{
  return buf[num];
}

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 template szignatú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;
//...
}

Ekkor 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[szerkesztés | forrásszöveg szerkesztése]

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[szerkesztés | forrásszöveg szerkesztése]

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[szerkesztés | forrásszöveg szerkesztése]

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
}

Irodalom[szerkesztés | forrásszöveg szerkesztése]

  • 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

Külső hivatkozások[szerkesztés | forrásszöveg szerkesztése]

Magyarul[szerkesztés | forrásszöveg szerkesztése]

Angolul[szerkesztés | forrásszöveg szerkesztése]

Források[szerkesztés | forrásszöveg szerkesztése]