dissabte, 8 de setembre del 2018

Aprendre a programar aux - Instal·lar Linux (o no)

Quan comencem a programar podem tenir el dubte. Ens instal·lem Linux? Cal? No cal? Què és millor? Com es fa?
Jo en aquest post intento una mica explicar si cal o no cal, i sobretot veure com es fa si decidim fer-ho.

Per què Linux?

Abans de res, cal saber què és Linux. Linux és bàsicament el nucli d'un sistema operatiu, que és GNU. De fet, gairebé sempre que diem Linux, ens referim a la combinació GNU/Linux, però això és més un tema lingüístic

Per programar ens interessa Linux per diverses raons. Una bàsica és que té una consola molt potent. Instal·lar el software per programar és molt fàcil, i es pot fer directament des d'aquesta. De fet, està tan bé aquesta consola, que a Windows 10 la pots instal·lar si habilites les opcions de desenvolupador. Hi ha més raons encara, però per principiants tampoc afecta massa

Per a ús quotidià també ens pot interessar. En general és gratuït i lliure, tot i que això depèn de les distros. Així que no hem de posar-nos a piratejar-lo ni pagar-lo. A més, tenim llibertat absoluta per fer el que vulguem amb ell, cosa que mola molt quan et vols posar a tocar-lo per dins i modificar coses. I és molt fàcil d'instal·lar quan n'aprens, molt més que Windows. Jo us ho recomano

Doncs a posar-nos Linux

Fixeu-vos bé que he dit "posar-nos" i no instal·lar. Això és perquè no necessàriament ens l'hem d'instal·lar per utilitzar-lo. Així que, malgrat que jo sempre recomani la instal·lació normal, us explicaré les opcions que conec, perquè pugueu triar

Tinc Windows 10, no vull treure'l ni haver de sortir d'ell

