Guida Completa: Come Programmare una Calcolatrice in C++
Creare una calcolatrice in C++ è un progetto eccellente per comprendere i fondamenti della programmazione, inclusi input/output, strutture di controllo, funzioni e gestione degli errori. Questa guida dettagliata ti condurrà attraverso tutti i passaggi necessari per sviluppare una calcolatrice funzionale, con esempi pratici e best practice.
1. Introduzione ai Concetti di Base
Prima di iniziare a scrivere codice, è essenziale comprendere i componenti fondamentali di una calcolatrice:
- Operandi: I numeri su cui vengono eseguite le operazioni (es. 5 e 3 in 5 + 3)
- Operatori: I simboli che definiscono l’operazione (+, -, *, /, %)
- Input/Output: Come l’utente inserisce i dati e visualizza i risultati
- Gestione degli errori: Come il programma gestisce input non validi (es. divisione per zero)
1.1 Tipi di Dati in C++ per le Calcolatrici
La scelta del tipo di dati influisce sulla precisione e sull’intervallo dei valori che la tua calcolatrice può gestire:
| Tipo di Dati |
Dimensione (byte) |
Intervallo |
Precisione |
Uso Tipico |
| int |
4 |
-2,147,483,648 a 2,147,483,647 |
Nessuna decimale |
Calcoli con numeri interi |
| float |
4 |
±3.4e-38 a ±3.4e+38 |
6-7 cifre decimali |
Calcoli con precisione singola |
| double |
8 |
±1.7e-308 a ±1.7e+308 |
15-16 cifre decimali |
Calcoli con alta precisione |
2. Implementazione di Base della Calcolatrice
Iniziamo con una versione semplice che esegue le quattro operazioni aritmetiche fondamentali.
#include <iostream>
using namespace std;
int main() {
char operation;
double num1, num2;
// Input dell’utente
cout << “Inserisci l’operatore (+, -, *, /): “;
cin >> operation;
cout << “Inserisci due numeri: “;
cin >> num1 >> num2;
// Struttura di controllo per le operazioni
switch(operation) {
case ‘+’:
cout << num1 << ” + ” << num2 << ” = ” << num1 + num2;
break;
case ‘-‘:
cout << num1 << ” – ” << num2 << ” = ” << num1 – num2;
break;
case ‘*’:
cout << num1 << ” * ” << num2 << ” = ” << num1 * num2;
break;
case ‘/’:
if(num2 != 0)
cout << num1 << ” / ” << num2 << ” = ” << num1 / num2;
else
cout << “Errore: Divisione per zero!”;
break;
default:
cout << “Errore: Operatore non valido!”;
}
return 0;
}
2.1 Spiegazione del Codice
- Libreria iostream: Fornisce le funzioni per input/output (cin e cout)
- Variabili:
operation: memorizza l’operatore (+, -, *, /)
num1, num2: memorizzano gli operandi (usiamo double per gestire sia interi che decimali)
- Input: L’utente inserisce operatore e numeri
- Switch-case: Esegue l’operazione corrispondente all’operatore inserito
- Gestione errori: Controllo per divisione per zero e operatore non valido
3. Gestione Avanzata degli Errori
Una calcolatrice robusta deve gestire vari tipi di errori:
- Divisione per zero
- Input non numerici
- Overflow/underflow (valori troppo grandi/piccoli)
- Operatori non validi
#include <iostream>
#include <limits>
#include <cmath>
using namespace std;
int main() {
char operation;
double num1, num2;
cout << “Calcolatrice Avanzata in C++\n”;
cout << “Operatori validi: +, -, *, /, % (modulo), ^ (potenza)\n”;
try {
cout << “Inserisci l’operatore: “;
cin >> operation;
if(operation != ‘+’ && operation != ‘-‘ && operation != ‘*’ &&
operation != ‘/’ && operation != ‘%’ && operation != ‘^’) {
throw invalid_argument(“Operatore non valido!”);
}
cout << “Inserisci due numeri: “;
if(!(cin >> num1 >> num2)) {
throw runtime_error(“Input non valido! Inserisci numeri.”);
}
double result;
bool error = false;
switch(operation) {
case ‘+’:
if((num1 > 0 && num2 > numeric_limits<double>::max() – num1) ||
(num1 < 0 && num2 < -numeric_limits<double>::max() – num1)) {
error = true;
} else {
result = num1 + num2;
}
break;
case ‘-‘:
if((num1 > 0 && num2 < -numeric_limits<double>::max() + num1) ||
(num1 < 0 && num2 > numeric_limits<double>::max() + num1)) {
error = true;
} else {
result = num1 – num2;
}
break;
case ‘*’:
if(num1 > numeric_limits<double>::max() / num2 ||
num1 < -numeric_limits<double>::max() / num2) {
error = true;
} else {
result = num1 * num2;
}
break;
case ‘/’:
if(num2 == 0) {
throw runtime_error(“Errore: Divisione per zero!”);
}
result = num1 / num2;
break;
case ‘%’:
if(num2 == 0) {
throw runtime_error(“Errore: Modulo per zero!”);
}
result = fmod(num1, num2);
break;
case ‘^’:
result = pow(num1, num2);
break;
}
if(error) {
throw overflow_error(“Errore: Overflow/underflow rilevato!”);
}
cout << “Risultato: ” << num1 << ” ” << operation << ” ” << num2 << ” = ” << result << endl;
} catch(const exception& e) {
cerr << “Errore: ” << e.what() << endl;
return 1;
}
return 0;
}
3.1 Tecniche di Gestione Errori Utilizzate
| Tecnica |
Descrizione |
Esempio nel Codice |
| Exception Handling |
Usa try-catch per gestire errori in modo elegante |
try { … } catch(const exception& e) |
| Controllo Input |
Verifica che l’input sia valido prima di elaborarlo |
if(!(cin >> num1 >> num2)) |
| Controllo Overflow |
Previene risultati che superano i limiti del tipo di dati |
if(num1 > numeric_limits::max() / num2) |
| Validazione Operatore |
Verifica che l’operatore inserito sia valido |
if(operation != ‘+’ && operation != ‘-‘ && …) |
4. Implementazione con Funzioni
Organizzare il codice in funzioni migliorare la leggibilità e la manutenibilità:
#include <iostream>
#include <limits>
#include <cmath>
#include <stdexcept>
using namespace std;
// Funzione per aggiungere due numeri con controllo overflow
double safeAdd(double a, double b) {
if((a > 0 && b > numeric_limits<double>::max() – a) ||
(a < 0 && b < -numeric_limits<double>::max() – a)) {
throw overflow_error(“Overflow nella somma”);
}
return a + b;
}
// Funzione per sottrare due numeri con controllo overflow
double safeSubtract(double a, double b) {
if((a > 0 && b < -numeric_limits<double>::max() + a) ||
(a < 0 && b > numeric_limits<double>::max() + a)) {
throw overflow_error(“Overflow nella sottrazione”);
}
return a – b;
}
// Funzione per moltiplicare due numeri con controllo overflow
double safeMultiply(double a, double b) {
if(a > numeric_limits<double>::max() / b ||
a < -numeric_limits<double>::max() / b) {
throw overflow_error(“Overflow nella moltiplicazione”);
}
return a * b;
}
// Funzione per dividere due numeri
double safeDivide(double a, double b) {
if(b == 0) {
throw runtime_error(“Divisione per zero”);
}
return a / b;
}
// Funzione per calcolo modulo
double safeModulus(double a, double b) {
if(b == 0) {
throw runtime_error(“Modulo per zero”);
}
return fmod(a, b);
}
// Funzione per elevamento a potenza
double safePower(double base, double exponent) {
// Controllo semplice per overflow (non esaustivo)
if(base == 0 && exponent < 0) {
throw runtime_error(“Zero elevato a potenza negativa”);
}
return pow(base, exponent);
}
// Funzione per visualizzare il menu
void displayMenu() {
cout << “Calcolatrice in C++ con Funzioni\n”;
cout << “Operatori disponibili: +, -, *, /, %, ^\n”;
cout << “Inserisci ‘q’ per uscire\n\n”;
}
int main() {
char operation;
double num1, num2;
while(true) {
displayMenu();
try {
cout << “Inserisci operatore (o ‘q’ per uscire): “;
cin >> operation;
if(operation == ‘q’ || operation == ‘Q’) {
break;
}
if(operation != ‘+’ && operation != ‘-‘ && operation != ‘*’ &&
operation != ‘/’ && operation != ‘%’ && operation != ‘^’) {
throw invalid_argument(“Operatore non valido!”);
}
cout << “Inserisci due numeri: “;
if(!(cin >> num1 >> num2)) {
throw runtime_error(“Input non valido! Inserisci numeri.”);
}
double result;
switch(operation) {
case ‘+’:
result = safeAdd(num1, num2);
break;
case ‘-‘:
result = safeSubtract(num1, num2);
break;
case ‘*’:
result = safeMultiply(num1, num2);
break;
case ‘/’:
result = safeDivide(num1, num2);
break;
case ‘%’:
result = safeModulus(num1, num2);
break;
case ‘^’:
result = safePower(num1, num2);
break;
}
cout << “Risultato: ” << num1 << ” ” << operation << ” ” << num2 << ” = ” << result << “\n\n”;
} catch(const exception& e) {
cerr << “Errore: ” << e.what() << “\n\n”;
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), ‘\n’);
}
}
cout << “Grazie per aver usato la calcolatrice!\n”;
return 0;
}
4.1 Vantaggi dell’Approccio Modulare
- Riutilizzo del codice: Le funzioni possono essere riutilizzate in altri programmi
- Manutenibilità: Più facile aggiornare o correggere una singola funzione
- Leggibilità: Il codice è più organizzato e comprensibile
- Testabilità: Ogni funzione può essere testata indipendentemente
- Gestione errori: Ogni funzione può gestire i propri errori specifici
5. Interfaccia Utente Avanzata
Per migliorare l’esperienza utente, possiamo implementare:
- Menu interattivo con opzioni multiple
- Storico delle operazioni
- Input validazione più robusta
- Formattazione migliorata dell’output
#include <iostream>
#include <limits>
#include <cmath>
#include <iomanip>
#include <vector>
#include <string>
using namespace std;
struct OperationRecord {
double operand1;
double operand2;
char op;
double result;
string error;
};
vector<OperationRecord> history;
void displayHistory() {
cout << “\n— Storico Operazioni —\n”;
cout << setw(10) << “Op1” << setw(5) << “Op” << setw(10) << “Op2” <<
setw(15) << “Risultato” << setw(20) << “Stato” << endl;
cout << string(60, ‘-‘) << endl;
for(const auto& record : history) {
cout << setw(10) << record.operand1 << setw(5) << record.op <<
setw(10) << record.operand2 << setw(15);
if(record.error.empty()) {
cout << record.result;
} else {
cout << “ERRORE”;
}
cout << setw(20) << (record.error.empty() ? “Successo” : record.error) << endl;
}
cout << endl;
}
double performOperation(double a, double b, char op) {
try {
switch(op) {
case ‘+’: return a + b;
case ‘-‘: return a – b;
case ‘*’: return a * b;
case ‘/’:
if(b == 0) throw runtime_error(“Divisione per zero”);
return a / b;
case ‘%’:
if(b == 0) throw runtime_error(“Modulo per zero”);
return fmod(a, b);
case ‘^’: return pow(a, b);
default: throw invalid_argument(“Operatore non valido”);
}
} catch(const exception& e) {
throw;
}
}
void displayMenu() {
cout << “\n=== Calcolatrice C++ Avanzata ===\n”;
cout << “1. Esegui operazione\n”;
cout << “2. Visualizza storico\n”;
cout << “3. Cancella storico\n”;
cout << “4. Esci\n”;
cout << “Scegli un’opzione (1-4): “;
}
int main() {
int choice;
char operation;
double num1, num2;
cout << fixed << setprecision(2);
while(true) {
displayMenu();
if(!(cin >> choice)) {
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), ‘\n’);
cout << “Input non valido. Riprova.\n”;
continue;
}
switch(choice) {
case 1: {
cout << “\nInserisci operatore (+, -, *, /, %, ^): “;
cin >> operation;
cout << “Inserisci due numeri: “;
if(!(cin >> num1 >> num2)) {
cin.clear();
cin.ignore(numeric_limits<streamsize>::max(), ‘\n’);
cout << “Input non valido. Operazione annullata.\n”;
break;
}
OperationRecord record;
record.operand1 = num1;
record.operand2 = num2;
record.op = operation;
try {
record.result = performOperation(num1, num2, operation);
cout << “\nRisultato: ” << num1 << ” ” << operation << ” ” << num2 <<
” = ” << record.result << endl;
} catch(const exception& e) {
record.error = e.what();
cerr << “\nErrore: ” << e.what() << endl;
}
history.push_back(record);
break;
}
case 2:
if(history.empty()) {
cout << “\nLo storico è vuoto.\n”;
} else {
displayHistory();
}
break;
case 3:
history.clear();
cout << “\nStorico cancellato.\n”;
break;
case 4:
cout << “\nUscita dal programma.\n”;
return 0;
default:
cout << “\nOpzione non valida. Riprova.\n”;
}
}
return 0;
}
6. Ottimizzazione e Best Practice
Per scrivere codice C++ di alta qualità per la tua calcolatrice:
6.1 Consigli per le Prestazioni
- Usa
const per variabili che non cambiano
- Preferisci
double a float per maggiore precisione
- Evita calcoli ridondanti (es. calcola una volta e memorizza)
- Usa riferimenti invece di copie quando possibile
- Considera l’uso di
inline per piccole funzioni chiamate frequentemente
6.2 Consigli per la Manutenibilità
- Commenta il codice in modo chiaro ma conciso
- Usa nomi significativi per variabili e funzioni
- Organizza il codice in funzioni logiche
- Mantieni una struttura coerente di indentazione
- Usa spazi bianchi per migliorare la leggibilità
6.3 Consigli per la Sicurezza
- Valida sempre l’input dell’utente
- Gestisci tutti i possibili errori (overflow, divisione per zero, etc.)
- Usa
try-catch per gestire eccezioni
- Limita la precisione dell’output per evitare problemi di visualizzazione
- Considera l’uso di librerie sicure per operazioni matematiche
7. Estensioni Avanzate
Per portare la tua calcolatrice al livello successivo:
7.1 Supporto per Espressioni Complesse
Implementa un parser per espressioni matematiche come “3 + 5 * (10 – 4)”:
#include <iostream>
#include <string>
#include <stack>
#include <cmath>
#include <cctype>
#include <stdexcept>
using namespace std;
// Funzione per verificare se un carattere è un operatore
bool isOperator(char c) {
return c == ‘+’ || c == ‘-‘ || c == ‘*’ || c == ‘/’ || c == ‘^’;
}
// Funzione per ottenere la precedenza degli operatori
int getPrecedence(char op) {
if(op == ‘^’) return 4;
if(op == ‘*’ || op == ‘/’) return 3;
if(op == ‘+’ || op == ‘-‘) return 2;
return 0;
}
// Funzione per applicare un operatore
double applyOp(double a, double b, char op) {
switch(op) {
case ‘+’: return a + b;
case ‘-‘: return a – b;
case ‘*’: return a * b;
case ‘/’:
if(b == 0) throw runtime_error(“Divisione per zero”);
return a / b;
case ‘^’: return pow(a, b);
default: throw invalid_argument(“Operatore non valido”);
}
}
// Funzione per valutare un’espressione
double evaluate(string expression) {
stack<double> values;
stack<char> ops;
for(int i = 0; i < expression.length(); i++) {
if(expression[i] == ‘ ‘) continue;
else if(expression[i] == ‘(‘) {
ops.push(expression[i]);
}
else if(isdigit(expression[i])) {
double val = 0;
while(i < expression.length() &&
(isdigit(expression[i]) || expression[i] == ‘.’)) {
if(expression[i] == ‘.’) {
i++;
double fraction = 0.1;
while(i < expression.length() && isdigit(expression[i])) {
val += (expression[i] – ‘0’) * fraction;
fraction *= 0.1;
i++;
}
i–;
} else {
val = (val * 10) + (expression[i] – ‘0’);
}
i++;
}
values.push(val);
i–;
}
else if(expression[i] == ‘)’) {
while(!ops.empty() && ops.top() != ‘(‘) {
double val2 = values.top(); values.pop();
double val1 = values.top(); values.pop();
char op = ops.top(); ops.pop();
values.push(applyOp(val1, val2, op));
}
if(!ops.empty()) ops.pop(); // Rimuovi ‘(‘
}
else if(isOperator(expression[i])) {
while(!ops.empty() && getPrecedence(ops.top()) >= getPrecedence(expression[i])) {
double val2 = values.top(); values.pop();
double val1 = values.top(); values.pop();
char op = ops.top(); ops.pop();
values.push(applyOp(val1, val2, op));
}
ops.push(expression[i]);
}
}
while(!ops.empty()) {
double val2 = values.top(); values.pop();
double val1 = values.top(); values.pop();
char op = ops.top(); ops.pop();
values.push(applyOp(val1, val2, op));
}
return values.top();
}
int main() {
string expression;
cout << “Calcolatrice per Espressioni Matematiche\n”;
cout << “Inserisci un’espressione (es. 3 + 5 * (10 – 4)): “;
getline(cin, expression);
try {
double result = evaluate(expression);
cout << “Risultato: ” << result << endl;
} catch(const exception& e) {
cerr << “Errore: ” << e.what() << endl;
}
return 0;
}
7.2 Interfaccia Grafica con Librerie Esterne
Per creare un’interfaccia grafica, puoi usare librerie come:
- Qt: Framework completo per applicazioni grafiche
- GTKMM: Binding C++ per GTK
- SFML: Simple and Fast Multimedia Library
- ImGui: Immediate Mode Graphical User Interface
Esempio minimale con SFML:
#include <SFML/Graphics.hpp>
#include <sstream>
#include <iomanip>
int main() {
sf::RenderWindow window(sf::VideoMode(400, 600), “Calcolatrice C++ con SFML”);
sf::Font font;
if(!font.loadFromFile(“arial.ttf”)) {
return EXIT_FAILURE;
}
sf::Text displayText;
displayText.setFont(font);
displayText.setCharacterSize(48);
displayText.setFillColor(sf::Color::White);
displayText.setPosition(20, 20);
displayText.setString(“0”);
std::string currentInput = “0”;
std::string currentOperation;
double firstOperand = 0;
bool newInput = true;
// Crea i pulsanti
std::vector<sf::RectangleShape> buttons;
std::vector<sf::Text> buttonTexts;
const std::vector<std::string> buttonLabels = {
“7”, “8”, “9”, “/”,
“4”, “5”, “6”, “*”,
“1”, “2”, “3”, “-“,
“0”, “.”, “=”, “+”,
“C”, “CE”, “√”, “^”
};
for(int i = 0; i < 16; ++i) {
buttons.emplace_back(sf::Vector2f(80, 80));
buttons.back().setPosition(20 + (i % 4) * 90, 120 + (i / 4) * 90);
buttons.back().setFillColor(sf::Color(70, 70, 70));
buttons.back().setOutlineThickness(2);
buttons.back().setOutlineColor(sf::Color(100, 100, 100));
buttonTexts.emplace_back();
buttonTexts.back().setFont(font);
buttonTexts.back().setString(buttonLabels[i]);
buttonTexts.back().setCharacterSize(36);
buttonTexts.back().setFillColor(sf::Color::White);
sf::FloatRect textBounds = buttonTexts.back().getLocalBounds();
buttonTexts.back().setPosition(
buttons.back().getPosition().x + (buttons.back().getSize().x – textBounds.width) / 2 – textBounds.left,
buttons.back().getPosition().y + (buttons.back().getSize().y – textBounds.height) / 2 – textBounds.top – 10
);
}
while(window.isOpen()) {
sf::Event event;
while(window.pollEvent(event)) {
if(event.type == sf::Event::Closed) {
window.close();
}
if(event.type == sf::Event::MouseButtonPressed) {
if(event.mouseButton.button == sf::Mouse::Left) {
sf::Vector2f mousePos(event.mouseButton.x, event.mouseButton.y);
for(int i = 0; i < buttons.size(); ++i) {
if(buttons[i].getGlobalBounds().contains(mousePos)) {
std::string label = buttonLabels[i];
if(label == “C”) {
currentInput = “0”;
currentOperation.clear();
firstOperand = 0;
newInput = true;
}
else if(label == “CE”) {
currentInput = “0”;
newInput = true;
}
else if(label == “=”) {
if(!currentOperation.empty() && !newInput) {
try {
double secondOperand = std::stod(currentInput);
double result = 0;
if(currentOperation == “+”) {
result = firstOperand + secondOperand;
}
else if(currentOperation == “-“) {
result = firstOperand – secondOperand;
}
else if(currentOperation == “*”) {
result = firstOperand * secondOperand;
}
else if(currentOperation == “/”) {
if(secondOperand == 0) {
currentInput = “Errore”;
} else {
result = firstOperand / secondOperand;
}
}
else if(currentOperation == “^”) {
result = pow(firstOperand, secondOperand);
}
if(currentInput != “Errore”) {
std::ostringstream oss;
oss << result;
currentInput = oss.str();
}
currentOperation.clear();
newInput = true;
} catch(…) {
currentInput = “Errore”;
}
}
}
else if(label == “+” || label == “-” || label == “*” ||
label == “/” || label == “^”) {
if(!newInput) {
firstOperand = std::stod(currentInput);
currentOperation = label;
newInput = true;
} else if(!currentOperation.empty()) {
currentOperation = label;
}
}
else if(label == “√”) {
try {
double value = std::stod(currentInput);
if(value < 0) {
currentInput = “Errore”;
} else {
value = sqrt(value);
std::ostringstream oss;
oss << value;
currentInput = oss.str();
}
newInput = true;
} catch(…) {
currentInput = “Errore”;
}
}
else {
if(newInput) {
currentInput = label;
newInput = false;
} else {
currentInput += label;
}
}
displayText.setString(currentInput);
break;
}
}
}
}
}
window.clear(sf::Color(30, 30, 30));
// Disegna lo sfondo del display
sf::RectangleShape displayBackground(sf::Vector2f(360, 80));
displayBackground.setPosition(20, 20);
displayBackground.setFillColor(sf::Color(50, 50, 50));
window.draw(displayBackground);
// Disegna il testo del display
window.draw(displayText);
// Disegna i pulsanti
for(int i = 0; i < buttons.size(); ++i) {
window.draw(buttons[i]);
window.draw(buttonTexts[i]);
}
window.display();
}
return 0;
}
7.3 Calcolatrice Scientifica
Aggiungi funzioni scientifiche:
- Funzioni trigonometriche (sin, cos, tan)
- Logaritmi (log, log10)
- Radice quadrata e cubica
- Costanti matematiche (π, e)
- Conversione di unità
8. Testing e Debugging
Il testing è cruciale per assicurare che la tua calcolatrice funzioni correttamente:
8.1 Tipi di Test
| Tipo di Test |
Descrizione |
Esempio |
| Test di Unità |
Testa singole funzioni in isolamento |
Testare la funzione safeDivide(10, 2) |
| Test di Integrazione |
Testa l’interazione tra componenti |
Testare il flusso completo da input a output |
| Test di Sistema |
Testa l’intero programma |
Eseguire una serie di operazioni consecutive |
| Test di Regressione |
Verifica che nuove modifiche non rompano funzionalità esistenti |
Rieseguire tutti i test dopo una modifica |
| Test di Stress |
Testa con input estremi |
Numeri molto grandi o operazioni ripetute |
8.2 Strumenti per il Debugging
- GDB: Debugger GNU per C++
- Valgrind: Rilevamento memory leak
- AddressSanitizer: Rilevamento errori di memoria
- Print Debugging: Stampe di debug strategiche
- IDE Debugger: Strumenti integrati in Visual Studio, CLion, etc.
8.3 Esempio di Test Unitario con Catch2
#define CATCH_CONFIG_MAIN
#include “catch.hpp”
#include “calculator.h” // Il tuo file header con le funzioni
TEST_CASE(“Test delle operazioni di base”, “[calculator]”) {
SECTION(“Addizione”) {
REQUIRE(safeAdd(2, 3) == 5);
REQUIRE(safeAdd(-1, 1) == 0);
REQUIRE(safeAdd(0, 0) == 0);
REQUIRE_THROWS_AS(safeAdd(numeric_limits<double>::max(), 1), overflow_error);
}
SECTION(“Sottrazione”) {
REQUIRE(safeSubtract(5, 3) == 2);
REQUIRE(safeSubtract(3, 5) == -2);
REQUIRE(safeSubtract(0, 0) == 0);
}
SECTION(“Moltiplicazione”) {
REQUIRE(safeMultiply(2, 3) == 6);
REQUIRE(safeMultiply(-1, 1) == -1);
REQUIRE(safeMultiply(0, 5) == 0);
REQUIRE_THROWS_AS(safeMultiply(numeric_limits<double>::max(), 2), overflow_error);
}
SECTION(“Divisione”) {
REQUIRE(safeDivide(6, 3) == 2);
REQUIRE(safeDivide(5, 2) == 2.5);
REQUIRE_THROWS_AS(safeDivide(5, 0), runtime_error);
}
SECTION(“Modulo”) {
REQUIRE(safeModulus(5, 2) == 1);
REQUIRE(safeModulus(10, 3) == 1);
REQUIRE_THROWS_AS(safeModulus(5, 0), runtime_error);
}
SECTION(“Potenza”) {
REQUIRE(safePower(2, 3) == 8);
REQUIRE(safePower(5, 0) == 1);
REQUIRE_THROWS_AS(safePower(0, -1), runtime_error);
}
}
9. Ottimizzazione delle Prestazioni
Per calcolatrici che eseguono molti calcoli, l’ottimizzazione è importante:
9.1 Tecniche di Ottimizzazione
- Inlining: Usa
inline per piccole funzioni chiamate frequentemente
- Evita copie: Usa riferimenti const quando possibile
- Precalcolo: Calcola valori costanti una volta
- Memorizzazione: Memorizza risultati di operazioni costose
- Parallelismo: Usa thread per operazioni indipendenti
9.2 Esempio di Ottimizzazione
#include <unordered_map>
#include <utility> // for std::pair
// Cache per memorizzare risultati di operazioni costose
std::unordered_map<std::pair<double, double>, double, pair_hash> operationCache;
// Funzione hash per std::pair (necessaria per unordered_map)
struct pair_hash {
template <class T1, class T2>
std::size_t operator() (const std::pair<T1, T2> &p) const {
auto h1 = std::hash<T1>{}(p.first);
auto h2 = std::hash<T2>{}(p.second);
return h1 ^ h2;
}
};
// Versione ottimizzata con caching
double optimizedPower(double base, double exponent) {
auto key = std::make_pair(base, exponent);
// Controlla se il risultato è già in cache
auto it = operationCache.find(key);
if(it != operationCache.end()) {
return it->second;
}
// Calcola e memorizza il risultato
double result = pow(base, exponent);
operationCache[key] = result;
return result;
}
10. Documentazione e Distribuzione
10.1 Documentazione del Codice
Una buona documentazione include:
- Commenti nel codice per spiegare la logica complessa
- Documentazione delle funzioni (parametri, ritorno, eccezioni)
- Un file README con istruzioni per la compilazione e l’uso
- Esempi di utilizzo
- Limitazioni note
10.2 Esempio di Documentazione con Doxygen
/**
* @file calculator.h
* @brief Implementazione di una calcolatrice in C++
* @author Tuo Nome
* @version 1.0
* @date 2023-11-15
*/
#ifndef CALCULATOR_H
#define CALCULATOR_H
/**
* @brief Esegue un’operazione matematica sicura tra due numeri
*
* @param a Primo operando
* @param b Secondo operando
* @param op Operatore (+, -, *, /, %, ^)
* @return double Risultato dell’operazione
* @throw std::invalid_argument Se l’operatore non è valido
* @throw std::runtime_error Per errori matematici (divisione per zero)
* @throw std::overflow_error Per overflow/underflow
*/
double performOperation(double a, double b, char op);
/**
* @brief Somma due numeri con controllo overflow
* @param a Primo addendo
* @param b Secondo addendo
* @return double Somma di a e b
* @throw std::overflow_error Se si verifica overflow
*/
double safeAdd(double a, double b);
/**
* @brief Sottrae due numeri con controllo overflow
* @param a minuendo
* @param b sottraendo
* @return double Differenza tra a e b
* @throw std::overflow_error Se si verifica overflow
*/
double safeSubtract(double a, double b);
// … altre funzioni …
#endif // CALCULATOR_H
10.3 Compilazione e Distribuzione
Per compilare il tuo programma:
# Compilazione di base con g++
g++ -std=c++17 -o calculator calculator.cpp
# Compilazione con ottimizzazioni
g++ -std=c++17 -O3 -o calculator calculator.cpp
# Compilazione con debugging symbols
g++ -std=c++17 -g -o calculator calculator.cpp
# Compilazione con AddressSanitizer per rilevare errori di memoria
g++ -std=c++17 -fsanitize=address -g -o calculator calculator.cpp
Per distribuire il tuo programma:
- Crea un archivio con il codice sorgente e le istruzioni
- Compila per diverse piattaforme se necessario
- Considera l’uso di strumenti come CMake per gestire la compilazione
- Fornisci esempi di utilizzo
- Includi informazioni sulla licenza
11. Risorse per Approfondire
Per continuare a migliorare le tue capacità di programmazione in C++:
11.1 Libri Consigliati
- “C++ Primer” di Lippman, Lajoie, Moo
- “Effective C++” di Scott Meyers
- “The C++ Programming Language” di Bjarne Stroustrup
- “C++ Concurrency in Action” di Anthony Williams
- “Modern C++ Design” di Andrei Alexandrescu
11.2 Risorse Online
11.3 Corsi Universitari Online
11.4 Comunità e Forum
12. Progetti Avanzati Basati sulla Calcolatrice
Una volta padroni della calcolatrice di base, puoi estenderla in vari modi:
12.1 Calcolatrice per Ingegneri
- Aggiungi supporto per numeri complessi
- Implementa conversioni tra unità (metri/piedi, kg/libbre, etc.)
- Aggiungi funzioni statistiche (media, deviazione standard)
- Implementa calcoli matriciali
12.2 Calcolatrice Finanziaria
- Calcolo di interessi composti
- Piani di ammortamento
- Valore attuale netto (NPV)
- Tasso interno di rendimento (IRR)
12.3 Calcolatrice per la Fisica
- Leggi della dinamica
- Calcoli termodinamici
- Equazioni di Maxwell
- Relatività speciale
12.4 Calcolatrice per la Chimica
- Bilanciamento di equazioni chimiche
- Calcoli stechiometrici
- pH e concentrazioni
- Termochimica
13. Confronto tra Diversi Approcci di Implementazione
Ecco un confronto tra diversi modi di implementare una calcolatrice in C++:
| Approccio |
Vantaggi |
Svantaggi |
Complessità |
Casi d’Uso |
| Procedurale (main + funzioni) |
Semplice da comprendere, buono per principianti |
Meno scalabile, codice può diventare disorganizzato |
Bassa |
Progetti piccoli, esercizi didattici |
| Orientato agli Oggetti |
Migliore organizzazione, riutilizzo del codice |
Curva di apprendimento più ripida |
Media |
Progetti di medie dimensioni, applicazioni estensibili |
| Template/Generics |
Flessibilità nei tipi di dati, codice riutilizzabile |
Sintassi complessa, errori di compilazione criptici |
Alta |
Librerie matematiche generiche |
| Functional (C++11/14/17) |
Codice conciso, facile testing |
Meno idiomatico in C++, prestazioni potenzialmente inferiori |
Media-Alta |
Calcoli matematici complessi |
| Concorrente/Multithread |
Prestazioni migliorate per calcoli intensivi |
Complessità nella sincronizzazione, potenziali race condition |
Molto Alta |
Calcolatrici scientifiche ad alte prestazioni |
14. Esempio Completo: Calcolatrice Orientata agli Oggetti
Ecco un’implementazione completa usando la programmazione orientata agli oggetti:
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <cmath>
#include <stdexcept>
#include <iomanip>
class Calculator {
private:
std::vector<std::string> history;
// Classe interna per rappresentare un’operazione
class Operation {
public:
virtual double execute(double a, double b) const = 0;
virtual char getSymbol() const = 0;
virtual ~Operation() = default;
};
// Implementazioni concrete delle operazioni
class Addition : public Operation {
public:
double execute(double a, double b) const override { return a + b; }
char getSymbol() const override { return ‘+’; }
};
class Subtraction : public Operation {
public:
double execute(double a, double b) const override { return a – b; }
char getSymbol() const override { return ‘-‘; }
};
class Multiplication : public Operation {
public:
double execute(double a, double b) const override { return a * b; }
char getSymbol() const override { return ‘*’; }
};
class Division : public Operation {
public:
double execute(double a, double b) const override {
if(b == 0) throw std::runtime_error(“Division by zero”);
return a / b;
}
char getSymbol() const override { return ‘/’; }
};
class Modulus : public Operation {
public:
double execute(double a, double b) const override {
if(b == 0) throw std::runtime_error(“Modulo by zero”);
return fmod(a, b);
}
char getSymbol() const override { return ‘%’; }
};
class Power : public Operation {
public:
double execute(double a, double b) const override {
return pow(a, b);
}
char getSymbol() const override { return ‘^’; }
};
// Factory per creare operazioni
std::unique_ptr<Operation> createOperation(char op) const {
switch(op) {
case ‘+’: return std::make_unique<Addition>();
case ‘-‘: return std::make_unique<Subtraction>();
case ‘*’: return std::make_unique<Multiplication>();
case ‘/’: return std::make_unique<Division>();
case ‘%’: return std::make_unique<Modulus>();
case ‘^’: return std::make_unique<Power>();
default: throw std::invalid_argument(“Invalid operator”);
}
}
public:
// Esegue un’operazione e aggiunge allo storico
double calculate(char op, double a, double b) {
auto operation = createOperation(op);
double result = operation->execute(a, b);
std::ostringstream oss;
oss << std::setprecision(15) << a << ” ” << operation->getSymbol() << ” ” << b << ” = ” << result;
history.push_back(oss.str());
return result;
}
// Visualizza lo storico delle operazioni
void displayHistory() const {
std::cout << “\n— Operation History —\n”;
for(const auto& entry : history) {
std::cout << entry << “\n”;
}
std::cout << “———————–\n”;
}
// Cancella lo storico
void clearHistory() {
history.clear();
}
};
int main() {
Calculator calc;
char op;
double num1, num2;
std::cout << “Object-Oriented Calculator in C++\n”;
std::cout << “Available operators: +, -, *, /, %, ^\n”;
std::cout << “Enter ‘h’ to view history, ‘c’ to clear history, ‘q’ to quit\n\n”;
while(true) {
std::cout << “Enter operator (or h/c/q): “;
std::cin >> op;
if(op == ‘q’ || op == ‘Q’) {
break;
}
else if(op == ‘h’ || op == ‘H’) {
calc.displayHistory();
continue;
}
else if(op == ‘c’ || op == ‘C’) {
calc.clearHistory();
std::cout << “History cleared.\n”;
continue;
}
try {
std::cout << “Enter two numbers: “;
if(!(std::cin >> num1 >> num2)) {
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), ‘\n’);
throw std::runtime_error(“Invalid input. Please enter numbers.”);
}
double result = calc.calculate(op, num1, num2);
std::cout << “Result: ” << result << “\n\n”;
}
catch(const std::exception& e) {
std::cerr << “Error: ” << e.what() << “\n\n”;
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), ‘\n’);
}
}
std::cout << “Thank you for using the OO Calculator!\n”;
return 0;
}
15. Conclusione
Implementare una calcolatrice in C++ è un progetto eccellente per apprendere i fondamenti della programmazione e le caratteristiche avanzate del linguaggio. Abbiamo esplorato:
- Implementazioni di base con operazioni aritmetiche fondamentali
- Gestione avanzata degli errori e validazione dell’input
- Organizzazione del codice con funzioni e approccio orientato agli oggetti
- Interfacce utente sia testuali che grafiche
- Tecniche di ottimizzazione e testing
- Estensioni avanzate come il parsing di espressioni e calcolatrici scientifiche
Man mano che acquisisci esperienza, puoi espandere questo progetto in molte direzioni, dalla creazione di un’interfaccia grafica sofisticata all’implementazione di funzionalità matematiche avanzate. Ricorda che la chiave per diventare un programmatore C++ competente è la pratica costante e la sperimentazione con nuovi concetti.
Per approfondire ulteriormente, considera di:
- Implementare il supporto per numeri complessi
- Aggiungere funzionalità di plotting grafico
- Creare un’interfaccia web usando Emscripten per compilare C++ in WebAssembly
- Implementare un sistema di plugin per estendere le funzionalità
- Ottimizzare le prestazioni per calcoli intensivi
La programmazione in C++ offre infinite possibilità, e una calcolatrice è solo l’inizio del tuo viaggio nello sviluppo software professionale.