Objekti

Razrede v C++ uporabljamo enako kot v Pythonu. Z njimi definiramo nove podatkovne tipe, ki ustrezajo našim potrebam. Najprej terminologija:

Definicija

Razred je definicija našega novega tipa. Spremenljivka tega tipa, ki jo naredimo, se imenuje objekt tega razreda ali instanca tega razreda. Tip in razred se nanašata na abstraktno definicijo, objekt ali instanca pa na konkretno spremenljivko. Funkcije, ki so vsebovane v tem objektu se imenujejo metode, spremenljivke tega objekta pa atributi. Običajno razrede označujemo z PascalCase začetnicami.

Definicija novega razreda gre v C++ tako:

class ImeTipa {
  public:
    tip1 member1;
    tip2 member2;
  private:
    tip3 member3;
    tip4 member4;
};

Danger

Na koncu definicije razreda mora biti podpičje. Če ga pozabite, lahko pride do zelo neprijetnih in težko razumljivih napak.

Naredimo primer razreda Pravokotnik in nastavimo njegove člane. Spremenljivke našega tipa (v tem primeru Pravokotnik) definiramo enako kot vse ostale, najprej povemo tip, nato pa ime. Kot pri funkcijah tudi razrede definiramo izven funkcije main preden jih želimo uporabljati. Kot v veliko drugih jezikih tudi v c++ do atributov in metod dostopamo s piko a.x.

class Pravokotnik {
  public:
    double a, b;
};

int main() {
    Pravokotnik p;
    p.a = 4;
    p.b = 1.2;
}

Metode

Dodajmo razredu metodo, ki izračuna ploščino, in še eno, ki nastavi obe stranici. Metode so po sintaksi popolnoma enake funkcijam in do atributov razreda imajo prost dostop (ni potrebno pisati npr. self.x).

class Pravokotnik {
  public:
    void set_sides(double x, double y) {
        a = x;
        b = y;
    }
    double area() {
        return a * b;
    }
  private:
    double a, b;
};
int main() {
    Pravokotnik p{4, 1.2};
    p.set_sides(1, 1);
    cout << p.area() << endl;
}  // prints 1

Private, public, protected

To so trije tipi scopa, “vidnosti” v razredih. Do public funkcij in spremenljivk lahko dostopajo vsi, izven in znotraj razreda, do privatnih pa samo znotraj razreda. Protected se uporablja pri dedovanju, in ga bomo spoznali kasneje.

Pogost vzorec v programiranju je, da so atributi private, spreminja in bere pa se jih lahko samo preko public metod, saj s tem uporabniku preprečimo nekontroliran dostop, ki bi lahko ogrozil smiselnost podatkov.

class Pravokotnik {
  public:
    void set_sides(double x, double y) {
        a = x;
        b = y;
    }
    double area() {
        return a * b;
    }
  private:
    double a, b;
};
int main() {
    Pravokotnik p;
    p.set_sides(1, 1);
    cout << p.area() << endl;
    p.a = 8;
}

Zgornja koda nam vrne napako, dostopamo namreč do privatne spremenljivke a.

a.cpp: In function ‘int main()’:
a.cpp:14:14: error: ‘double Pravokotnik::a’ is private
       double a, b;
              ^
a.cpp:20:9: error: within this context
       p.a = 8;
         ^

Če problematično vrstico zakomentiramo, je vse ok, saj sta set_sides in area public metodi, ki pa znotraj razreda lahko dostopata do privatnih spremenljivk.

Seveda je najlažje programirati, če vse naredimo kar public, vendar to ni nujno najboljša ideja. Pravzaprav je bolje, da vse naredimo privatno, in navzven pokažemo samo nekaj metod za delo z našim razredom, saj tako uporabniku našega razreda (torej tudi samemu sebi) preprečimo, da bi počeli kakšne neumnosti, spravili razred v neveljavno stanje, klicali funkcije, ki so zgolj del implementacije in se lahko spremenijo. Imeti stvari po defaultu private spodbuja boljši design kode: če je nekaj samo implementacijski detajl, potem je gotovo zasebno. Predstavljajte si, da bi imeli na voljo tudi vse interne metode, ki jih razred vector uporablja za alokacijo spomina, prepisovanje elementov, ipd. To je nepotrebno, saj ne veste kaj klicanje takih metod naredi, poleg tega pa se lahko te metode kadarkoli zamenjajo. Toda, ker so v privatne, bo vsa koda, ki vector uporablja, še vedno delovala (saj so bile metode privatne in jih nismo mogli uporabljati). Tako lahko rečemo, da public metode pravzaprav definirajo, kaj objekt je in kaj z njim lahko počnemo, pa tudi spodobi se, da vsaka public metoda pusti objekt v veljavnem stanju (kar ni nujno res za privatne metode). Primer: za public metode se ponavadi ne spodobi, da bi postavljali zahteve v stilu “Če želite poklicati x, morate najprej poklicati y.”, medtem ko je za privatne to običajno.

