Esercizio - La classe Complex (primo appello 2022-23, programmazione)
Prova di programmazione (60 minuti) del primo appello 2022-23 del corso di Laboratorio di Programmazione, Ing. Informatica UniPD. Teoria: 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++ - 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 →; un esercizio analogo è il Esercizio - Numeri razionali con classe e overloading (laboratorio 2).
Testo. Implementare la classe
Complexche 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 argomentoint), oppure un numero complesso se chiamato con due argomentiint(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.
Progetto
Un numero complesso è , con parte reale e immaginaria (qui int). Operazioni:
| richiesta | scelta | perché |
|---|---|---|
| 1. un solo costruttore | Complex(int re = 0, int im = 0) |
gli argomenti di default coprono zero, uno e due argomenti: 0, un reale, un complesso |
2. + - * |
helper function Complex operator+(Complex a, const Complex& b) che usano gli operatori composti +=, -=, *= (membri) |
l'helper accetta conversioni su entrambi gli operandi: a + 1 e 1 + a (grazie al costruttore Complex(int) non explicit); a per valore è già una copia da modificare e restituire |
3. << |
helper ostream& operator<<(ostream&, const Complex&) |
restituisce lo stream; stampa 3+4i, 3-4i, 0+0i |
| 4. altre funzioni | accessori real(), imag() (const); == e !=; meno unario; conjugate() |
gli accessori servono alle helper function senza aprire i dati; confronto e coniugato sono i compiti naturali del tipo |
| copia, assegnamento, distruttore | non scritti | due int: quelli del compilatore sono corretti (regola dello zero) |
Il segno nella stampa: per la parte immaginaria negativa il segno - lo stampa già l'int, quindi si scrive + solo se imag() >= 0.
Programma
#include <iostream>
using namespace std;
// Numero complesso a + bi. Invarianti: nessuno particolare (ogni coppia di int e' valida).
class Complex {
public:
// 1. UN solo costruttore: 0 senza argomenti, un reale con un argomento, un complesso con due
Complex(int re = 0, int im = 0) : re_{re}, im_{im} {}
// accessori const: servono alle helper function
int real() const { return re_; }
int imag() const { return im_; }
// gli operatori composti si scrivono come membri (modificano l'oggetto) e restituiscono *this
Complex& operator+=(const Complex& z) { re_ += z.re_; im_ += z.im_; return *this; }
Complex& operator-=(const Complex& z) { re_ -= z.re_; im_ -= z.im_; return *this; }
Complex& operator*=(const Complex& z)
{
int r = re_ * z.re_ - im_ * z.im_; // (a+bi)(c+di) = (ac - bd) + (ad + bc)i
int i = re_ * z.im_ + im_ * z.re_;
re_ = r; im_ = i;
return *this;
}
private:
int re_;
int im_;
};
// 2. + - * come helper function: simmetrici (Complex + int e int + Complex) grazie al costruttore Complex(int)
Complex operator+(Complex a, const Complex& b) { return a += b; } // a e' una COPIA: si modifica e si restituisce
Complex operator-(Complex a, const Complex& b) { return a -= b; }
Complex operator*(Complex a, const Complex& b) { return a *= b; }
// 4. altre funzioni utili alla gestione della classe
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); }
Complex operator-(const Complex& a) { return Complex(-a.real(), -a.imag()); } // meno unario
Complex conjugate(const Complex& a) { return Complex(a.real(), -a.imag()); }
// 3. operatore di uscita: "3+4i", "3-4i", "0+0i"
ostream& operator<<(ostream& os, const Complex& z)
{
os << z.real();
if (z.imag() >= 0) os << '+'; // il segno meno lo stampa gia' l'int
return os << z.imag() << 'i';
}
int main()
{
Complex zero; // 0+0i
Complex cinque(5); // 5+0i
Complex a(3, 4), b(1, -2);
cout << zero << ' ' << cinque << ' ' << a << ' ' << b << '\n';
cout << "a + b = " << a + b << '\n';
cout << "a - b = " << a - b << '\n';
cout << "a * b = " << a * b << '\n';
Complex i(0, 1);
cout << "i * i = " << i * i << '\n';
cout << "a + 1 = " << a + 1 << ", 1 + a = " << 1 + a << ", 2 * a = " << 2 * a << '\n';
cout << "-a = " << -a << ", coniugato di a = " << conjugate(a) << '\n';
cout << "a * coniugato(a) = " << a * conjugate(a) << '\n';
cout << "a == Complex(3, 4): " << (a == Complex(3, 4)) << ", a != b: " << (a != b) << '\n';
return 0;
}0+0i 5+0i 3+4i 1-2i
a + b = 4+2i
a - b = 2+6i
a * b = 11-2i
i * i = -1+0i
a + 1 = 4+4i, 1 + a = 4+4i, 2 * a = 6+8i
-a = -3-4i, coniugato di a = 3-4i
a * coniugato(a) = 25+0i
a == Complex(3, 4): 1, a != b: 1Controlli a mano: ; ; (il prodotto con il coniugato è reale); e danno entrambi ; .
Osservazioni di progetto:
operator+(Complex a, const Complex& b) { return a += b; }: il primo argomento è passato per valore, quindi è una copia che si può modificare, ereturn a += b;restituisce quella copia. UnaComplexsono dueint: copiarla costa quanto una reference.- Se
+fosse stata una funzione membro,1 + anon sarebbe compilato (a sinistra c'è unint). - Il meno unario
operator-(const Complex&)e il binariooperator-(Complex, const Complex&)sono due overload diversi dello stesso operatore, distinti dal numero di argomenti.
Versione ripasso
- Formule: ; ; .
- Scelte: un solo costruttore
Complex(int re = 0, int im = 0)(zero, reale, complesso); accessorireal()/imag()const; operatori composti+=,-=,*=come membri (restituiscono*this);+ - *come helper con il primo argomento per valore;<<helper che restituisce lo stream;==,!=, meno unario,conjugate(); copia, assegnamento e distruttore generati (dueint).
class Complex {
public:
Complex(int re = 0, int im = 0) : re_{re}, im_{im} {}
int real() const { return re_; } int imag() const { return im_; }
Complex& operator+=(const Complex& z) { re_ += z.re_; im_ += z.im_; return *this; }
Complex& operator*=(const Complex& z)
{ int r = re_ * z.re_ - im_ * z.im_; int i = re_ * z.im_ + im_ * z.re_;
re_ = r; im_ = i; return *this; }
private: int re_, im_;
};
Complex operator+(Complex a, const Complex& b) { return a += b; }
ostream& operator<<(ostream& os, const Complex& z)
{ os << z.real(); if (z.imag() >= 0) os << '+'; return os << z.imag() << 'i'; }- Uscita:
0+0i 5+0i 3+4i 1-2i;a + b = 4+2i;a - b = 2+6i;a * b = 11-2i;i * i = -1+0i;a + 1 = 4+4i,1 + a = 4+4i,2 * a = 6+8i;-a = -3-4i;a * coniugato(a) = 25+0i. - Errori tipici: stampare
3+-4i(segno doppio);*senza variabili temporanee (re_sovrascritto prima di calcolareim_); operatori come membri e pretendere1 + a; costruttori separati con ambiguità sugli argomenti di default; parte reale e immaginaria scambiate.