PDA

Visualizza la versione completa : [C++] Classi


ing82
11-09-2015, 23:42
Il problema è il seguente:

mi serve creare un oggetto che mi permetta di gestire la sezione e relative caratteristiche di elementi.

Per iniziare diciamo che avrò a che fare con tre tipi di sezioni, che poi aumenteranno: sezione rettangolare, circolare, generica.

Durante l'esecuzione del programma, l'utente potrebbe decidere di variare la sezione, passando ad esempio da sezione rettangolare a sezione circolare.

Pensavo di gestire la cosa così:

1) Creo una classe per ciascun tipo di sezione

class Sezione_rettangolare_type
{
public:
Sezione_rettangolare_type();
~Sezione_rettangolare_type();
void calcola_area();
void calcola_perimetro();
protected:
double area;
double perimetro;
......
}


class Sezione_circolare_type
{
public:
Sezione_circolare_type();
~Sezione_circolare_type();
void calcola_area();
void calcola_perimetro();
protected:
double area;
double perimetro;
......
}


class Sezione_generica_type
{
public:
Sezione_generica_type();
~Sezione_generica_type();
void calcola_area();
void calcola_perimetro();
protected:
double area;
double perimetro;
......
}

2) Creo una classe che mi permetta di gestire i diversi tipi di sezione


class Sezione_type
{
public:
Sezione_type(int tipo_sez);
~Sezione_type();
void cambia_sezione(int tipo_vecchio, int tipo_nuovo);
protected:
Sezione_generica_type *sezgenerica;
Sezione_rettangolare_type *sezrettangolare;
Sezione_circolare_type *sezcircolare;
int tipo_sezione;
}

Può funzionare, oppure un caso del genere è da gestire in modo diverso?

Grazie.

ing82
22-09-2015, 10:03
Approfondendo la questione (=studiare), mi viene in mente che la mia situazione possa essere gestita in maniera forse più efficiente nel seguente modo (a parte il fatto di aver implementato le funzioni membro direttamente all'interno della classe, ma non ho a disposizione il codice che ho testato con la classica divisione nei file .h e . cpp)

1)

class Sezione_type
{
public:
Sezione_type()
{
area=0;
};
~Sezione_type();
virtual void calcola_area();
...
protected:
double area;
};

2)

class Sezione_rettangolare_type
{
public:
Sezione_rettangolare_type(double B, double H):Sezione_type()
{
base=B;
altezza=H;
calcola_area();
};
~Sezione_rettangolare_type();
void calcola_area()
{
area=base*altezza;
};
protected:
double base;
double altezza;
};

3)

class Sezione_circolare_type
{
public:
Sezione_circolare_type(double D):Sezione_type()
{
diametro=D;
calcola_area();
}
void calcola_area()
{
area=3.14*pow(D,2.0)/4.0;
}
protected:
double diametro;
};

4)in questo modo, nel programma posso fare


int main()
{
Sezione_type *sezione;
...
//righe di codice che permettono di scegliere se sezione rettangolare o circolare e fare l'input dei dati come base e
//altezza o in alternativa diametro
...
//in base a quanto fatto prima se ho scelto sezione rettangolare
sezione = new Sezione_rettangolare_type(B,H);
//se ho scelto la sezione circolare
sezione = new Sezione_circolare_type(D);
....
//righe di codice che permettono di scegliere una sezione di forma diversa e relativo input dati
//quindi distruggo la sezione precedentemente creata
delete sezione;
...
//in base al nuovo tipo di sezione scelta, rifaccio
sezione = new ...//e qui ci metto il corrispondente nuovo tipo di costruttore
...
//fine del programma
delete sezione;
return 0;
}

Ora pongo le seguenti domande:
1) se al posto di

virtual void calcola_area(); avessi fatto
virtual void calcola_area()=0; nella classe "Sezione_type" tutto quanto riportato nella precedente parte di ipotetico main non avrei potuto
farlo, perdendone i vantaggi, ho capito giusto? (però se è stato inventato in qualche caso tornerà utile, ma per ora non riesco a capire quando)
2) se in "Sezione_type" avessi semplicemente scritto
void calcola_area(); e nelle altre classi avessi reimplemento la funzione membro (dovrebbe essere overload, spero di non sbagliarmi), avrei anche in questo caso perso la possibilità di definire all'inizio un oggetto di tipo "generale" come Sezione_type, e "particolareggiarlo" successivamente
durante l'esecuzione del programma, corretto?