Konstruktorji in destruktorji

Kaj pa, če želimo našemu objektu dati določene parametre že na začetku, ko ga ustvarimo? V tem primeru definiramo konstruktor, to je posebna metoda, ki omogoča ustvarjanje razreda.

Sintaksa je sledeča:

class ImeTipa {
  public: // konstruktor
    ImeTipa(tip1 member1_, tip2 member2_) : member1(member1_), member2(member2_) {
       // koda
    }
  private:
    tip1 member1;
    tip2 member2;
};

Konstruktor naredimo tako, da napišemo ime tipa, in kot običajni funkciji naštejemo argumente. Ponavadi želimo vsakemu članu razreda določiti vrednost, zato parametre imenujemo kar po članih razreda (z dodanim podčrtajem, saj enakih imen ne smemo uporabiti). Parametre lahko poimenujete seveda kakor želite, a to je najbolj sugestivno in standardno. Sledi dvopičje in seznam (class initializer list) članov, ki jim določimo vrednosti (kot bomo videli kasneje, kličemo njihove konstruktorje). Nato sledi telo, ki je kot vsako telo funkcije, in notri lahko počnemo običajne stvari. Ko smo znotraj telesa, so spremenljivke, ki smo jih nastavili v initializer list že nastavljene, in jih lahko uporabljamo.

Konstruktor pokličemo z oklepaji za imenom spremenljivke ImeTipa ime_spremenljivke(arg1, arg2)

Poglejmo si primer za pravokotnik.

class Pravokotnik {
  public:
    Pravokotnik(double a_, double b_) : a(a_), b(b_) {}
    void set_sides(double x, double y) {
        a = x;
        b = y;
    }
    double area() {
        return a * b;
    }
  private:
    double a, b;
};
int main() {
    Pravokotnik p(1, 1);
    cout << p.area() << endl;
}  // prints 1

Vrednosti a in b smo nastavili že v konstruktorju, zato klic funkcije set_values ni bil potreben.

Razred ima lahko več kot en konstruktor. Naredimo še dva.

class Pravokotnik {
  public:
    Pravokotnik() : a(0), b(0) {}
    Pravokotnik(double a_) : a(a_) {}
    Pravokotnik(double a_, double b_) {
         a = a_;
         b = b_;
    }
  private:
    double a, b;
};
int main() {
    Pravokotnik p;
    Pravokotnik q(1);
    Pravokotnik r(1, 3);
    p = 1;
    r = {3, 5};
}  // prints 1

Dodali smo default konstruktor, ki ne sprejme parametrov. Ta se pokliče ko spremenljivko samo deklariramo, Pravokotnik p;. Imamo še konstruktor iz ene spremenljivke, ki nastavi obe stranici na enako vrednost. Že znan konstruktor, pa smo prepisali na ekvivalenten, a manj običajen način, ki pa je morda začetnikom bolj jasen in morda bolj ilustrira intuicijo. V resnici je pristop z initializer listom tudi idejno pravi, saj tak pokličemo konstruktorje naših članov, na enak način kot smo poklicali konstruktor našega Pravkokotnika. Zadnji dve vrstici main funkcije kažeta sintakso z =, ki jo compiler tudi prevede v klic konstruktorja. V prvem primeru se pokliče drugi, v drugem pa tretji konstruktor.

Const metode in spremenljivke

Spoznali smo že konstantne spremenljivke in reference pri klicih funkcij. Na hitro ponovimo – konstantne spremenljivke so tiste, ki jih ne moremo spreminjati, niti direktno niti s klicanjem funkcij, ki bi jih spremenile.

const int a = 7;
a = 9;   // napaka
int b = 0;
swap(a, b); // napaka

Kaj pa, če je naša konstantna spremenljivka objekt?

const Pravokotnik p(1, 3);
std::cout << p.area() << std::endl;

Dobimo napako:

a.cpp: In function ‘int main()’:
a.cpp:29:22: error: passing ‘const Pravokotnik’ as ‘this’ argument discards qualifiers [-fpermissive]
       cout << p.area() << endl;
                      ^
