Le classi

31 gennaio 2017

La programmazione orientata agli oggetti consente di aggregare in un’unica entità i dati e le tecniche per la loro manipolazione. Ciò è reso possibile dalla definizione di classi che espandono l’insieme di tipi predefiniti previsti dallo standard C++.

Un modello di dati basato sul concetto di oggetto tuttavia non si limita solamente ad aggregare dati di vari tipi (predefiniti o composti) per rappresentare entità complesse. Esso infatti introduce per ogni oggetto anche un insieme di funzioni specializzate, e diversi livelli di accesso per ogni sua proprietà o membro.

Questa tecnica di modellazione si avvale quindi del principio di incapsulamento: una classe definisce un’interfaccia verso il mondo esterno, che consente di manipolarne di dati e, allo stesso tempo, maschera le dinamiche del suo funzionamento interno.

In questo modo, problematiche complesse possono essere decomposte e risolte mediante la definizione di oggetti e delle interazioni tra di essi. Ciò si traduce in molti casi in una metodologia di progettazione molto più concettuale o di alto livello di quella offerta dalla programmazione strutturata.

Nel linguaggio C++ tutto ciò è reso possibile dal costrutto class, la cui sintassi è la seguente:

class <nome>
{
public:
	// membri accessibili all'esterno della classe

protected:
	// membri accessibili dalla classe ed in tutte le sue derivate

private:
	// membri accessibili solo all'interno della classe
}		

Per la selezione del nome di una classe valgono le regole precedentemente discusse per gli identificatori.

Le tre sezioni distinte servono per la definizione del livello di accesso o visibilità di ogni membro: pubblico, protetto e privato. Come impostazione predefinita, se nessuna delle tre sezioni viene indicata esplicitamente, i membri della classe sono privati.

La semantica per ogni livello di accesso è legata ai concetti di ereditarierà e polimorfismo e si esplica meglio attraverso esempi pratici, che vedremo meglio nelle lezioni successive.

Definizione di una classe e implementazione dei suoi metodi

Alcuni linguaggi orientati alla programmazione ad oggetti, come Java e C#, prevedono che la definizione e l’implementazione dei metodi di una classe risiedano in un unico listato. Ci sono una serie di ragioni storiche e tecniche per cui in C++ è preferibile scomporre il codice relativo ad una classe in due file, uno contente la definizione dell’interfaccia (detto header) e l’altro l’implementazione dei metodi. Per convenzione, il nome di entrambi tali file è dato dal nome della classe in esso contenuta, con estenzione .h e .cpp rispettivamente.

Affinchè il processo di compilazione abbia successo, è necessario che tali file presentino alcune caratteristiche. L’esempio seguente introduce la classe Point2D, per la rappresentazione di un punto bidimensionale.

Nel listato seguente è mostrato il file point2d.h:

// point2d.h
 
#ifndef POINT_2D_H
#define POINT_2D_H
 
namespace Geometry {
	class Point2D;      
}   
 
class Geometry::Point2D
{
public:
	double X();
	void setX(double value);
			 
	double Y();
	void setY(double value);            
			 
	double distanceFrom(Point2D other);
 
private:
	double x;
	double y;
};       
 
#endif // POINT_2D_H  

La definizione della classe è compresa tra le cossiddette include guards: direttive al preprocessore che hanno lo scopo di impedire che il file header venga incluso più volte durante la compilazione dell’eseguibile, causando problemi di definizione multipla della stessa classe.

Per evitare di generare collisioni con altri nomi, è sempre saggio definire un namespace che contiene le definizioni delle nostre classi. In questo caso la classe appartiene al namespace Geometry, come dichiarato subito dopo le include guards. Usiamo una dichiarazione anticipata all’interno del namespace per spostare la definizione della classe all’esterno di esso, evitando così un ulteriore livello di indentazione. Si noti che questa è una scelta stilistica che ci obbliga ad usare il prefisso Geometry:: ogni volta che il nome della classe ricorre nel listato. Nulla vieta, però, di definire la classe entro il blocco che delimita il namespace, se lo si preferisce.

La classe contiene due membri privati di tipo double, denominati x e y, e dei metodi pubblici per l’accesso a tali membri rispettivamente in lettura, X() e Y(), ed in scrittura, setX() e setY().

È inoltre presente un metodo pubblico per il calcolo della distanza euclidea rispetto alle coordinate di un altro oggetto di tipo Point2D. La definizione della classe deve essere seguita dal simbolo ; per ragioni di uniformità e retrocompatibilità con la definizione di struct mutuata dal linguaggio C, che come vedremo, in C++ è un costrutto equivalente a quello di classe, seppure con delle piccole differenze.

L’implementazione dei metodi di classe è contenuta nel file point2D.cpp, riportato nel listato seguente:

// point2d.cpp
#include "point2d.h"
#include <cmath>
 
double Geometry::Point2D::X() {
	return x;       
}
 
void Geometry::Point2D::setX(double value) {
	if (!std::isnan(value) && !std::isinf(value))
		x = value;      
	else
		x = 0;
}
 
double Geometry::Point2D::Y() {
	return y;       
}
 
void Geometry::Point2D::setY(double value) {
	if (!std::isnan(value) && !std::isinf(value))
		y = value;      
	else
		y = 0;  
}
 
double Geometry::Point2D::distanceFrom(Point2D other)
{   
	return std::sqrt((x - other.x) * (x - other.x) + (y - other.y) * (y - other.y));
}

Il file .cpp relativo all’implementazione di una classe deve includere il file header della classe, seguito da tutti gli altri header necessari, affinchè il compilatore possa risolvere correttamente i nomi dei simboli in esso contenuti.

La definizione di membri interni privati e dei corrispettivi metodi getter e setter è una pratica comune nella definizione di classi che serve a garantire che, qualora sia necessario modificare i valori di x e y, ciò avvenga in maniera “controllata”. Ad esempio, in questo caso, i valori da assegnare come coordinate del punto vengono prima validati.

Se x e y fossero stati membri pubblici, sarebbe stato necessario ripetere questo tipo di validazione ogni volta che ad essi fosse stato assegnato un nuovo valore, aumentando così le possibilità di errore. Come ulteriore beneficio della presenza di tali medodi e della validazione che essi operano, il risultato della funzione distanceFrom sarà sempre un valore numerico valido.

Inoltre, i medoti getter e setter generalmente aiutano a preservare l’espandibilità e la retrocompatibilità dell’interfaccia delle classi, permettendo di mutare la semantica di uno o più membri privati o protetti, pur mantenendo intatta l’interfaccia pubblica della nostra classe. Ad esempio, protremmo decidere di mutare la rappesentazione interna del punto in coordinate polari (ρ e θ), mantenendo esternamente la rappesentazione cartesiana (x e y) grazie ai metodi di accesso pubblici. Ciò comporterebbe la sola modifica della classe Point2D, ma non di altre parti del codice in cui essa era usata prima di queste modifiche.

Il listato seguente mostra un esempio dell’utlizzo di tale classe:

#include <iostream>
#include "point2d.h"

using Geometry::Point2D;
using namespace std;

int main()
{
	Point2D p;

	p.setX(1);
	p.setY(1);

	Point2D p2;

	p2.setX(2);
	p2.setY(2);

	cout << p.distanceFrom(p2) << endl;

	return 0;		
}

Differenze tra class e struct

Il costrutto struct è del tutto equivalente a quello di classe in C++, nonostante esso sia un'eredità del linguaggio C.

L'unica differenza consiste nel fatto che, come impostazione predefinita, i membri di una struct sono pubblici anzichè privati. Ciò consente di preservare la retrocompatibilità con C.

In realtà, il compilatore C++ traduce una struct in una classe con soli membri pubblici, se non indicato diversamente, pertanto non esiste una ragione plausibile per usare il costrutto struct al posto di class quando si scrive codice in linguaggio C++.

Tuttavia, siamo noi programmatori che, a volte, attribuiamo una semantica differente ai due costrutti considerando le struct come POD (Plain Old Data) e le classi come oggetti con funzionalità. Non c'è nulla di male nell'adottare tale filosofia, a patto che non si creda che definire una "semplice" struttra C-like ci emancipi dalla complessità insita nella definizione di una classe, e risulti in un codice più performante. Ricordiamo sempre che un listato in puro linguaggio C, se compilato come C++, produce un eseguibile con un comportamento equivalente, ma con caratteristiche profondamente differenti.

Esistono infine alcuni casi in cui non possiamo fare a meno di usare struct, come ad esempio quando abbiamo l'esigenza di integrare nella nostra applicazione delle librerie scritte in puro linguaggio C. In tali circostanze, è possibile avvalersi di alcuni strumenti previsti dallo standard C++ per agevolare l'integrazione:

  • la libreria <cstring> che include le funzioni std::malloc, std::memcpy, std::memmove, etc...
  • la libreria <type_traits> che include la funzione template std::is_pod per discriminare tra oggetti e POD.

Per i dettagli sull'uso di tali librerie si rimanda alla documentazione apposita.

Tutte le lezioni

1 ... 15 16 17 ... 66

Se vuoi aggiornamenti su Le classi inserisci la tua e-mail nel box qui sotto:
Tags:
 
X
Se vuoi aggiornamenti su Le classi

inserisci la tua e-mail nel box qui sotto:

Ho letto e acconsento l'informativa sulla privacy

Acconsento al trattamento di cui al punto 3 dell'informativa sulla privacy