Índex
Miguel A. Almarza
Departament d'Informàtica
IES Mare de Deu de la Merce

       

 
Capítol 2

Algunes particularitats de C++.

  1. Forma completa de la declaració d'una classe.
  2. Punters a objectes: new i delete.
    1. Referències a objectes.
    2. Pas de paràmetres per referència a funcions.
    3. Vectors d'objectes amb new.
    4. Els vectors de tipus elementals de dades amb new.
  3. Funcions inline.
  4. Sobrecarga de funciones. Polimorfisme amb funcions.
    1. Exemple: la funció ValorAbsolut
    2. Constructors amb paràmetres per defecte
    3. Pas de paràmetres per defecte
    4. Ambigüitat
  5. El punter this.
  6. Exercicis.


2.1. Forma completa de la declaració d'una classe.

Una classe pot escriure's amb la declaració següent
 
class <identificador>
{
    <Secció privada>
public:
    <Secció pública>
private:
    <Secció privada>
proctected:
    <secció protegida>
} <llista d'objectes de la classe>

Fem notar que les instruccions escrites abans d'escriure cap de les tres etiquetes és una secció d'element privats de la classe.

Podem escriure més d'una secció private, public o protected, però és convenient ordenar les propietats i mètodes d'una classe escrivint només una secció de cada una d'elles.

No importa l'ordre en el que estan escrites les secciones de les classes. Però també és convenient escriure primer la secció privada, després la secció pública i en darrer lloc la protegida. Simplement és una qüestió d'estètica del programador i que ajuda molt a l'hora de llegir i entendre una declaració d'una classe.
 

Estructures i classes

També podem fer ús de la declaració següent en el moment de declarar una estructura:
 
struct <identificador>
{
    <Secció pública>
public:
    <Secció pública>
private:
    <Secció privada>
proctected:
    <secció protegida>
} <llista d'objectes de la classe>

Allò que és una estructura normal en C és en realitat un objecte en C++ i així la paraula clau struct ha canviat totalment els seu significat. Ara quan declarem una estructura en C++ li podem posar també funcions i una estructura serà conceptualment equivalent a una classe.

La diferència entre una estructura i una classe en C++ és que per defecte les instrucció escrites abans de cap etiqueta són elements públics de l'estructura.

Penso que és molt convenient pels programadors fer ús de les estructures en el sentit clàssic de C encara que estiguem escrivint en C++ i reservar totes les qüestions d'objectes per a les classes.
 

Unions i classes

Amb les unions de C++ també estem declarant en realitat classes, de la mateixa manera que amb les estructures. Pots ampliar aquest apartat en algun llibre de C++.

2.2. Punters a objectes: new i delete.

Un objecte d'una classe pot declarar-se igual que hem fet la declaració de qualsevol variable o qualsevol estructura.

Quan volem fer assignació dinàmica de memòria hem de crear un punter a un objecte i després assignar-li memòria mitjançant l'operador new. Per alliberar la memòria assignada mitjançant l'operador new es fa ús de l'operador delete.

Es clar, igual que amb les estructures del C, quan accedim a un objecte mitjançant un punter hem de fer ús de l'operador -> per tal de arribar als seus membres (propietats i mètodes) públics.
 
Ús de punters i reserva dinàmica de memòria
#include <iostream.h>
#include <conio.h>
#include <stdlib.h>

class Punt
{
        int x;
        int y;
        char Caracter;

    public:
        void PosaCaracter(char c);
        void PosaCoordenades(int n1, int n2);
        void Dibuixat();
        int TornaX();
        int TornaY();
        char TornaCaracter();
};

main()
{
    Punt * pA;
    pA = new Punt;
    if (!pA) exit(1);

    pA->PosaCoordenades(10,10);
    pA->PosaCaracter('O');
    pA->Dibuixat();

    Punt * pB
    if (! (pB = new Punt)) exit(1);
    pB->PosaCoordenades(15,10);
    pB->PosaCaracter('G');
    pB->Dibuixat();

    gotoxy(1,1);
    cout << pA->TornaX();
    cout << pA->TornaY();
    cout << pA->TornaCaracter();

    delete pA;
    delete pB;
}
La instrucció Punt * pA; crea una variable punter a un objecte de tipus Punt, però no fa la reserva de memòria necessària per a les dades de l'objecte.

