Esercizio - Numeri complessi (quarto appello 2023-24, programmazione)
In questa pagina 3
Testo (60 minuti, su carta). Implementare la classe Complex che rappresenta i numeri complessi. Tale classe deve fornire: (1) un solo costruttore che permette di inizializzare un nuovo oggetto a 0 (se chiamato senza argomenti), oppure a un numero reale (se chiamato con un solo argomento int), oppure a un numero complesso se chiamato con due argomenti int (il primo è la parte reale, il secondo la parte immaginaria); (2) l'overloading degli operatori matematici +, -, *; (3) l'overloading dell'operatore << che stampa in modo opportuno il numero complesso; (4) ogni altra funzione necessaria alla gestione della classe.
Teoria: C++ - enumerazioni e overloading degli operatoriun'enumerazione è un tipo definito dall'utente che specifica un insieme di valori simbolici (enumeratori); enum class crea uno scope (Month::jan), non si converte implicitamente in int, e i valori partono da 0 o dal valore del precedente più uno; l'overloading degli operatori permette di definire gli operatori esistenti per tipi definiti dall'utente (almeno un operando UDT, stesso numero di operandi e stessa sintassi) scrivendo una funzione di nome operator+, operator++, operator[], ...; si può fare con una helper function (due argomenti per un operatore binario, il primo può essere di qualunque tipo) o con una funzione membro (un argomento in meno, perché il primo operando è l'oggetto, quindi l'UDT deve stare a sinistra: T + int sì, int + T no); << e >> sono helper function che prendono e restituiscono ostream& e istream&; ++ ha due forme (pre e post, quest'ultima con un int fittizio); gli operatori più utili sono =, ==, !=, <, [], ().C++ - enumerazioni e overloading degli operatori →, C++ - classi, membri e invariantiuna classe è un tipo definito dal programmatore che rappresenta un concetto con una rappresentazione (dati membro) e delle operazioni (funzioni membro); l'interfaccia pubblica separa chi usa la classe da chi la implementa e i dati privati sono modificabili solo tramite l'interfaccia; struct è una class con membri pubblici di default; l'invariante è la regola che definisce gli stati validi, e il costruttore deve stabilirlo e ogni funzione membro conservarlo; il costruttore (stesso nome della classe) inizializza con una lista di inizializzazione e, se i dati non sono validi, lancia un'eccezione, l'unico modo per far fallire un costruttore; le funzioni membro si definiscono fuori dalla classe (solo le brevissime dentro, dove sono inline); le funzioni che non modificano l'oggetto sono dichiarate const; le funzioni che si possono scrivere con la sola interfaccia pubblica (operator==, next_sunday) sono helper function esterne, spesso nello stesso namespace.C++ - classi, membri e invarianti →, C++ - buona interfaccia e progetti su più fileUna classe C++ ben progettata decide esplicitamente le sette operazioni essenziali (costruttore con argomenti, di default, di copia, assegnamento di copia, costruttore e assegnamento di spostamento, distruttore): se serve il distruttore perché si possiede una risorsa, servono quasi sempre anche copia e spostamento; un costruttore a un argomento definisce una conversione implicita, che si blocca con explicit; per capire quando i costruttori e il distruttore vengono chiamati si traccia una struct che stampa ogni chiamata. Un progetto su più file separa interfaccia (header .h con definizioni di classe e dichiarazioni, protetto da include guard) e implementazione (.cpp, compilati uno per uno, mai inclusi); al compilatore si passano solo i .cpp, il main sta in un file a sé; gli errori di compilazione (sintassi, tipi, dichiarazioni mancanti) vengono dal compilatore, quelli di linking (definizione mancante, due main, nessun main) dal linker.C++ - buona interfaccia e progetti su più file →.
1. Lettura del testo
| frase del testo | scelta |
|---|---|
"un solo costruttore" con 0, 1 o 2 argomenti int |
Complex(int re = 0, int im = 0): gli argomenti di default coprono i tre casi (Complex() = 0, Complex(3) = 3, Complex(3, 4) = 3+4i) |
operatori +, -, * |
funzioni libere operator+(Complex, const Complex&), ricavate dagli operatori composti +=, -=, *= che sono membri (modificano l'oggetto) |
<< "in modo opportuno" |
funzione libera su std::ostream&; formato 3+4i, 3-4i, 5 (immaginaria nulla), -2i (reale nulla) |
| "ogni altra funzione necessaria" | real() e imag() (accesso in lettura, const), == e !=; nessuna copia o distruttore da scrivere: il compilatore genera operazioni corrette per due int (regola dello zero) |
Perché le operazioni binarie sono funzioni libere
Con operator+ come funzione membro, a + 5 funzionerebbe (il 5 si converte in Complex grazie al costruttore non explicit) ma 5 + a no: l'operando di sinistra di un membro non subisce conversioni. Come funzione libera operator+(Complex a, const Complex& b) entrambi gli operandi si convertono: a + 5 e 5 + a danno lo stesso risultato. Per lo stesso motivo il costruttore non è explicit: un int è un numero complesso, e la conversione implicita è voluta (a differenza di vector(int), C++ - buona interfaccia e progetti su più fileUna classe C++ ben progettata decide esplicitamente le sette operazioni essenziali (costruttore con argomenti, di default, di copia, assegnamento di copia, costruttore e assegnamento di spostamento, distruttore): se serve il distruttore perché si possiede una risorsa, servono quasi sempre anche copia e spostamento; un costruttore a un argomento definisce una conversione implicita, che si blocca con explicit; per capire quando i costruttori e il distruttore vengono chiamati si traccia una struct che stampa ogni chiamata. Un progetto su più file separa interfaccia (header .h con definizioni di classe e dichiarazioni, protetto da include guard) e implementazione (.cpp, compilati uno per uno, mai inclusi); al compilatore si passano solo i .cpp, il main sta in un file a sé; gli errori di compilazione (sintassi, tipi, dichiarazioni mancanti) vengono dal compilatore, quelli di linking (definizione mancante, due main, nessun main) dal linker.C++ - buona interfaccia e progetti su più file →).
Il primo operando è passato per valore: è già una copia modificabile, quindi return a += b; non richiede una variabile temporanea (idioma "binario in termini del composto"). Il secondo per const& per evitare la copia (qui sarebbero solo due int, ma lo schema resta valido per tipi più grandi).
Il prodotto
, perché . Nel operator*= le due parti nuove si calcolano prima di assegnare: re_ = re_ * b.re_ - im_ * b.im_; seguita da im_ = re_ * b.im_ + ... userebbe il re_ già modificato e darebbe un risultato sbagliato (bug classico, e rende sbagliato anche z *= z). Con le variabili locali re e im l'ordine non conta.
Esempio a mano: , come stampa il programma.
2. Il codice
#ifndef COMPLEX_NUMBER_H
#define COMPLEX_NUMBER_H
#include <iosfwd>
// Numero complesso con parte reale e immaginaria intere.
// Un solo costruttore con argomenti di default copre i tre casi del testo:
// Complex() -> 0
// Complex(3) -> 3 (numero reale)
// Complex(3, 4) -> 3 + 4i
// Non e' explicit di proposito: Complex z = 5; e 2 + z devono funzionare (un int E' un numero complesso).
class Complex {
public:
Complex(int re = 0, int im = 0) : re_{re}, im_{im} {}
int real() const { return re_; }
int imag() const { return im_; }
// operatori composti: modificano l'oggetto, quindi sono membri; quelli binari si ricavano da questi
Complex& operator+=(const Complex& b) { re_ += b.re_; im_ += b.im_; return *this; }
Complex& operator-=(const Complex& b) { re_ -= b.re_; im_ -= b.im_; return *this; }
Complex& operator*=(const Complex& b)
{
int re = re_ * b.re_ - im_ * b.im_; // (a+bi)(c+di) = (ac-bd) + (ad+bc)i
int im = re_ * b.im_ + im_ * b.re_;
re_ = re;
im_ = im;
return *this;
}
private:
int re_;
int im_;
};
// operatori binari: funzioni libere, cosi' la conversione implicita funziona su entrambi gli operandi
Complex operator+(Complex a, const Complex& b);
Complex operator-(Complex a, const Complex& b);
Complex operator*(Complex a, const Complex& b);
bool operator==(const Complex& a, const Complex& b);
bool operator!=(const Complex& a, const Complex& b);
std::ostream& operator<<(std::ostream& os, const Complex& z); // 3+4i, 3-4i, 5, -2i
#endif // COMPLEX_NUMBER_H#include "complex_number.h"
#include <ostream>
Complex operator+(Complex a, const Complex& b) { return a += b; } // a e' una copia: si puo' modificare
Complex operator-(Complex a, const Complex& b) { return a -= b; }
Complex operator*(Complex a, const Complex& b) { return a *= b; }
bool operator==(const Complex& a, const Complex& b)
{
return a.real() == b.real() && a.imag() == b.imag();
}
bool operator!=(const Complex& a, const Complex& b) { return !(a == b); }
std::ostream& operator<<(std::ostream& os, const Complex& z)
{
if (z.imag() == 0) return os << z.real(); // solo parte reale
if (z.real() == 0) return os << z.imag() << 'i'; // solo parte immaginaria
return os << z.real() << (z.imag() < 0 ? '-' : '+') << (z.imag() < 0 ? -z.imag() : z.imag()) << 'i';
}#include <iostream>
#include "complex_number.h"
int main()
{
Complex zero; // 0
Complex real{3}; // 3
Complex a{1, 2}; // 1+2i
Complex b{3, -4}; // 3-4i
std::cout << "zero = " << zero << ", real = " << real << ", a = " << a << ", b = " << b << '\n';
std::cout << "a + b = " << a + b << '\n'; // 4-2i
std::cout << "a - b = " << a - b << '\n'; // -2+6i
std::cout << "a * b = " << a * b << '\n'; // (1*3 - 2*(-4)) + (1*(-4) + 2*3)i = 11+2i
std::cout << "i * i = " << Complex{0, 1} * Complex{0, 1} << '\n'; // -1
std::cout << "(1+2i)(1-2i) = " << a * Complex{1, -2} << '\n'; // 5: il prodotto per il coniugato e' reale
std::cout << "a + 5 = " << a + 5 << ", 5 + a = " << 5 + a << '\n'; // conversione implicita int -> Complex
Complex z = 7; // 7
z += a; // 8+2i
z *= 2; // 16+4i
std::cout << "z = " << z << '\n';
std::cout << "a == Complex{1,2}: " << (a == Complex{1, 2}) << ", a != b: " << (a != b) << '\n';
std::cout << "solo immaginario: " << Complex{0, -2} << '\n';
}(Il file si chiama complex_number.h per non confonderlo con l'header complex.h della libreria C.)
Uscita eseguita (compilato anche per Linux con -Wall -Wextra senza avvisi):
zero = 0, real = 3, a = 1+2i, b = 3-4i
a + b = 4-2i
a - b = -2+6i
a * b = 11+2i
i * i = -1
(1+2i)(1-2i) = 5
a + 5 = 6+2i, 5 + a = 6+2i
z = 16+4i
a == Complex{1,2}: 1, a != b: 1
solo immaginario: -2iControlli a mano: ; ; ; il prodotto per il coniugato è reale: ; z = 7, z += a dà , z *= 2 dà .
Limiti: con int il prodotto può andare in overflow (comportamento indefinito) per parti oltre circa ; il testo impone int.
3. Errori da evitare
- Tre costruttori (
Complex(),Complex(int),Complex(int, int)) quando il testo vuole un solo costruttore: si usano gli argomenti di default. operator+membro (5 + anon compila) o con la conversione bloccata daexplicit.operator*=che usare_già aggiornato per calcolareim_.operator<<membro, o che non restituisce lo stream, o che stampa constd::cout; parte immaginaria negativa stampata come1+-2i.- Funzioni
add(),sub()al posto degli operatori (le consegne dicono di fare l'overloading). - Dati membro pubblici per far funzionare
operator<<ofriend.
Versione ripasso
- Testo: classe
Complexcon un solo costruttore (nessun argomento = 0, unint= reale, dueint= reale e immaginaria), overloading di+,-,*, di<<, e le altre funzioni necessarie. Teoria: C++ - enumerazioni e overloading degli operatoriun'enumerazione è un tipo definito dall'utente che specifica un insieme di valori simbolici (enumeratori); enum class crea uno scope (Month::jan), non si converte implicitamente in int, e i valori partono da 0 o dal valore del precedente più uno; l'overloading degli operatori permette di definire gli operatori esistenti per tipi definiti dall'utente (almeno un operando UDT, stesso numero di operandi e stessa sintassi) scrivendo una funzione di nome operator+, operator++, operator[], ...; si può fare con una helper function (due argomenti per un operatore binario, il primo può essere di qualunque tipo) o con una funzione membro (un argomento in meno, perché il primo operando è l'oggetto, quindi l'UDT deve stare a sinistra: T + int sì, int + T no); << e >> sono helper function che prendono e restituiscono ostream& e istream&; ++ ha due forme (pre e post, quest'ultima con un int fittizio); gli operatori più utili sono =, ==, !=, <, [], ().C++ - enumerazioni e overloading degli operatori →. - Scelte:
Complex(int re = 0, int im = 0)(argomenti di default), nonexplicit;+=,-=,*=membri;+,-,*funzioni libere (operator+(Complex a, const Complex& b) { return a += b; }): così valgonoa + 5e5 + a;real(),imag()const;==,!=; regola dello zero. - Prodotto: ; calcolare
reeimin variabili locali prima di assegnare (altrimentiim_usa ilre_già modificato). <<:3+4i,3-4i,5(solo reale),-2i(solo immaginaria).
#ifndef COMPLEX_NUMBER_H
#define COMPLEX_NUMBER_H
#include <iosfwd>
// Numero complesso con parte reale e immaginaria intere.
// Un solo costruttore con argomenti di default copre i tre casi del testo:
// Complex() -> 0
// Complex(3) -> 3 (numero reale)
// Complex(3, 4) -> 3 + 4i
// Non e' explicit di proposito: Complex z = 5; e 2 + z devono funzionare (un int E' un numero complesso).
class Complex {
public:
Complex(int re = 0, int im = 0) : re_{re}, im_{im} {}
int real() const { return re_; }
int imag() const { return im_; }
// operatori composti: modificano l'oggetto, quindi sono membri; quelli binari si ricavano da questi
Complex& operator+=(const Complex& b) { re_ += b.re_; im_ += b.im_; return *this; }
Complex& operator-=(const Complex& b) { re_ -= b.re_; im_ -= b.im_; return *this; }
Complex& operator*=(const Complex& b)
{
int re = re_ * b.re_ - im_ * b.im_; // (a+bi)(c+di) = (ac-bd) + (ad+bc)i
int im = re_ * b.im_ + im_ * b.re_;
re_ = re;
im_ = im;
return *this;
}
private:
int re_;
int im_;
};
// operatori binari: funzioni libere, cosi' la conversione implicita funziona su entrambi gli operandi
Complex operator+(Complex a, const Complex& b);
Complex operator-(Complex a, const Complex& b);
Complex operator*(Complex a, const Complex& b);
bool operator==(const Complex& a, const Complex& b);
bool operator!=(const Complex& a, const Complex& b);
std::ostream& operator<<(std::ostream& os, const Complex& z); // 3+4i, 3-4i, 5, -2i
#endif // COMPLEX_NUMBER_H- Uscita:
a = 1+2i,b = 3-4i;a + b = 4-2i;a - b = -2+6i;a * b = 11+2i;i * i = -1;(1+2i)(1-2i) = 5;a + 5 = 6+2i = 5 + a;z = 16+4i. - Errori tipici: tre costruttori,
operator+membro,*=conre_già aggiornato,<<membro o sucout,add()/sub(), dati pubblici, overflow conint.