dimarts, 19 de setembre del 2017

Aprendre a programar 17 - Implementació de classes i Makefile

Anem ja per la sessió 8 de laboratori, i cada cop queda menys. Aquesta sessió la dedicarem a implementar classes. Al lab programareu rentadores i cubetes, però us donen ja les capçaleres implementades, i a mi això no m'agrada. Per tant, jo explicaré des de 0, com fer capçaleres, i després com fer el seu codi.

Capçaleres

Aquest és el fitxer .hh, amb les declaracions de la classe. Només conté capçaleres, perquè el que fa la directiva include és substituir-se pel que hi ha al fitxer indicat. Això es fa al moment de compilar, de manera que si tinguéssim el codi al .hh, ens carregaríem la modularitat

Una mica de preprocessador

Primer de tot, cal explicar com funciona la directiva include. En C i C++, abans de compilar es fa una passada del preprocessador. Aquest té unes quantes directives. Nosaltres fins ara només hem utilitzat aquesta, include. El que fa és substituir-se pel contingut del fitxer que indiquem. Així, quan incloem vector, el que fem és buscar aquest fitxer, agafar les seves declaracions, i posar-les allà. 
Un problema que ens pot passar és que incloguem dos cops el mateix fitxer. Per exemple, si tenim un programa que utilitza vector i Cjt_estudiants, Cjt_estudiants inclou també vector, estaríem incloent dos cops les mateixes declaracions. I això dóna error de compilació. Per evitar aquest problema, fem el següent
#ifndef CLASSE_HH
#define CLASSE_HH
totes les capçaleres
#endif

Podríem parlar molt del preprocessador, però aquí només ens cal parlar de dues directives. La primera és ifndef. Aquesta és, bàsicament, un condicional. Si no està definida la macro CLASSE_HH, s'avalua el que hi ha dins, fins arribar al seu corresponent endif. Dins d'això, com que el que ens interessa és que tota la part d'aquest codi es compili només un cop, hem de definir CLASSE_HH. Així, a la propera inclusió, estarà ja definit, i no entrarem aquí.
A partir d'aquí, toca escriure tot el contingut d'aquesta classe

Inclusions

Ara toca incloure tot el que necessitem per aquesta classe. Per exemple, si volem utilitzar set, list i iostream, faríem

#ifndef CLASSE_HH
#define CLASSE_HH

#include <iostream>
#include <set>
#include <list>
using namespace std;

#endif

Comencem amb la classe

Ara toca posar la declaració de la classe. Aquesta es declara igual que una struct, de manera que ens quedaria

#ifndef CLASSE_HH
#define CLASSE_HH

#include <iostream>
#include <set>
#include <list>
using namespace std;

class Hola {

};

#endif


Atributs privats

Una classe tindrà sempre uns atributs, que jo els prefereixo com a privats. És on tindrem tot desat. Una cosa així:
#ifndef CLASSE_HH
#define CLASSE_HH

#include <iostream>
#include <set>
#include <list>
using namespace std;

class Hola {
private: //Optatiu, no cal posar-ho
    int a, b;
    set<int> c;
    list<double> d;
};

#endif

Allà on posa private, no és obligatori. Per defecte, en una classe, tot és privat fins que s'indica el contrari. No obstant, està bé posar-ho per claredat. 
Si volem posar funcions que ens ajudin a fer alguna cosa, les podem posar aquí. Aquestes no seran accessibles directament des de fora de la classe, sinó que només s'hi pot accedir des d'altres mètodes de la classe

Funcions públiques

Aquestes són les funcions que interactuen amb l'objecte. Les podem dividir en les categories següents:

Constructores:

Són les que creen una nova instància de la classe. Per això no retornen res, però tampoc són void. Simplement tenen el nom de la classe i, opcionalment, paràmetres

#ifndef CLASSE_HH
#define CLASSE_HH

#include <iostream>
#include <set>
#include <list>
using namespace std;

class Hola {
private: //Optatiu, no cal posar-ho
    int a, b;
    set<int> c;
    list<double> d;
public:
    //Constructores
    Hola();
    Hola(int a, int b);
    Hola(const Hola& h);
};

#endif