a.cpp:18:14: note:   in call to ‘double Pravokotnik::area()’
       double area() {

Malo kriptična napaka, pomeni, da ta funkcija “discards qualifiers”, torej ne spoštuje “dodatkov k tipu”, v tem primeru je to besedica const. Po domače, funkcija lahko spreminja objekt, zato je ne smeš klicati na konstantnem objektu.

Ampak saj ga ne!

Hja, ampak kako naj ubogi g++ to ve? To se reši tako, da mu poveš, da ta funckija objekta ne spreminja. Če bi kodo prevedli s clang-om, bi ta bolj prijazno rekel:

a.cpp:29:15: error: member function 'area' not viable: 'this' argument has type 'const Pravokotnik', but function is not marked const
      cout << p.area() << endl;
              ^
a.cpp:18:14: note: 'area' declared here
      double area() {

Torej, metodi moramo dodati const, ki pomeni, da ta metoda objekta, nad katerim je definirana, ne spreminja. Ponavadi so to metode, ki ga zračunajo, izpišejo, vrnejo. Dobra navada je, da metode, ki so const, označimo s const, saj imamo sicer lahko probleme, npr, ko objekt podamo preko const Pravokotnik&. Jasno, const metode, lahko kličejo samo druge const metode, neconst metode, pa lahko kličejo tako const kot neconst metode. Pravilnejša definicija pravokotnika:

class Pravokotnik {
  public:
    Pravokotnik() : a(0), b(0) {}
    Pravokotnik(double a_) : a(a_) {}
    Pravokotnik(double a_, double b_) : a(a_), b(b_) {}
    void set_sides(double x, double y) {
        a = x;
        b = y;
    }
    double area() const {  // this method is const
        return a * b;
    }
  private:
    double a, b;
};
int main() {
    const Pravokotnik p(1, 1);
    cout << p.area() << endl;
}  // prints 1, vse ok

Jasno metode set_sides nismo označili s const, saj spremenljivke nastavi in spremeni objekt, pa tudi compiler bi se pritožil, če bi jo, kajti znotraj kličemo funkcijo operator =, ki jasno ni const, kar je bi bilo dovoljeno.

Če torej sedaj naredimo funkcijo, ki preveri, ali je pravokotnik ploščine 1, ne bo imela težav:

bool je_kvadrat(const Pravokotnik& p) {
    return p.area() == 1.0;
}

Note

S const lahko označimo samo metode, za navadne funkcije to nima smisla, saj nimajo objekta pod seboj, ki bi ga lahko spreminjale. To, ali spreminjajo parametre ali ne, lahko določimo že s tem, da parametrom predpišemo const ali ne.

Static metode in spremenljivke

Statične so metode, ki za svoje delovanje ne potrebujejo objekta in ne dostopajo do nobenih (ne statičnih) spremenljivk ali metod razreda. Pravzaprav je to navadna funkcija, ki je zaradi logične strukture premaknjena znotraj razreda. V konkretnem primeru, statična funkcija bi bila taka, ki ji je vseeno, kateri konkreten pravokotnik ima, in lahko operira le z nečem, kar je skupno vsem pravokotnikom. Podobno velja za statične spremenljivke, to so spremenljivke, ki so neodvisne od konkretne instance, in so lastne classu.

Malo neumen primer:

class Pravokotnik {
  public:
    static const int number_of_sides = 2;
    static int instance_count;

    Pravokotnik() : a(0), b(0) { instance_count++; }
    Pravokotnik(double a_) : a(a_) { instance_count++; }
    Pravokotnik(double a_, double b_) : a(a_), b(b_) { instance_count++; }

    static void print_number_of_sides() {
        cout << number_of_sides << endl;
    }
    ...
};
int Pravokotnik::instance_count = 0;
int main() {
    int sides = Pravokotnik::number_of_sides;  // lahko samo preko imena razreda
    Pravokotnik p;
    sides = p.number_of_sides;  // lahko pa tudi tako
    Pravokotnik::print_number_of_sides();
    Pravokotnik q(1);
    q.print_number_of_sides();  // enako velja za funkcije
    cout << q.instance_count << endl;
}  // print 2

Kot vidimo lahko kličemo statične metode samo preko imena tipa, ne potrebujemo objekta spodaj, nič pa ni narobe, če ga imamo. Spremenljivka instance_count, ki jo povečamo v vsakem konstruktorju, je prav tako neodvisna od instance in je na koncu enaka 2.

Od vsega so po mojem mnenju najpogosteje uporabljene statične konstante in funkcije, ostalo pa bolj po redko. Statične spremenljivke imajo podobno uporabo tudi znotraj funkcij (ob dvojni inicilizaciji se ne ponastavijo). A o tem morda kasneje.