En aquest cas, el millor que podeu fer és instal·lar bash (l'intèrpret de comandes més utilitzat a Linux) com un programa més. Bàsicament permet tenir la "mateixa" consola que teniu a la FIB. A aquesta us instal·leu g++, editeu l'arxiu ~/.bashrc per afegir el p1++, i ja ho teniu. Per instal·lar bash, podeu seguir aquest article (o qualsevol altre, simplement és el primer que he trobat)

Tinc un Windows diferent del 10 però no vull treure'l ni haver de sortir d'ell

Aleshores, podeu virtualitzar el Linux. Podeu fer-ho amb VMware, amb VirtualBox o amb altres. Això el que fa és simular que teniu Linux instal·lat, però des de dins del Windows. 
A l'hora d'instal·lar, assigneu-li una quantitat considerable de RAM i una mica d'espai de disc (jo menys de 30 GB no ho veig). Us heu de descarregar la iso del Linux que vulgueu (després us aconsello sobre això), i fareu una instal·lació normal, però des del programa. Per tant, seguiu per la part de la instal·lació

Vull tenir el Linux amb el seu entorn gràfic, però no tinc espai per fer una partició

Pots posar-te'l en un live USB. Un live USB està pensat per utilitzar el sistema tenint-lo en un USB. Normalment aquest USB és de només lectura, i no podeu desar el que feu, però és una opció. Això es pot canviar, però si no és un USB 3.0 no us ho recomano (i si ho és, feu la prova i mireu que tal va la velocitat). 
Algunes distros de Linux ofereixen un mateix arxiu iso per virtualitzar i per instal·lar, altres n'ofereixen de diferents. Us baixeu el que toqui, l'instal·leu en un USB, arrenqueu des d'ell i ja podeu córrer Linux

El principal problema que podem tenir amb això és tornar a instal·lar g++, p1++ i tot això. Per evitar-vos la feina, podeu utilitzar un script com el següent

#!/bin/bash

alias='g++ -D_JUDGE_ -DNDEBUG -O2 -Wall -Wextra -Werror -Wno-sign-compare -Wshadow'

apt install g++
apt install geany
echo 'p2++='"'"$alias"'" >>~/.bashrc

Podeu canviar geany pel vostre editor preferit. Ho deseu com a script.sh, o el nom que vulgueu. Aleshores, per executar aquest script heu de fer una de les dues següents

(OPCIÓ 1)
$chmod +x script.sh
$sudo ./script.sh

(OPCIÓ 2)
$sudo bash script.sh

Si us sóc sincer, no he provat aquest script. Si us falla, em podeu enviar un mail de queixa i ja ho revisaré
Per cert, vigileu, ja que hi ha cometes simples i cometes dobles. Copieu i enganxeu per evitar problemes

Vull instal·lar Linux

Per instal·lar Linux hem de fer una sèrie de processos

Triem la distro

Hi ha moltíssimes distros de Linux. Jo, per a algú que sigui principiant, li recomano una que sigui basada en Ubuntu (o Ubuntu directament). Les que us recomano són les següents

Ubuntu
Lubuntu
Kubuntu
Xubuntu
Linux Mint Cinnamon
Linux Mint MATE
Linux Mint Xfce

Jo buscaria fotos i triaria la que us agradi estèticament. No obstant, us puc explicar en què es diferencien

Ubuntu és d'on parteixen totes aquestes (Ubuntu parteix de Debian, però Debian el deixaria per més tard). Utilitza GNOME, que és un entorn d'escriptori que em sembla particularment lleig i incòmode, però conec gent a qui li encanta. Per a gustos, colors. Mireu fotos i ja
Lubuntu és Ubuntu però amb l'entorn d'escriptori LXDE. És un entorn d'escriptori molt lleuger, de manera que corre bé en ordinadors més vells. El contra que li veig és que és una mica massa senzill estèticament
Kubuntu és Ubuntu amb KDE. És el mateix entorn d'escriptori que tenen els ordinadors de la uni, així que us sonarà i segurament sigui més fàcil d'utilitzar al principi. És el més pesat, però, i té algun bug. No obstant, suda, va bastant bé
Xubuntu és Ubuntu amb Xfce, un entorn molt lleuger, però no tant com LXDE

Linux Mint és una distro basada en Ubuntu, però amb més software preinstal·lat, de manera que pot ser més senzilla al principi. Podeu triar entre Cinnamon, MATE i Xfce. Jo prefereixo Cinnamon, però MATE també està bé. I si teniu un ordinador vell, doncs Xfce

Tampoc és com triar entre Charmander, Squirtle i Bulbasaur (Charmander 100%), ja que sempre podeu canviar en qualsevol moment. Jo us explicaré com fer-ho de manera que el canvi sigui fàcil

Crear l'USB d'instal·lació

En totes aquestes que he dit, s'utilitza la mateixa imatge pel live USB i per la instal·lació, de manera que no ens equivocarem. Vigilem de baixar la de 64 bits si tenim un sistema de 64 bits, que serà molt millor.
Per crear l'USB d'instal·lació, des de Windows utilitzarem un programa que es diu Rufus o un que es diu Unetbootin. Els dos ens ho faran. Necessitem un USB prou gran com perquè hi càpiga la imatge, i òbviament baixar-la. Cada una a la seva web (les tres de Linux Mint estan a la web d'aquest)

(si volem conservar el Windows)

Windows té un mal costum, que és arruinar-te les particions. Per evitar que et porti problemes, li hem de dir que s'oblidi de la part del disc on instal·larem Linux. Anem al gestor de discs, seleccionem el (o els) discs que volem utilitzar per instal·lar Linux, i li donem a la opció de reduir. Seleccionem l'espai que volem donar al Linux, i Windows passarà a ignorar aquell espai

Instal·lació

Hem d'arrencar des del USB. Això es pot fer quan s'està encenent l'ordinador, ens dirà quina tecla hem de prémer per fer-ho (F algo normalment, o Esc, o alguna altra). O podem configurar a la bios perquè arrenqui preferentment des del USB. Això com veieu

Algunes imatges ens permeten instal·lar directament, altres només donen l'opció d'arrencar el live USB, i des de dins poden instal·lar. Jo arrenco sempre el live USB, comprovo que vagi bé, i instal·lo des d'allà
La instal·lació en general és molt senzilla. No obstant, jo distingiria entre instal·lació amb HDD i instal·lació amb SSD+HDD

HDD

Si només tenim un HDD, tot anirà instal·lat al mateix disc. Jo us recomano crear dues particions, una pel directori / i una pel /home. Si teniu intenció d'instal·lar més d'una distro, una tercera pel swap, però si no, no cal. Pel / posaria unes 15-20 GB, tot i que podeu posar més, mentre que pel /home la resta. Si no feu una pel swap, poseu-li més al /, mínim 20
Per fer això, el que hem de fer és quan ens deixa triar si conservar l'antic SO o sobreescriure, a sota hi ha una opció que diu "alguna altra cosa". Amb aquesta, triem l'espai de disc que hem reservat al Windows (o tot el disc si volem sobreescriure), i fem les particions des d'aquí mateix

L'avantatge de crear particions separades pel / i el /home és que si volem canviar de distro, només sobreescriurem el /, i les dades, configuracions i tal es conservaran

SSD+HDD

Quan tenim un SSD i un HDD, el que ens interessa és tenir el sistema operatiu al SSD, però totes les dades al HDD. I això és el que us vull ensenyar
A l'hora d'instal·lar, feu una instal·lació normal, no cal que feu res estrany. No cal ni que us compliqueu la vida en crear una partició separada pel /home, les dades no estaran aquí tampoc. Sí que podeu fer-la per conservar configuracions al canviar de SO, però caldre, no cal. Com vulgueu

Un cop tinguem el SO instal·lat, però, cal fer cosetes per fer que vagi tot bé. Primer de tot hem d'identificar l'etiqueta que té la partició del HDD que volem muntar (jo recomano que no compartiu dades entre Windows i Linux, però podeu fer-ho). Això ho farem amb la comanda
$sudo fdisk -l
Podeu veure que en el meu cas, les particions ja estan fetes. Veiem que hi ha dos discs, que són /dev/sda (SSD) i /dev/sdb (HDD). Identifiqueu quina és la partició que voleu utilitzar

Si l'heu de formatar, jo us recomano utilitzar un programa que es diu gparted. L'instal·leu fent
$sudo apt install gparted
Un cop instal·lat, trieu el disc i formateu amb el format ext4
Per accedir al disc, heu de muntar-lo a algun lloc. Jo faria una cosa així
$sudo mkdir /mnt/HDD
$sudo chown -R oriol /mnt/HDD
$sudo mount /dev/sdb3 /mnt/HDD
(sdb3 o la que sigui)
Primer crea una carpeta (/mnt/HDD). Fa que sigui propietat teva, de manera que la podràs editar. I li diu que munti el disc allà, de manera que quan obris /mnt/HDD estaràs accedint al disc

El problema és que seria pesat muntar-lo cada cop que obrim l'ordinador. Per tant, editarem l'arxiu /etc/fstab per dir-li que munti el disc només arrencar l'ordinador. Ho farem fent
$sudo geany /etc/fstab
Aleshores, hem d'afegir una línia com la següent
/dev/sdb3       /mnt/HDD        ext4    defaults        0       1
Òbviament canviant sdb3 per la partició que vulguem que quedi muntada
Desem, i ja es muntarà sol al arrencar. Podeu reiniciar per comprovar que s'hagi fet bé, o seguir i ja ho mirareu després

Ara toca fer que totes les dades es desin al HDD. Ho farem des de la terminal, però ho podríem fer gràficament. 
$cd
$ls
Aquí veurem com tenim una sèrie de carpetes (Baixades, Documents, Imatges, Escriptori...). Hem de crear els seus equivalents al HDD. Ho farem fent
$mkdir /mnt/HDD/Baixades
$mkdir /mnt/HDD/Documents
...
Una per cada carpeta (amb els noms que tinguin, si heu instal·lat en castellà seran diferents)

Fet això, toca fer que totes les carpetes del home apuntin allà. Ho farem fent les següents comandes
$rm -r Baixades
$ln -s /mnt/HDD/Baixades Baixades

$rm -r Documents
$ln -s /mnt/HDD/Documents Documents

Això per totes les carpetes que tinguem creades. Així, un cop haguem acabat, seguirem tenint les mateixes carpetes, però estaran al HDD. I al SSD hi ha links cap a elles, de manera que quan al Chromium diu que la carpeta de baixades és /home/oriol/Baixades, en realitat està fent referència a /mnt/HDD/Baixades

Fent tot això, pot ser que el sistema hagi detectat que un directori no existia, i hagi decidit fer alguna cosa. Per exemple, jo em vaig trobar que l'escriptori passava a estar allotjat al $HOME enlloc d'estar al $HOME/Escriptori com estava prèviament. Per comprovar-ho, feu això
$cat ~/config/user-dirs.dirs

Si veieu que us mostra coses diferents (per exemple, que l'escriptori està al $HOME a seques), doncs ho modifiqueu i ja. Que us quedi dirigint als links que heu creat


Tot això ho hem fet, com ja haureu suposat, per evitar escriptures innecessàries al SSD. Ho agraireu, ja que us durarà més

Desambiguació:

Alguns dels temes que he comentat poden ser complicats d'entendre. Molts conceptes desconeguts si ets principiant. Així que aquí estan
Partició: Un disc dur té la capacitat que sigui. Però no és un bloc, sinó que el podem dividir en trossos. Així podem tenir una part d'aquest destinada a una cosa, i una altra per una altra cosa. Les particions es fan i desfan fàcilment, no s'ha de tenir por de fer-les
Home: El Home és on els usuaris tenen les seves coses, configuracions i tal. Hi ha la carpeta /home, que és d'on pengen els de tots els usuaris. Un usuari accedeix al seu amb ~ o amb $HOME, tot i que ho podria fer amb /home/nom_usuari
Consola: És l'intèrpret de comandes, que ens permet fer les coses en mode text. Un cop la domines és molt més fàcil que utilitzar l'entorn gràfic
Distro: Hem quedat en que GNU és un sistema operatiu, que s'utilitza amb Linux com a nucli. El que passa és que no instal·lem tot el que té GNU, sinó que volem els programes que puguem necessitar. Una distribució porta una sèrie de software preinstal·lat, tals com un entorn gràfic, intèrpret, gestor de paquets i altres coses
HDD: El disc dur de tota la vida. Si no saps quin tens, segurament és aquest
SSD: Un disc d'estat sòlid. Tècnicament no és un disc dur, però en un abús de llenguatge a vegades li diem així. És molt més ràpid que un HDD, però té escriptures limitades, de manera que no convé abusar-ne. Per això normalment tenim un SSD petit on posem el sistema operatiu, i un HDD gran amb les dades

divendres, 16 de febrer del 2018

Aprendre a programar 15.2 - La classe BinTree

Aquest és el típic article que no esperes haver de fer. Després de molt temps utilitzant la classe Arbre a PRO2, vaig escriure l'article que parlava d'ella. I va, i just al següent quadrimestre, decideixen canviar-la. No em malinterpreteu, penso que el canvi ha sigut a millor, però m'han donat feina. Així que aquí anem

Per què una nova classe?

Realment per moltes raons. La classe Arbre era una porqueria. És la típica que implementes tu, i et posen mala nota. I per moltes raons

Els comentaris

Sí, sembla una raó superficial, però jo sóc el típic que, quan utilitzo una classe, m'agrada saber bé com funciona. I amb aquesta, que a més el PDF era fluixet, doncs m'agradava mirar-me el .hh. I venien ganes de plorar. No pots posar la capçalera de la funció, un comentari al mig, i després el cos de la funció. És com partir la funció entre dos. Una mica com l'AVE de Múrcia. 
Deixant de banda que el comentari sembla un embarbussament, oi que talla la funció d'una manera que no és normal?

Noms de les funcions

En general estaven molt mal triats. Una funció per buidar un arbre que es diu a_buit, una per modificar-lo que es diu plantar, una que es diu arrel... Poc abans de l'examen, em van dir una cosa que ho representa bastant bé. "plantar, arrel... Això és programació o jardineria?"

Funcionament de la classe

Així com les dues raons anteriors no tenen gaire importància, aquesta en té, i molta. Com funciona la classe per dins?

Com podem veure si ens mirem el .hh, que suposo que penjaré a algun lloc (encara que si es perd per sempre, tampoc perdrem gaire), declarem una struct que es diu node_arbre, que conté dos node_arbre* i la informació d'aquell node. És a dir, la idea és que la classe té una sèrie de nodes, i cada node conté dos punters pels següents nodes, i un valor. I, com a únic atribut de la classe, un punter que apunta al primer node. 

Aleshores, un arbre podem dir que conté un valor i dos fills, dret i esquerre. Què passa si volem accedir a aquests fills? Aquí ve la part divertida

La funció fills

La funció fills rep dos arbres buits i diferents, i el que fa és posar el fill esquerre al primer, i el fill dret al segon. I ja? No! No volien copiar tot el fill, ja que això té un cost massa gran. Podrien haver fet una funció que retorni cada fill per referència, de manera que es pugui consultar sense copiar l'arbre, però això era una bona solució. 
Enlloc d'això, fan que per accedir als fills, es modifiqui l'arbre. Quan tu crides la funció fills, l'arbre actual es buida, el seu arbre esquerre se'n va al primer paràmetre, i el dret al segon. Per tant, primer de tot t'estan obligant a passar els arbres per referència normal, no constant, ja que si no els modifiques. I a sobre, t'obliguen a vigilar tu que al final de la funció l'arbre no s'hagi modificat.
Per exemple, imaginem que volíem implementar una funció comptar_nodes_arbre, que rep un arbre i compta quants nodes té. No volem fer cap modificació a l'arbre. Però ens obliguen a modificar-lo per fer la funció. I clar, era una font potencial d'errors

La classe BinTree

Aquesta classe és una classe ben feta. Molt ben feta, de fet. Només li veig un error, que és utilitzar "using namespace std" a dins d'una capçalera. Com funciona?
Internament, té un punter a un node. Igual que a la classe Arbre, no? No! És un tipus de punter molt especial. És un shared_ptr. Veurem per què serveix això més tard (tot i que de moment no cal saber-ho)
Podem veure també que té quatre constructores. 
BinTree (): Construeix un BinTree buit. Sense massa misteri
BinTree (const T& x): Construeix un BinTree que a l'arrel conté x, i no té fills
BinTree (const T& x, const BinTree& left, const BinTree& right): Construeix un BinTree que a l'arrel conté x, i té com a fills aquests dos arbres
BinTree (shared_ptr<Node> p): Serveix per construir un BinTree passant-li un punter. Tampoc cal saber per què cal, és una funció privada

Per altra banda, té una sèrie de funcions per treballar amb el BinTree, amb uns noms bastant ben triats respecte la classe antiga. 
bool empty () const: Ens diu si és buit o no. Cost constant
BinTree left () const: Ens retrna el fill esquerre. Cost constant, no lineal com podríem pensar
BinTree right () const: Ídem amb el dret
const T& value () const: Ens retorna el valor. Cost constant també

Sembla bastant fàcil treballar amb ella, no? Ho és. L'únic difícil és entendre la recursivitat que envolta els arbres. Podem veure unes quantes coses que podem fer amb ells

Comptar nodes

int compta_nodes(BinTree<int> b) {
    if (b.empty()) return 0;
    return 1+compta_nodes(b.left())+compta_nodes(b.right());
}

Mola veritat? Si és buit, té 0 nodes (obvi). Si no, sabem que la quantitat de nodes de l'arbre serà la suma de la quantitat de nodes dels seus subarbres i ell mateix (1). 

Incrementa en 1 tots els elements d'un arbre

void incrementa(BinTree<int>& b) {
    if (b.empty()) return;
    BinTree<int> left=b.left();
    BinTree<int> right=b.right();
    incrementa(left);
    incrementa(right);
    b=BinTree<int>(b.value()+1,left,right);
}

Senzill, veritat? Aquí veiem una cosa, i és que copio els subarbres dret i esquerre, i després faig una nova constructora. És perquè les funcions són const, i per tant, no podem modificar-ho. Igualment la constructora té temps constant, no ens hem de preocupar
Per altra banda, malgrat que tendim a fer això, jo penso que no és una opció massa bona. Hi ha una de millor, que seria

BinTree<int> i_incrementa(BinTree<int> b) {
    return BinTree<int>(b.value()+1,incrementa(b.left()),incrementa(b.right()));
}

void incrementa(BinTree<int>& b) {
    b=i_incrementa(b);
}

Obté camí més llarg

En aquest cas farem que, per obtenir el camí més llarg, el guardi en una pila. En cas d'empat, agafarem el de l'esquerra. Ho podem fer així
stack<int> cami_llarg(BinTree<int> b) {
    if (b.empty()) return stack<int>();
    stack<int> left=cami_llarg(b.left());
    stack<int> right=cami_llarg(b.right());
    if (left.size()<=right.size()) {
        left.push(b.value());
        return left;
    }
    right.push(b.value());
    return right;
}

Consells per treballar amb arbres

Treballar amb arbres és una cosa que a molta gent se li fa una muntanya. Però realment no és més difícil que la resta. De fet, jo crec que els problemes de llistes són més complicats que els d'arbres
Fonamentalment, per treballar amb arbres cal pensar de manera recursiva. Un exemple

Com llegim un arbre en preordre?

Ens diuen que un arbre en preordre és quan et donen l'arrel, després l'arbre esquerre, i finalment l'arbre dret. Doncs hem de programar exactament això. Llegim l'arrel, llegim dos arbres (utilitzant de manera recursiva la mateixa funció), i ja ho tenim. 

Com calculem l'alçada d'un arbre?

Suposem que definim l'alçada d'un arbre com l'alçada del subarbre més alt +1. Així, un arbre buit tindria alçada 0, i a partir d'això, definim l'alçada de qualsevol arbre. Com ho calculem?
Primer de tot, el cas base. Un arbre buit té alçada 0, de manera que si ens demanen l'alçada d'un arbre buit, retornem 0. Si no, retornem 1+max(alçada1,alçada2). I per calcular aquestes dues alçades, cridem a la mateixa funció amb els subarbres esquerre i dret 

Com sumem els elements d'un arbre?

En aquest cas faré directament el codi de la funció, perquè m'he cansat de descriure el que han de fer les funcions. Suposo que s'entén el que fa

int suma_nodes(BinTree<int> b) {
    if (b.empty()) return 0;
    return b.value+suma_nodes(b.left())+suma_nodes(b.right());
}

I un truc

Hi ha vegades que, pel que sigui, no ens funciona el pas per referència. Ens podem estar una bona estona per trobar l'error (perquè sí, serà un error). Però si no us voleu complicar la vida, podeu carregar-vos la referència, ja que la còpia d'un BinTree triga temps constant (que a més, no és gaire temps). Segurament afegir un const us arreglaria el problema, però per què? 

dimecres, 15 de novembre del 2017

Aprendre a programar aux - Bones i males pràctiques a PRO1

Aquest article se'm va acudir aquest dilluns, després de veure algunes correccions del primer parcial de PRO1. Veient diverses coses que ha entregat la gent, se m'ha acudit que potser calia parlar de bones i males pràctiques, de manera que no es tornin a veure coses així de rares.

Indentació

Aquí no hi ha gaire a dir. El codi, ben indentat. Què vol dir això? Que has de ser coherent. Primer cal determinar com ho faràs, si amb espais o tabulacions. Jo sóc molt més partidari de tabulacions, perquè així cadascú es configura l'editor per veure-ho com li agradi. Després cal definir l'amplada, jo tinc configurat perquè els tabulats ocupin l'equivalent a 4 espais. I un cop tens decidides aquestes dues coses, fes-ho sempre igual. 

Quan tabules?

Quan entris a un tros de codi que està dins d'alguna cosa (if, else, while, for, switch, una funció...), un nivell més. Així, quedaria
if (a) {
    codi
    if (b) {
        més codi
    }
}

Amplada de les línies

En resum, no es poden fer línies de més de 80 caràcters. Si tens una línia que ocupa més, mala idea, perquè costa molt de llegir (i més encara si utilitzes editors des de la consola). Si a nivell de PRO1 arribes a amplades així, probablement estàs fent alguna cosa malament. Però per arreglar-ho, sempre pots seguir a la línia següent. Poses simplement una \, i saltes a la següent línia 
int n=1+2+3+4+5+6+7+8+9+10+11+12+13+14+15+\
        16+17+18+19+20+21+22+23+24+25+26+27;

Les inclusions es fan bé

Sí, tal qual. Comencem forts. No pot ser veure codis de gent que utilitza una plantilla plena d'inclusions. O gent que no en posa algunes. Posa totes les que necessitis, i només les que necessitis. 

Algú s'estarà preguntant com pot ser que no incloguis coses que calen. Molt senzill. La llibreria iostream inclou la llibreria string, de manera que si utilitzes iostream, pots utilitzar strings sense incloure la seva llibreria. Però no té cap sentit fer-ho. Així que inclou tot el que calgui

Per altra banda, no em serveix que em diguis "no, si jo només incloc una llibreria, bits/stdc++.h. De debò, no inclogueu aquesta porqueria. Primer, perquè no és una llibreria estàndard, i segon, pel que he dit d'incloure només el que calgui. Ni explicaré què fa, no voldria ser responsable de que algú la utilitzi

El main no retorna res

Sí, en C era una pràctica comuna acabar el main amb un "return 0". En C++ no cal, i posar-lo és redundant

Respecta l'àmbit de les variables

Una variable té un àmbit. És la part del codi on existeix. Per exemple, al codi següent:
int s=0;
for (int i=0;i<n;++i) {
    int aux;
    cin>>aux;
    s+=aux;
}
cout<<s<<endl;

Aquí tenim quatre variables. Són s, i, n i aux. n no ens preocuparem del seu àmbit, assumeixo que ja ve declarada d'abans. Veiem que s es declara abans del for, mentre que al for declarem dues variables (i i aux). Doncs bé, l'àmbit d'aquestes dues és el for. Concretament, aux es declara a cada iteració del for, mentre que i dura mentre duri el for. Això serveix per utilitzar les variables només on calen. Si no ens cal la i fora del for, no la volem tenir declarada. Per això es diu que les variables s'han de declarar sempre a l'àmbit més intern possible

El for es fa bé

Un for té una estructura molt clara. I és la següent:
for (inicialització;comparació;modificació) {
    cos
}
Respecteu-la. No deixeu buida cap de les parts. Si ho feu, en el millor dels casos tindreu un codi fastigós, en el pitjor un bucle infinit. 
Quan s'inicialitza, es declara una (o més, però en general una) variable de control. Declara-la allà mateix. Al cap i a la fi, és una variable que en el 99,99% dels casos, només necessites al for. Normalment, quedarà una cosa molt similar a la següent
for (int i=0;i<n;++i)
Hi ha variacions, òbviament, potser arriba fins a n inclosa, a vegades es fa al revés (començant per n, i arribant a 0). I algun cop potser enlloc d'augmentar d'un en un, augmenta de dos en dos, o similar. Però la idea és que sigui similar a això

Important: Si has deixat buida una de les tres parts, probablement estigui malament
Important 2: No serveix posar a=a perquè no quedi buit. Sembla una tonteria, però és que ha passat
Per cert, també està prohibit modificar la variable de control. Jo no ho prohibiria, malgrat que em sembla una mala pràctica, però el tema és que no es pot. 

No trenquis el codi

Hi ha vegades on volem fer un bucle, però sabem que en algun moment potser ens hem d'aturar. I, buscant codis, a vegades et trobes una cosa com la següent

for (int i=0;i<n;++i) {
    ...
    if(...) break;
}

Això trenca molt el codi. No és gens visual, i està prohibit a PRO1. No ho feu, a més és cutre i lleig. En tot cas, una alternativa vàlida per PRO1 seria la següent

bool fi=false,
for (int i=0;i<n and not fi;++i) {
    ...
    fi=...;
}
Fent funcions hi ha una altra possibilitat, però ja l'explico més endavant 

Tot això no només s'aplica aquí, també s'aplica amb continue i goto

Booleans

Quan treballes amb booleans, saps que un booleà té dues possibilitats (true i false). Una condició (per exemple a==b) a C++ retorna també un booleà. Per tant, una cosa que no mola gens és la següent
bool b;
//codi, en algun moment b agafarà valors
if (b==true) ...
else ...

Això és completament absurd. b serà true o false. La comparació retornarà true o false. Per tant, per què cal comparar? Podem fer directament
if (b) ...
else ...

De la mateixa manera, si volguéssim comprovar que b és fals, podríem fer
if (not b) ...
else ...

Això també aplica per donar valors a un booleà. No pot ser que facis el següent
bool b;
if (condicio) b=true;
else b=false;

És molt millor fer
bool b=condicio;

Lazy evaluation

Això no ho he traduït, perquè no s'acostuma a veure traduït. Seria com avaluació mandrosa, o una cosa així. En essència es basa en les següents tautologies:

true or x=true
false and x=false

Aleshores, per què dedicar temps a mirar si x és cert o fals, si al cap i a la fi, podem saber-ho ja. Doncs el programa no ho mirarà. Això ens pot aportar avantatges. Per exemple
double a, b;
cin >>a>>b;
if (b!=0 and a/b>1) ...
else ...

No facis coses prohibides

Sembla una obvietat, no? Doncs no ho és. I no només pot implicar un 0 de manual, sinó que et poden invalidar l'automàtic també. Poso a continuació una sèrie de coses prohibides a PRO1 (ante la duda, pregunteu):
-Modificar la variable de control d'un for dins del cos d'aquest
-Utilitzar punters
-New i delete
-goto
-try, catch i throw
No necessitareu excepcions per res. Si el vostre codi us llença una excepció, probablement esteu fent alguna cosa molt malament, i per tant, arregleu el codi. No serveix capturar l'excepció i tractar-la
-template
-class
-crides a sistema
És molt típic veure gent que posa al final del codi un system("pause"). No ho feu. Si executeu des de consola, no cal. Si igualment necessiteu que s'esperi per veure-ho, poseu cin.get(). Però millor encara si no ho poseu
-Directives del preprocessador. És a dir, res que tingui un # a davant, excepte #include
-Llegir i escriure estil C. És a dir, scanf, printf, fprintf, write, read i similars
-Operacions sobre strings que no siguin declaració, assignació, comparar, llegir i escriure
Les següents es permeten:
string s;
string s="Hola";
if (s1<s2)...
cin >>s;
cout<<s<<endl;
Fins i tot, si jo fos el profe, us permetria la següent:
cout<<string(n,'*')<<endl;//mostra n asteriscs
Al cap i a la fi, no és res més que una de les opcions que creen un string. No obstant, pregunteu al profe

No es val utilitzar coses com la concatenació. A partir de que es facin vectors sí que es permeten més coses, en essència treballar amb ells com si fossin vectors, però inicialment no
-vectors abans de ser explicats a classe. Arrays mai es poden utilitzar
-operacions sobre vectors no explicades. 
És a dir, declarar-los sí, size també, accedir amb [] també. Push_back si inicialment no sabem la mida i ens fa un codi més senzill. La resta, no
-Inclusions que no siguin iostream, cmath, string, vector, algorithm
-Variables globals (excepte constants)
-Tipus no explicats. 
És a dir, utilitza bool, int, double i ja (string també, però no és un tipus). Cap altre. Ni short, ni long, ni unsigned, ni float, ni coses que se t'acudeixin
(enums es permeten. Estan desrecomanades, però es permeten)

Funcions i procediments. 

En el cas de les funcions/procediments (un procediment no retorna res, però jo en general parlaré de funcions), hi ha unes quantes coses a tenir en compte

Declaració

Les funcions es declaren donant l'especificació directament, ja que no farem recursivitat creuada ni coses rares. Per tant, posarem el codi en el moment de declarar-la. Com que fem això, hem de tenir en compte que si una funció B utilitza una funció A, la funció A ha d'estar abans de la B

Pas de paràmetres

Les funcions en general reben paràmetres. Quan són paràmetres normals (enters, doubles, strings normalets), es passen normal (és a dir, per còpia). Si volem que la funció modifiqui el paràmetre, o bé passar-li una cosa que ocupi molt (strings llargs, vectors o structs molt grans), li passarem per referència. Si volem que no es pugui modificar, serà referència constant. Per exemple
void foo (const string& biblia_en_vers) //No modifica
void foo2 (string& biblia_en_vers) //Modifica

Retorn

Per retornar es fa amb la paraula clau "return". Hem de posar què volem retornar (excepte als procediments), i ha de ser del tipus que retorna la nostra funció, o bé poder-s'hi convertir. És a dir, si ha de retornar un enter, podem retornar un enter directament, o bé un double (es convertirà a enter, perdent els decimals que pugui tenir)

Retorn i condicions

A vegades el retorn d'una funció depèn d'una o més condicions. En aquest cas, una cosa intuitiva seria:
int ret;
if (...) ret=..
else ret=...
return ret;

Això ho fan a algun problema de PRO2, però no els feu cas. És una mala decisió. Una altra cosa que se'ns pot acudir és
if (...) return ...;
else return ...;
Aquesta ja està prou bé. No obstant, com que quan retornem sortim de la funció, sabem que si seguim a la funció entrarem sí o sí al else. Per tant, pot quedar simplement
if (...) return ...;
return ...;

Retorn i bucles

Una mica com abans, hi ha situacions on podem saber ja el valor de retorn dins d'un bucle. En aquest cas, podem simplement retornar-ho dins del for. Per exemple
bool es_primer(int n) {
    for (int i=2;i*i<=n;++i) {
        if (n%i==0) return false;
    }
    return true;
}
Es pot fer diferent, podríem fer la que hi ha a continuació, però és més lleig i pitjor en general en tots els aspectes
bool es_primer(int n) {
  bool res=true;
    for (int i=2;i*i<=n and res;++i) {
        if (n%i==0) res=false;
    }
    return res;
}

Funcions i procediments recursius

En general, una funció recursiva es defineix amb un cas base, i una manera de reduir-ho tot fins al cas base. Per tant, hem de fer-ho de manera que en el cas base no torni a cridar a la recursiva. Per exemple, si tenim la funció per calcular el factorial, hem de fer que quan n==1 o n==0 ens retorni 1, i si no, cridi a la funció recursiva. Hi ha dues maneres, i partidaris de les dues
bool factorial (int n) {
    if (n>1) return n*factorial(n-1);
    return 1;
}
bool factorial (int n) {
    if (n<=1) return 1;
    return n*factorial(n-1);
}
Aquí pot semblar que les dues són iguals. No obstant, en casos més llargs són bastant diferents. Per exemple, existeix l'algorisme d'exponenciació ràpida, que calcula a^n en temps logarítmic (traducció: Molt ràpid)
int exp_rap(int a, int n) {
    if (n>0) {
        int aux=exp_rap(a,n/2);
        if (n%2==0) return aux*aux;
        return aux*aux*a;
    }
    return 1;
}
int exp_rap(int a, int n){
    if (n==0) return 1;
    int aux=exp_rap(a,n/2);
    if (n%2==0) return aux*aux;
    return aux*aux*a;
}
Jo personalment sóc més partidari de la segona versió. En aquesta, el cas base en poses a davant de tot, i així no has de posar tota la resta dins d'un if.
En funcions gairebé sempre es fa així. Però en procediments... En procediments molts cops es fa diferent. Per exemple
//Mou n torres de la torre a fins la torre c
//utilitzant b com a auxiliar
void hanoi (int a, int b, int c, int n) {
    if (n>0) {
        hanoi(a,c,b,n-1);
        cout <<a<<"->"<<c<<endl;
        hanoi(b,a,c,n-1);
    }
}
void hanoi (int a, int b, int c, int n) {
    if (n==0) return;
    hanoi(a,c,b,n-1);
    cout <<a<<"->"<<c<<endl;
    hanoi(b,a,c,n-1);
}

Jo personalment sóc més partidari de la segona en els dos casos. Normalment, a PRO1, utilitzen la segona per funcions, i la primera per procediments. Això ja a criteri de cadascú, però intenteu ser coherents, per no liar-vos més encara

Nomenclatura

En aquest àmbit hi ha molta discussió, així que perfectament us podeu trobar que jo us dic una cosa, i algú us diu una altra diferent. No obstant, jo us diré que em feu cas a mi (òbviament, si ho faig així, és perquè crec que és millor)

Variables

Sempre que defineixis una variable, aquesta ha d'estar definida en minúscules. Per altra banda, si vols separar paraules, sempre han d'estar separades per _. Hi ha gent que utilitza la notació camelcase, que vindria a ser posar en majúscula la inicial de les paraules, si n'hi ha més d'una
int salari_minim;//La que jo recomano
int salariMinim;//lower camelcase
int SalariMinim;//upper camelcase

Sobretot a PRO1 no utilitzeu la tercera, ja que interferiria amb el que direm més tard. En general en variables no es discuteix tant, la majoria utilitza la primera

A més, és important posar noms explicatius, però no ens passem. Els noms han de ser prou curts (tampoc posem lletres per tot)

Funcions

En el cas de funcions, a C era molt típic utilitzar camelcase, mentre que a C++ s'opta més per la _ (almenys la STL). Jo particularment opto per això
Per altra banda, en C, quan volíem fer diverses funcions que facin el mateix però rebent diferents paràmetres (per exemple, el màxim de 2 números, o de 4), calia diferenciar els noms. Per això, en C existien coses com abs, labs, i similars. En C++ això ja no passa, pots tenir funcions que es diguin igual sempre i quan rebin paràmetres diferents (que el compilador pugui diferenciar clarament). Així, podem tenir
void torres_hanoi(int A, int B, int C, int n) {
    //el codi, no ve al cas
}
void torres_hanoi(int n) {
    torres_hanoi(1,2,3,n);
}
int main() {
    int n;
    while (cin >>n) torres_hanoi(n);
}

En el main llegeix la n, i crida a la funció torres_hanoi(int n). Aquesta crida a l'altra. Si estiguéssim programant una llibreria, l'usuari només necessitaria utilitzar la funció que rep la n. Això permetria canviar l'algorisme fàcilment sense que a ell li afecti. Per exemple, podríem fer que la funció rebi caràcters enlloc d'enters, i no passaria res.
double max(const double& a, const double& b) {
    //comprovacions pels NaN, inf i -0.0
    return a<b?b:a;
}
int max (int a, int b) {
    return b<a?a:b;
}
En aquest cas, ens convindria declarar diverses funcions per diferents tipus. Perquè en els nombres en coma flotant, hi ha casos estranys (el NaN, els infinits, el -0.0...), i ens pot anar bé treballar així. De fet, en aquest aspecte fallen bastant std::max i std::min, així que si creiem que ens pot afectar, és millor utilitzar fmax i fmin

Structs

Una struct en general és com si crees un nou tipus. El conveni per mi és senzill, i seria posar en majúscula només la primera lletra. Quedaria una cosa així:
struct Punt {
    double x, y;
};

No reinventis la polvora

Hi ha coses que són tan òbvies que segurament algú les ha fet ja. Per exemple, imaginem que et donen un programa que en algun punt necessita tenir el vector ordenat. No et posaràs a programar un algorisme d'ordenació, no?

La STL

Existeix la STL, que vol dir Standard Template Library. He fet un article que explica unes quantes coses de les que té, però la majoria no es poden utilitzar a PRO1, així que aquí posaré les que penso que sí. Davant del dubte, sempre es pot preguntar

pair<T1,T2>

Imaginem que necessitem retornar dos valors a l'hora. Una idea que se'ns podria acudir seria
struct Parella {
    int x, y;
};

Com que això és una cosa que pot caldre per moltes ocasions, ja se'ls va acudir crear una classe per fer això. És la classe pair, que conté dos valors (que poden ser de diferent tipus). Es declara fent
pair<T1,T2> p;
Pots donar-li valor, o bé en la declaració
pair<int,int> p(1,3);
O element per element
pair<int,int> p;
...
p.first=3;
p.second=4;
Fins i tot podem fer-ho amb la funció make_pair
p=make_pair(5,7);

Les pairs tenen l'operador < definit, de manera que es poden ordenar vectors de parelles sense fer una nova funció. Aquest compara el primer element, i si són iguals, compara el segon

sort

Òbviament, no ens posarem a programar algorismes d'ordenació a cada problema que necessitem ordenar. No ens posarem a fer un bubblesort (perquè és pura merda); o selection sort, o insertion sort, o mergesort, per cada problema on necessitem ordenar (encara que està bé saber com són aquests). Enlloc d'això, podem utilitzar el sort, definit a la llibreria algorithm, i no ens preocuparem de com ordena
El sort bàsicament és una funció que rep dos (o tres) paràmetres. La versió senzilla rep dos paràmetres, que indiquen on comença el vector i on acaba. Concretament, rep iteradors apuntant allà. No cal saber què són els iteradors, simplement que s'obté un que apunta a la primera posició fent v.begin(), i un que apunta al final fent v.end(). Per tant, per ordenar un vector faríem
sort (v.begin(), v.end());
La versió que rep tres paràmetres, el que fa és rebre una funció que indica com es compara. Això serveix per si volem ordenar decreixentment, o per si el que volem ordenar no té definit l'operador <. 

Functors per comparar

Això és una cosa que no tinc ni idea de si està permès o no. Per tant, pregunteu. La idea és que quan es va programar la STL, ja s'imaginaven que podria caldre comparar amb operadors diferents de <. Així que van definir functors (no cal saber què són) per comparar diferent. La típica és greater<T>, que compara dos elements de tipus T comprovant si el primer és més gran que el segon. Així, podríem fer
vector<int> v(n);
...
sort(v.begin(),v.end(),greater<int>);

Això ens permet ordenar el vector decreixentment sense preocupar-nos de programar cap funció

Vectors i funcions

A l'hora d'utilitzar funcions i vectors, la cosa és una mica complexa. Suposo que a PRO1 us hauran dit que mai passeu vectors per còpia ni els retorneu. Que sempre els heu de passar per còpia a les funcions, i modificar-los allà mateix. Això és una manera senzilla de no liar-se. Però què coi, som informàtics o no? Per tant, podem veure com podrien estar implementats els vectors

template<typename T>
class Vector {
    T* dades;
    size_t size;
    //totes les funcions
};

Això, òbviament, és un resum. Hi ha més coses. Però amb això ja ens serveix per veure com es fa tot. dades és un punter que apunta a les size dades que tenim. 

Què passa quan passem per còpia un vector?

Quan passem per còpia un vector, hem de copiar tot això. Això implica reservar espai per les dades, copiar-les totes allà... Bastant cost. Concretament, és cost lineal. Per tant, el que farem nosaltres és no copiar-ho. Si només volem fer consultes, referència constant. I si volem modificar-lo (compte, les modificacions són definitives) doncs referència a seques

Què passa quan retornem un vector?

Aquí hi ha dos casos. El primer, seria quan retornem un vector creat a la funció. Per exemple:
vector<int> llegir_vector() {
    int n;
    cin >>n;
    vector<int> v(n);
    for (int i=0;i<n;++i) cin >>v[i];
    return v;
}
En aquest cas, crea un vector, reserva espai per n elements i els llegeix. En el moment que va a retornar, el compilador s'adona que v es destruirà després del retorn. Així que el que fa és fer un swap amb el contingut del vector que rep això. És a dir, si fem
vector<int> vec=llegir_vector();
Tenim vec, que està buit. I tenim el v de la funció, que té el que hem llegit. Com que v es destruirà, i el contingut de vec no el volem per res, intercanvia size i dades dels dos. No ha de copiar les n dades, sinó que simplement fa que el punter de vec apunti al lloc on apuntava el punter de v, i viceversa. És a dir, el retorn té cost constant. 

L'altre cas seria quan retornem un vector que ja existeix. Per exemple
vector<int> fila_iessima(vector<vector<int>>& matriu, int i) {
    return matriu[i];
}

En aquest cas, com que la fila no es destruirà, el compilador determina que s'han de copiar tots els elements, cosa que pot ser necessària en algun cas, però en general no

dimarts, 19 de setembre del 2017

Aprendre a programar 18 - Consells per la pràctica

Arribats a aquest moment del curs, toca posar-se amb la pràctica de PRO2. Li dedicaràs gran part del teu temps, sobretot si fas com la majoria i t'hi poses a l'últim moment. Jo he pensat que podia aprofitar aquest article per donar uns quants consells, que intentaré que no siguin els que es donen sempre. No seran aquells de "no deixis les coses per l'últim moment", sinó que intenten ser útils de debò.

La STL és la teva amiga

La STL (Standard Template Library) són una sèrie de llibreries, que contenen classes, funcions i functors molt útils. A PRO1 la veiem molt superficialment, i a PRO2 la veiem una mica més, però tampoc molt. No obstant, sense tornar-nos uns experts tampoc, podem aprofitar-la una mica

Llistes, les justes

Quan hem de triar una estructura de dades, molts cops acabem triant la llista. És molt típic que, si volem tenir una estructura de dades que la seva mida vagi canviant, utilitzem la llista. Però molts cops això és una mala elecció. 
Si volem insertar només al final, o ens és indiferent la posició: En aquest cas, jo utilitzaria un vector. El push_back en un vector té temps constant amortitzat. Però en general, és més ràpid fer push_back en un vector que en una llista, cosa que es pot entendre quan estudiem la implementació interna de les llistes. A més, així conservem l'accés aleatori en temps constant. 
Si volem insertar al principi, o principi i final: En aquest cas, jo optaria per la deque. Té un funcionament similar al vector, però podem insertar en temps constant tant al principi com al final (constant de debò, no amortitzat). El push_back a més és molt més ràpid que en un vector

I, molt important, no les utilitzem si és obvi que hem d'utilitzar una altra. Hi ha vegades que és obvi que una clsa s'ha pensat per ser resolta d'una manera determinada. Per exemple, a la pràctica del Q2 2016-2017, s'havia de generar un arbre genealògic. Aquí, el millor era utilitzar un arbre, ja sigui el que ens donen els professors, com un propi (millor el que ens donen, per temps bàsicament). Hi ha gent que va utilitzar una llista per fer-ho. Ens en podem sortir, sí, però de la mateixa manera, pot ser que no ens en sortim, i la caguem. 

Si l'ordre no importa, per què mantenir-lo? 

Fins ara hem estudiat els sets i els maps. Però, i si us digués que tenir-ho ordenat no sempre és positiu? Sí, ja sé que el que ens permet insertar i eliminar en temps logarítmic és justament que estan ordenats. Però existeixen un parell d'estructures a la STL, que es diuen unordered_set i unordered_map. Tenen les mateixes funcions que els sets i els maps, però internament estan implementats amb taules de hash. Què vol dir això? Doncs, que a nosaltres ens afecti, que insertar, eliminar i buscar triguen temps constant en el cas mitjà, de manera que, si l'ordre no ens importa, tot anirà molt més ràpid
Per utilitzar aquestes dues, hem d'utilitzar C++11. És el que s'utilitza a PRO2, però si vols assegurar-te de que l'utilitzes, fixa't si quan compiles tens el flag -std=c++11 o -std=c++0x. Si hi ha un dels dos, ja està. Si hi ha algun superior a C++11, igual, tot i que m'estranyaria

No programis el que ja està programat

Sembla una tonteria, no? Quants cops hem fet una cosa així?
sort(v.begin(),v.end(),cmp);
Milers i milers. Molts cops, per ordenar decreixentment. Doncs bé, la pròpia STL té un functor que és greater<T>, de manera que podem fer
sort(v.begin(),v.end(),greater<T>());
Només cal que T tingui definit l'operador >
A la llibreria functional en trobem molts més d'aquest estil. Les típiques coses que podem necessitar, és probable que hi siguin

Cout, els justos

És típic. Volem veure on comença a fallar el programa, i posem molts cout per tot el programa, que ens van avisant d'on estem. Així podem identificar on està l'error. Ara, quan hem arreglat el problema, ens hem de posar a eliminar tots els cout, i és molt fàcil que ens en deixem uns quants. 
Però la pròpia llibreria iostream ens dóna una eina per fer això. El flux de sortida cerr. Normalment tenim una sèrie de canals d'entrada i sortida. En C podíem utilitzar les funcions read i write per operar a través d'ells. Teníem el canal 0, que era el de lectura (en C++ hi podem accedir amb cin), el canal 1, que era el d'escriptura normal (en C++ hi podem accedir amb cout), i el canal 2, que era el d'errors (en C++ hi podem accedir amb cerr).

La gràcia d'aquest flux de sortida, és que com que utilitza un canal diferent, pel jutge és "invisible". Sí que veu el seu contingut, però no l'avalua. Per tant, si se'ns cola un, no serà un vermell, afectarà (molt poc) al rendiment i ja. De la mateixa manera, no ens cal eliminar-los mentre estiguem fent proves, redirigim la sortida normal a un fitxer, i els errors a un altre, i cap problema
Per redirigir la sortida d'errors, es fa amb 2>. Com ja sabeu, l'entrada és amb <, i la sortida estàndard amb >. Aleshores, si volem que l'entrada surti del fitxer entrada.txt, la sortida normal vagi a sortida.txt, i la d'errors a errors.txt, faríem
$./programa <entrada.txt >sortida.txt 2>errors.txt

Compte!

S'ha d'anar amb compte amb una cosa. Que el Jutge no miri la sortida d'errors, no vol dir que els professors no la vegin. I, si poseu missatges com "bitch", "puta", "polla", "hey madafaka", doncs tindreu una imatge que potser us interessa poc. Així que poseu missatges aptes per ser llegits per un professor, perquè ha passat

Compte 2!

Que el Jutge no els vegi, no vol dir que no els faci. Què vol dir això? Que faran anar el programa una mica més lent. Per exemple, al joc d'EDA, hi va haver uns quants estudiants eliminats només per això, tenien tants missatges d'error que els petava el jugador. Així que, per l'enviament definitiu, assegura't de treure'ls tots. Per les teves proves és indiferent

No es retornen iteradors

Aquesta és molt important. A la meva pràctica, per exemple, vam fer una classe Biblioteca, que en un moment havia de retornar un Text. Un Text tenia un set<string> i un vector<Frase>, de manera que vam pensar que, ja que retornar-lo sencer era una burrada de cost, podíem retornar un iterador que hi apunti, i ja està. MALAMENT! En el meu cas, era un map<string,Text>::iterator, de manera que el primer element era el títol, i el segon el text. Què passa si de cop decideixo que vull canviar la implementació interna, i tenir el text en un unordered_map? O en una estructura més diferent encara? No només hauríem de modificar aquesta classe, sinó que tocaria modificar el main, que utilitzava aquesta funció. I la idea de la programació modular, és que els mòduls siguin independents entre ells. 

La solució?

La solució és clara. No cal retornar un iterador, quan podem retornar una referència. És a dir, igual que en la funció següent no copiem la matriu, sinó que indiquem on és, per què no podem fer el mateix, però retornant?
void escriure (const Matriu& mat);

Sí, quan sortim d'una funció, tot el que haguem declarat dins d'aquesta desapareix. Per això, normalment, no s'explica res d'això a PRO1. Podríem cometre l'error de fer
vector<int>& llegir(int n) {
    vector<int> v(n);
    for(int i=0;i<n;++i) cin >>v[i];
    return v;
}
I, un cop haguem sortit, v desapareix, i falla. No obstant, la gràcia aquí és que volem retornar coses que segueixen existint, ja que fins que no destruïm la classe, tot seguirà allà.
Retornar per referència és una cosa així:
vector<int>& fila_mes_llarga(Matriu& mat){
    int pos_max=0;
    for (int i=1;i<int(mat.size());++i) {
         if (v[i].size()>v[pos_max].size()) pos_max=i;
    }
    return mat[pos_max];
}
Aquí no copiem la fila aquesta, sinó que indiquem on està. 

Compte a l'hora d'agafar-ho!

Quan ho agafem, hem de vigilar. Estem retornant una referència, però si volem desar-ho en una variable normal, acabarà copiant-se igualment. 
vector<int> v=fila_mes_llarga(mat); //Aquí ho copiem
Per tant, ha de ser desat en una referència. Una cosa així:
vector<int>& v=fila_mes_llarga(mat);
També hem d'anar amb compte. Si ara modifiquem v, es modificarà la fila corresponent a mat. Si no volem modificar-ho, el millor que podem fer és que sigui constant

Sobrecàrrega d'operadors

Quan tu crees una classe, la classe no té els operadors que tenen els tipus bàsics. No tenim els operadors de comparació, no podem sumar, restar... Però això té una solució fàcil. I és programar-los. 

Operadors de comparació

Són els primers que vaig necessitar sobrecarregar. Normalment el que es fa és programar l'operador <, i potser també el ==, i la resta es fan en funció d'aquests. Al cap i a la fi, tenim les següents propietats
a>b <-> b<a
a<=b <-> not a>b
a>=b <-> not a<b
a==b <-> not (a<b or b<a)
a!=b <-> not a==b

Com que la comparació == implica fer dos comparacions de <, molts cops preferim fer-la també, ja que així va més ràpid.

class Frase {
private:
    vector<string> paraules;
    ...
public:
    ...
    bool operator<(const Frase& f);
    bool operator<=(const Frase& f);
    bool operator>(const Frase& f);
    bool operator>=(const Frase& f);
    
    bool operator==(const Frase& f);
    bool operator!=(const Frase& f);
};

bool Frase::operator<(const Frase& f) {
    int n=min(paraules.size(),f.paraules.size());
    for (int i=0;i<n;++i) {
        if (paraules[i]!=f.paraules[i]) return paraules[i]<f.paraules[i];
    }
    return paraules.size()<f.paraules.size();
}

bool Frase::operator>(const Frase& f) {
    return f<*this;
}

bool Frase::operator<=(const Frase& f) {
    return not *this>f;
}

bool Frase::operator>=(const Frase& f) {
    return not *this<f;
}

bool Frase::operator==(const Frase& f) {
    if (paraules.size()!=f.paraules.size()) return false;
    int n=paraules.size();
    for (int i=0;i<n;++i) {
        if (paraules[i]!=f.paraules[i]) return false;
    }
    return true;
}

bool Frase::operator!=(const Frase& f) {
    return not *this==f;
}

És a dir, la funció es diu operator< (o el que sigui), i rep un altre objecte del mateix tipus. Aleshores, els comparem, i retornem cert si el paràmetre implicit és menor (o el que sigui) que l'altre

Per comparar amb una altra classe diferent, ho faríem com una funció friend. No ho poso aquí, però si algú en algun moment ho necessita, pot posar-me un comentari, i li explico

Operador d'assignació

Aquest és l'operador =. Tindria una forma així:
Frase& operator=(const Frase& f);//Capçalera, al fitxer .hh

Frase& Frase::operator=(const Frase& f) {
    paraules=f.paraules;
    return *this;
}

La idea és, bàsicament, copiar el que hi hagi, i retornar una referència a l'element actual. Això és per poder fer a=b=c=d. Quan fem c=d, retornem el valor per referència, de manera que li podem posar a b, i igual amb b

Lectura 

Una cosa que hem treballat molt poc és la lectura i escriptura de variables. El que se'ns ha explicat sempre és que si fem cin>>v, llegirem la variable v. Però, per què això funciona?

Flux d'entrada

En realitat, cin no és cap funció que llegeixi. cin és un flux de dades d'entrada, que les llegeix del canal estàndard (el número 0). Nosaltres el que fem és llegir d'allà utilitzant l'operador >>, que en els fluxos està sobrecarregat per fer que la seva funció sigui llegir. Està fet de manera que els espais, tabulats i salts de línia siguin els que indiquen que s'ha acabat el valor actual. Per això, si fem
string s;
cin>>s;
I l'entrada és "Hola que tal", el que hi haurà a s és "Hola". En canvi, si inicialment haguéssim fet cin.get(), i després cin>>s, el valor retornat per cin.get() seria 'H', i a s hi hauria "ola"

Getline, la clau

Imaginem que ens diuen que a cada línia hi haurà una ordre, i una sèrie de números, que hauràs d'operar segons això. Per exemple, posem que les ordres són "SUMA", "MITJANA" i "PRODUCTE". Una cosa que podem fer és anar llegint com a string, i si veiem que no és cap instrucció, suposar que és un número, convertir-lo i operar. Però per què complicar-nos?
Podem fer una cosa així:
string s;
getline(cin,s);
I a s ens quedarà tota la línia. És a dir, enlloc de llegir fins al primer separador, llegirà fins al primer salt de línia

Crear els nostres fluxos

Un cop tenim llegida tota la línia, queda decidir com treballem amb ella. I, com que estem acostumats a treballar amb fluxos, per què no seguir així?
Tenim una llibreria, que és sstream (amb doble s), que serveix per crear fluxos amb un string. Ens interessa, concretament, tenir un flux d'entrada (istringstream). Així que el que faríem és
string s;
while (getline(cin,s)) {
    istringstream flux(s);
    flux>>s;
    if (s=="SUMA") {
        int res=0;
        int act;
        while (flux>>act) res+=act;
        cout<<res<<endl;
    }
    else if (s=="MITJANA"){
        int i=0, act;
        double res=0;
        while(flux>>act) {
            res+=act;
            ++i;
        }
        res/=i;
        cout<<res<<endl;
    }
    else if (s=="PRODUCTE") {
        int res=1;
        int act;
        while (flux>>act) res*=act;
        cout<<res<<endl;
    }
    else cout<<"ERROR"<<endl;
}

Funcions útils

Hi ha una sèrie de coses que són tan, però tan típiques, que programar-les de nou és absurd. Estan ja programades, ja sigui en llibreries, o en webs i llocs així. He pensat que podia fer-ne un petit resum, i anar-lo ampliant

Convertir un string a enter

stoi

Aquesta és bastant simple. Està a la llibreria string, rep un string, i retorna l'enter al que representa. No obstant, per experiència pròpia, a vegades falla i no sé per què. En aquest cas, hi ha una altra opció

atoi

Aquesta és molt típica. En C, i a C++ ho heretem, podem passar arguments al main. És a dir, quan fem
$./programa hola adeu
Podem fer que el programa rebi "hola" i "adeu". Els rebíem com un vector de strings (strings de C), de manera que, si hi havia un número, l'havíem de convertir a enter (o al tipus que sigui). Ho fèiem amb la funció atoi (Array TO Integer). Aquesta rep un array de chars, i retorna l'enter al que representa. Per fer-ho, haurem d'utilitzar la funció c_str, de la classe string, que retorna l'array que representa al nostre string. És a dir, una cosa així
int n=atoi(s.c_str());
La funcio atoi està a la llibreria cstdlib

Implementacions pròpies

Si no ens funciona stoi, podem implementar-lo nosaltres. Per fer-ho, se m'acudeixen dues maneres

int stoi(const string& s) {
    return atoi(s.c_str());
}

int stoi(const string& s) {
    istringstream f(s);
    int res;
    f>>res;
    return res;
}

La primera, bàsicament, utilitza atoi. Molt simple, i no ens compliquem la vida. La segona, per altra banda, utilitza un flux d'entrada, creat amb la llibreria sstream. 

Altres usos

Aquí he mostrat com convertir un string a enter. Podem convertir a altres, per exemple, tenim stol (per convertir a long), stoll (per convertir a long long), stof i stod (per convertir a float i a double, respectivament). Amb atoi funciona igual, tenim atol, atoll...

Convertir un número a string

Per convertir a string, és prou fàcil. Tenim la funció to_string, de la llibreria string, que converteix un número a string (el número pot ser de qualsevol tipus). 
Si no ens funciona la funció, sempre podem fer això:
string to_string(int n) {
    ostringstream f;
    f<<n;
    return f.str();
}

Nombres aleatoris

Imaginem que volem un número aleatori. Això, per la pràctica de PRO2, no hauria de fer falta, però mai se sap. De qualsevol manera, mai està de més saber-ho fer

Estil C

En C, la manera de generar nombres aleatoris utilitzant funcions estàndard era utilitzant la funció rand(). Aquesta retornava un enter aleatori entre 0 i RAND_MAX (està definit a la mateixa llibreria). El que fèiem és acotar aquest interval fent el mòdul, i sumant. Una cosa així
int random(int min, int max) {
    return rand()%(max-min+1)+min;
}
És senzill. Volem tenir max-min números. Fem el mòdul, i tenim un número a l'interval [0,max-min]. Per tant, li sumem el mínim, i tindrem un número a l'interval [min,max]. 
Per fer que sigui gairebé aleatori, hem de fer que a cada execució del programa sigui diferent. Això es faria posant la crida següent a l'inici del main
srand(time(NULL));
La funció srand serveix per inicialitzar la seed dels nombres aleatoris. La funció time retorna un valor que serà diferent cada vegada que la cridem, tampoc cal saber gaire més. 

Aleshores, per fer això, hauríem d'incloure la llibreria cstdlib (per les funcions rand i srand), i la llibreria ctime (per la funció time)

Estil C++

Com hem vist, a C la cosa estava molt limitada. Podíem generar nombres aleatoris, sí, però només enters, i ens havíem de complicar massa la vida. Per això a C++ apareix la llibreria random, que ens permet generar nombres pseudoaleatoris molt millor
Primer de tot, hem de fer un generador. Tenim una classe per això, que és default_random_engine. Un cop tenim aquest generador, podem utilitzar les diverses funcions que té per crear nombres aleatoris. Les més interessants per nosaltres són uniform_int_distribution i uniform_real_distribution, que generen nombres aleatoris utilitzant una distribució uniforme (la qual té la característica que tots els seus elements tenen la mateixa probabilitat). En tenim altres, com bernoulli, binomial, exponencial, poisson, normal... Les podem veure aquí. Un exemple d'us seria

int main() {
    default_random_engine generador;
    uniform_int_distribution<int> aleatori(0,9);
    for (int i=0;i<10;++i) {
        cout<<aleatori(generador)<<endl;
    }
}

És a dir, declarem el generador i el functor que ens farà nombres aleatoris. Cada cop que volem cridar nombres aleatoris, cridem al functor, passant-li com a paràmetre el generador.

Temps

A vegades dubtem entre dues implementacions, i volem comprovar si afecten significativament a l'eficiència. Estant a PRO2, no sabem gaire de calcular eficiència, així que ens pot ser útil mirar-ho empíricament. I, com que no ens posarem a cronometrar-ho amb el rellotge, és útil aprendre a fer-ho amb l'ordinador

Des de la consola

Aquesta opció només l'he provat a consoles de Linux. Suposo que en altres hi ha manera de fer-ho, però no la conec. Bàsicament utilitzem l'instrucció time. Així:
$time ./programa.exe <entrada.txt >sortida.txt
És important que l'entrada vingui d'un fitxer, ja que si la posem manualment, aquest temps també el compta. I ens pot tergiversar els càlculs. La sortida prefereixo redirigir-la perquè així només ens surti a la consola el temps. 

Editant el codi

Aquesta opció també és útil, tot i que prefereixo la primera, ja que no ens fa modificar el codi. Utilitzarem la llibreria ctime, que té una funció (clock) que ens retorna els ticks de rellotge consumits fins al moment. Aquests depenen de la unitat, però tenim una constant, que és CLOCKS_PER_SEC, que serveix per convertir els ticks de rellotge a segons. 
Sabent això, la idea és agafar els ticks a l'inici i al final, calcular la diferència, i convertir a segons. És a dir:
int main() {
    int c=clock();
    ...
    c=clock()-c;
    cout<<double(c)/CLOCKS_PER_SEC<<endl;
}

Per altra banda, si no volem saber el temps en segons, sinó que volem comparar entre dues opcions, el més senzill és mostrar c directament, sense passar-ho a segons

Limits numèrics

Mai us ha passat que voleu tenir el nombre més gran possible? O el més petit? O qualsevol dada així? És una putada, perquè clar, la mida dels números pot variar en funció del sistema

Mètode cutre

Per aquest mètode necessitem conèixer la representació interna dels números. Sabem que els enters amb signe es representen utilitzant Complement a 2, de manera que el bit de més pes és negatiu, i la resta positius. Així, si tenim nombres de 32 bits, el nombre més petit possible serà 0x80000000, i el més gran 0x7FFFFFFFF. 
Per altra banda, com a dada important, el -1 és 0xFFFFFFFF. Sabent això, ja podem aconseguir els mínims i màxims
int max=(-1)>>1;
int min=1<<(sizeof(int)*8-1);
És a dir, la primera és agafar el -1 (0xFFFFFFFF), i desplaçar-lo una posició a la dreta (quedant 0x7FFFFFFF). La segona, per la seva banda, és desplaçar 1 cap a la dreta tantes posicions com bits representin el número menys 1. Així tindrem un 1 al bit de més pes

Per fer els respectius mínims i màxims d'enters sense signe, és encara més fàcil. El mínim sense signe és 0 (òbviament). El màxim serà -1 (per una raó molt simple, -1 és 0xFF..., que si no hi ha signe, és el màxim). 

Mètode guai

Aquest mètode és el que us recomanaria. No ens compliquem la vida en càlculs estranys. Què passaria si intentem calcular el mínim d'un enter amb signe en un sistema on els bytes no són de 8 bits? Perquè un byte no necessàriament és de 8 bits, malgrat que és el més comú
Per fer-ho bé, utilitzarem la llibreria limits. Aquesta conté una classe, que és numeric_limits, amb funcions que ens permeten obtenir informació sobre cada tipus numèric (no només de tipus enter, també de coma flotant). Les funcions que em semblen més útils són:
min: En els enters, retorna el número més petit representable. En coma flotant, el més petit positiu i diferent de 0 (un possible valor de retorn seria 1.17549e-038).
max: Retorna el màxim valor finit
lowest: En enters, igual que min. En coma flotant, normalment retorna -max()
En general, amb aquestes 3 ja faríem. En tenim moltes més, les podeu consultar aquí
int minima_diferencia(const vector<int>& v) {
    int min_dif=numeric_limits<int>::max();
    int n=v.size();
    for (int i=0;i<n-1;++i) {
        int a=v[i];
        for (int j=i+1;j<n;++j) {
            int aux=abs(a-v[j]);
            if (min_dif>aux) min_dif=aux;
        }
    }
    return min_dif;
}
Un exemple d'us seria aquest

C++11

C++11 és una actualització de C++ que apareix al 2011 (sí, sóc un geni). Inclou moltes coses molt i molt útils. Està més explicada aquí, però faré un resum de coses que poden ser útils

Inferència de tipus

No us heu plantejat que hi ha situacions on el tipus d'una variable té el nom molt llarg, i realment no cal que ho sigui. Si tenim un map<int,pair<string,int>>, i volem fer-ne un iterador, quedaria una cosa així
map<int,pair<string,int>>::iterator
Molt llarg. I, si volem fer un for, queda encara més llarg
for(map<int,pair<string,int>>::iterator it=m.begin();it!=m.end();++it)...
I, realment, la funció begin() sempre retorna un iterador. Així que, per què no deixar al compilador que dedueixi ell sol?
Doncs C++11 permet fer això. Utilitzant la paraula clau auto (que abans tenia un altre significat), li diem que determini ell mateix el tipus
for (auto it=m.begin();it!=m.end();++it)...
En general, la idea d'això és fer-ho en casos on sigui molt obvi, i al moment. No serveix fer
auto x;
...
x=m.begin();
Hem de tenir sempre un inicialitzador, que utilitzem per saber quin és el tipus

For en un rang

Sóc molt fan d'aquesta. Permet recórrer tota una estructura de dades, de manera més senzilla. Bàsicament fem:
for (T x:estructura) {
...
}

Sempre i quant l'estructura emmagatzemi elements de tipus T. Per exemple, per recórrer una llista d'enters faríem
list<int> l;
...
for (int x;l) {
...
}
Això equivaldria a fer
for (list<int>::iterator it=l.begin();it!=l.end();++it) {
    int x=*it;
...
}
Però què passaria si volem modificar l'estructura? Per exemple per llegir-la. En aquest cas, faríem
for (int& x:l) ...
Això serveix perquè x faci referència als elements, permetent-nos modificar l'estructura. 

Com deveu intuir, això només serveix amb estructures que tinguin iteradors. Per altra banda, utilitzant la paraula clau auto, podem fer coses molt guais

Llistes d'inicialització

Això és molt útil encara. Imaginem que volem fer un programa que faci coses relacionades amb els escacs. I penses "ei, anem a fer un vector amb els moviments possibles del cavall". Aquest no canviarà, de manera que el podem fer constant. Però com l'inicialitzem?
Abans de C++11, amb un vector no es podia fer, i havíem d'utilitzar un array. Amb C++11, podem fer això amb les estructures de dades de la STD:
const vector<int> v={1,2,3};
I v tindrà els elements 1, 2 i 3. 

Recursivitat: La clau

Una llista d'inicialització es defineix recursivament. Podem dir que està composada per una de les dues opcions següents:
-Una sèrie d'elements, separats per comes
-Una sèrie de llistes d'inicialització, separades per comes
És bastant senzill, aleshores. Podem fer
vector<pair<int,int>> SALTS={{1,2},{1,-2},{-1,2},{-1,-2},{2,1},{2,-1},{-2,1},{-2,-1}};
És a dir, s'analitzen primer les llistes més internes. Com que sabem que ha de ser un vector<pair<int,int>>, sabem que les llistes internes seran parelles. 

/*
 * Pre: v no buit
 * Retorna el mínim i el màxim del vector
 * Post: el primer element de la parella retornada és el mínim,
 *  El segon és el màxim
 */
pair<int,int> min_max(const vector<int>& v) {
    int minim, maxim;
    minim=maxim=0;
    for (int x:v) {
        if (x<minim) minim=x;
        if (x>maxim) maxim=x;
    }
    return {minim,maxim};
}

Com podem veure, podem utilitzar les llistes d'inicialització, no només per inicialitzar variables, sinó també per crear el valor de retorn d'una funció

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