Salta al contenuto
Note per Studenti Esercizio - La classe Complex (primo appello 2022-23, programmazione)

Esercizio - La classe Complex (primo appello 2022-23, programmazione)

Esame

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 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 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.

Progetto

Un numero complesso è a+bia + bi, con parte reale aa e immaginaria bb (qui int). Operazioni:

(a+bi)+(c+di)=(a+c)+(b+d)i(a+bi)−(c+di)=(a−c)+(b−d)i(a+bi) + (c+di) = (a+c) + (b+d)i \qquad (a+bi) - (c+di) = (a-c) + (b-d)i (a+bi)(c+di)=(ac−bd)+(ad+bc)i(a+bi)(c+di) = (ac - bd) + (ad + bc)i

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

complex.cppcpp
#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: 1

Controlli a mano: (3+4i)(1−2i)=3−6i+4i−8i2=3−2i+8=11−2i(3+4i)(1-2i) = 3 - 6i + 4i - 8i^2 = 3 - 2i + 8 = 11 - 2i; i⋅i=−1i \cdot i = -1; (3+4i)(3−4i)=9+16=25(3+4i)(3-4i) = 9 + 16 = 25 (il prodotto con il coniugato è reale); a+1a + 1 e 1+a1 + a danno entrambi 4+4i4+4i; 2⋅a=6+8i2 \cdot a = 6+8i.

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, e return a += b; restituisce quella copia. Una Complex sono due int: copiarla costa quanto una reference.
  • Se + fosse stata una funzione membro, 1 + a non sarebbe compilato (a sinistra c'è un int).
  • Il meno unario operator-(const Complex&) e il binario operator-(Complex, const Complex&) sono due overload diversi dello stesso operatore, distinti dal numero di argomenti.

Versione ripasso

  • Formule: (a+bi)+(c+di)=(a+c)+(b+d)i(a+bi)+(c+di) = (a+c)+(b+d)i; (a+bi)−(c+di)=(a−c)+(b−d)i(a+bi)-(c+di) = (a-c)+(b-d)i; (a+bi)(c+di)=(ac−bd)+(ad+bc)i(a+bi)(c+di) = (ac-bd)+(ad+bc)i.
  • Scelte: un solo costruttore Complex(int re = 0, int im = 0) (zero, reale, complesso); accessori real()/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 (due int).
cpp
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 calcolare im_); operatori come membri e pretendere 1 + a; costruttori separati con ambiguità sugli argomenti di default; parte reale e immaginaria scambiate.

Esercizi su questo argomento

Teoria collegata