next up previous contents index
Next: Due esempi concreti Up: Introduzione alle classi Previous: Sovrapposizione degli operatori   Indice   Indice analitico

Costruttore di copia e operatore di assegnamento

L'operatore di assegnamento (=) in C++ è certamente uno dei più importanti, al punto da meritare una trattazione separata per quanto riguarda la sovrapposizione di esso in una classe. Il compilatore C++, nel caso non sia presente un operatore di assegnamento, ne crea uno default, il quale non ha altro compito che copiare membro a membro la struttura dati dell'oggetto a destra di = in quello a sinistra di esso. Vediamo un esempio:

// ex9_6_1.cpp
#include <iostream.h>

class A {
  int n;
  double x;
public:
  A (int N, double X) {
    n = N;
    x = X; }
  void stampa () {
    cout << "(" << n << " , " << x << ")\n"; }
  // ~A ();  // il distruttore non serve:
             // tutti i membri sono non dinamici
};

void main() {
  A a (5, 2.71);
  a.stampa ();
  A* b = new A (-2, 3.14);
  b->stampa();
  a = *b;
  a.stampa();
  delete b;
  a.stampa();
}

output:
(5 , 2.71)
(-2 , 3.14)
(-2 , 3.14)
(-2 , 3.14)

Sebbene l'operatore di assegnamento non stato sovrapposto, non c'è davvero nessun problema: tutti i campi di *b vengono copiati in a, la quale è del tutto indifferente delle sorte di b. Nel seguente esempio però i problemi vengono fuori prepotentemente:


// ex9_6_2.cpp
// ATTENZIONE: non funziona
#include <iostream.h>

class B {
  int* n;
  double* x;
public:
  B (int N, double X) {
    n = new int(N);
    x = new double(X); }
  void stampa () {
    cout << "(" << *n << " , " << *x << ")\n"; }
  ~B () {          // il distruttore serve:
    delete n;      // ci sono membri sono dinamic
    delete x; }
};

void main() {
  B a (5, 2.71);
  a.stampa ();
  B* b = new B (-2, 3.14);
  b->stampa();
  a = *b;
  a.stampa();
  delete b;
  a.stampa();
}

esempio di output:
(5 , 2.71)
(-2 , 3.14)
(-2 , 3.14)
(1075285264 , 3.14)
Segmentation fault

Cosa è cambiato in quest'ultimo esempio: i membri sono dinamici, per cui la memoria ad essi destinata viene allocata nel costruttore e deallocata nel distruttore (il quale è in questo caso fondamentale); i campi che vengono copiati con l'operatore di assegnamento sono gli indirizzi dei due puntatori n e x: gli indirizzi contenuti in tali campi vengono semplicemente assegnati ai corrispondenti dell'oggetto a, il quale dunque non dealloca la memoria precedentemente occupata (producendo garbage) e non alloca la memoria per i nuovi campi. Un disastro: basta eliminare l'oggetto b (o anche cambiare il valore dei campi di esso) e il sistema va in crash (il messaggio di errore che mi è stato riportato è ``Segmentation fault'', ma potrebbe essere qualunque altra cosa).

La soluzione a questo problema è semplice: basta sovrapporre l'operatore di assegnamento per tutte le classi che allocano memoria dinamica (praticamente tutte quindi, se si escludono quelle costruite per scopi squisitamente didattici). Il compito di tale operatore è dunque quello di deallocare la memoria precedentemente allocata e di allocarne di nuova per i campi assegnati; infine si possono copiare i valori dell'oggetto a destra dell'operatore = nella neonata struttura dati. Esso funge dunque da distruttore e da costruttore insieme. Vediamo una possibile implementazione dell'operatore di assegnamento per la classe ArrayS, studiata nella sezione precedente:


class ArrayS {
  /* ... */
public:
  /* ... */
  const ArrayS& operator= (const ArrayS& a);
};
const ArrayS& ArrayS::operator= (const ArrayS& a) {
  if (&a != this) {
    if (n != a.n) {
      n = a.n;
      delete[] array;
      array = new double[n];
    }
    for (int i = 0; i < n; i++)
      array[i] = a.array[i];
  }
  return *this;
}

Ci sono diverse importanti osservazioni:

Un semplice esempio che mostra l'utilizzo dell'operatore di assegnamento è il seguente 9.4:


// ex9_6_3.cpp
#include "arrays2.h"

void main() {
  ArrayS x(8),y(10);
  for (int i = 0; i < 10; i++) {
    if (i < 8) x[i] = i;
    y[i] = 10 - i;
  }
  cout << "\nx:\n"; x.stampa(5);
  cout << "\ny:\n"; y.stampa(5);
  x = y;
  cout << "\nx:\n"; x.stampa(5);
}

