Klase

Što je klasa?

  • Svaka klasa u C++-u predstavlja novi tip podatka u programu.
  • Instancu klase nazivamo objekt. Program se konstruira kao niz interakcija među objektima.
  • Na razini sintakse klasa je proširenje strukture iz jezika C. U osnovi klasa je struktura koja pored varijabli članica može sadržavati i funkcije članice.
  • Klasama implementiramo korisničke tipove podataka koji modeliraju objekte iz aplikacijske domene.

Klase: Prvi primjer

Klasa koja modelira točku u trodimenzionalnom prostoru (datoteka zaglavlja point3D.h).

// Datoteka point3D.h
#ifndef  POINT3D_H_IS_INCLUDED
#define  POINT3D_H_IS_INCLUDED

class Point3D
{
    public:
        Point3D(double x=0.0, double y=0.0, double z=0.0);
        double get(int i) const { return data[i]; }
        void   set(int i, doubl val) { data[i] = val; }
        void   print();
    private:
        double data[3];
};
#endif

Datoteke zaglavlja uključujemo u osigurače protiv višestrukog uključivnja:

#ifndef  POINT3D_H_IS_INCLUDED
#define  POINT3D_H_IS_INCLUDED
// sav kod dolazi između #define i #endif
#endif

Implementacija funkcija članica

Definicija klase se stavlja u datoteku zaglavlja koja se uključuje u svaku izvornu datoteku koja klasu koristi. Neke su funkcije članice klase implementirane u samoj klasi, dok su druge samo deklarirane. Njih definiramo u zasebnoj izvornoj datoteci:

// Datoteka point3D.cc
#include <iostream>
#include "point3D.h"

// Konstruktor klase Point3D
Point3D::Point3D(double x, double y, double z)
{
    data[0]=x;
    data[1]=y;
    data[2]=z;
}

// Metoda print klase Point3D
void Point3D::print()
{
    std::cout << "("<<data[0]<<","<<data[1]<<","<<data[2]<<")";
}

Objekti (instance klasa)

#include "point3D.h"
#include <iostream>

int main()
{
    Point3D A;                     // Točka s koordinatama (0,0,0)
    Point3D B(1.0,1.0), C(1,0,1);  // B=(1,1,0) i C=(1,0,1)

    std::cout << "A= "; A.print(); std::cout << std::endl;
    std::cout << "B= "; B.print(); std::cout << std::endl;
    std::cout << "C= "; C.print(); std::cout << std::endl;

    return EXIT_SUCCESS;
}

Point3D funkcionira kao novi tip u programu. Varijable A B i C su tipa Point3D i na njima možemo pozivati javne (public) funkcije iz klase Point3D pomoću operatora točka (.), pomoću kojeg dohvaćamo i varijable članice (sintaksa preuzeta od strukture).

Za varijable A B i C kažemo da su instance klase Point3D i one predstavljaju objekte tipa Point3D.

Zadatak 1. Dodajte klasi Point3D funkciju članicu distance koja računa udaljenost točke do ishodišta. Korigirajte funkcije set i get tako da provjeravaju da li je indeks unutar dozvoljenih granica, ali samo ako je program kompiliran u debug modu.

Zadatak 2. - Napišite klasu Point2D koja reprezentira točku u ravnini na isti način kao što Point3D reprezentira točku u prostoru. (Kasnije ćemo vidjeti kako izbjeći multipliciranje istog koda.) - Napišite funkciju koja uzima tri argumenta: broj točaka n, radijus kružnice R i bazno ime datoteke ime, te generira n jednoliko raspoređenih točaka na kružnici radijusa R koje zatim upisuje u datoteku s imenom ime i ekstenzijom pts. - Napišite drugu funkciju koja čita točke iz datoteke i nalazi radijus kružnice na kojoj se točke nalaze. Pri tome funkcija ima predefiniranu toleranciju s kojom određuje jesu li sve točke na jednoj kružnici. Obratite pažnju na preciznost zapisivanja u datoteku. Funkcija vraća nađeni radijus ili -1 ako točke nisu na kružnici. Pročitane točke se spremaju u vektor točaka koji je argument funkcije.

Za spremanje niza točaka koristiti std::vector<Point2D>. Ispravnost čitanja i pisanja testirati ispisivanjem točaka na ekran.

Osnovni elementi klase

Klasa sadrži četiri vrste članova:

Funkcije članice se dijele na 4 vrste:

Tipovi unutar klase

Pomoću using (ili typedef) klase često definiraju svoja lokalna imena za tipove. Na primjer:

// Datoteka point3D.h
#ifndef  POINT3D_H_IS_INCLUDED
#define  POINT3D_H_IS_INCLUDED

