dimecres, 7 de juny del 2017

Aprendre a programar 2: Llibreries i Funcions

A l'últim article ja vaig explicar com fer sentències condicionals i bucles, així com vam resoldre alguns problemes d'examen. En aquest, parlaré de llibreries de C++ i de funcions

Què és una llibreria?

Nota: Li dic llibreria per costum, però realment, seria més precís biblioteca. Dir-li llibreria és una mala traducció de l'anglès library, que ha calat molt endins
Què millor que un exemple? Imaginem que volem calcular el màxim de dos números. Podríem fer un codi com el següent:

if (a>b) cout <<"El maxim es "<<a<<endl;
else cout <<"El maxim es "<<b<<endl;

També, utilitzant l'operador ?:, podríem fer

cout <<"El maxim es "<<a>b?a:b<<endl;

No obstant, ja podem imaginar que posar això cada vegada que necessites el màxim fa una mica de pal. Si en un programa necessites el màxim de dues variables 100 cops, no posaràs 100 cops això. Amb aquest objectiu neixen les funcions, que les podem visualitzar com una funció matemàtica. Per exemple, la funció matemàtica
f(x)=2x-3
Per cada valor de x dóna 1, i només 1 valor de f(x). Aleshores, això ho podem extendre a la programació. Doncs bé, una llibreria, podríem dir que serveix per encapsular funcions i altres coses (variables, constants, classes, tuples...) Així, si per exemple volem calcular l'arrel quadrada d'un número, enlloc de fer un algorisme que ho faci, podem utilitzar una funció que ho faci (i que, probablement, estarà més ben feta). En C++ hi ha moltes llibreries amb funcions molt útils, i intentaré aquí explicar-ne unes quantes. 

El conveni de C deia que una llibreria era un fitxer que tenia un nom, i l'extensió .h. Aleshores, existien llibreries com stdlib.h, stdio.h, math.h... No obstant, en C++ no és així, sinó que el que es fa és que les llibreries heretades de C tenen una c minúscula davant del seu nom (cstdlib, cstdio, cmath...), i no tenen cap extensió. Per altra banda, les llibreries de C++, moltes de les quals no es poden utilitzar en C per raons que ara no venen al cas, tenen el seu nom a seques (per exemple, iostream)

Com diem al compilador que utilitzarem una llibreria?