En el meu exemple, he fet que hi ha una que crea una instància de la classe buida, una altra que té els dos enters, i una altra que fa una còpia del paràmetre

Destructores:

Normalment només n'hi ha una, que destrueix l'objecte quan sortim del seu àmbit. En les classes que fem de moment, no ens cal fer res, ja que tot el que utilitzem ja es destrueix sol (tant els tipus bàsics, com les classes de la STL, ja tenen programades les funcions destructores). No obstant, quan fem classes amb punters, allà sí que haurem de programar la destructora. Aquí la posem buida, quedant així:
#ifndef CLASSE_HH
#define CLASSE_HH

#include <iostream>
#include <set>
#include <list>
using namespace std;

class Hola {
private: //Optatiu, no cal posar-ho
    int a, b;
    set<int> c;
    list<double> d;
public:
    //Constructores
    Hola();
    Hola(int a, int b);
    Hola(const Hola& h);
    //Destructora
    ~Hola();
};

#endif

Modificadores

Aquestes serveixen per modificar l'objecte. Normalment són funcions void, ja que només les volem per modificar coses, però per res més. Una cosa així:

#ifndef CLASSE_HH
#define CLASSE_HH

#include <iostream>
#include <set>
#include <list>
using namespace std;

class Hola {
private: //Optatiu, no cal posar-ho
    int a, b;
    set<int> c;
    list<double> d;
public:
    //Constructores
    Hola();
    Hola(int a, int b);
    Hola(const Hola& h);
    //Destructora
    ~Hola();
    //Modificadores
    /*
     * Pre: Cert
     * Post: La classe conté un nou element, que és n
     */
    void afegir(int n);
    /*
     * Pre: Existeix un element igual a n
     * Post: L'element ha sigut eliminat
     */
    void eliminar(int n);
};

#endif

En algun cas sí que retornen alguna cosa. Per exemple, poden retornar un booleà, que confirmi que s'ha eliminat allò

Normalment posem en un comentari què fa la funció, amb una precondició (que s'ha de complir per executar la funció), una postcondició (que es complirà quan haguem executat la funció), i, opcionalment, un resum del que fa

Consultores

Aquestes consulten coses a l'objecte. Per tant, retornen alguna cosa, sigui un booleà, un enter, o qualsevol cosa que ens pugui interessar. Jo no afegeixo les pre i post, perquè m'estic inventant la classe i no fa res útil, però realment s'haurien de posar

#ifndef CLASSE_HH
#define CLASSE_HH

#include <iostream>
#include <set>
#include <list>
using namespace std;

class Hola {
private: //Optatiu, no cal posar-ho
    int a, b;
    set<int> c;
    list<double> d;
public:
    //Constructores
    Hola();
    Hola(int a, int b);
    Hola(const Hola& h);
    //Destructora
    ~Hola();
    //Modificadores
    /*
     * Pre: Cert
     * Post: La classe conté un nou element, que és n
     */
    void afegir(int n);
    /*
     * Pre: Existeix un element igual a n
     * Post: L'element ha sigut eliminat
     */
    void eliminar(int n);
    //Consultores
    bool hi_es(int n);
    int maxim();
    int minim();
};

#endif

El codi

Ara ens toca implementar totes les funcions que hem fet. La cosa no és gaire difícil. Primer de tot, hem d'incloure les capçaleres que ja hem creat. Podem incloure més coses aquí, però jo personalment, penso que totes les inclusions estan millor al .hh. Igual que l'ordre "using namespace std", penso que també està millor al .hh. Per tant, inicialment tindríem en el codi, una cosa així:

#include "Hola.hh"

Ara toca començar a implementar tot. Començarem per ordre, tot i que això ja és indiferent, aquí l'ordre pot ser el que ens vingui de gust. Per indicar on està la funció, ho fem amb ::. És a dir, posem la classe, els ::, i el nom de la funció. Una cosa així:

#include "Hola.hh"

Hola::Hola() {
    a=b=0,
    c=set<int>();
    d=list<double>();
}

Hola::Hola(int a, int b) {
    this->a=a;
    this->b=b;
    //Aquí inicialitzaríem c i d
}

Hola::Hola(const Hola& h) {
    a=h.a;
    b=h.b;
    c=h.c;
    d=h.d;
}

Aquí estem veient les 3 inicialitzadores. 
La primera, com que està buida, li poso 0 als números, i tant el conjunt com la llista els poso buits. 
La segona, rebem un valor de a i de b, així que els posem. Per distingir entre la a paràmetre, i la a atribut, el que fem és utilitzar el punter this, que és un punter que apunta a la nostra classe. Si fem this->a, la a serà l'atribut, mentre que si posem a a seques, serà el paràmetre (ja que emmascara l'atribut). 
La tercera, el que fa és copiar un objecte. Per tant, copiem tots els membres i ja. Veurem gent que utilitza this per deixar clar quin és l'atribut destí i quin l'atribut de l'objecte copiat. 