class Point3D
{
    public:
        using Real = double; // ILI typedef double Real;
        Point3D(Real x=0.0, Real y=0.0, Real z=0.0);
        Real  get(int i) const { return data[i]; }
        void  set(int i, Real val) { data[i] = val; }
        void  print();
    private:
        Real data[3];
};
#endif

Tu je uvedeno novo ime Real za tip double. U aplikacijskom kodu bismo pisali:

    Point3D B(1.0,1.0), C(1,0,1);  // B=(1,1,0) i C=(1,0,1)

    Point3D::Real sum=0.0;
    for(int i=0; i < 3; ++i) sum += C.get(i);
    std::cout << "C: sum = " << sum << std::endl;

Public, private, protected

Labele pristupa, public, private i protected definiraju prava pristupa unutar klase i nameću enkapsulaciju. Klasa može imati jednu ili više tih labela, a značenje im je sljedeće:

U primjeru klase Point3D podaci su smješteni u privatnu sekciju klase. To je redovito dobra odluka. Izbor tipova podataka koje koristimo stvar je implementacije. Korisniku nudimo samo sučelje za pristup podacima koje ne otkriva kako su podaci implementirani.

Preopterećenje (overloading) funkcija članica

Funkcije članice mogu biti preopterećene (više funkcija istog imena ali različitih parametara). Članica klase može preopteretiti samo drugu članicu. Primjer:

// Datoteka point3D.h
#ifndef  POINT3D_H_IS_INCLUDED
#define  POINT3D_H_IS_INCLUDED

class Point3D
{
    public:
        typedef double Real;
        Point3D(Real x=0.0, Real y=0.0, Real z=0.0);
        Real  get(int i) const { return data[i]; }
        void  set(int i,  Real val) { data[i] = val; }
        void  print();
        void translate(const Point3D& dir);
        // Rotacija oko ishodišta za Eulerove kuteve phi psi i theta
        void rotate(Real phi, Real psi, Real theta);
        // Rotacija oko točke centar za Eulerove kuteve phi, psi i theta
        void rotate(const Point3D& centar, Real phi, Real psi, Real theta);
    private:
        Real data[3];
};
#endif

Umetnute (inline) funkcije

Labela inline koja se stavlja na početak deklaracije funkcije predstavlja zahtjev prevodiocu da ne generira funkcijski poziv kad naiđe na funkciju, već da njen kod ekspandira na tom mjestu, slično kao što to radi preprocesor s funkcijskim makroima. Na primjer,

 inline int Max(int a, int b) { return (a<b) ? b : a;}

Prevodilac će u ovom primjeru na mjestu gdje se pojavljuje poziv funkcije Max ubaciti kod

(a<b) ? b : a;

Na mjestu poziva inline funkcije prevodiocu mora biti vidljiva definicija funkcije, a ne samo njena deklaracija (prototip).

Primjer:

// Datoteka point3D.h
#ifndef  POINT3D_H_IS_INCLUDED
#define  POINT3D_H_IS_INCLUDED

class Point3D
{
    public:
        typedef double Real;
        Point3D(Real x=0.0, Real y=0.0, Real z=0.0);
        Real  get(int i) const { return data[i]; }
        void  set(int i,  Real val) { data[i] = val; }
        inline void  print();
        void translate(const Point3D& dir);
        // Rotacija oko ishodišta za Eulerove kuteve phi psi i theta
        void rotate(Real phi, Real psi, Real theta);
        // Rotacija oko točke centar za Eulerove kuteve phi, psi i theta
        void rotate(const Point3D& centar, Real phi, Real psi, Real theta);
    private:
        Real data[3];
};
#endif

get, set i print su deklarirane inline.

Operatori

C++ nam dozvoljava propterećenje operatora za korisničke tipove. U C++-u operator je funkcija sa specijalnom sintaksom.

U klasi Point3D prirodno je definirati operator dohvata [] koji će nam omogućiti sljedeću sintaksu:

Point3D a;
a[1] = 1.2;

Signatura operatora je sljedeća:

povratna_vrijednost operator simbol_operatora ( argumenti );

U klasi Point3D definiramo konstantnu verziju operatora dohvata koja vraća kopiju koordinate i nekonstantnu verziju koja vraća referencu na koordinatu.

// Datoteka point3D.h
#ifndef  POINT3D_H_IS_INCLUDED
#define  POINT3D_H_IS_INCLUDED

class Point3D
{
    public:
        using Real = double;
        Point3D(Real x=0.0, Real y=0.0, Real z=0.0);
        Real  operator[](int i) const{ return data[i]; }
        Real& operator[](int i)      { return data[i]; }
        void  print();
    private:
        Real data[3];
};
#endif

Operator ispisa

Za klasu Point3D definiramo operator ispisa << na izlazni stream. Taj operator ne može biti članica klase već se definira kao globalna funkcija:

std::ostream & operator<<(std::ostream & out, Point3D const & p){
  out << "["<< p[0] <<"," << p[1] << "," << p[2] << "]";
  return out;
}