output:
x:
 0 1 2 3 4
 5 6 7    

y:
 10 9 8 7 6
 5 4 3 2 1


x:
 10 9 8 7 6
 5 4 3 2 1


La filosofia di utilizzo dell'operatori di assegnamento è molto simile a quella di un altra tipica funzione membro di una classe: il costruttore di copia. Esso viene chiamato ogni volta che si ha la necessità di creare un oggetto a partire da uno preesistente, ovvero nelle inializzazioni sulla base di oggetti dello stesso tipo; il costruttore di copia (copy constructor) è indicato solitamente con X::X(X&). Le operazioni da eseguire all'interno di esso sono formalmente identiche a quelle dell'operatore di assegnamento, con l'unica importante differenza che non è necessario deallocare memoria: gli oggetti da inizializzare non esistono prima di tale operazione. Si faccia bene attenzione che, supponendo che x1 sia un oggetto di tipo X, lo statement:


X* x2 = new X(x1);
effettua una inizializzazione di x1 con i dati di x2; non ci si faccia trarre in inganno dal simbolo =: non si tratta di un assegnamento!

Vediamo un esempio con la solita classe ArrayS 9.5:


class ArrayS {
  /* ... */
public:
  /* ... */
  ArrayS (const ArrayS& a);
};
ArrayS::ArrayS (const ArrayS& a) {
  n = a.n;
  array = new double[n];
  for (int i = 0; i < n; i++)
    array[i] = a.array[i];
}

Il costruttore di copia, come ogni costruttore, viene chiamato automaticamente dal C++, non è possibile cioè invocarlo esplicitamente come l'operatore di assegnamento, o qualunque altra funzione membro. Se il costruttore di copia non è presente, esso viene automaticamente generato dal compilatore; in tal caso l'unica operazione compiuta è la copia membro a membro dei dati della classe; come l'operatore di assegnamento e il distruttore, il costruttore di copia è dunque necessario solo se la classe alloca memoria dinamica al suo interno. Tali speciali funzioni membro, qualora non fosse necessaria la loro esistenza, è bene dichiararle commentate, come abbiamo visto per il distruttore nella sezione precedente. Un esempio del costruttore di copia per ArrayS è il seguente:


// ex9_6_4.cpp
#include "arrays2.h"

void main() {
  int n;
  cout << "quanti elementi? "; cin >> n;
  ArrayS x(n);
  for (int i = 0; i < n; i++) {
    cout << "? ";
    cin >> x[i];
  }
  // copiamo il vettore x in un vettore y dinamico
  // ed in un vettore z non dinamico
  ArrayS y(x);
  ArrayS* z = new ArrayS(x);
  // ordiniamo i due vettori y e z
  z->ordina();
  cout << "\ncopia del vettore:\n"; y.stampa();
  cout << "\ncopia ordinata:\n"; z->stampa();
}

esempio di output:
quanti elementi? 4
? 2.71
? 3.14
? -1.4
? 5e-6

copia del vettore:
 2.71 3.14 -1.4 5e-06

copia ordinata:
 3.14 2.71 5e-06 -1.4

Un'ultima osservazione: l'operazione di copia di una classe potrebbe essere priva di senso; si pensi ad esempio ad una classe la quale rappresenti il sistema sul quale viene eseguito il programma: che senso avrebbe crearne una copia? In casi come questi, è possibile utilizzare uno stratagemma sottile: si dichiarino il costruttore di copia e l'operatore di assegnamento come funzioni membro private, senza definirle; sarà compito del compilatore inibire l'utilizzatore della nostra classe ad effettuare copie di un oggetto di tale classe. Si consideri il seguente esempio:


// ex9_6_5.cpp

class Sistema {
  /* tanti dati */
  Sistema (const Sistema&);
  const Sistema& operator= (const Sistema&);
public:
  Sistema () {
    /* inizializzazione dei dati */ }
  ~Sistema () {
    /* distruzione delle variabili dinamiche */ }
  /* tante funzioni membro */
};

void main() {
  Sistema s;
  // Sistema s2(s);  // no! il costruttore di copia e` privato
  Sistema s3;
  // s3 = s;         // no! l'operatore di assegnamento e` privato
}

ex-2
si scriva una classe (chiamata RealePositivo) che rappresenti un numero reale positivo allocato dinamicamente; si sovrappongano gli operatori di somma, prodotto, differenza, divisione, minore, minore-uguale;


next up previous contents index
Next: Due esempi concreti Up: Introduzione alle classi Previous: Sovrapposizione degli operatori   Indice   Indice analitico
Claudio Cicconetti
2000-09-06