In C++, le dichiarazioni sono spesso separate dalle definizioni. Le dichiarazioni sono raggruppate in quelli che vengono chiamati file header, mentre le rispettive implementazioni si trovano nei file sorgente. Puoi pensare ai file header come a un'API. Il file header ti dice cosa un codebase ha da offrire, senza entrare nei dettagli del come.
L'estensione di file più comune per i file header è .h.
Alcuni progetti usano .hpp o non usano affatto un'estensione.
Le definizioni si trovano in un file .cpp separato.
Per riunire le parti, il file sorgente inizia includendo il rispettivo file header.
Se vuoi scrivere una libreria chiamata "quick_math" che offre una funzione "super_root" che vuoi usare spesso, i file sarebbero questi:
// A file named quick_math.h
#pragma once
namespace quick_math {
double super_root(double x, int n);
}
// A file named quick_math.cpp
#include "quick_math.h"
#include <cmath>
double quick_math::super_root(double x, int n) {
while(n) { x = std::sqrt(x), --n;}
return x;
}
Se devi includere un header che serve solo all'implementazione, la riga #include corrispondente è necessaria solo nel file sorgente.
Tutto ciò che è incluso nell'header è disponibile anche nel file .cpp, come la libreria string nell'esempio qui sotto.
Attenzione: il ; è necessario dopo la dichiarazione nel file header, ma non dopo la definizione nel file sorgente.
Molti esercizi di C++ su Exercism iniziano con due file quasi vuoti: header e sorgente.
Per risolvere l'esercizio, devi controllare il file *_test.cpp per vedere i nomi e i namespace delle funzioni attese.
Le classi possono diventare molto complesse, e il loro rapporto con la suddivisione tra header e sorgente può confondere. Una possibile organizzazione è tenere tutti i dettagli dell'implementazione nel file sorgente e tutte le dichiarazioni e le variabili membro nell'header:
// A file named robot_flower.h
#if !defined(ROBOT_FLOWER_H)
#define ROBOT_FLOWER_H
#include <string>
namespace robots {
class Flower {
private:
bool needs_water{};
int size{};
std::string name{};
public:
Flower(std::string name, int size = 0);
void give_water();
std::string get_name();
int get_size();
void start_next_day();
};
}
#endif
// A file named robot_flower.cpp
#include "robot_flower.h"
robots::Flower::Flower(std::string name, int size) {this->name = "Robotica " + name; this->size = size;}
void robots::Flower::start_next_day() {if (!needs_water) ++size; needs_water = true;}
std::string robots::Flower::get_name() {return name;}
int robots::Flower::get_size() {return size;}
Quando l'header viene usato come panoramica dell'API, è lì che si cercano informazioni come i valori predefiniti.
Il valore predefinito del parametro size del costruttore viene quindi gestito nell'header e non nell'implementazione.
Le definizioni nel file sorgente sono precedute dal namespace robots e dal tipo di classe Flower.
Un'altra opzione di organizzazione è una libreria header only, che non ha affatto un file .cpp:
// A file named robot_flower.h
#pragma once
#include <string>
namespace robots {
class Flower {
private:
bool needs_water{};
int size{};
std::string name{};
public:
Flower(std::string name, int size = 0) {this->name = "Robotica " + name; this->size = size;}
void give_water() {needs_water = false;}
std::string get_name() {return name;}
int get_size() {return size;}
void start_next_day() {if (!needs_water) ++size; needs_water = true;}
};
}
I progetti possono usare combinazioni di queste organizzazioni, e si discute molto su quale sia la più adatta a ogni caso d'uso.
Avrai notato la riga #pragma once nel file header di esempio qui sopra.
Questa si chiama guardia di inclusione: assicura che il contenuto del file venga incluso una sola volta durante la compilazione, per evitare errori.
Esiste un'altra variante, più complessa, della guardia di inclusione, che inizia con #ifndef e termina con #endif.
Ha lo stesso scopo, e il suo utilizzo è mostrato nell'esempio della classe Flower qui sopra.
Inizi il tuo primo giorno in un'azienda australiana chiamata "Doctor Data", specializzata nel recupero di informazioni. Hai superato il colloquio di lavoro grazie alla tua conoscenza del C++ e delle sonde di von Neumann. Dato che hai visto molti file di test, il tuo nuovo capo vuole che tu ricrei i rispettivi file sorgente e di intestazione a partire da del codice di test che l'azienda ha recuperato di recente.
In questo esercizio ricreerai dei file perduti. Al momento il contenuto dei file è un insieme di dati inutilizzabili.
Il flusso di lavoro di questo esercizio sui concetti è molto simile alla struttura degli esercizi di pratica di Exercism. Il testo di introduzione dell'esercizio è solo una parte della specifica. Il file di test è la tua guida definitiva per risolvere un dato problema. Dato come funziona la compilazione del C++, i risultati dei test potrebbero non comparire nell'interfaccia web finché non avrai implementato una versione minima di ogni classe, funzione ed enumerazione richiesta dai test. Se ricevi errori di compilazione, potrebbero sparire una volta che avrai affrontato tutte le attività e i test.
Hai quattro attività, tutte legate al recupero del codice perduto nei file di intestazione e sorgente.
Dai un'occhiata a due righe recuperate dai file di test:
heaven::Vessel bob{"Robert Johansson", 1};
heaven::Vessel will{"Riker", 2, star_map::System::BetaHydri};
Il tuo occhio attento ha riconosciuto all'istante un namespace heaven.
Vedi anche che deve esserci una classe chiamata Vessel.
A quanto pare, il costruttore può essere chiamato con due o tre argomenti.
Il primo argomento sembra essere di tipo string, il secondo è un numero.
È possibile inizializzare la classe Vessel con un terzo argomento.
Il terzo argomento proviene da un namespace star_map.
È un enumeratore di tipo System.
Hai persino ottenuto una delle enumerazioni: BetaHydri.
Prepara i file sorgente e di intestazione con le informazioni che hai scoperto.
Ti servono due namespace: heaven e star_map.
Il namespace heaven ha una classe Vessel, che può essere chiamata con due o tre argomenti.
L'enum System va inserito nel namespace star_map e ha un'enumerazione BetaHydri.
Tieni d'occhio le altre enumerazioni, così da poter aggiornare continuamente l'enum System.
Scopri altre righe dal test:
heaven::Vessel bob{"Robert Johansson", 1};
REQUIRE(bob.current_system == star_map::System::Sol);
REQUIRE(bob.generation == 1);
heaven::Vessel bob5 = bob.replicate("Mario");
REQUIRE(bob5.current_system == star_map::System::Sol);
REQUIRE(bob5.generation == 2);
Le righe di test appena trovate rivelano un'altra funzione membro della classe Vessel: replicate.
Questa funzione riceve una stringa e restituisce un'altra istanza di Vessel.
Ti fai un'idea del valore predefinito del terzo argomento del costruttore a partire dall'attività precedente.
Vedi anche due variabili membro pubbliche della classe Vessel e la specifica della funzione membro replicate.
Aggiungi la funzione replicate e le variabili membro pubbliche current_system e generation ai file di intestazione e sorgente.
Trovi altre righe interessanti nei file di test recuperati:
heaven::Vessel bob6{"Homer", 3, star_map::System::EpsilonEridani};
REQUIRE(bob6.busters == 0);
bob6.make_buster();
REQUIRE(bob6.busters == 1);
bool success = bob6.shoot_buster();
REQUIRE(success);
REQUIRE(bob6.busters == 0);
success = bob6.shoot_buster();
REQUIRE_FALSE(success);
A quanto pare, la classe Vessel ha una variabile membro busters, che può essere modificata con le due funzioni membro della classe make_buster e shoot_buster.
Finché non emergono altre informazioni, ipotizzi che la funzione make_buster restituisca void.
Dato che c'è un test per il valore restituito di shoot_buster, supponi che la funzione restituisca un bool.
Aggiungi le due funzioni e la variabile membro alla classe Vessel.
Continua a cercare altri enumeratori di System.
Mentre scorri i file di test, trovi solo due sezioni di codice ancora scoperte:
heaven::Vessel bob1{"Bob", 1, star_map::System::AlphaCentauri};
heaven::Vessel marv{"Marvin", 2, star_map::System::DeltaEridani};
heaven::Vessel milo{"Milo", 3, star_map::System::DeltaEridani};
heaven::Vessel howie{"Howard", 4, star_map::System::Omicron2Eridani};
REQUIRE("Bob" == heaven::get_older_bob(bob1, marv));
REQUIRE(heaven::in_the_same_system(marv, milo));
REQUIRE_FALSE(heaven::in_the_same_system(marv, howie));
Vedi due funzioni che non sono membri della classe Vessel, dato che non vengono chiamate su un'istanza.
get_older_bob confronta due istanze di Vessel e restituisce una string.
in_the_same_system confronta due istanze di Vessel e restituisce un bool.
Implementa le ultime funzioni mancanti a partire dalle righe recuperate qui sopra.
Iscriviti a Exercism per imparare e padroneggiare C++ con 19 concetti100 esercizi e il mentoring di persone reali, tutto gratis.