Kako se streamovi ne mogu kopirati uzimamo i vraćamo referencu na ostream.

Napomena
Kod operatora koji uzimaju dva argumenta prvi argument je lijevi operand, a drugi je desni operand. Ako je operator član klase onda je lijevi operand objekt na kome se operator poziva.

Definicija klase bi izledala ovako:

// Datoteka point3D.h
#ifndef  POINT3D_H_IS_INCLUDED
#define  POINT3D_H_IS_INCLUDED

class Point3D
{
    public:
        using Real = double;
        Point3D(Real x=0.0, Real y=0.0, Real z=0.0);
        Real  operator[](int i) const{ return data[i]; }
        Real& operator[](int i)      { return data[i]; }
    private:
        Real data[3];
};

std::ostream & operator<<(std::ostream & out, Point3D const & p){
  out << "["<< p[0] <<"," << p[1] << "," << p[2] << "]";
  return out;
}
#endif

Sada možemo pisati:

Point3D a;
a[1] = 2.0;
std::cout << a << std::endl;  // ispisuje [0,2,0]

Predložak klase (class template)

Klasu kao i funkciju možemo parametrizirati nekim tipom ili cjelobrojnom konstantom. Osnovna razlika u odnosu na predložak funkcije je taj što se kod predloška klase parametri predloška moraju eksplicitno zadati.

Kao i predložak funkcije, predložak klase stavlja se u datoteku zaglavlja zajedno sa definicijama svojih metoda članica.

// Datoteka point3D.h
#ifndef  POINT3D_H_IS_INCLUDED
#define  POINT3D_H_IS_INCLUDED

template <typename T>
class Point3D
{
    public:
        Point3D(T x=0.0, T y=0.0, T z=0.0);
        T  operator[](int i) const{ return data[i]; }
        T& operator[](int i){ return data[i]; }
    private:
        T data[3];
};

template <typename T>
Point3D<T>::Point3D(T x, T y, T z){
  data[0] = x;
  data[1] = y;
  data[2] = z;
}

template <typename T>
std::ostream & operator<<(std::ostream & out, Point3D<T> const & p){
  out << "["<< p[0] <<"," << p[1] << "," << p[2] << "]";
  return out;
}
#endif

U glavnom programu bismo imali:

Point3D<float> a;
a[1] = 2.0;
std::cout << a << std::endl;  // ispisuje [0,2,0]

Definicija i deklaracija

Klasa je definirana nakon zatvaranja vitičaste zagrade. Tada je poznata dimenzija objekta tipa klase u memoriji. U svakoj izvornoj datoteci klasa treba biti definirana samo jednom i definicije u različitim datotekama moraju biti identične. To postižemo stavljanjem definicije klase u datoteku zaglavlja te korištenjem osigurača protiv višestrukog uključivanja.

Deklaracija bez definicije (forward declaration):

class Matrica;

Matrica ovdje predstavlja nepotpun tip i može se koristiti tek za deklaraciju pokazivača i referenci tipa Matrica.

U definiciji klasa je deklarirana nakon što se njezino ime pojavilo iza ključne riječi class, tako da ga možemo koristiti unutar klase za definiciju pokazivača i referenci:

class ListElm {
      double data;
      ListElm *next;
      ListElm *prev;
};

Klasa i struktura

struct u C++-u uvodi deklaraciju klase isto kao i ključna riječ class s razlikom u dodijeljenim (defaultnim) pravima pristupa.

Ako je klasa deklarirana pomoću ključne riječi class, onda su sve varijable čija prava pristupa nisu dana eksplicitno privatne. Na primjer:

class A{
    int x;             // privatna varijabla
    double f(int x);   // privatna metoda
public:
    int y;
};

Ako klasu definiramo pomoću ključne riječi struct onda su sve varijable i metode bez eksplicitnog prava pristupa javne:

struct B{
    int x;             // javna varijabla
    double f(int x);   // javna metoda
public:
    int y;
};

Grafičko prikazivanje klasa — UML

Grafičko prikazivanje klasa i njihovih međusobnih odnosa je vrlo korisno pa se na toj ideji razvio cijeli jedan jezik dijagrama (grafičkih elemenata) koji se naziva Unified Modelling Language (kratko UML).

UML predstavlja notacijski sustav za specifikaciju, vizualizaciju i dokumentiranje modela objektno orijentiranih softverskih sustava.

UML može predstaviti različite aspekte softverskog sustava koristeći različite vrste dijagrama, no nas će u ovom trenutku zanimati samo dijagrami klasa (class diagrams) koji prikazuju klase i odnose među njima. Na primjer,

Point3D.png

Sintaksa deklaracija atributa (varijabli) je oblika

ime_varijable : tip_varijable

UML - oznake

