dijous, 6 de juliol del 2017

Aprendre a programar 11 - Introducció a la Programació Modular

Doncs sembla que ja hem acabat PRO1. Arribats a aquest punt, espero que als lectors els hagin agradat els meus articles, i els hagin ajudat a entendre coses. Aquí començarem a explicar l'assignatura de PRO2. En aquesta, el laboratori està més dividit en sessions, així que jo també ho faré així. Posaré més o menys de quina sessió és cada cosa. Ara començarem pel principi, que és la sessió 1 i la sessió 2. Les sessions i tot el que és necessari està a disposició de tothom, així que no cal estar estudiant PRO2 per fer això

Què és la programació modular?

La programació modular consisteix en dividir un programa en subprogrames més petits. Això ens permet tenir diferents mòduls escrits per diferents programadors, així com fer-lo més llegible i modificable. En essència, dividim un problema en problemes més petits, que són més fàcils de resoldre, i els resolem. Finalment, ho ajuntem tot. En C++, això es fa utilitzant una eina, que és la Classe. 

Què és una classe?

Una classe és una abstracció d'un objecte. Conté dins mètodes i atributs. Aquests poden ser privats, protegits i públics, tot i que a PRO2 els protegits no s'utilitzen. Per exemple, podem tenir una classe com la següent:

class Complex {
private:
double r, i;
public:
void inicialitzar (double real, double imaginaria) {
r=real;
i=imaginaria;
}
double real() {
return r;
}
double imaginaria() {
return i;
}
void mostrar() {
if(i<0) cout <<r<<" - "<<abs(i)<<'i'<<endl;
else cout <<r<<" + "<<i<<'i'<<endl;
}
};

Aquí tenim una classe que serviria per representar nombres complexos. Podem veure que d'atributs tenim dos nombres reals, corresponents a la part real i la part imaginària. No obstant, fixem-nos que són privats. Això ens impedeix accedir-hi directament. Per accedir-hi hem d'utilitzar les funcions públiques. És a dir, podem veure dos codis:

//Codi erroni, intentem accedir als atributs privats
Complex c;
c.r=3;
c.i=4;
c.mostrar();

//Codi correcte
Complex c;
c.inicialitzar(3,4);
c.mostrar();

Podem veure que no li passem c com a paràmetre. Això és perquè cada objecte (cada variable que creem amb una classe és un objecte) és propietari dels seus mètodes. 

Utilitzar classes permet que qui les utilitzi no s'hagi de preocupar gens per com funcionen les coses internament. Què més dóna si emmagatzemem la part real i la part imaginària en dues variables, en una parella, o de qualsevol manera? Només ens importa que ens les doni si les demanem. 

Com utilitzem les classes?

Inicialment no en programarem cap, sinó que utilitzarem les que ens donen. Ens donaran bàsicament tres fitxers, que són les capçaleres, el codi i un pdf on explica què tenim. 

Espera, per què fitxers diferents?

Hem dit que una de les raons per utilitzar programació modular és que podem tenir diversos programadors programant diferents mòduls. Aleshores, hem de permetre que cada mòdul es programi en un fitxer diferent, i després es pugui ajuntar fàcilment (i no em refereixo a copiar el codi de tots els mòduls en un únic fitxer). Per tant, el que es fa normalment és crear dos fitxers per cada mòdul

Capçaleres:

Podem dir que és un fitxer que ens diu "què tindrem". És a dir, conté les declaracions de les classes, funcions i tot el que necessitem, però sense el seu codi. És com dir-nos "ei, quan ajuntis el codi amb el meu fitxer, tindràs aquestes funcions". Per això, aquest fitxer l'ha de tenir tothom qui pretengui utilitzar el mòdul. Nosaltres farem que sigui un fitxer amb extensió hh (la h de headers), i l'inclourem mitjançant la directiva
#include "capçalera.hh"
Utilitzem "" enlloc de <> perquè, en el cas de <>, primer busca als directoris estàndard (que és on troba iostream, vector, algorithm i totes aquestes) i si no ho troba allà, se'n va al nostre directori. En canvi, amb "", primer busca al nostre directori i, si no ho troba, se'n va allà. 

Codi:

