View Full Version : C++: Questa proprio non la sapevo
ilsensine
26-10-2001, 16:26
Guardate questo codice:
#include <iostream.h>
class C1 {
virtual void print () { cout << "print() di C1\n"; }
public:
C1 () { print(); }
void do_print () { print (); }
};
class C2: public C1 {
void print () { cout << "print() di C2\n"; }
};
int main() {
C2 C;
C.do_print();
return 0;
}
L'output del programma è:
print() di C1
print() di C2
ed è uguale sia sul g++ 2.91, g++ 2.96 che sul visual c.
Succo: nei costruttori di classe le funzioni virtuali a quanto pare non sono usate come tali - qualcuno ne sa qualcosa di più a riguardo?
Sinceramente non mi ricordo come funziona l'ereditariatà sul costruttore di default...in ogni caso sembra che tu abbia ereditato anche quello...
Infatti con "C2 C;" richiami il costruttore di default di C2 erediatato da C1 e poi con "do_print()" che è ereditata da C1 richiami la print() di C2 come è sicuramente giusto (visto che l'stanza è di tipo C2)...
Comunque funziona allo stesso modo anche in VC++...
ilsensine
26-10-2001, 19:23
No, non è così, il costruttore di C1 viene chiamato prima di quello di default di C2, è normale che sia così. Il problema è che a quanto sembra, C viene inizialmente costruito come C1, poi come C2. Solo questo può spiegare il fatto che, se il costruttore chiama una funzione virtuale, non viene chiamata quella effettiva della classe di appartenenza.
Black imp
27-10-2001, 02:28
.
Black imp
27-10-2001, 02:44
la chiamata a print è fatta in una funzione di C1 e chiama un'altra funzione dello scope di C1. Quindi mi sembra normale che NON possa vedere un elemento funzione di C2.
facciamo un altro esempio che scrivo adesso sperando di non confonderti le idee:
class A {
public:
A();
virtual print() {printf("classe A\n"};
};
class B:public A {
public:
print() { printf("classe B\n"); };
}
int main() {
A a;
a.print();
B b;
b.print();
}
deve stamparti "classe A" e poi "classe B" e tutto funziona correttamente secondo le regole. Nel tuo esempio la funzione che ridefinisci innanzi tutto è privata e non può essere ridefinita perchè la classe derivata può accedere solo ai membri pubblc della super classe. In secondo luogo tu chiami esplicitamente una funzione della superclasse - NON della derivata - e che perciò non può " vedere " nello scope della derivata - ci mancherebbe - e chiama corettamente la funzione print() del SUO scope. Occhio perchè questo esempio è molto incasinato. Non si usa in questo modo il polimorfismo. Inoltre usa sempre i puntatori quando hai a che fare con il polimorfismo: altrimenti non serve a nulla.
Ad es. tu sai che devi tenere un elenco di oggetti Sensor in un tuo programma ma non sai ancora se questi oggetti in particolare saranno dei TouchSensor o dei LightSensor ecc. Allora tieni un vettore di puntatori a Sensor e per ognuno a run time allocherai la giusta classe derivata. In questo modo, chiamando i metodi virtuali - stiamo parlando di metodi virtuali NON puri ovviamente - della classe Sensor, verranno in realtà invocati i corrispettivi metodi della derivata:
Sensor* lista[10];
lista[0]= new LightSensor();
ecc.
ilsensine
27-10-2001, 09:36
la chiamata a print è fatta in una funzione di C1 e chiama un'altra funzione dello scope di C1. Quindi mi sembra normale che NON possa vedere un elemento funzione di C2.
No, questo non è corretto, in quanto print è dichiarata virtuale, e quindi soggetta a polimorfismo in caso di ereditarierà. Infatti, come puoi vedere, do_print() è una funzione di C1 ma è in grado di invocare la print() polimorfa corretta, ovvero quella di C2 nell'esempio. A quanto pare solo i costruttori fanno eccezione a questa regola, ma non ne capisco il motivo (sicuramente ce n'è uno)
Non capisco qual è il problema...la print di C1 è virtuale...quindi, visto che è definita anche la print di C2 nella vtable della classe è presente il puntatore alla funzione print di C2...
/\/\@®¢Ø
27-10-2001, 13:39
Anche a me e' accaduto un problema simile,
solo che la funzione nella classe base non era definita, e quindi il programma crashava con un "Pure virtual method called" o qualcosa di simile :eek: ( provare per credere :D ).
Probabilmente ogni costruttore provvede solamente ad aggiungere ( ovvero sovrascrivere ) nella virtual table i metodi ridefiniti. QUando sei nel costruttore della classe base di conseguenza, non hai nessun metodo ridefinito.
Perche' non hanno deciso di scrivere subito la virtual table completa ?
Secondo me l'hanno fatto per poter riutilizzare i costruttori delle classi basi senza doverli spezzare in piu' parti, a vantaggio dell'efficienza.
( Anche se quest'ultima cosa e' solo una mia ipotesi ).
ilsensine
27-10-2001, 13:41
Non capisco qual è il problema...la print di C1 è virtuale...quindi, visto che è definita anche la print di C2 nella vtable della classe è presente il puntatore alla funzione print di C2...
Appunto: tutte le funzioni di C1 invoncano la funzione "giusta" di C2; tutte tranne costruttore e distruttore di C1...è il "perchè" che mi sfugge. Questo vuol dire inoltre che non puoi definire una classe astratta nella quale una funzione virtuale pura (magari da implementare fisicamente in una classe che eredita) viene invocata dal costruttore.
ilsensine
27-10-2001, 13:44
Originariamente inviato da /\/\@®¢Ø
QUando sei nel costruttore della classe base di conseguenza, non hai nessun metodo ridefinito.
Perche' non hanno deciso di scrivere subito la virtual table completa ?
Secondo me l'hanno fatto per poter riutilizzare i costruttori delle classi basi senza doverli spezzare in piu' parti, a vantaggio dell'efficienza.
( Anche se quest'ultima cosa e' solo una mia ipotesi ).
Non credo che si tratti di efficienza, in quanto aggiornare la vtable prima o dopo la chiamata al costruttore non cambia la velocità. Credo che tutto dipenda da qualche specifica del c++ della quale però mi sfugge la motivazione...
/\/\@®¢Ø
27-10-2001, 14:00
Cerco di spiegarmi meglio:
La 'costruzione' di un oggetto richiede vari cose giusto ( 'aggiustamento' )della vtable esecuzione del costruttore definito dall'utente e cosi' via ).
Ora se io volessi avere da subito la virtual table "esatta", dovrei riuscire a effettuare solo parte di queste operazioni, in particolare escludere la scrittura della vtable da parte dei costruttori derivati, ( perche' altrimenti la stessa tornerebbe 'carente' ). E' probabile che questo sia di difficile esecuzione: se pensi che potrei aver derivato un oggetto che mi arriva da una libreria esterna, questo vuol dire che ogni libreria dovrebbe esportare una certa quantita' di funzioni ( o comunque di informazioni ) da chiamare in caso di derivazione.
ilsensine
27-10-2001, 14:32
mmm questo mi sembra già più convincente, anche se continuo ad avere dei dubbi sul fatto che modi alternativi siano impossibili da gestire
Posso provare a dare una spiegazione...un po' a occhio...
Implementando il costruttore di default per C2 viene lo stesso richiamato il costruttore di default di C1...
Quindi al momento della chiamata del costruttore di C1 si tratta di costruire un oggetto di tipo C1...la cui vtable verrà poi sovrascritta dalla creazione dell'oggetto C2 che viene derivato da C1 di cui eredita la parte pubblica...
Quindi secondo me il compilatore opera in questo modo...
Crea un'istanza di C1 e ne chiama il costruttore (a questo punto l'oggetto è ancora di tipo C1...ed è per questo che viene chiamata la print di C1)...l'istanza di C2 eredita i membri e le funzioni pubbliche di C1...viene modificata la vtable (viene sovrascritta l'entry di print())...poi successivamente viene chiamato il costruttore di C2...
Modificando il codice in questo modo si vede che quello che avviene sembra essere questo...
#include <iostream.h>
class C1 {
virtual void print () { cout << "print() di C1\n"; }
public:
C1 () { print(); }
void do_print () { print (); }
};
class C2: public C1 {
void print () { cout << "print() di C2\n"; }
public :
C2 () {print();};
};
int main() {
C2 C;
C.do_print();
return 0;
}
Black imp
27-10-2001, 18:31
MA RAGAZZI!!! ho capito adesso la domanda. MA NON SCHERZIAMO!! quando ereditate una classe VIENE SEMPRE CHIAMATO IL COSTRUTTORE DELLA SUPERCLASSE se no non funzionerebbe più niente. E che senso avrebbe?
per IL_sensine: non sono sicuro di come funzioni in questo caso. ripasserò un po'. comunque se chiamate q.sa che sta solo nella superclasse questo NON può vedere la parte 'aggiunta ' ereditando.
ilsensine
27-10-2001, 19:10
MA RAGAZZI!!! ho capito adesso la domanda. MA NON SCHERZIAMO!! quando ereditate una classe VIENE SEMPRE CHIAMATO IL COSTRUTTORE DELLA SUPERCLASSE se no non funzionerebbe più niente. E che senso avrebbe?
Questo è ovvio, non è in discussione. C1::C1() viene sempre chiamato.
per IL_sensine
(ilsensine svp)
se chiamate q.sa che sta solo nella superclasse questo NON può vedere la parte 'aggiunta ' ereditando.
Qui non stiamo parlando di parti aggiunte, ma di parti polimorfe. Come vedi, do_print() sta solo in C1 ma chiama comunque correttamente la funzione (polimorfa, in quanto virtuale in C1 e ridefinita in C2) implementata in C2.
Il problema (che cionci e /\/\@®¢Ø) hanno intuito) a quanto pare è che la vtable viene costruita mano mano che vengono chiamati i costruttori delle classi, e non _prima_ come mi sarei aspettato. Tutto qui.
Certo...
Al momento della chiamata del costruttore di C1 l'istanza è a tutti gli effetti della classe C1...poi successivamente viene modificata la vtable al momento dell'allocazione di C2...
ilsensine
27-10-2001, 19:20
poi successivamente viene modificata la vtable al momento dell'allocazione di C2...
a questo punto direi: "al momento della trasformazione in C2". In effetti quello che accade sembra essere proprio questo, anche se il termine è improprio.
Black imp
28-10-2001, 02:50
allora non avevo capito. forse perchè questo mi sembrava più ovvio mentre in realtà mi sono accorto che non mi ricordo bene altri aspetti a dire il vero :)
Black imp
28-10-2001, 02:59
AAAAAHHHH adesso ho capito ancora meglio! in sostanza il problema è che secondo voi non doveva invocare la print di C1 durante l'invocazione al costruttore, perchè questa è già stata ridefinita. Mhhh beh quando viene chiamato il costruttore della super classe - vale anche per una catena di ereditarietà - l'oggetto modifica tipo man mano: se C eredita da B che eredita da A, quando si crea un oggetto C, prima viene costruito un oggetto A poi esteso a B poi esteso a C. quindi mi sembra coerente che durante l'esecuzione del costruttore di C1 chiami ancora la print di C1. Non so se questo in effetti sia un limite anzi. Direi che se si cercasse di fare veramente una cosa come nell'esempio con quell'intento, sarebbe sbagliata la scelta di ereditare C2 da C1 - non sto criticando chi lo ha scritto :) - non vi pare?
ilsensine
28-10-2001, 09:02
quindi mi sembra coerente che durante l'esecuzione del costruttore di C1 chiami ancora la print di C1.
Assodato che la costruzione di una classe derivata avviene per gradi, è coerente.
Non so se questo in effetti sia un limite anzi.
Forse è un limite. Supponi a titolo di esempio di definire una classe generica Numero, il cui costruttore chiami una funzione virtuale che alloca la memoria necessaria a contenere il numero. Poichè ancora non è stato definito di che numero si tratti e di quanta memoria sia necessaria per contenerlo, tale funzione può essere definita virtuale pura (ovviamente, da ciò che abbiamo visto, non è possibile). Poi ogni derivazione di Numero (ad es. Intero, Complesso ecc.) potrebbe ridefinire una propria istanza di tale funzione per allocare la memoria necessaria. Se la vtable fosse stata aggiornata prima della chiamata alla catena di costruttori, una cosa del genere avrebbe funzionato. Prendiamo atto che però non è così.
Ma in ogni caso la nuova allocazione modificata potrebbe essere fatta nel costruttore della classe derivata (senza deallocare la memoria precedentemente allocata dal costruttore della classe base) e riservandosi un altro puntatore alla memoria allocata (altrimenti, se si usa lo stesso puntatore quando viene chiamato il distruttore della classe base succede un bel casino)...
Black imp
28-10-2001, 12:31
Originariamente inviato da ilsensine
[B]
Poi ogni derivazione di Numero (ad es. Intero, Complesso ecc.) potrebbe ridefinire una propria istanza di tale funzione per allocare la memoria necessaria. Se la vtable fosse stata aggiornata prima della chiamata alla catena di costruttori, una cosa del genere avrebbe funzionato. Prendiamo atto che però non è così.
?? perchè? se si progetta così una classe è proprio concettualmente sbagliato. le funzioni virtuali servono a chi sta fuori dalla classe non a chi sta dentro. se non sai come allocare lo spazio non la definisci proprio in numero e la definisci nelle derivate. Le funzioni virtuali risolvono un problema di interfaccia quindi hanno senso nel caso di funzioni public. A che ti serve un meccanismo come quello che mi hai descritto?
A parte che le virtuali pure sono diverse: non sono definite ma solo dichiarate e vengono poste =0; e a parte che non ha senso che ci sia una funzione di allocazione della memoria in una classe, perchè lo spazio che ti serve lo dichiari come normale variabile e se lo vuoi dinamico userai un puntatore alla classe anzichè un ogg. automatico come nell'es.
mi spiego: vuoi un numero complesso e vuoi che venga allocato dinamicamente: nella classe complex infili due variabili float, una per la parte reale e una per la parte immaginaria e poi crei un oggetto complex tramite un puntatore e la new.
se non ho capito abbi pazienza e rispiegami perchè sto dormendo poco e sono depresso in questo periodo :)
ilsensine
28-10-2001, 12:39
le funzioni virtuali servono a chi sta fuori dalla classe non a chi sta dentro
Su questo non sono d'accordo. Possono essere utilizzate con successo anche all'interno delle classi (anzi questo è un aspetto molto interessante delle funzioni virtuali).
Black imp
28-10-2001, 23:33
ma è proprio questo il punto: è sporca una programmazione del genere. è un modo non corretto di utilizzare questo meccanismo e infatti guarda che casino è venuto fuori. la virtualizzazione rende trsparente a un utente della classe la ridefinizione di una funzione. se hai bisogno di usare questo meccanismo dall'interno hai progettato male le classi.
Se ti serve questa cosa in ogni caso la funzione della classe base potrebbe essere richiamata dalla funzione in quella derivata...però la print di C1 deve essere pubblica...
In MFC (anche se non è certo il + bel esempio di programmazione) viene usato sempre questo metodo per richiamare la funzione della classe base...
#include <iostream.h>
class C1 {
public:
C1 () { print(); }
virtual void print () { cout << "print() di C1\n"; }
void do_print () { print (); }
};
class C2: public C1 {
void print () { cout << "print() di C2\n"; C1::print();} //Ecco qui come l'ho richiamata
public :
C2 () {print();};
};
int main() {
C2 C;
C.do_print();
return 0;
}
ilsensine
29-10-2001, 09:49
Originariamente inviato da Replicant
ma è proprio questo il punto: è sporca una programmazione del genere. è un modo non corretto di utilizzare questo meccanismo e infatti guarda che casino è venuto fuori.
Scusa ma continuo a non vederne il motivo. Il polimorfismo di metodi privati/protetti (quindi, se vogliamo, del "comportamento" di una classe) mi sembra perfettamente congruente con la programmazione a oggetti, al pari del polimorfismo delle (o, meglio, del comportamento delle) interfacce.
Comunque sono punti di vista...
Black imp
30-10-2001, 01:01
il polimorfismo è un metodo per passare, dove veniva richiesta una classe, la sua derivata e viceversa - non sto qui a menzionare il meccanismo - . Le funzioni virtuali sono fatte per garantire una compatibilità in queste situazioni, non per colmare gli errori di progettazione. L'esempio non ha alcun senso se non come curiosità. In ing. del software non puoi vedere una roba del genere. Si deriva per ampliare non tanto per modificare - anche se è legittimo -. Si modifica quando serve strettamente per ottenere un comportamento più specifico da parte di una funzione. Quando lavori con classi già fatte e devi ereditarle, non troverai mai un meccanismo per cui devi ridefinirti delle funzioni private chiamate da funzioni pubbliche. è una cosa assurda - senza offesa -. poi ognuno fa quello che vuole, soprattutto se non lo fa di mestiere. Io cerco un approccio più pulito possibile visto che ci avrò a che fare di lavoro e soprattutto un paio di progetti a classi li ho già fatti. Puoi anche ridefinire tutti i metodi di una classe nella sua derivata anche se non ha senso. Puoi anche usare i dangling pointers e fregartene ottenendo dei valori a caso. Il C++ è potentissimo, come un linguaggio naturale: sta a chi scrive farlo correttamente -anche in modo personale ovviamente -.
Originariamente inviato da Replicant
[B]Il C++ è potentissimo, come un linguaggio naturale: sta a chi scrive farlo correttamente -anche in modo personale ovviamente -.
Questa me la segno ;)
Black imp
30-10-2001, 19:34
Originariamente inviato da Replicant
[B]
la virtualizzazione rende trsparente a un utente della classe la ridefinizione di una funzione
volevo dire OPACO non trasparente
/\/\@®¢Ø
01-11-2001, 21:47
SUlla prima parte del discorso sono d'accordo con Replicant: basare la costruzione di un oggetto su quello che decide una classe derivata non mi sembra una buona cosa. Anche perche' visto che in ogni caso poi viene chiamato anche l'altro costruttore, forse meglio fare il tutto dopo o , se non e' possibile questo, passare i parametri con la chiamata al costruttore.
Poi, ovviamente, quando si parla di "buona programmazione" ognuno ha le sue idee. ;)
Originariamente inviato da Replicant
Il C++ è potentissimo, come un linguaggio naturale: sta a chi scrive farlo correttamente -anche in modo personale ovviamente -.
Adesso non esageriamo. Ci sono certe cose che in C++ non si possono fare se non codice assomgliante ad una profezia bizantina :D.
( Vedi soprattutto quando si ha a che fare con i templates ).
A proposito... gia' che ci siamo ne approfitto per una cosa che non sono riuscito a risolvere ( e che secondo me e' piu' "grave" del problema dei costruttori ):
Se io ho due classi che si contengono a vicenda ( ovviamente tramite puntatori ) posso fare cosi':
class A;
class B
{ public:
A* a;
int x;
};
class A
{
B* b;
int y;
};
semplicemente mi basta dichiarare in anticipo la classe A.
Ora pero' supponiamo di dover utilizzare anche dei templates
il tutto diverrebbe
template<class T>
class A<T>;
template<class T>
class B
{ public:
A<T>* a;
T x;
};
[b]template <class T>
class A
{
B<T>* b;
T y;
};
peccato che la dichiarazione iniziale non venga accettata !
E ho provato anche forme "alternative". Niente da fare. Ne con gcc ne' con Bcc ( con VC++ neanche provo :D ).
Quancle idea ? Dove sbaglio ?
MickMacello
01-11-2001, 22:28
ma non dovresti specificare
template<class T> class A<T> anche nella definizione ?
/\/\@®¢Ø
01-11-2001, 23:01
No, dovro' metterlo dopo quando dovro' scrivere il codice dei metodi:
template<class T>
A<T>::metodo( blabla ) { ... }
ma non nella dichiarazione dell'interfaccia della classe, visto che e' evidente ( per sicurezza ho appena provato )
/\/\@®¢Ø
01-11-2001, 23:04
ops scusa.
ora ho visto l'errore, ho sbagliato a ricopiare.
adesso correggo
Comunque non e' quello il problema, visto che si blocca nella dichiarazione iniziale.
MickMacello
01-11-2001, 23:42
con questo codice:
template<class T> class A;
template<class T> class B { public:
A<T>* a;
T x;
};
template <class T> class A
{
B<T>* b;
T y;
};
il VC++ 6 non mi dà nessun errore di compilazione.
/\/\@®¢Ø
02-11-2001, 00:18
Originariamente inviato da MickMacello
[B]con questo codice:
template<class T> class A;
template<class T> class B { public:
A<T>* a;
T x;
};
template <class T> class A
{
B<T>* b;
T y;
};
il VC++ 6 non mi dà nessun errore di compilazione.
ma porc... avro' fatto dieci tentativi... e in nessuno di questi mi e' venuto in mente di togliere la <T> dopo la prima "class A" ( che tra l'altro e' evidente debba stare senza ).
ah... il sottile confine tra il sonno e la veglia.... :D
grazie mille :)
vBulletin® v3.6.4, Copyright ©2000-2025, Jelsoft Enterprises Ltd.