Nadalje, svakom atributu ili operaciji može prethoditi znak +, - ili # koji ima sljedeće značenje:

  • + označava javni atribut/operaciju
  • - označava privatni atribut/operaciju
  • # označava protected atribut/operaciju

Grafički prikaz klasa pomoću dijagrama često se pojednostavljuje radi preglednosti:

Point3D-1.png

Doseg (scope)

Doseg nekog identifikatora je dio programa u kojem je on vidljiv.

Vrijede pravila iz C-a:

Doseg klase

C++ dodaje još dvije vrste dosega: doseg klase (class scope) i doseg imenika (namespace scope).

Svaka klasa definira svoj vlastiti doseg. Sve članice klase, varijable i funkcije, su u tom dosegu. Članice različitih klasa mogu imati ista imena upravo stoga što se pripadanjem različitim klasama nalaze u različitim dosezima.

Doseg imenika

  • Svaki imenik (namespace) je jedan doseg.
  • Ime deklarirano i imeniku se izvan tog imenika kvalificira imenom imenika.

Primjer. cout je definirano u imeniku std. Stoga ga izvan imenika std pišemo kao std::cout.

Primjer: definicija funkcije članice izvan klase

// Datoteka point3D.cc
#include <iostream>
#include <cmath>
#include "point3D.h"

void Point3D::rotate(Real alpha, Real beta, Real gamma)
{
    Real x1 =  std::cos(alpha) * data[0] + std::sin(alpha) * data[1];
    Real y1 = -std::sin(alpha) * data[0] + std::cos(alpha) * data[1];
    Real z1 = data[2];

    Real x2 =  x1;
    Real y2 =  std::cos(beta) * y1 + std::sin(beta) * z1;
    Real z2 = -std::sin(beta) * y1 + std::cos(beta) * z1;

    data[0] =  std::cos(gamma) * x2 + std::sin(gamma) * y2;
    data[1] = -std::sin(gamma) * x2 + std::cos(gamma) * y2;
    data[2] =  z2;
}

Nakon što je viđeno Point3D::rotate nalazimo se u dosegu klase Point3D i zato možemo dohvatiti privatnu varijablu članicu data.

Povratna vrijednost

Povratna vrijednost funkcije članice nije u dosegu klase, ukoliko je ova definirana izvan klase. Da smo htjeli funkciju get(int i) definirati izvan klase morali bismo pisati:

Point3D::Real  Point3D::get(int i) const { return data[i]; }

Izvan dosega klase

Izvan dosega klase javne varijable članice i funkcije članice (koje nisu statičke) možemo dohvatiti samo kroz neki objekt tipa klase pomoću operatora točka (.) i strelica (). Na primjer,

Point3D D(1,1.2,3);
Point3D* pD = &D;    // pokazivač na Point3D
Point3D& rD = D;     // referenca na Point3D

pD->set(2,2.9);
rD.set(1,1.1);
D.set(0,0.9);

std::cout << D.data[0] << std::endl; // Greška, data je privatna varijabla klase

Nalaženje imena (name lookup)

Proces nalaženja deklaracije koja odgovara nekom imenu izgleda ovako:

Ako deklaracija nije nađena niti u jednom dosegu imamo grešku pri kompilaciji. Ova pravila impliciraju da se svako ime mora deklarirati prije prve upotrebe.

Kod nalaženja imena funkcija imamo dodatno pravilo:

Primjer: std::cout << std::endl;

Taj kod je ekvivalentan s operator<<()(std::cout, std::endl); i bez ADL-a operator << koji je definiran u imeniku std ne bio nikad pronađen.

Nalaženje imena unutar klase

Pravilo:

Pri kompilaciji definicija funkcija članica klase postupa se ovako: - Deklaracija imena koje se koristi u tijelu funkcije članice prvo se traži u dosegu funkcije (tijelo + formalni argumenti).

Posljedica: U tijelu funkcije članice klase možemo koristiti sva imena deklarirana u klasi, čak i ona koja su deklarirana iza tijela funkcije.

this pokazivač

Kako se poziva funkcija članica klase?

Na primjer,

Point3D x(4.0 );
x.set(2 ,8.0 ); // Postavi z-koordinatu na 8.0
  • Objekti koji su instanca dane klase sadrže u sebi samo varijable članice. Metode definirane u klasi ne povećavaju veličinu objekata. S druge strane, funkcije definirane u klasi imaju jedan skriveni parametar koji je pokazivač na objekt na kojem je funkcija pozvana. Taj se pokazivač naziva this i može se eksplicitno dohvatiti pomoću ključne riječi this.

Gornji poziv izgleda, ustvari, ovako:

set(&x,2 ,8.0 );

Napomena: Virtualne funkcije se implementiraju, kao što ćemo vidjeti, na složeniji način.

Eksplicitna uptreba this pokazivača

Primjer:

