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:
-
Varijable članice
-
Funkcije članice
-
Tipove
-
Druge klase (tj. ugnježdene klase)
Funkcije članice se dijele na 4 vrste:
-
Obične funkcije članice
-
Konstruktori — imaju isto ime kao i klasa i nemaju povratni tip. Konstruiraju objekt tipa klase.
-
Destruktori — imaju isto ime kao i klasa s tildom ispred imena i nemaju povratni tip. Pozivaju se kod destrukcije objekta tipa klase.
-
Operatori
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:
-
Članovi koji su definirani iza public labele dohvatljivi su u svim dijelovima programa. Oni čine javno sučelje.
-
Članovi koji su definirani iza private labele nisu dohvatljivi kodu izvan klase. To je dio koda koji predstavlja implementaciju i koji je skriven (enkapsuliran) od koda koji koristi tip definiran klasom.
-
Labela protected vezana je uz naslijeđivanje klasa i o njoj će biti više riječi kasnije.
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).
-
Stoga vrijedi pravilo da se definicija inline funkcije stavlja u datoteku zaglavlja u kojoj je i deklarirana.
-
Funkcije članice definirane unutar klase automatski se tretiraju kao inline.
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,
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:
Doseg (scope)
Doseg nekog identifikatora je dio programa u kojem je on vidljiv.
Vrijede pravila iz C-a:
-
Lokalni objekti imaju doseg bloka u kome su definirani i skrivaju objekte istog imena definirane izvan bloka (koji imaju širi doseg).
-
Formalni argumenti funkcije u njenoj definiciji imaju doseg tijela funkcije, isto kao i lokalne varijable definirane unutar tijela.
-
Formalni argumenti u prototipu funkcije (deklaraciji bez definicije) imaju doseg prototipa i stoga se mogu ispustiti.
-
Globalna varijabla ima globalni doseg ako nije deklarirana
static
, kada dobiva doseg datoteke.
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.
-
Unutar klase nalazimo se u dosegu klase i tamo su vidljiva sva imena varijabli i funkcija članica.
-
Kada neku funkciju članicu definiramo izvan klase, onda se njeno tijelo nalazi u dosegu klase i sva imena članica klase su vidljiva.
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:
-
Kada se naiđe na prvu upotrebu nekog imena njegova se deklaracija potraži u istom dosegu (bloku), ali samo prije mjesta na kojem je prvi put upotrebljeno.
-
Ako deklaracija nije nađena, onda se pretražuje doseg okružujućeg bloka i tako dalje, no uvijek samo u onom dijelu dosega koji prethodi prvoj upotrebi imena.
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:
-
Ime funkcije se traži u trenutnom i okružujućem dosegu, ali i u imenicima u kojima su deklarirani njeni parametri (argument dependent lookup).
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:
-
Prvo se kompiliraju sve deklaraciju u klasi.
-
Tek nakon toga se kompiliraju definicije članica.
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).
-
Ako deklaracija imena nije nađena u dosegu funkcije pretražuju se sve deklaracije unutar klase (klasa je prvi širi doseg).
-
Ako deklaracija nije nađena u klasi pretražuje se doseg prije mjesta definicije funkcije. Ako je funkcija definirana u klasi, onda se pretražuje doseg koji prethodi definiciji klase. Ako je pak, funkcija definirana izvan klase, onda se pretražuje okružujući doseg prije definicije funkcije.
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
-
Ponekad je korisno eksplicitno koristiti taj skriveni argument kroz ključnu riječ
this
. -
this
je konstantan pokazivač na objekt tipa klase.
Primjer:
-
this
koristimo da bismo vratili referencu na objekt na kome je metoda pozvana.
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
-
this
vraća pokazivač na nekonstantan objekt u nekonstantnoj funkciji i pokazivač na konstantan objekt u konstantnoj funkciji. Pointer je uvijek konstantan, tj. adresa u njemu se ne može mijenjati. Stoga se funkcije koje vraćaju referencu na objekt često javljaju u dvije preopterećene varijante: jedna konstantna i jedna nekonstantna. Prevodilac će selektirati konstantnu funkciju na konstantnom objektu, a nekonstantnu na nekonstantnom.
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:
-
Inicijalizacijska faza: u njoj se sve varijable inicijaliziraju,
-
Računska faza: u njoj se izvršavaju dodatne operacije potrebne za potpunu inicijalizaciju objekta.
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:
-
Varijabla tipa klase inicijalizira se pozivom konstruktora koji ne uzima argumente.
-
Varijable ugrađenih tipova (built-in types) i složenih (compound types) tipova inicijaliziraju se na nulu ako je objekt u globalnom dosegu i ne inicijalizaraju se kad je objekt u lokalnom dosegu.
Kad je inicijalizacija u inicijalizacijskoj listi nužna?
-
Ako klasa ima varijablu članicu koja je tipa neke klase koja nema konstruktor koji ne uzima argumente, onda ju moramo eksplicitno inicijalizirati u inicijalizacijskoj listi.
-
Konstantne varijable članice i varijable članice tipa reference na bilo koji tip moraju biti eksplicitno inicijalizirane u inicijalizacijskoj listi.
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:
-
Varijable članice tipa klase inicijaliziraju se pozivanjem njihovog dodijeljenog konstruktora;
-
Varijable članice ugrađenih tipova (
int
,double
, …) te polja i pokazivači, inicijaliziraju se (nulama) samo ako je objekt u globalnom dosegu. U lokalnom dosegu ostaju neinicijalizirane.
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
-
Konverziju objekta klase X u neki drugi tip vrši poseban operator konverzije koji će biti uveden kasnije.
-
Konverziju objekta nekog drugog tipa u tip klase X vrši konstruktor klase X koji može biti pozvan sa samo jednim argumentom. Ta će konverzija biti primijenjena u svim situacijama u kojima se primijenjuju 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="" );
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
-
Statička varijabla članica mora biti definirana izvan klase i to samo jednom. Za razliku od nestatičkih članica ne može se inicijalizirati u konstruktoru (tj. u njegovoj inicijalizacijskoj listi) pa se prirodno inicijalizira na mjestu definicije.
-
Kako definicija statičke članice mora biti viđena samo jednom, ona ne ide u datoteku zaglavlja već se stavlja zajedno s ostalim definicijama metoda koje nisu inline. Uočimo još da se ključna riječ static koristi samo u deklaraciji, a ne i u definiciji članice.
-
U slučaju predloška klase statička varijabla se definira u datoteci zaglavlja. Svakoj instancijaciji predloška pridružena je posebna statička varijabla.
// 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čkainline
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:
-
globalna funkcija,
-
funkcija članica neke druge klase,
-
čitava klasa (sa značenjem da su tada sve funkcije članice prijatelji).
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.