In C++, le dichiarazioni sono spesso separate dalle definizioni. Le dichiarazioni vengono 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 dirà cosa una base di codice ha da offrire, senza entrare nei dettagli di come.
L'estensione di file più comune per i file header è .h.
Alcuni progetti usano .hpp oppure omettono del tutto l'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 così:
// 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 <cmath>
#include "quick_math.h"
double quick_math::super_root(double x, int n) {
while (n) {
x = std::sqrt(x), --n;
}
return x;
}
Se devi includere un header richiesto solo dall'implementazione, la rispettiva riga #include serve solo nel file sorgente.
Tutto ciò che è incluso nell'header è disponibile anche nel file .cpp, come la libreria string nell'esempio seguente.
Attenzione: il ; serve dopo la dichiarazione nel file header, ma non dopo la definizione nel file sorgente.
Molti esercizi C++ su Exercism iniziano con due file quasi vuoti: header e sorgente.
Devi controllare il file *_test.cpp per vedere i nomi e i namespace delle funzioni attese, in modo da risolvere l'esercizio.
Le classi possono diventare molto complesse e il loro rapporto con la suddivisione header / sorgente può creare confusione. Una possibile organizzazione è tenere tutti i dettagli implementativi 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();
};
} // namespace robots
#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 di soli header, 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;
}
};
} // namespace robots
I progetti possono usare combinazioni di queste organizzazioni e si discute molto su quale sia la soluzione migliore per ogni caso d'uso.
Potresti aver notato la riga #pragma once nel file header di esempio qui sopra.
Questa si chiama guardia di inclusione e garantisce che il contenuto del file venga incluso una sola volta durante la compilazione, per evitare errori.
Esiste un'altra variante più complessa di una guardia di inclusione, che inizia con #ifndef e termina con #endif, descritta in dettaglio qui sotto.
Il codice C++ viene valutato in modo procedurale. Se vuoi usare una funzione, il compilatore deve conoscerla nel momento in cui la usi. A volte non è possibile trovare un ordine lineare per disporre le definizioni nel codice sorgente. Dai un'occhiata all'esempio seguente:
int myFunction(int n) {
if (n < 10) {
return n;
} else {
return myOtherFunction(n / 10);
}
}
int myOtherFunction(int m) { return myFunction(m / 2); }
Quando viene definita myFunction, il compilatore non conosce ancora myOtherFunction.
Purtroppo, il problema dei riferimenti circolari non si può risolvere invertendo l'ordine.
C++ offre le dichiarazioni anticipate per far conoscere al compilatore myFunction e myOtherFunction prima che vengano definite.
Il compilatore presume che la definizione segua in un momento successivo, dopo la dichiarazione.
L'esempio seguente mostra come si usa una dichiarazione anticipata per le funzioni.
int myFunction(int n); // Forward declaration of myFunction
int myOtherFunction(int m); // Forward declaration of myOtherFunction
// Definition of myFunction
int myFunction(int n) {
if (n < 0) {
return 0;
} else {
return myOtherFunction(n - 2);
}
}
// Definition of myOtherFunction
int myOtherFunction(int m) { return myFunction(m / 2); }
Non importa se lo stesso file viene incluso più volte all'interno di un progetto. I file header non dovrebbero contenere definizioni. Il progetto completo non può avere la stessa definizione più di una volta. Questa è chiamata «regola della definizione unica». Sarà il compilatore a farla rispettare.
È facile evitare inclusioni multiple indesiderate della stessa definizione con le guardie di inclusione.
Vengono create da una procedura speciale durante le fasi di compilazione.
Lo scopo è includere un file solo se una certa variabile non è stata impostata, e poi impostarla una volta che il file è stato incluso.
Spesso la sequenza di questa variabile viene scelta come variazione del nome del file.
Un altro metodo è la generazione di un UUID per ridurre il rischio di usare accidentalmente la sequenza due volte.
La sintassi è visibile qui sotto, con MY_HEADER_FILE_H come variabile.
#ifndef MY_HEADER_FILE_H /* any name uniquely mapped to file name */
#define MY_HEADER_FILE_H
// file content
#endif
Il problema con #pragma once è che i pragma non fanno parte ufficialmente del linguaggio C++ e l'implementazione varia da compilatore a compilatore.
Molti grandi progetti sono passati al metodo pragma più semplice, ma alcuni sono ancora cauti.