Per tant pA és un punter a un objecte de tipus Punt.

La instrucció pA = new Punt; busca memòria lliure, en tota la memòria a la que pot accedir el programa, per posar-hi les dades d'un objecte de tipus punt apuntat pel punter pA. Si troba memòria suficient, l'operador new fa la reserva d'aquesta memòria i torna l'adreça del lloc on comencen les dades de l'objecte punt adreça de memòria que és assignada al punter pA. 

Cas que no es trobés memòria suficient l'operador new torna un NULL i el punter pA pren aquest valor NULL.

Veiem que totes les instruccions per accedir als membres públics a través del punter pA ara fan ús de l'operador -> en la forma següent:

pA->TornaX();


La instrucció if (! pA) exit(1); s'escriu pel cas que l'operador new no hagués trobat memòria suficient pel punt i evitar accedir a memòria no reservada pel nostre programa. 

2.2.1. Referències a objectes.

Tenim l'operador & que podem traduir per adreça de que ens dona l'adreça de memòria de l'objecte al que és aplicat.

Així si afegim les següents instruccions al programa anterior
 

Punt Centre;
Punt * O;
O = &Centre;

O->PosaCoordenades(20,10);
O->PosaCaracter('C');
O->Dibuixat();

tenim un nou punt que es diu Centre. Fem un punter a punt que anomenem O i la instrucció O = &Centre assigna l'adreça de memòria del punt Centre al punter O. Així tenim que O és una referència al punt Centre.

Hem de pensar que com que O és un punter hem d'accedir als elements de l'objecte mitjançant l'operador ->.

És normal fer ús del pas d'objectes a funcions per referències i no per valor quan hem d'estalviar memòria o volem més rapidesa en l'execució dels nostres programes. El mecanisme de pas d'objectes per referència és s'explica en el paràgraf degüent.

2.2.2. Pas de paràmetres per referència a funcions.

Com veiem a l'exemple següent la declaració d'una variable amb pas de paràmetres per referència en C++ porta el símbol & davant i això crea una referència de la variable.

Així tenim que Numero és un nou nom per a les dades de la variable Diners de la funció main. Am els dos noms accedim a la mateixa adreça de dades. Numero és una referència de la variable Diners.

Observem que al llarg de la funció Duplica fem ús de la referència Numero per modificar el valor de Diners sense fer ús de cap operador, com l'operador * de C.

Pas de paràmetres per referència a funcions.
Programa fet en CPrograma fet en C++
#include <stdio.h>

void Duplica(int * Numero);