Grazie

shodan
22-09-2015, 14:18
1)


virtual void calcola_area();

In questo caso devi fornire un'implementazione del metodo in sezione_type, in quanto potresti usare sezione_type come classe a se stante.


virtual void calcola_area()=0;

In questo caso sei obbligato a dare un'implementazione della funzione nella classe derivata, in quanto il metodo è astratto.
2)
Si, in quel modo verrebbe richiamato sempre il metodo della classe base.
Ricapitolando:


void calcola_area(); // richiama sempre metodo classe base a prescindere dalle derivate (static binding)
virtual void calcola_area(); // richiama metodo classe derivata solo se presente. Se no metodo classe base (dynamic binding)
virtual void calcola_area()=0; // occorre definire il metodo nelle classi derivate, altrimenti non compila. (dynamic binding)


3) Le classi basi devono avere il distruttore dichiarato virtual, altrimenti il distruttore delle derivate non sarà invocato.

ing82
23-09-2015, 12:15
Ok, grazie.
Chiariti questi aspetti, mi torna il dubbio iniziale: per gestire diversi tipi di sezione che possono cambiare durante l'esecuzione, quale soluzione è migliore? Quella prospettata nel primo post, oppure la seconda?
Oppure sono valide entrambe e in base all'esperienza e soprattutto a quello che è lo scopo del programma, è da scegliere una o l'altra?

shodan
23-09-2015, 19:56
Il tuo codice (riportato qui) non compila nemmeno. Solo ora mi sono accorto che non esiste ereditarietà tra le classi, il rende la discussione falsata in partenza (hai riportato male il codice?)

