Modifiche recenti - Cerca:

emi7oP <a href="http://etyxbsmwlbop.com/">etyxbsmwlbop</a>, [url=http://ejkbnyaqouhe.com/]ejkbnyaqouhe[/url], [link=http://iytqgxxmxvee.com/]iytqgxxmxvee[/link], http://ddjproogyauv.com/

ObjectOrientedCpp

Object Oriented in C++

Autore: Estefan Civera

Sommario
Scopo di questo articolo è di fare una introduzione alla programmazione ad oggetti in C++.
Storia
Abstract data type
Diffusione del linguaggio
Abstraction
Using constructor
Inizializzazione del costruttore
Distruttore
Operatori new e delete
Function declaration
Passaggio di parametri
Metodi e attributi statici
Funzioni inline
Argomenti di default
Overloading delle funzioni

Storia

Non dovrebbe sorprendere più di tanto il fatto che il C++ abbia un'origine simile al C. Lo sviluppo del linguaggio C++ all'inizio degli anni Ottanta è dovuto a Bjarne Stroustrup dei laboratori Bell (Stroustrup ammette che il nome di questo nuovo linguaggio è dovuto a Rick Mascitti). Originariamente il C++ era stato sviluppato per risolvere alcune simulazioni molto rigorose e guidate da eventi; per questo tipo di applicazione la scelta della massima efficienza precludeva l'impiego di altri linguaggi.

Uno degli scopi principali del C++ era quello di mantenere piena compatibilità con il C. L'idea era quella di conservare l'integrità di molte librerie C e l'uso degli strumenti sviluppati per il C. Grazie all'alto livello di successo nel raggiungimento di questo obiettivo, molti programmatori trovano la transizione al linguaggio C++ molto più semplice rispetto alla transizione da altri linguaggi.

Il C++ consente lo sviluppo di software su larga scala. Grazie a un maggiore rigore sul controllo dei tipi, molti degli effetti collaterali tipici del C, divengono impossibili in C++.

Il miglioramento più significativo del linguaggio C++ è il supporto della programmazione orientata agli oggetti (Object Oriented Programming: OOP), Per sfruttare tutti i benefici introdotti dal C++ occorre cambiare approccio nella soluzione dei problemi. Ad esempio, occorre identificare gli oggetti e le operazioni ad essi associate e costruire tutte le classi e le sottoclassi necessarie.

Abstract data type

Gli ADTs possono essere definiti come un dato indipendente dalla sua rappresentazione interna e dalla effettiva implementazione delle operazioni su tale dato. Viene definito specificando: rappresentazione interna e dalla effettiva

  • implementazione delle operazioni su tale dato
  • viene definito specificando:
  • un insieme di valori, detto dominio del tipo di dato
  • un nome con cui si indica il dominio
  • un insieme di funzioni che opera sul dominio
  • costruttori
  • operatori
  • predicati
  • un insieme di costanti appartenenti al dominio

A differenza del C, il C++ permette la gestione degli ADTs (concetto realizzato dal costrutto class)

Diffusione del linguaggio

Grazie ad un notevole sforzo da parte dei progettisti il C++ è risultato uno tra i più (se non il primo) linguaggi robusti ed efficienti. La notevole diffusione è stata avvantaggiata anche dal fatto che

  • Conserva una compatibilità quasi assoluta (alcune cose sono diverse) con il suo più diretto antenato, il C, da cui eredita la sintassi e la semantica per tutti i costrutti comuni, oltre alla notevole flessibilità e potenza.
  • Permette di realizzare qualsiasi cosa fattibile in C senza alcun overhead addizionale.
  • Estende le caratteristiche del C, rimediando almeno in parte alle carenze del suo predecessore (che manca soprattutto di un buon sistema dei tipi) ad esempio con l'aggiunta dei tipi bool. In particolare l'introduzione di costrutti quali i Template e le Classi rende il C++ un linguaggio multiparadigma (con particolare predilezione per il paradigma ad oggetti e la programmazione generica).

Altre caratteristiche fondamentali che sono state inserite in C++ sono la gestione delle eccezioni, l'utilizzo del costruttore di copia, il passaggio per riferimento, e la gestione dell'overloading/overriding.

Comunque il C++ presenta anche degli aspetti negativi (come ogni linguaggio), in parte ereditate dal C:

  • La potenza e la flessibilità tipiche del C e del C++ non sono gratuite. Se da una parte è vero che è possibile ottenere applicazioni in generale più efficienti (rispetto ad agli altri linguaggi), e anche vero che tutto questo è ottenuto lasciando in mano al programmatore molti dettagli e compiti che negli altri linguaggi sono svolti dal compilatore; è quindi necessario un maggiore lavoro in fase di progettazione e una maggiore attenzione ai particolari in fase di realizzazione, pena una valanga di errori spesso subdoli e difficili da individuare che possono far levitare drasticamente i costi di produzione;
  • Il compilatore e il linker del C++ soffrono di problemi relativi all'ottimizzazione del codice dovuti alla falsa assunzione che programmi C e C++ abbiano comportamenti simili a runtime: il compilatore nella stragrande maggioranza dei casi si limita ad eseguire le ottimizzazioni tradizionali, sostanzialmente valide in linguaggi come il C, ma spesso inadatte a linguaggi pesantemente basati sulla programmazione ad oggetti; il linker poi da parte sua non è cambiato molto e non esegue alcune ottimizzazioni che non sono fattibili a compile-time.
  • Dipendenza del codice prodotto con l'architettura hardware sottostante.
  • Non esiste il garbage collector,ogni oggetto, variabile,... deve essere esplicitamente deallocata dal programmatore con problemi relativi alla consistenza e all'efficienza.
  • Come il C, anche il C++ ha una libreria standard. Di particolare importanza è la STL, Standard Template Library, la parte della libreria standard che utilizza i template per implementare contenitori generici, come vettori, code, array associativi, e così via. La programmazione ne risulta molto semplificata, al prezzo di un gran lavoro del compilatore per interpretare i complessi template.

Abstraction

Il concetto di abstraction sta ad indicare che i dettagli implementativi vengono nascosti all'utente che interagisce con essi tramite metodi e funzioni.

Type definition Struct vs Class

Questo esempio realizzato tramite il costrutto struct vuole mettere in evidenza le problematiche relative all'utilizzo delle strutture in un linguaggio imperativo come il C.

 // Create a structure, set its members, and print it
 #include <iostream.h>
 struct Time { // structure definition
 int hour; // 0-23 
 int minute; // 0-59
 int second; // 0-59 
 };  
 void printMilitary(const Time &); // prototype
 void printStandard(const Time &); // prototype

 //===========================================
 main()
 {
    Time dinnerTime; // variable of new type Time
    // set members to valid values
    dinnerTime.hour = 18;
    dinnerTime.minute = 30;
    dinnerTime.second = 0;
    cout << "Dinner will be held at";
    printMilitary(dinnerTime); // 18:30:00

    // set members to invalid values
    dinnerTime.hour = 29;
    dinnerTime.minute = 73;
    dinnerTime.second = 103;
    cout << "\nTime with invalid values: ";
    printMilitary(dinnerTime); // 29:73:103 bad values!
 }

L'esempio qui descritto mostra come l'istruzione Time dinnerTime; non porti a nessuna inizializzazione degli attributi interni. Ciò può cause problemi (ad esempio durante un calcolo matematico, o se un puntatore punta ad un area non valida). E' possibile assegnare agli attributi della struttura qualsiasi valore. (non type-safe). Non esiste il concetto di interfaccia, infatti se l'implementazione della struttura cambia, anche il programma deve subire un riadattamento.

Ecco la stessa soluzione ottenuta tramite l'implementazione della classe Time
#include <iostream.h>

 // Time abstract data type (ADT) definition
 class Time {
 public:
    Time(); // default constructor
    void setTime(int, int, int);
    void printMilitary();
    void printStandard();
 private:
    int hour; // 0 - 23
    int minute; // 0 - 59
    int second; // 0 - 59

};

 // Time constructor initializes each data member to zero.
 // No return value
 // Ensures all Time objects start in a consistent state.
 Time::Time() { hour = minute = second = 0; }

 // Set a new Time value using military time.
 // Perform validity checks on the data values.
 // Set invalid values to zero (consistent state)
 void Time::setTime(int h, int m, int s)
 {
   hour = (h >= 0 && h < 24) ? h : 0;
   minute = (m >= 0 && m < 60) ? m : 0;
   second = (s >= 0 && s < 60) ? s : 0;
 }

  1. hour, minute, e second sono membri privati e quindi non accedibili dall'esterno. [Information Hiding]
  2. L'utilizzo del costruttore permette di inizializzare i data members. Ciò permette di poter dire che l'oggetto si troverà sempre in uno stato consistente.
  3. L'utilizzo del metodo setTime permette di gestire correttamente l'assegnazione dei valori evitando error-type e error-range.

Using constructor

Nella programmazione orientata agli oggetti, i costruttori sono metodi associati alle classi che hanno lo scopo di inizializzare le nuove istanze della classe durante il processo di creazione. In molti linguaggi (per esempio in Java e C++) hanno lo stesso nome della classe a cui appartengono. Come tutti gli altri metodi, i costruttori possono essere definiti in molteplici versioni attraverso overloading.

Facendo sempre riferimento all'esempio precedente ecco come è possibile fare l'overloading del costruttore e come ogni volta cambia la sua implementazione

 //Interface
 Time(); // default constructor
 Time(int hr);
 Time(int hr, int min, int sec);
 //Implementation
 Time::Time(){ hour = minute = second = 0; }
 Time::Time(int hr) { setTime(hr, 0, 0); }
 Time::Time(int hr, int min, int sec)
 { setTime(hr, min, sec); }

 Time t1;

Dichiaro un oggetto t1. In questo caso viene richiato il costruttore di default.

 Time t2(08);
 Time t2 = Time(08);
 Time t2 = 08;
 Time t2 = (Time) 08;

Le quattro precedenti istruzioni sono equivalenti (pongono l'ora a 8), ma la medesima istruzione è eseguita sintatticamente in maniera diversa. Nel primo caso (classico) si invoca il costruttore sovraccaricato. Nel secondo caso si assegna a t2 l'istanza ottenuta dall'invocazione di Time(08). Il terzo caso è un po' più particolare, infatti se un costruttore riceve un solo parametro in ingresso è possibile gestirlo (creazione e inizializzazione) come se fosse una comune variabile (in questo caso un intero). Il quarto caso è l'evoluzione del terzo. Viene fatto un cast nel caso il tipo passato fosse diverso dal tipo di dato aspettato dal parametro del costruttore (in questo caso il cast sarebbe omissibile).

 Time t3(08,15,04);
 Time t3 = Time(08,15,04);

Per quanto riguarda l'allocazione dinamica...
Type_name * pointer_name = new Type_name;

 Time *t;
 t = new Time; // Time() is invoked
 t = new Time(08); // Time(int) is invoked
 t = new Time(08,15,04); // Time(int, int, int) is invoked

Come vettore...
Time arrayOfTimes[8] = { 3, Time(05), Time(),Time(01,12,03)} Per le celle valgono le regole definite precedentemente.

e come vettore dinamico....
Time *t = new Time[x];

Inizializzazione del costruttore

Per richiamare il costruttore padre si usa l'operatore ":".

 class Base;
 class Derived public Base {
 public:
      Derived(int x, y)
       :
      Base(x){
         y = 0;
      }
 }

Distruttore

Un distruttore è una funzione membro di una classe normalmente utilizzata per restituire al sistema la memoria allocata da un oggetto. Il distruttore, come il costruttore, ha sempre lo stesso nome della classe nella quale è definito ma è preceduto dal carattere tilde (~). In pratica, i distruttori hanno una funzione opposta a quella dei costruttori. Il distruttore viene richiamato automaticamente quando si applica l'operatore delete ad un puntatore alla classe oppure quando un programma esce dal campo di visibilità di un oggetto della classe. A differenza dei costruttori, i distruttori non possono accettare argomenti e non possono essere modificati tramite overloading. Infine, anche i distruttori, quando non vengono definiti esplicitamente, vengono creati automaticamente dal compilatore (distruttore di default).

Operatori new e delete

In C++, l'operatore new costruisce uno o più oggetti nell'area heap e ne restituisce l'indirizzo. In caso di errore (memoria non disponibile) restituisce NULL.

Per deallocare la memoria dell'area heap in C++ mette a disposizione l'operatore delete. Questo operatore non restituisce alcun valore e quindi deve essere usato da solo in un'istruzione. Il prototipo dell'operatore delete si presenta in questo modo: delete nome_puntatore

Contrariamente a quanto sembra l'operatore delete non cancella la variabile nome_puntatore, né altera il suo contenuto: l'unico effetto é di liberare la memoria puntata rendendola disponibile per ulteriori allocazioni.

Se l'operando punta a un'area in cui sono stati allocati più oggetti (array), delete va specificato con una coppia di parentesi quadre (senza la dimensione, che il C++ é in grado di riconoscere automaticamente). Ad esempio:

 int i = new int;
 delete i;

 float* punt = new float [100] ; // alloca 100 oggetti float
 delete [] punt; // libera tutta la memoria allocata

L'operatore delete costituisce l'unico mezzo per deallocare memoria heap, che, altrimenti, sopravvive fino alla fine del programma, anche quando non é più raggiungibile.

Ad ogni operazione di allocazione di memoria (new) deve corrispondere una e una sola operazione di rimozione (delete). In caso contrario si possono verificare resource leak con conseguente instabilità del processo.

Per evitare problemi con l'operatore delete ogni qualvolta che si dichiara un pointer lo si pone a NULL. (La cancellazione di un pointer nulla non da alcun problema).

Function declaration

return_type method_name(param,...). La dichiarazione di una funzione (a differenza della sua implementazione) si aspetta anche il modificatore di visibilità.

L'operatore const (final in java) ha il compito di evitare l'overload e l'overriding del metodo. In tal modo se la classe venisse estesa il metodo non subirebbe alcuna modifica. L'operatore const è anche usato per evitare che i parametri in ingresso varino il loro valore.
Eccone un esempio...

 int Car::fun_weight(const double weight)
 {
 // weight++; ERROR non è ammesso perché è presente il mod. const
 new_weigth += weight;
 return (int) new_weight;
 }

Passaggio di parametri

Il passaggio di parametri in C/C++ avviene per valore e per riferimento.

Il passaggio per valore comporta la copia sullo stack dell'intero oggetto (molto inefficienti per oggetti di notevole dimensione). Inoltre ogni modifica apportata non impatta sull'oggetto ma solo sulla copia.

Il passaggio per riferimento come indica il termine è caratterizzato dal fatto che viene passato l'indirizzo di memoria dell'oggetto in questione. Questa soluzione è molto performante in quanto non viene copiato sullo stack l'oggetto (con notevole risparmio di tempo) ma anche perché le modifiche apportate all'oggetto in questione e non ad una sua copia.

C++ permette di realizzare il passaggio per riferimento in due modi.

 # void foo(Type* t); // Basato sull'utilizzo dei puntatori,  vantaggi e svantaggi      //dell'utilizzo dei puntatori 
 # void foo(Type& t); // Non necessita del not null checking.

Metodi e attributi statici

Per usare un metodo generalmente serve sempre una istanza. Questo può essere una limitazione perché alcune procedure sono funzioni: prendono un input e producono un output, senza che debba essere memorizzato uno stato. È fastidioso dover creare ogni volta una istanza. Per fortuna è possibile dichiarare un metodo in modo che non richieda una istanza per essere utilizzata: si tratta dei metodi statici. Come i campi statici, possono essere invocati solamente utilizzando il nome della classe, senza che occorra avere alcuna istanza. I metodi statici hanno qualcosa in meno rispetto ai metodi non statici:

  1. non possono accedere campi e metodi non statici
  2. non possono invocare metodi non statici
  3. non possono usare il pointer this
  4. non possono essere dichiarati virtuali

Funzioni inline

Le funzioni consentono di scomporre in più parti un grosso programma facilitandone sia la realizzazione che la successiva manutenzione. Tuttavia spesso si è indotti a rinunciare a tale beneficio perché l'overhead imposto dalla chiamata di una funzione è tale da sconsigliare la realizzazione di piccole funzioni. Per non rinunciare ai vantaggi forniti dalle (piccole) funzioni e a quelli forniti da un controllo statico dei tipi, sono state introdotte nel C++ le funzioni inline. Quando una funzione viene definita inline il compilatore ne memorizza il corpo e, quando incontra una chiamata a tale funzione, semplicemente lo sostituisce alla chiamata della funzione; tutto ciò consente di evitare l'overhead della chiamata e, dato che la cosa è gestita dal compilatore, permette di eseguire tutti i controlli statici di tipo. Se si desidera che una funzione sia espansa inline dal compilatore, occorre definirla esplicitamente inline:

inline int sum(int a, int b){return a+b;}

La keyword inline informa il compilatore che si desidera che la funzione sum sia espansa inline ad ogni chiamata; tuttavia ciò non vuol dire che la cosa sia sempre possibile: molti compilatori non sono in grado di espandere inline qualsiasi funzione, tipicamente le funzioni ricorsive o con cicli sono molto difficili da trattare. In questi casi comunque la cosa generalmente non è grave, poiché un ciclo tipicamente richiede una quantità di tempo ben maggiore di quello necessario a chiamare la funzione, per cui l'espansione inline non avrebbe portato grossi benefici. Quando l'espansione inline della funzione non è possibile solitamente si viene avvisati da una warning.

Argomenti di default

A volte siamo interessati a funzioni il cui comportamento è pienamente definito anche quando in una chiamata non tutti i parametri sono specificati, vogliamo cioè essere in grado di avere degli argomenti che assumano un valore di default se per essi non viene specificato alcun valore all'atto della chiamata. Ecco come fare:

inline int sum(int a=0, int b=0){return a+b;}

Quella che abbiamo appena visto è la definizione della funzione sum ai cui argomenti sono stati associati dei valori di default (in questo caso 0 per entrambi gli argomenti), ora se la funzione sum viene chiamata senza specificare il valore di a e/o b il compilatore genera una chiamata a sum sostituendo il valore di default (0) al parametro non specificato. Una funzione può avere più argomenti di default, ma le regole del C++ impongono che tali argomenti siano specificati alla fine della lista dei parametri formali nella dichiarazione della funzione: La risoluzione di una chiamata di una funzione con argomenti di default naturalmente differisce da quella di una funzione senza argomenti di default in quanto sono necessari un numero di controlli maggiori; sostanzialmente se nella chiamata per ogni parametro formale viene specificato un parametro attuale, allora il valore di ogni parametro attuale viene copiato nel corrispondente parametro formale sovrascrivendo eventuali valori di default; se invece qualche parametro non viene specificato, quelli forniti specificano il valore dei parametri formali secondo la loro posizione e per i rimanenti parametri formali viene utilizzato il valore di default specificato (se nessun valore di default è stato specificato, viene generato un errore):

Overloading delle funzioni

L'overloading consiste nel sovraccaricare un operatore o una funzione per far si che essa funzioni con variabili aventi tipi diversi.

  void Foo(int a, float f);
  int Foo(int a, float f);       // Errore!
  int Foo(float f, int a);       // Ok!
  char Foo();                    // Ok!
  char Foo(...);                 // OK

La seconda dichiarazione è errata perché per scegliere tra la prima e la seconda versione della funzione, il compilatore si basa unicamente sui tipi dei parametri che nel nostro caso coincidono; la soluzione è mostrata con la terza dichiarazione, ora il compilatore è in grado di distinguere perchè il primo parametro anziché essere un int è un float. Infine le ultime due dichiarazioni non sono in conflitto per via delle regole che il compilatore segue per scegliere quale funzione applicare; in linea di massima e secondo la loro priorità:

  1. Match esatto: se esiste una versione della funzione che richiede esattamente quel tipo di parametri (i parametri vengono considerati a uno a uno secondo l'ordine in cui compaiono)
  2. Match con promozione: si utilizza (se esiste) una versione della funzione che richieda al più promozioni di tipo (ad esempio da int a long int, oppure da float a double);
  3. Match con conversioni standard: si utilizza (se esiste) una versione della funzione che richieda al più conversioni di tipo standard (ad esempio da int a unsigned int);
  4. Match con conversioni definite dall'utente: si tenta un matching con una definizione (se esiste), cercando di utilizzare conversioni di tipo definite dal programmatore;
  5. Match con ellissi: si esegue un matching utilizzando (se esiste) una versione della funzione che accetti un qualsiasi numero e tipo di parametri (cioè funzioni nel cui prototipo è stato utilizzato il simbolo ...);

Se nessuna di queste regole può essere applicata, si genera un errore (funzione non definita!).

Seconda parte, esclusi Template e STL

Template e STL

Altre risorse utili

Thinking in C++, di Bruce Eckel

Modifica - Versioni - Stampa - Modifiche recenti - Cerca
Ultima modifica il 02/08/2006 ore 23:17 CEST (Vincenzo)