dimecres, 7 de juny del 2017

Aprendre a programar 1: Estructures de control

En l'article anterior explicava com crear variables, llegir-les i operar amb elles. No obstant, amb tot el que he explicat fins ara, quan tens el programa, s'executen totes les instruccions del programa. Normalment no volem que això passi. En general volem que, en funció de certs paràmetres, passi una cosa o una altra. I aquí entren en joc les estructures de control.

Execució condicional: IF-THEN-ELSE

La primera estructura de control és l'if-then-else. En C++ s'escriuria així:

if (condició) {
    codi1
}
else {
    codi2
}

Això el que faria és que, si la condició és certa, executaria codi1. Si és falsa, executa codi2. La part del else, no obstant, és optativa. Aleshores, tenint això, podríem escriure el codi següent (obviaré tota la part anterior, per no escriure massa):

...
if (a>b) cout <<"Es mes gran"<<endl;
else cout <<"No es mes gran"<<endl;

Podríem llegir-ho com "Si tal, llavors qual. Si no, ...". Una cosa destacable aquí és que no he posat els '{' i '}'. Això és perquè, quan només vols que s'executi una instrucció, no cal. Per més d'una, les has de posar per marcar l'inici i el final del que vols executar si es compleix la condició. És com una manera d'encapsular el codi, i dir-li "executa tot això"

Utilitzant això, podem anar enganxant condicions, i fer coses com

if (a>b) cout <<"Es mes gran"<<endl;
else if (a<b) cout <<"Es mes petit"<<endl;
else cout <<"Son iguals"<<endl;

Important 1!

C++ és un llenguatge on el tabulat, espaiat i salt de línia no tenen gaire importància. És indiferent si tot el codi està en la mateixa columna o no, si està tabulat amb tabuladors o amb espais... No obstant, un codi com el següent:
int a, b;
cin >>a>>b;
if (a%2==0) {
if (b%2==0) {
cout <<"Els dos son parells"<<endl;
}
else {
cout <<"El segon no ho es"<<endl;
}
}
else {
if (b%2==0) {
cout <<"El primer no ho es"<<endl;
}
else {
cout <<"El segon no ho es"<<endl;
}
}