Quedaria afegir les altres funcions. Seria fent així amb totes:

void Hola::afegir(int n) {
    //Codi de la implementació
}

Makefile

Ara ens queda el makefile. Aquest és un arxiu que serveix per compilar un projecte, de manera que no haguem d'escriure totes les ordres cada vegada que vulguem compilar. A més, ell mateix s'ocupa de tornar a compilar només els fitxers que calguin, de manera que si tenim 3 classes, i només hem modificat una, la única que tornem a compilar és aquella. 

Per explicar-ho, imaginarem que tenim un projecte format per
-C1
-C2, que inclou C1
-C3, que també inclou C1
-main.cc, que inclou C2 i C3

El fitxer és el següent:
OPCIONS = -D_JUDGE_ -D_GLIBCXX_DEBUG -O2 -Wall -Wextra -Wno-uninitialized -Wno-sign-compare -std=c++0x

all: main.exe
main.exe: main.o C1.o C2.o C3.o
    g++ $OPCIONS -o main.o C1.o C2.o C3.o -o main.exe
main.o: main.cc C1.hh C2.hh C3.hh
    g++ $OPCIONS -c main.cc
C2.o: C2.cc C2.hh C1.o C1.hh
    g++ $OPCIONS -c C2.cc
C3.o: C3.cc C3.hh C1.o C1.hh
    g++ $OPCIONS -c C3.cc
C1.o: C1.cc C1.hh
    g++ $OPCIONS -c C1.cc
clean:
    rm *.o
    rm main.exe

La primera línia fa que, quan posem $OPCIONS, sigui igual a tot el següent. Són els flags que s'utilitzaven a PRO2 quan la vaig fer i, en general, crec que encara són els mateixos més o menys. 
Aleshores, tenim les opcions. All, que es crida per defecte si no diem res, i tota la resta (main.exe, main.o, C2.o, C3.o, C1.o i clean). Cada una té, després dels dos punts, les de les que depèn i, a sota i amb un tabulat (compte, ha d'estar TABULAT, no serveixen espais), la o les instruccions que serveixen per crear-lo. Per exemple, per crear C1.o depenem de que existeixin C1.cc i C1.hh, i ho fem amb g++ $OPCIONS -c C1.cc. Això serveix perquè, quan cridem al make (per defecte anirà a all), mirarem si tenim main.exe actualitzat. Si no el tenim, mirarem què cal per tenir-lo. Necessitem tenir main.o, C1.o, C2.o i C3.o. Els farem en aquest ordre. Podem veure que tant main.exe, com C2.o, com C3.o, necessiten a C1.o. La gràcia del makefile és que només compila si hem modificat algun dels fitxers font, de manera que compilarà només un cop C1.o, i la resta ja sabrà que el tenim creat i actualitzat. 

Tenint aquest fitxer, podem compilar fàcilment qualsevol dels fitxers, compilar-ho tot a l'hora, netejar quan haguem acabat. 
$make
Aquesta compila tot el projecte
$make C1.o
Aquesta ens crea C1.o
$make main.o
Aquesta ens crea main.o, i tots els fitxers necessaris per crear-lo
$make clean
Aquesta elimina els fitxers .o, i main.exe

1 comentari:

  1. Entorn de desenvolupament virtualitzat:
    https://virtual-machines.github.io/PRO1-PRO2-FIB-VirtualBox

    ResponElimina