Modificiramo klasu Point3D<T>:

template <typename T>
class Point3D
{
    public:
        Point3D(T x=0.0, T y=0.0, T z=0.0);
        T  operator[](int i) const{ return data[i]; }
        T& operator[](int i)      { return data[i]; }
        // Skaliranje točke množenjem sa skalarom t
        Point3D & scale(T t);
        Point3D & translate(const Point3D& dir);
        // Rotacija oko ishodišta za Eulerove kuteve phi psi i theta
        Point3D & rotate(T phi, T psi, T theta);
        // Rotacija oko točke centar za Eulerove kuteve phi, psi i theta
        Point3D & rotate(const Point3D& centar, T phi, T psi, T theta);

    private:
        T data[3];
};

template <typename T>
std::ostream & operator<<(std::ostream & out, Point3D<T> const & p){
  out << "["<< p[0] <<"," << p[1] << "," << p[2] << "]";
  return out;
}

Implementacija

Kad treba vratiti referencu na objekt na kojem se nalazimo vraćamo *this.

template <typename T>
Point3D<T>& Point3D<T>::rotate(T alpha, T beta, T gamma)
{
    T x1 =  std::cos(alpha) * data[0] + std::sin(alpha) * data[1];
    T y1 = -std::sin(alpha) * data[0] + std::cos(alpha) * data[1];
    T z1 = data[2];

    T x2 =  x1;
    T y2 =  std::cos(beta) * y1 + std::sin(beta) * z1;
    T z2 = -std::sin(beta) * y1 + std::cos(beta) * z1;

    data[0] =  std::cos(gamma) * x2 + std::sin(gamma) * y2;
    data[1] = -std::sin(gamma) * x2 + std::cos(gamma) * y2;
    data[2] =  z2;

    return *this;
}

Primjena

Sada možemo ulančavati pozive funkcija koje vraćaju *this:

int main()
{
    Point3D<double> A;                     // Točka s koordinatama (0,0,0)
    Point3D<double> B(1.0,1.0), C(1,0,1);  // B=(1,1,0) i C=(1,0,1)

    std::cout << "A= "; std::cout << A << std::endl;
    std::cout << "B= "; std::cout << B << std::endl;
    std::cout << "C= "; std::cout << C << std::endl;

    B.rotate(0.8,0.7,1.6).translate(C); // rotiraj pa translatiraj
    std::cout << "B= "; std::cout << B << std::endl;
    C.scale(-1);
    B.translate(C).rotate(-1.6,-0.7,-0.8); // vrati u početni položaj
    std::cout << "B= "; std::cout << B << std::endl;

    return 0;
}

Razlika između konstantnog i nekonstantnog objekta

struct X{
  X const & getX() const { return *this; } // this je tipa: X const * const
  X       & getX()       { return *this; }       // this je tipa: X * const
};

Konstantna funkcija članica

Funkcija članica koja nije deklarirana kao konstantna ne može biti pozvana na konstantnom objektu čak i onda kada ne mijenja objekt. Samo konstantna funkcija članica može biti pozvana na konstantnom objektu.

Što znači da je objekt konstantan?

U C++-u konstantnost objekta je bit-po-bit konstantnost i stoga konstantna funkcija ne može promijeniti niti jednu varijablu članicu objekta. To se ne odnosi na statičke varijable članice, kao što ćemo vidjeti kasnije.

bit-po-bit konstantnost nije nužno uvijek prirodna

Primjer:

class A{
private :
    char * text;// Konstruktor alocira polje dinamički
public :
    A(const char * string);
    char & get(unsigned int i) const { return text[i]; }// Konstantna f.č.
    // ...
};

int main()
{
   const A a(""); // Konstantan objekt
   a.get(0) = 'Z' ; // Mijenjam string a.text !
}

Lijeno izračunavanje

Objekt može logički ostati konstantan premda je neka od njegovih varijabli izmijenjena.

Takva je situacija česta kod lijenog izračunavanja..

class A{
private :
    char * text;
    mutable bool lengthValid;// zastavica
    mutable std::size_t text_size;// duljina stringa
public :
    A(const char * string);
    char & get(unsigned int i) const { return text[i]; }// Konstantna f.č.
    std::size_t size() const ;
    // ...
};

Funkcija size je prirodno konstantna jer samo izračunava duljinu stringa. Implementacija je jednostavna:

std::size_t A::size() const
{
    if (!lengthValid){
        text_size = std::strlen(text);
        lengthValid = true ;
    }
    return text_size;
}

mutable

Varijabla članica deklarirana mutable može se mijenjati i u konstantnom objektu, tj. može ju mijenjati i konstantna funkcija.

Konstruktori