In caso di ereditarietà, molto meglio la seconda soluzione (pattern Strategy http://www.vincehuston.org/dp/strategy.html)

Ancora meglio usando smart pointer (qui uso std::unique_ptr).

Notare che quello che viene fatto nel main può essere a sua volta incapsulato in una classe secondo il pattern State:
http://www.vincehuston.org/dp/state.html



int main()
{

std::unique_ptr<Sezione_type> sezione;
...
//righe di codice che permettono di scegliere se sezione rettangolare o circolare e fare l'input dei dati come base e
//altezza o in alternativa diametro
...
//in base a quanto fatto prima se ho scelto sezione rettangolare
sezione.reset(new Sezione_rettangolare_type(B,H));
sezione->calcola_area();

//in base al nuovo tipo di sezione scelta, rifaccio
sezione.reset(new Sezione_circolare_type(D));
sezione->calcola_area();

return 0;
}

ing82
23-09-2015, 23:45
Il tuo codice (riportato qui) non compila nemmeno. Solo ora mi sono accorto che non esiste ereditarietà tra le classi, il rende la discussione falsata in partenza (hai riportato male il codice?)


Per quanto riguarda il codice del primo mio intervento, non lo ho mai provato, per quanto riguarda il secondo, avevo accennato qualcosa

Approfondendo la questione (=studiare), mi viene in mente che la mia situazione possa essere gestita in maniera forse più efficiente nel seguente modo (a parte il fatto di aver implementato le funzioni membro direttamente all'interno della classe, ma non ho a disposizione il codice che ho testato con la classica divisione nei file .h e . cpp)
Grazie

Sono andato a memoria di quanto avevo fatto...molto probabile che ci sia qualche strafalcione, però non capisco perchè dici

non esiste ereditarietà tra le classi

...oooops

adesso, rileggendo meglio, direi proprio di si, dopo Sezione_rettangolare_type e Sezione_circolare_type ci andava un bel

class Sezione_rettangolare_type:public Sezione_type
...

class Sezione_cicolare_type:public Sezione_type
...

Per il resto, vado ad approfondire.

Grazie

ing82
21-12-2015, 17:36
Ancora meglio usando smart pointer (qui uso std::unique_ptr).


Ma quindi tendenzialmente, se ho dati membro di una classe dichiarati come puntatori, conviene definirli come smart pointer, così evito nel distruttore di dover richiamare il delete, corretto?



class Classe
{
public:
//costruttore, ecc;
//il distruttore è
~Classe(){delete(puntatore);/*altre eventuali cose*/};
private:
//quello che serve
tipoDelPuntatore* puntatore;
}
diventa


class Classe
{
public:
//costruttore, ecc;
//il distruttore è
~Classe(){/*altre eventuali cose, ma delete(puntatore) non dovrebbe servire più*/};
private:
//quello che serve
std::unique_ptr<tipoDelPuntatore> puntatore;
}

Ne approfitto per una ulteriore domanda: se il dato membro di tipo puntatore sarà usato solo "interno" alla classe, uso unique_ptr, se invece sarà impiegato anche da qualche altro oggetto, uso shared_ptr, corretto?

Grazie, grazie, grazie

shodan
21-12-2015, 20:20
Ma quindi tendenzialmente, se ho dati membro di una classe dichiarati come puntatori, conviene definirli come smart pointer, così evito nel distruttore di dover richiamare il delete, corretto?

Tendenzialmente si, ma non c'è una regola fissa: occorre valutare quello che si vuole fare.
Lo shared_ptr mantiene un contatore interno di quante volte è stato copiato e l'ultimo a uscire cancella il puntatore, MA la memoria condivisa è la stessa. Se devi fare una deep copy dell'oggetto puntato lo devi fare esplicitamente.



shared_ptr<int> p = new int(10); // contatore = 1
shared_ptr<int> q = p; // contatore = 2
*q = 20;
cout << *p << endl; // 20 perché il puntatore interno è condiviso tra p e q.

shared_ptr<int> z = new int;
*z = *q;
*q = 30;

cout << *z << endl; // 20 perché il puntatore interno di z non è condiviso con p e q.
cout << *p << endl; // 30 perché il puntatore interno è condiviso tra p e q.

Con unique_ptr la faccenda è molto più semplice: non può essere copiato.


unique_ptr<int> p(new int(10))
unique_ptr<int> q = p; // errore: unique_ptr non può essere copiato

unique_ptr<int> q = std:move(p); // ma può essere spostato.
// cout << *p << endl; crash: il puntatore interno di p ora è nullptr
cout << *q << endl; // vale 10.

unique_ptr<int> z (new int);
*z = *q;




Ne approfitto per una ulteriore domanda: se il dato membro di tipo puntatore sarà usato solo "interno" alla classe, uso unique_ptr, se invece sarà impiegato anche da qualche altro oggetto, uso shared_ptr, corretto?

Non proprio: userari shared_ptr se la tua classe dev'essere copiabile, quindi condividere il dato interno con copie di classe uguale; userai unique_ptr se non vuoi che la tua classe sia copiabile e quindi non condividere il dato interno con copie di classe uguale.
Ambedue i puntatori possono essere restituiti da funzioni, ma se restituisci uno shared_ptr avrai il dato interno esistente, se usi uno unique_ptr stai dicendo: "classe X non devi più occuparti della gestione di unique_ptr<dato> perché ci pensa qualcun altro.

Spero di essermi spiegato.

ing82
22-12-2015, 12:02
Spero di essermi spiegato.

Direi di sì. Il mio problema è la mancanza di esperienza, quindi dovrò ora fare qualche prova e capire con la pratica.
Così a naso, direi che potrei partire usando unique_ptr (sempre riferito a dati membro di una classe) e quando mi accorgo che in realtà avrei fatto meglio ad usare shared_ptr, cambiare.

Per ora grazie della disponibilità, ora faccio qualche prova.

ing82
23-12-2015, 23:22
Prova n° 1


#include <iostream>
#include <stdlib.h>
#include <memory>

using namespace std;

class Prova
{
public:
Prova(){cout<<"\nCreato l'oggetto Prova!";};
~Prova(){cout<<"\nDistrutto l'oggetto Prova!";};
void run(){cout<<"\nProva is running!";};
void pause(){cout<<"\nProva is paused!";};
void termin(){cout<<"\nProva is terminated!";};
};

class Oggetto
{
public:
Oggetto(){cout<<"\nCreato l'oggetto Oggetto!";};
Oggetto(shared_ptr<Prova> puntatore):mProva(puntatore){cout<<"\nCreato l'oggetto Oggetto!";};
~Oggetto(){cout<<"\nDistrutto l'oggetto Oggetto!";};
void run(){cout<<"\nOggetto is running!";mProva->run();};
void pause(){cout<<"\nOggetto is paused!";mProva->pause();};
void termin(){cout<<"\nOggetto is terminated!";mProva->termin();};
private:
shared_ptr<Prova> mProva;
};

int main()
{
{
unique_ptr<Prova> prova(new Prova);
prova->run();
prova->pause();
prova->run();
prova->termin();
}
system("pause");
return 0;
}

Al termine dell'esecuzione vedo che viene richiamato il distruttore di prova.

Loading