Disposem del programa següent:
#include <iostream>
using namespace std;
struct Rellotge {
int hores, minuts, segons;
};
bool rellotge_llegeix(Rellotge& r) {
char _; // per llegir el caràcter ':'
return bool(cin >> r.hores >> _ >> r.minuts >> _ >> r.segons);
}
void escriu_XX(int x) { // utilitat per mostrar en dos caràcters
cout << (x < 10 ? "0" : "") << x;
}
void rellotge_escriu(const Rellotge& r) {
escriu_XX(r.hores);
cout << ":";
escriu_XX(r.minuts);
cout << ":";
escriu_XX(r.segons);
}
void rellotge_avansa(Rellotge& r, int s) {
r.segons += s;
if (r.segons >= 60) {
r.minuts += r.segons / 60;
r.segons %= 60;
if (r.minuts >= 60) {
r.hores += r.minuts / 60;
r.minuts %= 60;
if (r.hores >= 24) {
r.hores %= 24;
}
}
}
}
int {
segons;
Rellotge r;
((r)) {
cin >> segons;
(r, segons);
(r);
cout << endl;
}
}
que llegeix una seqüència de rellotges i els mostra després de
sumar-hi un número de segons a cadascun. (L'operador %= és com
+= per al mòdul)
El tipus Rellotge té, doncs, a més de les seves dades, una
sèrie d'operacions associades que permeten modificar el
rellotge mantenint la seva consistència.
En comptes de tenir la tupla i les funcions separades (quan en realitat són part de la mateixa cosa), es poden declarar les funcions com a membres de la tupla. Això les converteix en mètodes.
struct Rellotge {
int hores, minuts, segons;
bool llegeix();
void escriu() const;
void avansa(int segons);
};
És a dir, declarem un tipus nou, Rellotge, que no només té les
dades necessàries sinó que també té les operacions que es poden
fer amb el Rellotge. Ara, les operacions llegeix, escriu i
avansa ja no són funcions, són mètodes.
En el pas de funcions a mètodes hi ha hagut els següents canvis:
Es posa només la capçalera dels mètodes dins de la tupla.
Hem tret el prefix rellotge_ dels noms de les funcions.
Ha desaparegut el paràmetre de tipus Rellotge a totes les
operacions.
Si el Rellotge que es rebia com a paràmetre era const, ara
aquest const apareix al final de la declaració del mètode
(en particular, al mètode escriu).
Donat que a dins la tupla només hi ha les declaracions, cal també fer canvis a les implementacions:
bool Rellotge::llegeix() {
char _; // per llegir el caràcter ':'
return bool(cin >> hores >> _ >> minuts >> _ >> segons);
}
void escriu_XX(int x) {
cout << (x < 10 ? "0" : "") << x;
}
void Rellotge::escriu() const {
escriu_XX(hores);
cout << ":";
escriu_XX(minuts);
cout << ":";
escriu_XX(segons);
}
void Rellotge::avansa(int s) {
segons += s;
if (segons >= 60) {
minuts += segons / 60;
segons %= 60;
if (minuts >= 60) {
hores += minuts / 60;
minuts %= 60;
if (hores >= 24) {
hores %= 24;
}
}
}
}
A les implementacions, hi ha els següents canvis:
Tots els mètodes tenen el prefix Rellotge::. Els quatre
punts :: volen dir "pertany a". És a dir, els mètodes
pertanyen a la tupla Rellotge.
A la implementació, s'accedeix al hores, minuts i segons
directament, donat que no hi ha cap tupla Rellotge com a
paràmetre.
Si un mètode té un const al final, la implementació també el
té.
El programa principal també canvia, i potser és el que millor demostra el perquè dels canvis:
int main() {
int segons;
Rellotge r;
while (r.llegeix()) {
cin >> segons;
r.avansa(segons);
r.escriu();
cout << endl;
}
}
El canvi en el programa principal és que quan cridem un mètode,
el cridem com un membre de la tupla Rellotge, amb un ., tal
com faríem amb els camps hores, minuts, i segons. Això
explica que el paràmetre r hagi desaparegut de dins dels
parèntesis en els 3 mètodes, i també que haguem tret el prefix
rellotge_. Donat que r és un rellotge, escriure r.avansa
ja implica que el que volem avançar és el rellotge r.
Així doncs, els mètodes del Rellotge només es poden cridar si
tenim una variable de tipus Rellotge, i per tant, tots els
mètodes poden suposar tranquilament que sempre tenen un
Rellotge amb el que poder treballar.
Aquest rellotge omni-present és la variable r que està a
l'esquerra de totes les crides
r.llegeix()
r.avansa(segons)
r.escriu()
En les funcions originals, aquest paràmetre era explícit, perquè
no eren mètodes. Però com a mètodes, llegeix, avansa i
escriu reben el rellotge igualment, tot i que de forma
implícita. Per això se sol dir que el Rellotge és el
paràmetre implícit.
Això també explica que poguem accedir als camps del Rellotge
(hores, minuts, i segons) directament (i sense haver de
posar un .) dins la implementació d'un mètode. Perquè sabent
que hi ha un paràmetre implícit que és un Rellotge, no hi pot
haver dubte sobre quina és la tupla amb la que s'està treballant.
const, promet no modificar el paràmetre implícitEn particular, la funció original escriu rebia el paràmetre
Rellotge amb un const
void escriu(const Rellotge& r) { ... }
perquè escriure un rellotge no requereix modificar-lo. Per poder
dir que el paràmetre implícit és const, al no tenir-lo explícit
en els paràmetres, hem de posar el const al final de la
declaració del mètode
struct Rellotge {
...
void escriu() const; // <-- El paràmetre implícit és `const`
};
Així doncs, un Tipus Abstracte de Dades (TAD) és el resultat d'agrupar:
struct amb certs camps de dades).C++ ens permet expressar aquesta agrupació mitjançant els mètodes, que són les operacions disponibles per treballar amb certa tupla.
Quan els TADs creixen, es fa necessari separar-los del programa principal. I si hi ha molts TADS, cadascun pot estar en un fitxer diferent.
Per separar el TAD Rellotge explicat, faríem 3 fitxers:
rellotge.hh, rellotge.cc i main.cc.
A rellotge.hh aniria només la declaració de la tupla amb els
seus mètodes, i amb una protecció de doble include:
#ifndef RELLOTGE_H
#define RELLOTGE_H
struct Rellotge {
int hores, minuts, segons;
bool llegeix();
void escriu() const;
void avansa(int segons);
};
#endif
A rellotge.cc hi van només les implementacions dels mètodes de
Rellotge i per declarar la tupla Rellotge només cal fer un
#include local del fitxer rellotge.hh:
#include "rellotge.hh" // <-- Declaració de Rellotge
#include <iostream>
using namespace std;
bool Rellotge::llegeix() {
char _; // per llegir el caràcter ':'
return bool(cin >> hores >> _ >> minuts >> _ >> segons);
}
void escriu_XX(int x) {
cout << (x < 10 ? "0" : "") << x;
}
void Rellotge::escriu() const {
escriu_XX(hores);
cout << ":";
escriu_XX(minuts);
cout << ":";
escriu_XX(segons);
}
void Rellotge::avansa(int s) {
segons += s;
if (segons >= 60) {
minuts += segons / 60;
segons %= 60;
if (minuts >= 60) {
hores += minuts / 60;
minuts %= 60;
if (hores >= 24) {
hores %= 24;
}
}
}
}
El fitxer main.cc només necessita el programa principal, però
també inclou rellotge.hh perquè necessita la declaració de
Rellotge:
#include "rellotge.hh" // Declaració de Rellotge
#include <iostream>
using namespace std;
int main() {
int segons;
Rellotge r;
while (r.llegeix()) {
cin >> segons;
r.avansa(segons);
r.escriu();
cout << endl;
}
}
Així doncs, hi ha realment 2 fitxers de codi: rellotge.cc i
main.cc, però per no haver de repetir la declaració de
Rellotge, que es necessita en els dos, la posem en el fitxer
rellotge.hh i fem #include "rellotge.hh" en els dos fitxers.
(Les cometes dobles li diuen a C++ que el fitxer rellotge.hh és
local, o sigui que es troba en el mateix directori que el fitxer
.cc.)
Al laboratori es veurà com compilar un programa que es composa de més d'un fitxer
Els TADs estan per garantir la consistència d'un tipus com un
Rellotge. No volem que en un moment donat aparegui un
Rellotge amb les hores a 43 o -15, o bé els minuts a 74239
o els segons a -5698398. Els mètodes són les funcions
"responsables" de garantir aquesta consistència, ja que els hem
implementat amb cura de no cometre errors.
Però per garantir la consistència en la totalitat d'un programa,
caldria que només els mètodes de Rellotge poguessin manipular
rellotges. Perquè si no és així, quan surti un error, no podrem
saber quina part del codi del programa és culpable de l'error. Si
assegurem que només els mètodes de Rellotge poden modificar un
rellotge, llavors, quan aparegui un error, estarem segurs que
l'error només el poden haver comès els mètodes de Rellotge.
Per tant, hem d'ocultar totalment els detalls de com els
Rellotges estan implementats a la resta del programa, per
assegurar que ni tan sols és possible accedir-hi, i per tant
tenir la garantia que no apareixen errors derivats d'un ús
incorrecte dels rellotges en el futur.
Per fer això, C++ permet indicar que certs membres d'una classe són privats, que vol dir que només els altres membres de la tupla poden accedir-hi. Que un membre (ja sigui un camp de dades o un mètode) sigui privat vol dir que el programa en general no hi té accés.
El pas a membres privats és el que converteix una tupla en una
"classe". El primer pas és substituir struct per class:
class Rellotge {
...
};
Per defecte, en una classe, tots els membres són privats, per
tant aquest canvi és insuficient per aconseguir el que volíem.
Cal posar les directives public: o private: en punts concrets
de la declaració de Rellotge:
class Rellotge {
private:
int hores, minuts, segons;
public:
bool llegeix();
void escriu() const;
void avansa(int segons);
};
Les dues marques public: i private: afecten als membres
declarats a sota, fins que no aparegui alguna altra marca que ho
contradigui. En particular, en la declaració que hem fet,
private: afecta als camps de dades hores, minuts, i
segons, i public: afecta a tots els mètodes.
private, accedir-lo fora del TAD resulta en un error de compilacióAmb la nova declaració tots els fitxers del programa que facin
servir el Rellotge (mitjançant l'#include "rellotge.hh"),
produiran un error de compilació si intenten fer el següent:
Rellotge r;
r.hores = 25; // ERROR: `hores` és privat
r.minuts = 1007; // ERROR: `minuts` és privat
r.segons = -90922; // ERROR: `segons` és privat
La garantia de consistència és doncs, que el compilador es negarà a crear l'executable perquè violar la privacitat d'una classe és un error de compilació.
De resultes de la privacitat, però, tenim una mica més de feina.
Perquè ara mateix, per culpa de la privacitat, no podem
inicialitzar els Rellotges, donat que els membres hores,
minuts, i segons són privats! Hem volgut garantir la
consistència dels rellotges i per tant, ara apareix el fet que
inicialitzar un Rellotge forma part de les operacions que
haurien de garantir-ne la consistència, i no podem deixar-li això
a qualsevol!
El que falta, precisament, és un mètode especial que
s'encarregui de la inicialització i que sigui responsable de
deixar un rellotge en un estat correcte per començar. El mètode
que inicialitza una variable d'una classe com Rellotge és
especial i es diu el constructor.
El que caracteritza el constructor és
void), perquè no
retorna res.Per exemple, podem declarar un constructor per al Rellotge
així:
class Rellotge {
...
public:
// Afegim això deixant la resta com estava
Rellotge(int h, int m, int s);
...
};
i després cal implementar-lo al fitxer rellotge.cc:
Rellotge::Rellotge(int h, int m, int s) {
hores = h;
minuts = m;
segons = s;
}
Un cop fet això, en el programa principal podem crear Rellotges
amb:
Rellotge r1(0, 0, 0); // les 00:00:00
Rellotge r2(23, 59, 59); // les 23:59:59
Les dues línies declaren variables r1 i r2, però en comptes
d'haver-hi un = per inicialitzar, es posen paràntesis per posar
els paràmetres del constructor (és una crida indirecta), que C++
cridarà automàticament.
Per flexibilitzar la inicialització, C++ ens permet declarar i implementar més d'un constructor, ja que si per exemple fem:
Rellotge r3;
i no posem paràmetres, quins seran els valors inicials de
hores, minuts i segons?
Una declaració a on no es crida cap constructor en realitat en crida un, anomenat constructor per defecte, que C++ considera que és el que no rep cap paràmetre (perquè la inicialització no aporta cap dada inicial).
En el cas del Rellotge, farem que el constructor per defecte
inicialitzi un Rellotge amb zeros a les hores, minuts i
segons.
class Rellotge {
private:
...
public:
...
Rellotge(); // <-- Constructor per defecte
...
};
La implementació és:
Rellotge::Rellotge() {
hores = 0;
minuts = 0;
segons = 0;
}
A part del constructor per defecte i del constructor amb paràmetres, hi ha un constructor especial que C++ utilitza quan volem crear un objecte a partir d'un altre objecte del mateix tipus:
Rellotge r1(10, 30, 0);
Rellotge r2(r1); // <-- Constructor de còpia
La declaració de r2 crea un nou Rellotge que és una còpia de
r1. El constructor que s'encarrega d'això es diu constructor
de còpia, i es declara rebent una referència constant a un
objecte del mateix tipus:
class Rellotge {
...
public:
...
Rellotge(const Rellotge& altre); // Constructor de còpia
};
La seva implementació simplement copia els camps:
Rellotge::Rellotge(const Rellotge& altre) {
hores = altre.hores;
minuts = altre.minuts;
segons = altre.segons;
}
Si no declarem cap constructor de còpia, C++ en genera un automàticament que copia tots els membres un per un. En molts casos, doncs, no cal escriure'l explícitament. Però convé saber que existeix, perquè hi ha situacions (que veurem més endavant) en què cal implementar-lo manualment.
La programació orientada a objectes (POO) té 3 principis principals, el primer dels quals és l'encapsulació (el fet de posar les coses en "càpsules" o "caixes"), que és el que hem aconseguit agrupant les tuples i els mètodes i ocultant certs membres per fer-los privats.
La terminologia que es fa servir en la POO inclou els 3 conceptes següents:
Els mètodes, que són les operacions del TAD subjacent, que es posen com a membres de la classe.
Les classes, que són tuples que introdueixen la privacitat per garantir la consistència.
Els objectes, que són les variables que tenen com a tipus la
classe en qüestió (en el nostre cas, els Rellotges).