Aquí tenim el codi de tot el que hem declarat a les capçaleres. És un fitxer amb extensió cc, com els que hem utilitzat sempre. Aquest es pot compilar per separat de la resta, quedant un fitxer amb extensió o (d'objecte), i només necessitem tenir les capçaleres que utilitzarem, sense el seu codi. Per exemple, si tenim una classe Rectangle, que utilitza la classe Punt, només necessitarem Rectangle.hh, Rectangle.cc i Punt.hh. Quan compilem, tindrem un arxiu Rectangle.o. Això ens permet utilitzar classes sense saber ni quin és el seu codi, ja que a l'hora d'enllaçar tots els codis, utilitzarem els fitxers .o

Aleshores, com va això de compilar i enllaçar?

Fàcil. Primer compilem tots els mòduls, i després els enllacem. Així:
$g++ -c Classe1.cc
...
$g++ -c main.cc
$g++ Classe1.o Classe2.o ... main.o -o programa.exe

És a dir, primer compilem utilitzant -c, i un cop tenim tots els mòduls (recordem que ens cal un main que cridi tot), podem linkar. Utilitzant -o, diem com volem que es digui l'arxiu de sortida. Fixem-nos, però, que molts professors de PRO2 prefereixen fer
$g++ -o programa.exe Classe1.o Classe2.o ... main.o
És el mateix, simplement ho canvien d'ordre. Jo prefereixo la primera forma, però crec que és també qüestió de gustos.
Per altra banda, a PRO2 s'utilitza p2++, que és un àlies, igual que ho era p1++. Jo penso que és important utilitzar-lo, i parlo des de l'experiència, ja que no vaig arribar a utiltizar p1++ ni p2++. Si no el fas servir, et pots trobar que executes un programa que utilitza alguna cosa de C++11, i no et funciona (recordem que p2++ utilitza C++11). Si decideixes passar olímpicament, al menys recorda utilitzar els flags -Wall i -std=c++11

Comencem: La classe Estudiant

Els primers exercicis que utilitzen classes no estàndard són els que utilitzen Estudiant. Per utilitzar-ho ens donen un fitxer .pdf on crec recordar que està ben explicat. No obstant, en el moment que escric això, no tinc accés al disc de la FIB, i parlo de memòria. Per sort, m'he aconseguit els .hh i .cc. 
La classe en qüestió conté les següents funcions

Estudiant(): Declara un estudiant sense cap paràmetre. Concretament, li posa dni 0, i cap nota
Estudiant(int dni): Declara un estudiant amb dni=dni, i sense nota. El dni ha de ser >=0

void afegir_nota(double nota): Li afegeix una nota a un estudiant sense nota. Cal que estigui entre 0 i nota_maxima() incloses, cosa que en general, vol dir 0<=nota<=10. No obstant, no sempre és així
void modificar_nota(double nota): Modifica la nota. Cal que tingui nota abans

int consultar_DNI() const: Retorna el dni de l'estudiant
double consultar_nota() const: Retorna la nota de l'estudiant
static double nota_maxima(): Retorna la nota màxima d'un estudiant (per tots serà la mateixa). És static perquè així no cal declarar cap estudiant, pots fer Estudiant::nota_maxima(), ja que per tots serà la mateixa
bool te_nota()  const: Retorna true si l'estudiant té nota, false si no

void llegir(): Llegeix pel canal estàndard un estudiant (és a dir, dos enters, el dni i la nota). Si la nota no està al rang [0,nota_maxima()], l'estudiant resultant no té nota
void escriure() const: Escriu pel canal estàndard de sortida l'estudiant. Si no té nota, escriu un NP

És a dir, per utilitzar la classe Estudiant, tenim aquestes, i només aquestes (records de FM) funcions. Les dues primeres són les constructores, serveixen per crear un estudiant. Les 2 següents, són per modificar un estudiant ja existent. Les 4 següents, per consultar en un estudiant, i les dos últimes, de lectura i escriptura.
Per exemple, podríem tenir un exercici que digui "donat un estudiant i una sèrie de notes, treu pel canal estàndard el seu dni i la nota màxima". Podríem fer-ho així:

int main() {
    int dni;
    cin >>dni;
    Estudiant e(dni);
    double nota;
    while (cin >>nota) {
        if (e.te_nota()) {
            if (nota>e.consultar_nota() and nota<=e.nota_maxima())
                e.modificar_nota(nota);
        }
        else {
            if (nota>=0 and nota<=e.nota_maxima()) e.afegir_nota(nota);
        }
    }
    e.escriure();
}


Suposo que es veu més o menys com funciona. Bàsicament una manera senzilla d'imaginar-se això és
- Un objecte és la representació informàtica d'alguna cosa. El cotxe de la teva mare, o tu
- Una classe és l'abstracció d'un objecte. La classe Cotxe, per exemple, o la classe Estudiant
- Per tant, cada instància que fem d'una classe és un objecte
Aleshores, per interactuar amb qualsevol objecte, ens ho imaginem com una caixa negra. No podem veure què hi ha dins, i, per tant, no ho podem modificar. Però tenim les funcions que ens permeten interactuar amb ell, ja sigui consultar coses, modificar-les...

Resum de la classe Estudiant:

Com es crea un estudiant?
Crear un estudiant es fa amb les funcions creadores. En aquest cas tenim dues versions. La primera crea un estudiant buit, la segona amb dni. Com que no podem modificar el dni de l'estudiant, la primera versió serà només per llegir-lo per l'entrada estàndard, mentre que la segona serà la que ens permetrà afegir-li la nota directament. Es criden igual que quan creàvem una variable entera, o un vector (recordem que el vector és una classe). És a dir
Estudiant e1;//Crea un estudiant buit
Estudiant e2(666);//Crea un estudiant amb el DNI de Satanàs
Com es modifica l'estudiant?
Bàsicament podem actuar sobre la nota. Hi ha dues possibilitats. Si no té nota, li podem afegir, mentre que si ja la té, la podem modificar. Per exemple
Estudiant e(666);
double d, d2;
cin >>d;
e.afegir_nota(d);
cin >>d2;
if(d2>d) e.modificar_nota(d2);
Aquí no he tingut en compte què passaria si una de les notes és <0, o >nota_maxima(), perquè encara no he explicat les consultores. Però caldria fer-ho
Com consultem informació d'un estudiant?
Aquí tenim més coses que podem consultar. Podem consultar el seu DNI. Podem consultar també la seva nota. O la nota màxima, ja que no necessàriament ha de ser 10. Finalment, podem consultar si té nota o no, perquè potser encara no té nota. Es faria així:

Estudiant e;
...
double nota;
cin >>nota;
if (e.te_nota()) {
    if (nota>e.consultar_nota() and nota>=0 and nota<=e.nota_maxima())
        e.modificar_nota(nota);
}
else {
    if (nota>=0 and nota<=e.nota_maxima()) e.afegir_nota(nota);
}
Com es llegeix i escriu?
Aquesta és la més fàcil de totes. Simplement tenim dues funcions, una llegeix i una escriu. La de lectura llegeix dos números, un és el DNI i un és la nota. La d'escriptura escriu el DNI i la nota, si en té, o "NP" si no


Dit tot això, jo crec que ja es poden resoldre tots els exercicis de la classe Estudiant

La classe Cjt_estudiants:

Un cop ja hem entès a la perfecció la classe Estudiant, comencem amb la classe Cjt_estudiants. Aquesta és una classe més complexa, que emmagatzema una sèrie d'estudiants. Té més funcions públiques, que són


Cjt_estudiants(): Constructora. Crea un conjunt buit

void afegir_estudiant(const Estudiant &est): Afegeix l'estudiant est. Aquest no hi ha de ser, i el conjunt ha de tenir espai
void modificar_estudiant(const Estudiant &est): Modifica l'estudiant amb el DNI d'est. 
void modificar_iessim(int i, const Estudiant &est): Modifica l'estudiant a la posició i

int mida() const: Ens dóna la mida del conjunt
static int mida_maxima(): Ens dóna la mida màxima. 
bool existeix_estudiant(int dni) const: Ens diu si existeix un estudiant amb DNI=dni
Estudiant consultar_estudiant(int dni) const: Ens retorna l'estudiant amb DNI=dni. Ha d'existir
Estudiant consultar_iessim(int i) const: Ens retorna l'estudiant a la posició i. 1<=i<=mida()

void llegir(): Llegeix un estudiant pel canal estàndard. Primer llegeix un número n, i després n Estudiants
void escriure(): Escriu els estudiants en ordre ascendent per dni

El que hem de tenir controlat és que els estudiants estan ordenats de manera creixent per dni. Això fa que consultar un estudiant per dni tingui cost logarítmic (fem una cerca dicotòmica), mentre que tenint la posició, té cost constant

Podem provar de resoldre el problema X74882, Actualitzar un conjunt d'estudiants. 

void modificar(Cjt_estudiants& c1, const Cjt_estudiants& c2) {
int n=c1.mida();
for (int i=1;i<=n;++i) {
Estudiant e1, e2;
e1=c1.consultar_iessim(i);
e2=c2.consultar_iessim(i);
if (e1.te_nota()) {
if (e2.te_nota() and e1.consultar_nota()<e2.consultar_nota()) 
c1.modificar_iessim(i,e2);
}
else if (e2.te_nota()) c1.modificar_iessim(i,e2);
}
}

int main() {
Cjt_estudiants c1, c2;
c1.llegir();
c2.llegir();
modificar(c1,c2);
c1.escriure();
}

Bàsicament la funció el que fa és recórrer tot el conjunt. Com que sabem que està ordenat, i que consultar l'ièssim és molt més eficient, anem consultant l'element 1, després el 2, després el 3, i així fins al final. 

Cap comentari:

Publica un comentari a l'entrada