Es fa utilitzant la directiva de preprocessador include. Les directives del preprocessador tampoc venen al cas ara, només cal saber que es posen amb un coixinet davant (#). El nom de la llibreria el posem entre < i >. Aleshores, si volguéssim incloure iostream, faríem

#include <iostream>

I sabent això, ja podem començar a parlar de les llibreries

Iostream:

Aquesta serveix per l'entrada i sortida de dades d'un programa. No utilitzarem les funcions que inclou, però sí que utilitzarem:
-std::cin
-std::cout
-std::endl

Suposo que us sonen els tres. Els utilitzem per llegir dades i escriure-les per pantalla. 
std::cin:
És el flux de dades que ens dóna les dades de l'entrada estàndard. 

std::cout:
És el flux de dades que surt per la sortida estàndard (la consola, si no li especifiquem una altra cosa). 

std::endl:
És una macro per indicar el final de la línia. En C normalment utilitzàvem el caràcter '\n', però com que en algun sistema podria canviar la manera d'indicar-lo, std::endl és més genèric

Suposo que haureu notat que estic posant un std:: a davant, però quan programem no el posem. Això és perquè utilitzem la instrucció "using namespace std;", que indica que utilitzarem l'espai de noms estàndard, si no diem una altra cosa. No cal tampoc explicar què és això, simplement cal saber que si no hi ha aquesta instrucció, cal especificar a quin espai de noms fem referència

Cmath:

És la llibreria amb funcions matemàtiques. Equival a la llibreria math.h de C, i conté una sèrie de funcions matemàtiques. Les que utilitzarem són les següents:

std::max(a,b):
Retorna el maxim dels dos paràmetres que se'ls ha passat. 
std::min(a,b):
Retorna el mínim dels dos paràmetres
std::pow(a,n):
Retorna el resultat de fer a^n
std::sqrt(a):
Retorna l'arrel quadrada d'a
std::sin(a):
Retorna el sinus d'a
std::cos(a):
Retorna el cosinus d'a
std::tg(a):
Retorna la tangent d'a
std::abs(a):
Retorna el valor absolut d'a (és a dir, li treu el signe)

N'hi ha més, com per exemple l'arcsinus, logaritmes, arrodoniments... Es poden consultar aquí

String:

Nota: No és el mateix la llibreria string, que la llibreria cstring/string.h. S'ha d'anar amb compte de no confondre-les, ja que el codi deixaria de funcionar. Aquesta llibreria és molt útil, i la utilitzarem molt.

Què és un string? Un string el podem veure com un nou tipus de variable. No és un nou tipus, realment, però ja ens entenem així. Serveix per emmagatzemar diversos caràcters. És a dir, vindria a ser una cadena de caràcters. Podem desar-hi paraules, frases... Això ja depèn de nosaltres.

Té definits tots els operadors que necessitareu. <, >, <=, >=, ==, !=, =, +... Per saber com es compara, cal saber una cosa:

Un char equival a un número. Perquè us feu una idea, la 'A' és el 65, la 'B' el 66... Això fa que, per comparar chars, 'A'<'B' (ja que 65<66). Així es comparen els chars. Com a concepte, cal recordar que les lletres minúscules es desen seguides, les majúscules també, i els números també. Això fa que les comparacions siguin en ordre lexicogràfic

Doncs bé, un string es compara com esperaríem. Si volem comparar "Hola" amb "Hei!", mirem el primer element, i veiem que són iguals. Mirem el segon, i veiem que 'e'<'o'. Per tant, "Hei!" és menor que "Hola". Per sort, algú es va dedicar a programar tots aquests operadors, i no cal que ho fem.

L'únic que ens pot semblar estrany és el +. Aquest el que fa és concatenar strings. És a dir, si fem
string s="Hola", s2="Adeu";
cout <<s+s2<<endl;

Ens mostrarà "HolaAdeu"


Nota per programadors de C: En C, com sabreu, els strings són arrays de chars. I allà, no pots fer s=s2, sinó que tenim la funció strcpy. Doncs l'operador = és el mateix, copia un string a un altre. Igual amb strcmp, que enlloc d'utilitzar-lo, utilitzem els operadors de comparació

Funcions de la llibreria string:

string(int,char):
Retorna un string on apareix el char que li hem passat, tantes vegades com indiqui l'enter
size/length:
Retorna la llargada del nostre string. En aquest cas, com que opera des del propi string, hem de fer s.size(), enlloc de size(s).
La funció size() (o length(), són equivalents) retorna un unsigned int. Compte a l'hora de comparar, a vegades millor desar-ho en una variable entera abans i que es converteixi. Davant del dubte, tinc un post que explica tot això de les conversions
operator[]:
Serveix per accedir a un element concret. s[i] accediria a l'element i del string. Quan expliqui vectors m'aturaré en això, però és important saber que els informàtics som una mica estranys i comencem a comptar per 0. La idea seria que fer s[0] ens serveix per accedir al primer caràcter, s[1] al segon, fins a s[s.size()-1], que serveix per l'últim
getline(stream,string):
Aquesta funció serveix per llegir una línia sencera. Quan fem cin >>s, llegim fins al primer separador que trobem (espais, tabulats, salts de línia...). El getline llegeix fins al primer salt de línia que troba.
On diu "stream", acostumem a posar cin, però podria haver-hi un altre flux de dades d'entrada. Un exemple
string s;
while (getline(cin,s)) {
...
}



Definir les nostres funcions

Ara entrem a la part més important de la programació. Almenys de la programació funcional. Les funcions serveixen per encapsular el nostre codi, fer-lo més llegible, més fàcil de modificar i debugar... Són molt, però molt importants. Fins ara hem fet tots els programes dins del main, però això s'acabarà. 

Com és una funció?

La declaració d'una funció segueix l'estructura següent:
T nom_funcio(T1 parametre_1, T2 parametre_2, ..., Tn parametre_n)

On T, T1, T2,..., Tn són tipus de variable (poden coincidir entre ells). La funció rebrà una sèrie de paràmetres, i els utilitzarà per retornar alguna cosa. Un exemple fàcil:

int max (int a, int b);

Aquesta serviria per calcular el màxim de a i b. 
Dins de la funció, hi ha codi normal, com el que hem fet fins ara. L'únic nou que veiem és la sentència return. Serveix per retornar coses de la funció. Per exemple, en la funció max, retornaríem el número més gran dels dos. Anem a veure-la completa:

int max(int a, int b) {
    if (a>b) return a;
    else return b;
}

Sembla fàcil, no? Si a és la més gran, la retornem. Si no, vol dir que b és més gran, o que són iguals, i per tant retornem b. 
Truc: Una funció, quan troba el return, torna directament cap al lloc on l'havíem cridat, sense seguir executant codi. Per tant, la funció anterior equival a

int max(int a, int b) {
    if (a>b) return a;
    return b;
}

Ja que, si a fos la més gran, ja no estaríem a la funció en el moment d'arribar a la segona instrucció. Aquesta segona versió és una mica més eficient que la primera, tot i que la diferència és bastant negligible 

Funcions que no retornen res:

Aquestes es coneixen també com a "procediments". Tenen diverses utilitats, que ja les anirem veient. Es representen amb el "tipus" void

void info() {
    cout <<"Hola! Soc el Patata!"<<endl;
}

Aquest seria un exemple de funció void. En aquest cas serveix per escriure informació, però es poden utilitzar per altres coses


Per cridar funcions, es fa exactament igual que amb les definides a les llibreries, simplement no cal incloure-les, perquè ja estan al nostre codi. Un exemple de programa que utilitza la funció max (que, per altra banda, ja està definida) seria:

int main() {
    int a, b;
    cin >>a>>b;
    cout <<"El maxim es "<<max(a,b)<<endl;
}


Funcions amb el mateix nom:

Imaginem que volem definir una funció que calcula el màxim de 4 elements. Una opció seria fer com en un problema del jutge, i fer que la funció es digui max4. No obstant, C++ permet tenir més d'una funció amb el mateix nom, i les distingeix pels paràmetres que reben. És a dir, el cas següent funcionaria:

int max(int a, int b) {
    if (a>b) return a;
    return b;
}

int max(int a, int b, int c, int d) {
    return max(max(a,b),max(c,d));
}

Aquí, si li demanes que calculi max(1,2,-35, 47), com que veurà que li passes 4 paràmetres, cridarà a la de sota. Aquesta cridarà a la de dalt 3 cops, i finalment retornarà el resultat.

El main

Suposo que veient això, algú ha pensat "ei, jo escrivia en el meu codi 'int main()', i després allà a dins la resta. Això té forma de funció, no?". Efectivament. En C++, tot el codi està dins d'una funció. Quan tu executes un programa, per defecte es crida la funció main. Aquesta pot rebre o no rebre paràmetres (nosaltres ho farem sense), i retorna un enter. Concretament, retorna 0 si tot ha anat bé, i alguna cosa diferent de 0 si no ha anat bé. 

"Ei, però jo no li he posat cap return. Com retorna el 0?"
Tens raó. Abans (és a dir, en C) el main tenia un return 0 al final del codi. En canvi, ara, en C++, s'assumeix que si arriba al final del main, hi ha un return 0 (ja que, si arriba al final, se suposa que ha funcionat tot). Per tant, en C++ no cal posar el return 0. No obstant, hi ha gent que el posa. 

Àmbit de les variables

Fins ara havíem dit que una variable declarada dins d'un bucle, deixava d'existir a fora. De la mateixa manera, una variable creada dins d'un bucle n'emmascara una externa. Doncs amb les funcions passa el mateix. Dins d'una funció pots declarar variables, que només existiran dins d'aquella funció. Aquestes són variables locals. De la mateixa manera, podem declarar variables que no estiguin dins d'una funció. Aquestes són les variables globals. No obstant, a PRO1 no utilitzem variables globals (jo realment els tinc molta mania). La única excepció és per declarar constants. Quan declarem una constant, aquesta és global. Per exemple

const int MIDA_MAX=100000;
...
int main() {
    ...
    if (n>MIDA_MAX) cout <<"massa gran"<<endl;
}

Aquí hem d'imaginar que, per algun motiu, ens donaran números, i no volem que siguin més grans que 100.000. Podríem posar el 100.000 al codi tal qual, però si necessitem aquest número en diversos punts, i més tard ens adonem que ens hem equivocat (per exemple, ens hem deixat un 0 i era 1.000.000), canviar-ho seria complicat. El més fàcil és crear una constant, i utilitzar-la. En C++, les constants
- Es declaren utilitzant la paraula clau "const". Mai amb la directiva de preprocessador define, com es feia en C
- S'escriuen en majúscules
- Es declaren a dalt de tot del codi




Al proper article resoldré uns quants problemes del Jutge. Per d'aquí dos, vull plantejar un problema, perquè qui vulgui el porti ja pensat.

Les Torres de Hanoi

 

Explica la llegenda que, quan Déu va crear el món, va posar 3 barres de diamant verticals. A la primera hi va posar 64 discs d'or, cada un una mica més petit que l'anterior. Allà va crear un monestir, i va donar una ordre als monjos. L'ordre era
"Heu de moure els 64 discs de la primera torre a la tercera. Però només podeu moure els discs d'un en un. Podeu utilitzar d'ajuda la segona torre. Tingueu en compte que mai un disc pot estar a sobre d'un més petit"
La llegenda també diu que, el dia que els monjos acabin de moure els 64 discs, s'acabarà el món. 

Doncs bé, el meu problema és senzill. Cal fer una funció amb la capçalera següent
void torres_hanoi (int A, int B, int C, int n);
Que mostri per la sortida estàndard (cout) com moure n discs de la torre A, a la torre C, utilitzant B com a auxiliar. 

Cap comentari:

Publica un comentari a l'entrada