Sintaksa konstruktora

  • Konstruktor je vrlo posebna funkcija članica klase zadužena za konstrukciju objekta.
  • Konstruktor ima tijelo kao i svaka druga funkcija, ali za razliku od njih ima inicijalizacijsku listu u kojoj će biti inicijalizirana većina varijabli. -Konstruktor ne deklarira povratni tip i ima isto ime kao i klasa.
  • Konstruktor može uzimati različite argumente i vrlo je česta situacija da klasa ima više preopterećenih konstruktora.

Primjer:

#include  <iostream>
#include  <ctime>

class Time{
public :
    Time(int s=0 , int min=0 , int sec=0 );
    int getSat() const { return sat; }
    int getMinuta()const { return minuta;}
    int getSekunda() const { return sekunda; }
    void setTrenutno();
    void printStandard(std::ostream&) const ;
private :
    int sat;
    int minuta;
    int sekunda;
};

Implementacija konstruktora

Time::Time(int s, int min, int sec) : sat(s), minuta(min), sekunda(sec)
{
    if (sat < 0 || sat > 23 ) sat = 0 ;
    if (minuta < 0 || minuta> 59 ) minuta = 0 ;
    if (sekunda < 0 || sekunda> 59 ) sekunda = 0 ;
}

Pozivanje konstruktora

Konstruktor se poziva automatski u svim slučajevima kad se objekt klase konstruira. Kako preopterećenih konstruktora može biti više, koji će od njih biti pozvan ovisi o argumenima danim pri pozivu. Na primjer,

Time t1;
Time t2(12 ,30 ,0 );
Time* t3 = new Time(13 ,13 );

Inicijalizacijska lista

Konstrukcija objekta se koncepcijski dešava u dvije faze:

Inicijalizacija ide u inicijalizacijsku listu dok preostalo ide u tijelo konstruktora.

Inicijalizacija varijable uslijedit će prije izvršavanje tijela konstruktora i onda kada varijablu ne stavimo direktno u inicijalizacijsku listu. Prevodilac će takve varijable inicijalizirati prema sljedećim pravilima:

Kad je inicijalizacija u inicijalizacijskoj listi nužna?

Redosljed inicijalizacije

  • Redosljed kojim se vrši inicijalizacija varijabli određen je redosljedom kojim su varijable članice deklarirane u klasi

Ta činjenica postaje važna kada jednu članicu inicijaliziramo pomoću druge, već inicijalizirane.

class X {
    int i;
    int j;
public :
    X(int val) : j(val), i(j) {}
};

U ovom će primjeru varijabla i ostati neinicijalizirana i nikakva greška pri kompilaciji neće se desiti.

Dodijeljeni (default) konstruktor

Dodijeljeni konstruktor je onaj konstruktor koji ne uzima parametare. Konstruktor koji za sve svoje parametre ima dodijeljene vrijednosti također je dodijeljeni konstruktor.

Ako klasa ne definira niti jedan konstruktor prevodilac će sintetizirati konstruktor bez parametara. Sintetizirani dodijeljeni konstruktor inicijalizira objekt na sljedeći način:

U osnovi svaka klasa treba imati konstruktor bez parametara, definiran unutar klase ili sintetiziran (kod vrlo jednostavnih klasa).

Nedostaci klase koja nema dodijeljeni konstruktor

  • Objekt takve klase uvijek mora biti eksplicitno inicijaliziran. Svaka klasa koja sadrži takav objekt mora deklarirati konstruktor koji će eksplicitno inicijalizirati objekt.
  • Nije moguće imati dinamički alocirano polje objekta koji nemaju dodijeljenog konstruktora.
  • Statički alocirana polja u tom slučaju moraju eksplicitno inicijalizirati članove.
  • Spremnici kao što je vector<> ne mogu koristiti konstruktor koji uzima samo dimenziju, budući da on na svakom elementu poziva njegov dodijeljeni konstruktor.

Primjer klase koja nema dodijeljeni konstruktor

#include  <vector>

// Klasa bez dodijeljenog konstruktora
class X {
public :
    X(int i) : data(i){}
private :
    int data;
};

int main()
{
    X x; // Greška, nema konstruktora koji ne uzima parametre
    X* px = new X[5];// Greška, na svakom od 5 X-ova želi se pozvati dodijeljeni konstruktor

    X arrayA[5] = {1 ,2 ,3 ,4 ,5 }; // ok
    X arrayB[5];// Greška, nema dodijeljenog konstruktora

    std::vector<X>vecA(5 ,1); // ok. Poziva konstruktor
    std::vector<X>vecB(5);    // Greška, nema dodijeljenog konstruktora

    return 0 ;
}

= default

Ako klasa ima barem jedan konstruktor prevodilac neće sintetizirati dodijeljeni konstruktor. U takvoj situaciji možemo zatražiti sintetiziranje konstruktora pomoću ključne riječi default (C++11):

#include  <vector>

// Klasa bez dodijeljenog konstruktora
class X {
public :
    X() = default;
    X(int i) : data(i){}
private :
    int data;
};

Zadatak. Implementirati metode klase Time. Tu je jedina netrivijalna metoda setTrenutno koju treba implementirati koristeći ANSI C standardnu biblioteku. Potrebno je u program uključiti zaglavlje <time.h>, no u C++-u standardna C- zaglavlja imenujemo s "c" ispred imena i bez ekstenzije ".h": dakle, uključujemo zaglavlje <ctime>.

Delegirajući konstruktor (C++11)

C++11 dozvoljava da konstruktor bude implementiran tako da poziva drugi konstruktor. Poziv drugom konstruktoru se nalazi u inicijalizacijskoj listi koja ne smije sadžavati ništa osim poziva konstruktoru. Eventualni preostali dio inicijalizacije obavlja se u tijelu konstruktora. Takav konstruktor se naziva delegirajući (delegating constructor).

#include <iostream>
#include <ctime>
#include <string>

class Time{
    public:
        // Općeniti konstruktor -- implementiran u drugoj datoteci
        Time(int s, int min, int sec, std::string name);
        // Delegirajući konstruktori
        Time() : Time(0,0,0,"") {}
        Time(int s) : Time(s,0,0,"") {}
        Time(int s, int min) : Time(s,min,0,"") {}
        Time(int s, int min, int sec) : Time(s,min,sec,"") {}
        Time(std::string name) : Time(0,0,0,name) {}

        int getSat()     const { return sat;     }
        int getMinuta()  const { return minuta;  }
        int getSekunda() const { return sekunda; }
        void setTrenutno();
        void printStandard(std::ostream&) const;
    private:
        int sat;
        int minuta;
        int sekunda;
        std::string ime;
};

Implicitne konverzije

Klasa Time ima jedan konstruktor koji može uzeti samo jedan parametar:

void f(Time t)
{
    std::cout << t.getMinuta() << std::endl;
}

int main()
{
    Time t(12 ,30 ,0 );
    f(t); // ok. Uzima Time.
    f(3); // implicitna konverzija. Kreira privremeni objekt

    return 0 ;
}

Zadatak. Što će se desiti ako funkciju f deklariramo na sljedeći način:

void f(Time & t);
void f(Time const & t);

explicit

Implicitna konverzija je nepoželjna uvijek kada nije intuitivna i kada je programer ne očekuje. Stoga nam jezik omogućava njeno dokidanje tako što ćemo konstruktor deklarirati explicit:

class Time{
public :
// explicit onemogućava implicitne konverzije pomoću konstruktora
    explicit Time(int s=0 , int min=0 , int sec=0 , std::string name=&quot;&quot; );
    int getSat() const { return sat; }
    int getMinuta()const { return minuta;}
    int getSekunda() const { return sekunda; }
    void setTrenutno();
    void printStandard(std::ostream&) const ;
private :
    int sat;
    int minuta;
    int sekunda;
    std::string ime;
};

Sad prevodilac neće dozvoliti implicitnu konverziju, no eksplicitna je dozvoljena:

 f(Time(3)); // implicitna konverzija nije dozvoljena ali možemo koristiti eksplicitnu

Direktna inicijalizacija nestatičkih varijabli unutar klase (C++11)

Prema novom standardu nestatičke varijable članice klase možemo inicijalizirati na mjestu deklaracije. Sintetizirani dodijeljeni konstruktor poštuje takvu inicijalizaciju, ali vrijednosti navedene u inicijalizacijskoj listi konstruktora imaju prednost.

#include <iostream>

class X{
  // Direktna inicijalizacija nestatičkih varijabli
  int i = 50;
  int j{75};
public:
  X() = default;
  X(int _i) : i(_i) {}
  X(int _i, int _j) : i(_i), j(_j) {}
  void print() const { std::cout << i << ", " << j << std::endl; }
};


int main()
{
   X x;  // default Ctor
   x.print(); // 50, 75
   X y(3);
   y.print(); // 3, 75
   X z(3,4);
   z.print();  // 3, 4

   return 0;
}

Statički članovi klase

Statičke varijable

  • Statičke varijable članice se deklariraju pomoću ključne riječi static.
  • Statičke varijable nisu pridružene instanci klase već samoj klasi.

Statička varijabla je zamjena za globalnu varijablu i nudi ove prednosti prednosti pred globalnom varijablom:

  • Nalazi se u dosegu klase i stoga ne dolazi do kolizija s varijablama istog imena u drugim dosezima.
  • Na nju djeluju ograničenja pristupa kao i na bilo koju drugu varijablu članicu, pa ju možemo deklarirati private i tako zaštiti od koda izvan klase.

Primjer

Ovdje imamo klasu Point3D modificiranu tako da pamti ukupan broj instanciranih točaka.

template <typename T>
class Point3D
{
    public:
        Point3D(T x=0.0, T y=0.0, T z=0.0);
        T  operator[](int i) const{ return data[i]; }
        T& operator[](int i){ return data[i]; }
        // Skaliranje točke množenjem sa skalarom t
        Point3D & scale(T t);
        Point3D & translate(const Point3D& dir);
        // Rotacija oko ishodišta za Eulerove kuteve phi psi i theta
        Point3D & rotate(T phi, T psi, T theta);
        // Rotacija oko točke centar za Eulerove kuteve phi, psi i theta
        Point3D & rotate(const Point3D& centar, T phi, T psi, T theta);
    // Broj alociranih točaka
        static int print_cnt() {return cnt;}

    private:
        T data[3];
        static int cnt;
};

Definicija statičke varijable

// Definicija statičke varijable iz predloška klase.
template <typename T>
int Point3D<T>::cnt = 0;

template <typename T>
Point3D<T>::Point3D(T x, T y, T z){
  data[0] = x;
  data[1] = y;
  data[2] = z;
  ++cnt;
}

Dvije iznimke

  • Konstantne integralne statičke varijable mogu biti definirane unutar klase
  • U standardu C++-17 varijable mogu biti deklarirane inline. Statička inline varijabla može biti inicijalizirana unutar klase i ne treba definiciju izvan datoteke zaglavlja.
struct X{
  static const int x = 3;
  inline static double y = 6.14; // C++17
};

int main()
{
  std::cout << X::x << std::endl; // 3
  std::cout << X::y << std::endl; // 6.14
  return 0;
}

Statičke funkcije

Funkcije članice klase mogu isto biti deklarirane static. One tada ne dobivaju this pokazivač i ne mogu dohvatiti nestatičke varijable članice. One mogu djelovati samo na statičkim varijablama klase. Mogu se dohvatiti kao i nestatičke metode putem instance klase ili pomoću operatora dosega ::.

// Broj alociranih objekata
std::cout << Point3D<double>::print_cnt() << std::endl;

Napomena: Statička funkcija ne može biti const, jer ionako ne može promijeniti objekt (što je smisao metoda tipa const), niti može biti virtual.

Javne statičke varijable mogu se dohvatiti pomoću operatora dosega ::, jednako kao i javne statičke metode.

Prijatelji klase

U nekim je klasama zgodno dopustiti pristup privatnim podacima nekim funkcijama koje nisu članice klase. To se čini tako što se funkcija kojoj se dozvoljava pristup deklarira kao friend funkcija unutar klase. Sa friend se može deklarirati:

Deklaracija prijatelja klase počinje ključnom riječju friend i može se nalaziti bilo gdje unutar klase; public, private i protected deklaracije nemaju utjecaja na njih. Prijatelji nisu članice klase premda su deklarirani unutar klase. Oni samo imaju pristup privatnom dijelu klase.

Primjer

Zamislimo klasu A koja reprezentira točku na ekranu, ima get-metode za dohvat koordinata, ali nema javne set-metode. Metoda swap koja zamijenjuje dva objekta klase A prirodno nije članica klase. Stoga je u ovom primjeru swap kandidat za friend funkciju.

class A
{
public :
    explicit A(int x_=0 , int y_=0 ): x(x_), y(y_) {}
    // get metode, ali nemamo set metoda
    int width() const { return x;}
    int height() const { return y;}
    void translate(A& a);
    void rotate(double phi);
    friend void swap(A& a, A& b);
private :
    int x;
    int y;
};
void swap(A& a, A& b)
{
    int x = a.x;
    int y = a.y;
    a.x = b.x;
    a.y = b.y;
    b.x = x;
    b.y = y;
}

Taj bi kod bio neispravan da swap nije deklarirana kao prijatelj klase A.

Pokazivač na metodu članicu klase

Pogledajmo sada kako bismo kroz pokazivač dohvatili nestatičku metodu članicu klase. Uzmimo stoga jednostavan primjer klase:

class Functions{
public :
 double f(double x) { return x*x; }
 double g(double x) { return x*x*x; }
};

1. Deklaracija pokazivača

double (Functions::*pFunc)(double );

ili

// typedef double (Functions::*PFUN)(double );  -- stara sintaksa
using PFUN =  double (Functions::*)(double );
PFUN pFunc;

2. Inicijalizacija pokazivača

PFUN pFunc = &Functions::g;

3. Poziv funkcije kroz pokazivač

Poziv (nestatičke) funkcije kroz pokazivač može se izvršiti samo na nekoj instanci klase, dakle na objektu.

pFunc = &Functions::f;
Functions FF;
std::cout <<  (FF.*pFunc)(2.0) << std::endl;

Sintaksa je vrlo prirodna. Moramo dereferencirati pokazivač da bismo na njega mogli primijeniti operator točka (.) kako bismo pozvali funkciju.