diumenge, 2 de juliol del 2017

Aprendre a programar 8 - Structs

Imaginem que, pel que sigui, necessitem fer una funció que calculi la distància entre dos punts. Abans de començar a pensar el codi, hem de pensar com emmagatzemem el punt. Se'ns poden acudir dues idees:

vector<double> Punt(2);
double Puntx, Punty;

És a dir, o bé fer un vector amb dos elements, o crear dues variables per emmagatzemar el punt. En el cas de la primera, què determina quina és la x, i quina la y? En el cas de la segona, què passa si volem tenir 10 punts? Fem 20 variables? Què fem si volem un vector de punts? Ens veiem forçats a triar la primera opció?

Struct

Una struct, també coneguda com a tupla (tot i que en C++ tenim una classe tupla, que no és el mateix), és una manera d'encapsular variables. En C++, les declarem així:

struct Nom {
    //Aquí, totes les variables
};

Sense oblidar-nos el ; al final de la declaració. Aleshores, si volem fer una struct que sigui un punt, podríem fer alguna cosa així a dalt de les funcions:

struct Punt {
    double x, y;
};

I, quan declarem un punt, fem

Punt p;

No sembla complex, no? Si volem accedir a la variable x, faríem p.x, i ja accediríem. Nosaltres, a PRO1, no podrem operar amb les structs directament, sinó que ho farem amb cada element de la struct. Com sempre, millor un exemple que no pas estar tota l'estona amb teoria i més teoria

struct Punt {
    double x, y;
};

double distancia (Punt p1, Punt p2) {
    double distx, disty;
    distx=p1.x-p2.x;
    disty=p1.y-p2.y;
    return sqrt(abs(distx*distx+disty*disty));
}

int main() {
    Punt p1, p2;
    while (cin >>p1.x>>p1.y>>p2.x>>p2.y) {
        cout <<distancia(p1,p2)<<endl;
    }
}

És a dir, creem dos punts, els llegim (recordem que no podem fer cin >>p1 directament, sinó que hem de llegir els elements individualment. 

Funcions per structs

Si amb els vectors era molt útil programar funcions per fer diverses coses (llegir-lo, escriure'l, operar amb ell...), amb les structs ho és encara més. Imaginem, si no, que tenim el següent:

struct Punt {
    double x, y;
};

struct Rectangle {
    Punt p1, p2, p3, p4;
};

Imaginem que volem llegir un rectangle que es diu r. Si no tenim cap funció, quedaria una cosa com:

cin >>r.p1.x>>r.p1.y>>r.p2.x>>r.p2.y>>r.p3.x>>r.p3.y>>r.p4.x>>r.p4.y;

Divertit, no? O, si no, podem fer funcions així (recordem que C++ permet fer funcions que es diguin igual si els paràmetres que reben són diferents):

void llegir (Punt& p) {
    cin >>p.x>>p.y;
}

void llegir(Rectangle& r) {
    llegir(r.p1);
    llegir(r.p2);
    llegir(r.p3);
    llegir(r.p4);
}

I ja està .Quan hem de llegir un rectangle, fem 
llegir(r);
I ja està llegit

Hem vist que podem posar una struct dins d'una altra. De la mateixa manera, podem posar un vector dins d'una struct. Podríem fer 

struct Estudiant {
    int dni;
    vector<double> notes;
};

I tindríem un estudiant, que s'identifica pel DNI, i té emmagatzemades les seves notes. Si volem accedir a una de les notes (suposem que volem consultar la nota del segon examen), podem fer e.notes[1], i així estarem accedint-hi. De la mateixa manera, podem fer-ne un vector. Imaginem que un professor vol desar els seus estudiants. Pot fer

vector<Estudiant> vest(N);

I, si vol consultar la nota del segon examen del tercer estudiant, faria vest[2].notes[1], i hi accediria.


Aleshores, resoldre problemes no és gaire difícil. Et poden dir que, donada una struct Punt i dos punts, calculis la distància, o coses per l'estil, però no canvia res més.

Altres coses

Les structs de C++ tenen coses molt guais. Aquestes, no obstant, no es fan a PRO1, així que si només t'interessa el que es fa a PRO1, pots saltar-te això. 

funcions membres:

Una struct, a més de tenir variables a dins, també té funcions. Podem veure una cosa així:
struct Punt {
    double x, y;
    double dist(const Punt& p) {
        double aux1=x-p.x;
        double aux2=y-p.y;
        return sqrt(aux1*aux1+aux2*aux2);
    }
};
struct Rectangle {
    //Els punts estan ordenats de manera que, unint-los amb un traç continu, formen el rectangle 
    Punt p1,p2,p3,p4;
    double perimetre() {
        return p1.dist(p2)+p2.dist(p3)+p3.dist(p4)+p4.dist(p1);
    }
};

Com podem veure, cada punt té una funció que calcula la distància respecte un altre punt. De la mateixa manera, cada rectangle té una funció que calcula el seu perímetre. Tot és molt optimitzable, però és un exemple. 

Operands:

Una struct, a més de tenir funcions membre, també pot tenir operands. N'hi ha molts que es poden sobrecarregar, però normalment se'n sobrecarreguen uns quants:

Constructor:

Aquest és molt típic. Bàsicament fas un constructor, que servirà per quan fas una instància de la struct. Un exemple seria

struct Punt {
    double x, y;
    Punt(double xx, double yy) {
        x=xx;
        y=yy;
    }
};

Això ens permet declarar un punt fent
Punt p(x,y);
I no haver-li de donar els valors accedint als membres

Destructor:

Aquest, per defecte, està fet. No obstant, si fas coses rares (per exemple, una estructura de dades utilitzant punters), l'has de programar. No obstant, no m'hi aturaré gaire

Operands matemàtics

Aquí tenim +, -, *, /, %, +=, -=, *=, /=, %=, ++, --... No obstant, jo explicaré com sobrecarregar +, +=, ++, i la resta podem imaginar com van

Començarem, contra el que podríem esperar, amb l'operand +=. Suposem que tenim una classe que representa un número complex. Faríem:

struct Complex {
    double r, i;//Real i imaginària
    Complex& operator+= (const Complex& c) {
        r+=c.r;
        i+=c.i;
        return *this;
    }
};

Veiem que retorna una referència a un complex. Això és perquè així podem fer a=(b+=c), i el que farem és primer sumar-li c a b, i després l'assignació.
Així, si féssim c1+=c2, seria equivalent a 
c1.r+=c2.r;
c1.i+=c2.i;
Queda decidir què retornem. La paraula clau this, és un punter a la struct que estem utilitzant. Si fem *this, per tant, tindrem la nostra struct, que és el que volem retornar
Amb una cosa similar podem definir -=, *=, /=, %=, =, i fins i tot <<=, >>=, |=, &=, ^=

Ara toca definir l'operador +

struct Complex {
    double r, i;//Real i imaginària
    Complex& operator+= (const Complex& c) {
        r+=c.r;
        i+=c.i;
        return *this;
    }
    
    Complex operator+(const Complex& c) {
        Complex res=*this;
        res+=c;
        return c;
    }
};

Sí, programem l'operador + utilitzant l'operador +=. Per això sóc reticent a dir que a+=b equival a a=a+b. Perquè realment utilitzem la primera per programar la segona. I, òbviament, serà millor la primera

Per últim, toca definir l'operador ++. I no, no el definirem utilitzant +=, ni utilitzant +. El definirem independentment. En aquest cas, com que incrementar un número complex no tindria sentit, utilitzarem una altra struct, que serà un unsigned double long long. És a dir, un número amb el doble de mida que un unsigned long long

struct udlonglong {
    unsigned long long h, l;
    udlonglong& operator++() {
        ++l;
        if(l==0) ++h;
        return *this;
    }
};

No obstant, ara hem definit el preincrement. Recordem que el preincrement i el postincrement no són equivalents. Per tant, anem a definir el postincrement

struct udlonglong {
    unsigned long long h, l;
        udlonglong& operator=(const udlonglong& u) {
        h=u.h;
        l=u.l;
        return *this;
    }
    udlonglong& operator++() {
        ++l;
        if(l==0) ++h;
        return *this;
    }
    udlonglong& operator++(int) {
        udlonglong aux=*this;
        ++(*this);
        return aux;
    }
};

Com veiem, rep un enter com a paràmetre. Això es va definir així per diferenciar, ja que era la única manera de distingir entre el preincrement i el postincrement. Com podem veure, el postincrement el definim de manera que incrementi igual el número, però retorni el valor que tenia abans d'incrementar-lo

Comparacions

Una cosa molt útil també és definir els operadors de comparació. Per estructures com el set, o per utilitzar el sort, per exemple. Es poden definir de diferents maneres

struct udlonglong {
    unsigned long long h, l;
    
    bool operator<(const udlonglong& u) const{
        if (h!=u.h) return h<u.h;
        return l<u.l;
    }
    
    friend bool operator<(const udlonglong& u1, const udlonglong& u2) {
        if (u1.h!=u2.h) return u1.h<u2.h;
        return u1.l<u2.l;
    }
};

Aquí veiem les dues maneres. La primera és com un operador membre, i la segona com un operador amic. 

Hi ha una certa tendència a declarar-les com a operadors amics. Això és perquè la primera opció obliga a que l'operador de l'esquerra sigui sempre un objecte d'aquest tipus, mentre que la segona opció permet ser més laxes. Podem definir la comparació amb unsigned long long, amb unsigned, i amb qualsevol altre tipus que se'ns passi pel cap. 

Per definir els altres operadors, normalment utilitzem <. Es fa perquè es compleixen les següents relacions

a>b equival a b<a
a<=b equival a not a>b
a>=b equival a not a<b
a!=b equival a a<b or a>b (de fet, en alguns llenguatges l'operador != es diu <>
a==b equival a not a<b and not a>b, que veiem que equival a not a!=b, aplicant les lleis de De Morgan

No obstant, a vegades definim l'operador == nosaltres, i l'operador != respecte a aquest, per temes de temps. 

streams

En C++ apareix una cosa coneguda com a stream. Vindria a ser un flux de dades, que s'acostuma a utilitzar per llegir o escriure dades d'algun lloc. Els més típics són cin (entrada estàndard), cout (sortida estàndard) i cerr (sortida d'errors). No obstant, n'hi poden haver més. Per exemple, amb la llibreria fstream en podem fer per llegir o escriure fitxers, i amb la llibreria sstream per fer-ho amb strings. Doncs nosaltres podem definir els operadors de lectura i d'escriptura d'una struct nostra.

struct Complex {
    double r, i;//Real i imaginària
};

istream& operator>>(istream& flux, Complex& c) {
    flux>>c.r;
    char ch;
    flux>>ch;
    string s;
    flux>>s;
    s.pop_back();
    c.i=atoi(s.c_str());
    return flux;
}

ostream& operator<<(ostream& flux, Complex& c) {
    if (c.i<0) {
        flux<<c.r<<" - "<<-c.i<<'i';
    }
    else {
        flux<<c.r<<" + "<<c.i<<'i';
    }
    return flux;
}

En el cas dels streams, al ser aquest l'operand de l'esquerra, queda descartada l'opció de sobrecarregar-ho dins. Bàsicament perquè el que hauríem de sobrecarregar és el flux, i no el podem modificar. Per tant, el que fem és fer-ho a fora. 

En aquest cas concret, representem un complex de la forma "a + bi" (o "a - bi", si la part imaginària és negativa). Per això ho llegim i escrivim com es pot veure al codi

Si ens fixem, retornem l'operador per referència. Això és perquè així podem fer coses com "cin>>a>>b", ja que fem cin>>a, ens retorna cin, i fem cin>>b. Ídem amb la sortida 

I molts altres

Si vols consultar els altres, és tan fàcil com anar a aquest enllaç, que hi són tots

Cap comentari:

Publica un comentari a l'entrada