main()
{
    int Diners=230;
    Duplica(&Diners);
    printf("La variable Diners 
	  té com a dada %d\n",Diners);
}

void Duplica(int * Numero)
{
    *Numero = *Numero * 2;
}
#include <iostream.h>

void Duplica(int &Numero);

main()
{
    int Diners=230;
    Duplica(Diners);
    cout << "La variable Diners 
          té com a dada " << Diners;
}

void Duplica(int &Numero)
{
    Numero = Numero * 2;
}

2.2.3. Vectors d'objectes amb new.

L'operador new també pot reservar memòria per a un vector d'objectes. Encara que no ho hem dit abans, l'avantatja que té l'operador new és que la reserva de memòria es fa en tota la memòria HEAP, és a dir, aquella memòria que el sistema operatiu és capaç de gestionar i posar a disposició del nostre programa.

Per tant, quan necessitem fer una reserva masiva de memòria, per a un vector d'objectes és millor fer-la am l'operador new.

Mitjançant les instruccions següents estem declarant un vector a objecte de la classe Punt, anomenat VertexRectangle i reservant quatre mides de memòria necessàries per a quatre objectes Punt on posarem les dades d'aquest quatre Punts.

Punt * Rectangle;
Rectangle = new Punt [4];

Recordem que la memòria reservada amb l'operador new s'allibera amb l'operador delete i per tant la instrucció que allibera el vector creat abans és

delete Rectangle;

Vectors d'objectes amb new.
main()
{
    Punt * Rectangle;
    Rectangle = new Punt [4];

    Rectangle[0].PosaCoordenades(1,1);
    Rectangle[0].PosaCaracter('O');
    Rectangle[1].PosaCoordenades(79,1);
    Rectangle[1].PosaCaracter('O');
    Rectangle[2].PosaCoordenades(1,24);
    Rectangle[2].PosaCaracter('O');
    Rectangle[3].PosaCoordenades(79,24);
    Rectangle[3].PosaCaracter('O');

    for(int i=0;i<4;i++) Rectangle[i].Dibuixat();

    delete Rectangle;
}

2.2.4. Els vectors de tipus elementals de dades amb new.

Els tipus elementals de dades són, en certa manera, objectes en C++. Es per això que poden fer ús de l'operador new per reservar memòria per a vectors d'aquests tipus de dades.

Així, una nova versió de la nostra classe string serà a partir d'ara la que fa ús dels operadors new i delete que són propis de C++ i no malloc i free que són propis de C.

Els vectors de tipus elementals de dades amb new.
#include <iostream.h>
#include <string.h>
#include <stdlib.h>


class String
{
    char * Cadena;
    int Longitut;
public:
    String(char * Punter);
    ~String();
    void Posa(char * Punter);
    void ImprimeixDades();
}


String::String(char * Punter)
{
    if ((Cadena = new char [strlen(Punter)+1]) == NULL)
    {
        cout << "Falta mem•ria per a aquest string\n";
        exit(1);
    }
    strcpy(Cadena,Punter);
    Longitut=strlen(Cadena);
}

String::~String()
{
    cout << "Desassignat mem•ria de dades\n";
    delete Cadena;
}

void String::Posa(char * Punter)
{
    delete Cadena;
    if ((Cadena = new char [strlen(Punter)+1]) == NULL)
    {
        cout << "Falta mem•ria per a aquest string\n";
        exit(1);
    }
    strcpy(Cadena,Punter);
    Longitut=strlen(Cadena);
}

void String::ImprimeixDades()
{
    cout << "Objecte string.\n";
    cout << "Cadena:   " << Cadena << "\n";
    cout << "Longitut: " << Longitut << "\n";
}

main()
{
    String Nom("Pere Lopez Perez");
    Nom.ImprimeixDades();
    Nom.Posa("Adela Lugano Luengo");
    Nom.ImprimeixDades();
}

2.3. Funcions inline.

La paraula clau inline permet declarar i definir les anomenades funcions en línia. El codi de les funcions inline és inserit en el codi del programa cada vegada que es crida la funció, en lloc del codi corresponent a la crida d'aquesta funció.

Això permet fer més ràpid la crida d'aquestes funcions. És clar que el codi d'aquestes funcions no ha de ser gran, car el codi del programa sencer hauria de ser molt gran. També és impropi fer ús dins aquestes funcions de estructures iteratives o de la recursivitat, i de fet hi ha alguns compiladors que no permeten el seu ús.

En el programa següent demostrem com es fa ús d'aquesta paraula clau:
 
Ús de funcions inline.
#include <iostream.h>
#include <stdlib.h>

class Dau
{
    int Numero;
    int NumeroCaras;
public:
    Dau();
    Dau(int NumeroC, int Num);
    void FesSorteig();
    int MostraNumero();
};

inline Dau::Dau()
{
    Numero = 1;
    NumeroCaras = 6;
}

inline Dau::Dau(int NumeroC, int Num)
{
    NumeroCaras = NumeroC;
    Numero = Num;
}

inline void Dau::FesSorteig()
{
    Numero = rand() % NumeroCaras +1;
}

inline int Dau::MostraNumero()
{
    return Numero;
}

main()
{
    randomize();

    Dau ElMeuDau;
    cout << ElMeuDau.MostraNumero() << '\n';

    for(int i=0;i<10;i++)
    {
        ElMeuDau.FesSorteig();
        cout << ElMeuDau.MostraNumero() << '\n';
    }
}
Aprofitem per crear una classe anomenada Dau. En aquesta classe posem dues propietats, el número de cares i el número (la cara visible).

Els dos constructors i els dos mètodes (FesSorteig i MostraNumero) són totes funcions molt petites i que tenen codi sense crides recursives ni instruccions iteratives. Això fa possible declarar-les totes com a funcions inline en el nostre codi.

Quan fem una classe també podem escriure el codi de les funcions dins la declaració de la classe. Això és una altre manera de dir-li al compilador que aquestes funcions són funcions inline.

L'aspecte típic d'una classe amb funcions inline de petit codi és la del programa següent:
 

La classe Punt amb funcions inline.
#include <iostream.h>
#include <conio.h>

class Punt
{
        int x;
        int y;
        char Caracter;

    public:
        void PosaCaracter(char c) { Caracter = c;}
        void PosaCoordenades(int n1, int n2) {x = n1;y = n2;}
        void Dibuixat() {gotoxy(x,y); cout << Caracter;}
        int TornaX() { return x;}
        int TornaY() { return y;}
        char TornaCaracter() {return Caracter;}
};

main()
{
    Punt A;
    A.PosaCoordenades(10,10);
    A.PosaCaracter('O');
    A.Dibuixat();

    Punt B;
    B.PosaCoordenades(15,10);
    B.PosaCaracter('G');
    B.Dibuixat();

    gotoxy(1,1);
    cout << A.TornaX();
    cout << A.TornaY();
    cout << A.TornaCaracter();
}

2.4. Sobrecarga de funciones. Polimorfisme amb funcions.

Si volem fer una funció que calculi la mitjana aritmètica d'un vector haurem de fer tres o més funcions, una ha de rebre un vector de números enters, altra ha de rebre un vector de números long una altra de números float, etc.

Si estem programant en el llenguatge C haurem de fer funcions diferents amb noms també diferents cadascuna amb els seus paràmetres.

Si estem programant en C++ haurem de fer funcions diferents però podrem donar el mateix nom a totes elles, i es diferenciaran en el tipus de paràmetres que rebran.

Això es diu sobrecàrrega de funcions. Una funció sobrecarregada és aquella que té diverses definicions, totes elles amb el mateix nom de funció, de manera que una definició és diferencia de les altres en el numero de paràmetres que rep o en el tipus d'aquests paràmetres.

Quan una funció està sobrecarregada el compilador tria una de les definicions examinant els paràmetres que li passem quan la cridem.

No és suficient que el tipus de paràmetre que torna la funció sigui diferent per tal de considerar la funció sobrecarregada. De fet si escrivim dues definicions d'una funció sobrecarregada amb els mateixos paràmetres d'entrada però tornant diferent tipus de paràmetres el compilador donarà errors d'ambigüitat ja que quan la cridarem no sabrà quina definició triar.

Ja hem vist que els constructors són funcions que es sobrecarreguen normalment.

La sobrecàrrega és un dels mètodes que utilitza el C++ per aconseguir el polimorfisme, ja que amb el mateix nom de funció podem fer coses diferents.

La funció ValorAbsolut sobrecarregada.
#include <iostream.h>

int ValorAbsolut(int a)
{
        cout << "Valor absolut amb enters ";
        if (a > 0) return a;
        else return - a;
}

long ValorAbsolut(long a)
{
        cout << "Valor absolut amb enters llargs ";
        if (a > 0) return a;
        else return - a;
}


float ValorAbsolut(float a)
{
        cout << "Valor absolut amb reals (floats) ";
        if (a > 0) return a;
        else return - a;
}

main()
{
        int x = -5;
        long y = -23L;
        float z =-10.4;

        cout << ValorAbsolut(x) << "\n";
        cout << ValorAbsolut(y) << "\n";
        cout << ValorAbsolut(z) << "\n";
}
Una funció que calculi el valor absolut d'un número és necessària. En C tenim les funcions abs per a enters, fabs per a floats, i labs per a long.

Nosaltres fem tres funcions, totes anomenades ValorAbsolut, una per a enters, altra per a floats i una tercera per a longs.

En cadascuna d'aquestes funcions hem posat una instrucció cout per tal d'entendre que en el moment de cridar-les el compilador tria una d'elles segons el tipus de paràmetre que li passem.

El resultat d'executar aquest programa és el següent:

És obvi que si estem fent un objecte per a una llibreria no posarem les instruccions cout de les funcions ValorAbsolut.

2.4.2 Pas de paràmetres per defecte

Els paràmetres amb valor per defecte d'una funció són aquells que prenen un valor si no s'especifica el seu valor en el moment de cridar la funció. Si tenim la funció SumaQuadrats declarada en ela forma següent:

long SumaQuadrats(int a1=0, int a2=0, int a3=0)

Podem cridar-la de quatre formes diferents de manera que e cadascuna els paràmetres prenen els valors del quadre:

Ens adonem que podem cridar la funció sense especificar tots els paràmetres, però han d'omplir-se des de el primer fins a l'últim que s'especifica. No podem fer una crida a la funció especificant el primer i el tercer paràmetre, per exemple.
 

Crida de la funció Valor dels paràmetres
a1 a2 a3
SumaQuadrats() 0 0 0
SumaQuadrats(3) 3 0 0
SumaQuadrats(4,7) 4 7 0
SumaQuadrats(23,12,13) 23 12 13

D'una certa manera podem dir que aquesta és una altra forma de polimorfisme, ja que tenim diferents formes de cridar una mateix funció.
 
Exemple 1 de pas de paràmetres per defecte.
#include <iostream.h>

long SumaQuadrats(int a1=0, int a2=0, int a3=0)
{
        return a1*a1 + a2*a2 + a3*a3;
}

main()
{
        cout << SumaQuadrats() << "\n";
        cout << SumaQuadrats(1) << "\n";
        cout << SumaQuadrats(1,2) << "\n";
        cout << SumaQuadrats(1,2,3) << "\n";
}
Sortida per pantalla:

0
1
5
14

També es poden posar paràmetres "normals" i després paràmetres per defecte. En aquest cas sempre han d'estar escrits els paràmetres per defecte després dels paràmetres normals. És típic fer ús d'aquesta forma per fer funcions que actuïn de manera diferent segons el paràmetres per defecte.
 
Exemple 2 de pas de paràmetres per defecte.
#include <iostream.h>

float Calcula(int a, int b, char Operador='+')
{
    float resultat=0;

    switch (Operador)
    {
        case '+' :  resultat = a + b;
                    break;
        case '-' :  resultat = a - b;
                    break;
        case '*' :  resultat = a * b;
                    break;
        case '/' :  if (b!=0)
                        resultat = (float)a/(float)b;
                    break;
    }
    return resultat;
}

main()
{
    cout << Calcula(3,6) << '\n';
    cout << Calcula(3,6,'-') << '\n';
    cout << Calcula(3,6,'*') << '\n';
    cout << Calcula(3,6,'/') << '\n';
}
Si volem fer una funció que serveixi normalment per sumar dos números, però que també volem fer-la més flexible i que ens serveixi en altres moments per restar, multiplicar o dividir podem fer la funció següent:

float Calcula(int a, int b, char Operador='+');

Veiem que quan cridarem la funció serà obligatori escriure els números que han de sumar-se i no haurem d'especificar l'operand suma. Així, per defecte, la funció sumarà els dos números que rebrà com a paràmetres. Només si especifiquen el paràmetre corresponent a l'operand farà una altra operació.

2.4. 2 Constructors amb paràmetres per defecte

Podem fer ús de les funcions amb pas de paràmetres per defecte per crear constructors per defecte com el que es veu a l'exemple de la classe Punt.

Recordem que els paràmetres que es passen quan cridem la funció han d'escriure's en el mateix ordre de la declaració i que no mes es poden deixar paràmetres en blanc de dreta a esquerra.

Constructor amb pas de paràmetre per defecte.
#include <iostream.h>

class Punt
{
    int x,y;
    char Caracter;
public:
    Punt(int n1=0, int n2=0, char C = ' ')
    {
        x=n1; y=n2; Caracter = C;
    };
    void ImprimeixDades(void);
};

void Punt::ImprimeixDades(void)
{
    cout << "x = " << x << " y = " << y << "\n";
    cout << "Carcter " << Caracter << "\n";
}

main()
{
    Punt A;
    Punt B(1);
    Punt C(1,3);
    Punt D(1,3,'z');

    A.ImprimeixDades();
    B.ImprimeixDades();
    C.ImprimeixDades();
    D.ImprimeixDades();
}

2.4.4 Ambigüitat

En el moment que es fa una sobrecàrrega d'una funció ha de pensar-se molt bé per evitar que el compilador no tingui problema en el moment de triar la funció adequada a la crida que haguem fet.

Així suposem que tenim les definicions següents de la ValorAbsolut
 

Programa amb ambigüitat
#include <iostream.h>

long ValorAbsolut(long a)
{
    cout << "Valor absolut amb enters llargs ";
    if (a h> 0) return a;
    else return - a;
}

float ValorAbsolut(float a)
{
    cout << "Valor absolut amb reals (floats) ";
    if (a h> 0) return a;
    else return - a;
}

main()
{
    cout << ValorAbsolut(10) << "\n";
}

Quan fem la crida ValorAbsolut(10) el compilador ha de fer la tria entre una de les dues funcións ValorAbsolut. Llavors, com el compilador, que pot interpretar el número 10, que és un enter, com un float o com un long indistintament, no sap quina de les dues funcions ha de triar, cosa que produeix un error d'ambigüitat en el moment de la compilació del programa.

2.5. El punter this.

Les dades d'un objecte es troben a una adreça de la memòria, que el programa sol·licita i obté quan es declara l'objecte.

Es a dir que quan fem una declaració del tipus NomClase Ob, es fa la reserva de memòria adient per tal de posar-hi les dades de l'objecte Ob.

Quan es crida una funció membre d'una classe a través d'un objecte amb una instrucció del tipus Ob.Funcio(paràmetres); aquesta funció ha de saber on es troben aquestes dades. Això es fa automàticament per mitjà d'un punter, anomenat punter this, que rep la funció de forma automàtica en fer la crida anterior.
 
Demo punter this
#include <iostream.h>

class Complex
{
    float a,b;
public:
    Complex(float x, float y)
    {
        this->a=x;
        this->b=y;
    }
    void Imprimeix()
    {
        cout << this->a << " + "  << this->b << "i";
    }
}

main()
{
    Complex z(2,5);
    z.Imprimeix();
}
La instrucció Complex z(2,5); declara un objecte, anomenat z, de la classe Complex. Les dades, (2,5), es posaran en una certa adreça de memòria.

La instrucció z.Imprimeix() s'executa i rep l'adreça de l'objecte z per mitja del punter this, punter que es crea i passa de forma automàtica.

Segons veiem a les dues funcions membre, la funció constructora Complex i la funció Imprimeix, hem fet ús del punter this per accedir a les dades a i b dels objectes de la classe Complex.

Cal dir que escriure this->a = x; és igual que escriure a=x;. Es a dir, que podem suprimir l'escriptura del punter this, encara que de fet es fa ús d'ell encara que no l'escriguem.

Aquest punter apareix també en llenguatges de programació posteriors a C++ com és el llenguatge Java on adquireix molta més importància.
Sortida per pantalla

2.6. Exercicis.

  1. Fes ús de la classe DadesPersonals per crear amb assignació dinàmica de memòria un objecte d'aquesta classe. També hauràs d'omplir les dades d'aquest objecte i mostrar-les per pantalla.

  2. Crea una referència a la fitxa anterior i treballa amb la referència per canviar i mostrar les dades.

  3. Fes un vector de tres objectes fitxa i omple el vector mitjançant una funció. Crea una funció que mostri les dades del vector.

  4. Modifica totes les classes dels exercicis i la teoria que han sortit fins ara escrivint les funcions que puguis de forma inline.

  5. Escriu una funció que canviï les dades de dues variables de dos maneres diferents:

    1. Amb punters com es feia en C.
    2. Amb referències com es fa millor en C++.

    Escriu en els dos casos una funció main que faci ús d'aquestes funcions.

  6. Siguin les declaracions de funció següents:
    int func(int);         //Funció 1
    int func(float);            //Funció 2
    void func(int, float);      //Funció 3
    void func(float, int);      //Funció 4
    Declarem les variables següents:
    int n,p;
    float x, y;
    char c;
    double z;
    Decideix de les crides següents quines són correctes i si ho són quina funció s'executa i quines conversions de dades es produeixen.
    func(n);
    func(x);
    func(n,x);
    func(x,n);
    func(c);
    func(n,p);
    func(n,c);
    func(n,z);
    func(z,z);

  7. Escriu una funció polimorfa que serveix per calcular el número de segons del dia actual que han transcorregut fins a l'instant que es passa a la funció. El programador pot escriure l'hora en els formats següents:
    Segons(hores, minuts)
    Segons(hores, minuts, segons)         //Totes tres variables sons enters
    Segons("hh:mm:ss")                    //La funció rep un string amb separador :
    Segons("hh/mm/ss")                    //La funció rep un string amb separador /
    Pensa quantes funcions has de fer i els paràmetres que han de rebre.