Aquest codi, malgrat teòricament és correcte (no l'he provat tampoc), és gairebé impossible de llegir. El que s'acostuma a fer és que, tot el que s'executa dins d'una condició, es tabula un cop. És més tema de gustos, però jo quan tabulo, utilitzo el tabulador amb 4 espais d'amplada. Alguns prefereixen 8, altres 3, o fins i tot 2. Hi ha gent que prefereix tabular amb espais també, tot i que jo no ho recomano gens, ja que si utilitzes el tabulador, un lector potencial del codi podrà veure'l amb l'amplada que li resulti còmode. Així, el codi anterior quedaria:
int a, b;
cin >>a>>b;
if (a%2==0) {
    if (b%2==0) {
        cout <<"Els dos son parells"<<endl;
    }
    else {
        cout <<"El segon no ho es"<<endl;
    }
}
else {
    if (b%2==0) {
        cout <<"El primer no ho es"<<endl;
    }
    else {
        cout <<"El segon no ho es"<<endl;
    }
}

Com es pot veure, molt més llegible.

Si copieu el codi, veureu que aquest justament està amb espais. Això és perquè el bloc converteix els tabuladors en un espai, i no queda com hauria.

És important també remarcar que, si bé no hi ha cap norma estricta sobre l'amplada que han de tenir els tabulats, sí que jo diria que com a mínim han de ser d'amplada equivalent a 4 espais. Hi ha altres, com Linus Torvalds (creador del nucli Linux) que no estan d'acord amb mi, com es pot veure en aquest escrit


És a dir, resumint: Els tabulats són de 8 caràcters, i punt. Si et molesta perquè s'emporta el codi massa a la dreta, és que el teu codi és puta merda, i l'hauries d'arreglar. No obstant, també coneixem la mania que té a C++, així que no lli farem gaire cas

Important 2!

Existeix un concepte molt útil, que és la "lazy evaluation". Això vol dir que, quan s'avaluen les condicions amb and i or, si en algun moment ja no necessita seguir per saber el resultat, s'atura. Com pot ser això?

Sabem que true or és cert, per qualsevol valor de x, ja que la or és certa sempre que ho sigui alguna de les dues. De la mateixa manera, false and x és fals per qualsevol valor de x. Això ho podem utilitzar per evitar-nos problemes. En un codi com el següent:

if (a/b>3) cout <<"a es mes del triple que b"<<endl;

Si tenim que b és 0, ens fallarà el programa (ja sabem què passa amb les divisions per 0). Però sempre podem fer el següent:

if (b!=0 and a/b>3) cout <<"a es mes del triple que b"<<endl;

Això el que farà és que, si b és 0, ja sap que és fals, i no caldrà dividir. 


Els bucles:

Què passa si volem executar repetides vegades una part del codi? Si sabem que ho farem 2 cops, podem copiar-ho. Però si ho volem fer n cops, no podem. Per això entren en joc els bucles. En tenim bàsicament tres. Són el while, el do-while i el for. A PRO1 només s'utilitzen el while i el for

WHILE:

L'estructura del while és molt fàcil. S'assembla molt a un if. Bàsicament és
while (condicio) {
    codi
}

I executarà codi mentre es compleixi condicio. S'ha d'anar amb compte, si condicio és sempre certa, tindrem un bucle infinit (tot i que els bucles infinits, en certes ocasions, són útils). Un exemple seria
while(x<100) ++x;
Això el que faria és anar incrementant x fins que x arribi a 100. Si ja ho és, salta a la instrucció següent

FOR:

Un for té una estructura més complexa que el while. És la següent:

for (inicialització;condició;modificació) {
    codi
}

Com que la manera més clara és un exemple, imaginem que tenim una n ja inicialitzada

for (int i=0;i<n;++i) {
    cout <<"Iteracio "<<i<<endl;
}

Això el que farà és fer 100 iteracions, mostrant a cada una el número que és. En essència, el que es fa és, el primer cop que s'entra executa la inicialització. Després, a l'inici de cada iteració (inclosa aquesta), comprova que la condició sigui certa. Finalment, quan acaba cada iteració, fa la modificació. Vindria a ser com fer

int i=0;
while (i<n) {
    cout <<"Iteracio "<<i<<endl;
    ++i;
}

No és ben bé el mateix, però ens serveix per il·lustrar l'exemple

DO-WHILE

(si te'l vols saltar, salta-te'l)
Aquest és el tercer tipus de bucle que existeix. La seva estructura és molt similar al while. Bàsicament, seria

do {
    codi
}
while (condicio);

Com podem veure, s'assembla molt. Podríem pensar que és el mateix que un while però amb la condició al final, però realment no és cert. En aquest bucle, primer s'executa i després es comprova. Això fa que, com a mínim, s'executi un cop el bucle, indiferentment de si la condició és certa o no. Això pot ser útil, ja que a vegades saps que sempre necessitaràs una iteració. Per exemple, podríem fer un codi com el següent:

char c;
do {
cout <<"Quina creus que es la inicial del meu nom?"<<endl;
cin >>c;
while (c!='O' and c!='o');
cout <<"L'has encertat!"<<endl;

Com podem veure, aniria llegint lletres fins que encerti que la lletra és la O. Aquest tipus de bucle és una mica més eficient que el while, tot i que pel que canvia, no cal tampoc preocupar-se


ÀMBIT DE LES VARIABLES

Quan tu declares una variable, el moment on la declares importa. Si la declares fora del bucle, seguirà existint quan l'acabis, mentre que si la declares dins del bucle, un cop surtis deixarà d'existir. Això és pràctic perquè potser en un bucle necessites una variable auxiliar, però fora no la tornaràs a utilitzar. Un exemple clar és la variable de control d'un for, que quan acabis ja no t'interessa per res. Per això, el codi

for (int i=0;i<100;++i) cout <<i<<endl;
cout <<i<<endl;

Donarà error al compilar, i serà pel cout de sota, ja que i ha deixat d'existir. En canvi, si el codi fos
int i=0;
while (i<100) {
    cout <<i<<endl;
    ++i;
}
cout <<i<<endl;

No donaria error, perquè i segueix existint (ja que ha sigut declarada fora)

Aleshores, si per exemple volguéssim fer un programa que dibuixés una matriu n*n amb guions (molt útil, no trobeu?), podríem fer
for (int i=0;i<n;++i) { //Bucle extern
for (int i=0;i<n;++i) {//Bucle intern
cout <<'-';
}
cout <<endl;
}

I és completament correcte. La i que utilitza el bucle extern té un valor que és independent de la del bucle intern. Quan entres al bucle intern, la de l'extern queda emmascarada, però no es perd, de manera que quan acaba, es desemmascara. 

En canvi, si volguéssim fer que cada posició i j contingui (i+j)%10, si ho féssim com abans, fallaria, ja que la i interna emmascara l'externa. Per això, normalment en els fors dins de fors s'utilitzen noms diferents

for (int i=0;i<n;++i) {
for (int j=0;j<n;++j) {
cout <<(i+j)%10;
}
cout <<endl;
}

Nota: Les variables de control dels bucles poden tenir el nom que es vulgui. Utilitzar i i j per elles és una mica costum. Normalment s'agafen, per ordre, "i, j, k, l", i si necessites més, vigila perquè potser no ho estàs enfocant bé

Altres maneres de controlar un bucle:

Existeixen altres maneres de controlar bucles. Bàsicament tenim dues instruccions. No obstant, a PRO1 no els agraden gens, i jo les evitaria 100%. Em sembla que és útil que les conegueu, i potser en algun moment els doneu utilitat, però estan vetades completament. Són les següents:
break:
Aquesta és molt clara. Un cop apareix, abandones el bucle on estàs. Un exemple:

while (n<100) {
    n+=3;
    if (n==53) break;
}

Si en algun moment n passa a valdre 53, el bucle s'acabarà en aquell moment. 
Continue:
Aquesta potser és menys òbvia. Quan apareix, el que fa és saltar directe a la següent iteració, sense acabar l'actual. Un exemple, suposem que volem fer una llista de números primers:

for (int i=2;i<n;++i) {
    if (i%2==0 and i!=2) continue;
    ...
}
Aquí, el que faria és que en el moment que i sigui parell i diferent de 2, no comprova res, ja que l'únic nombre parell que és primer és el 2. 


Un altre tipus d'execució condicional: SWITCH-CASE

Imaginem un programa que llegeix un número i, en funció d'aquest, fa una cosa o una altra. Té 10 opcions, associades als números del 0 al 9, i si el número no és aquest, mostra un missatge d'error. Podríem implementar-ho així:

if (n==0) {

}
else if (n==1) {

}
else if (n==2) {

}
...
else if (n==9) {

}
else cout <<"ERROR!"<<endl;

El problema d'això és evident, no? Massa llarg, i massa comprovacions. Si és l'opció 10, hem de comprovar 10 condicions per arribar-hi. A més, llegir-ho és molt lleig. Per això s'inventà el switch-case. Aquest seguiria el format:

switch(variable) {
    case cas1:

    case cas2:
    ...
    default:
}

El default és opcional, és el que s'executarà quan no sigui cap dels casos contemplats. Per l'exemple anterior, seria així:
switch (n) {
    case 0:
        ...
        break;
    case 1:
        ...
        break;
    case 2:
        ...
        break;
    ...
    case 10:
        ...
        break;
    default:
        cout <<"ERROR!"<<endl;
}
I suposo que es veu que és bastant més elegant. També és més ràpid, per altra banda
Una cosa a destacar és que al final de cada cas hi ha un break. Això és perquè, si no, saltaria a la següent (per un tema d'implementació interna). En l'últim no hi és perquè ens és igual que salti al següent, ja que el següent ja és fora del switch. 


Un truc molt útil:

Existeix un truc que permet llegir tants elements com ens interessi. Quan tu fas cin>>n, pots avaluar això com una condició. Serà cert sempre que quedi alguna cosa per llegir, i fals si ja no pot llegir res més. Un exemple seria, donat aquest problema:
Sembla fàcil, però va ser un dels problemes que em va sortir al primer examen. Per resoldre aquest problema, seria tan fàcil com fer:

#include <iostream>

using namespace std;

int main () {
    int a, b;
    while (cin >>a>>b) cout <<a+b<<endl;
}

I ja tindríem fet el programa. Ho podem llegir com "mentre puguis llegir dues variables, llegeix-les. i fes això. En el moment que no puguis, surt del bucle"

 El que cal tenir en compte és que entra tot el de l'esquerra seguit, i ha de sortir tot el de la dreta (idèntic, si surt una mica diferent ja no serveix). Per fer la prova del programa hi ha dues maneres:
1. Quan acabi l'entrada, poses el caràcter end of file. Aquest, en sistemes Unix o basats en Unix (com Linux), es posa amb la combinació Ctrl+D. En Windows es posa amb Ctrl+Z
2. Redirigint l'entrada i/o la sortida (això és el que fa el Jutge). Quan tu executes un programa (en Linux fent $./programa), pots dir-li que agafi l'entrada d'un fitxer, així com dir-li que enviï la sortida a un altre fitxer. Per redirigir l'entrada, es fa posant <entrada.txt, i per redirigir la sortida, >sortida.txt. El Jutge et dóna ja fitxers amb l'entrada i la sortida que espera, perquè ho puguis fer fàcilment. Per exemple, en el cas del problema d'abans, l'entrada es deia sample-000.inp, i la sortida correcta sample-000.cor. Per executar, faríem:

$./programa <sample-000.inp >sortida.txt
I ens quedaria la sortida que dóna el nostre programa al fitxer sortida.txt (compte, si ja existia, el sobreescriurà). Per veure si ho hem fet bé, faríem
$diff -dB sortida.txt sample-000.cor
I si no ens mostra res, vol dir que està bé. Si ens mostra coses, vol dir que el nostre programa no fa el que ha de fer. 

Anem a programar una mica més

Aquest és el segon problema que em va sortir a l'examen. Anem a fer-lo pas a pas:

Primer de tot, veiem que tenim una línia d'enters, dels quals no sabem la quantitat. Per tant, hem de posar un while(cin>>n).
Veiem que hem de saber quants positius hi ha, quants negatius, i quants en total. Ho podem fer de diverses maneres. 
-Podem desar quants són positius, quants negatius i quants 0
-Podem desar quants són positius, quants negatius i quants han entrat en total
I altres...
Jo triaré guardar els positius, els negatius i el total. Aleshores, en el cos del bucle hauríem de mirar si és positiu, i si ho és, incrementar pos. Si no, si és negatiu, incrementem neg. I, sigui com sigui, incrementem sempre tot. 

Aleshores, el programa seria

#include <iostream>

using namespace std;

int main () {
    int pos, neg, tot;
    pos=neg=tot=0;
    int n;
    while (cin >>n) {
        if (n>0) ++pos;
        else if (n<0) ++neg;
        ++tot;
    }
    cout <<"Pos: "<<pos<<endl;
    cout <<"Neg: "<<neg<<endl;
    cout <<"Tot: "<<tot<<endl;
}

Suposo que s'ha entès el procediment. 

Al proper article, parlaré de llibreries (que contenen funcions ja definides), i també de com crear les nostres pròpies funcions

1 comentari: