A C++-ban a deklarációk gyakran elválnak a definícióktól. A deklarációk úgynevezett header fájlokba csoportosulnak, a hozzájuk tartozó implementációk pedig forrásfájlokba kerülnek. A header fájlokra gondolhatsz úgy, mint egy API-ra. A header fájl megmondja, mit kínál egy kódbázis, anélkül hogy belemennénk a hogyan részleteibe.
A header fájlok leggyakoribb kiterjesztése a .h.
Egyes projektek .hpp-t használnak, vagy teljesen elhagyják a kiterjesztést.
A definíciók egy külön .cpp fájlban találhatók.
Hogy a részek újra összeálljanak, a forrásfájl azzal kezdődik, hogy include utasítással behúzza a megfelelő header fájlt.
Ha egy „quick_math” nevű könyvtárat akarsz írni, amely egy „super_root” nevű függvényt kínál, amit gyakran szeretnél használni, a fájlok így néznének ki:
// 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;
}
Ha olyan header fájlt kell include-olni, amelyre csak az implementációnak van szüksége, akkor a megfelelő #include sorra csak a forrásfájlban van szükség.
Minden, ami a header fájlban szerepel, elérhető a .cpp fájlban is, ahogy az alábbi példában a string könyvtár is.
Figyelem: a ; a header fájlban a deklaráció után szükséges, a forrásfájlban viszont a definíció után nem.
Sok C++ feladat az Exercism-ön két majdnem üres fájllal indul: egy header fájllal és egy forrásfájllal.
Ahhoz, hogy megoldd a feladatot, meg kell nézned a *_test.cpp fájlt, hogy lásd a várt függvények neveit és névtereit.
Az osztályok nagyon összetettek lehetnek, és az, hogy hogyan viszonyulnak a header és a forrásfájl közötti felosztáshoz, könnyen zavaróvá válhat. Az egyik lehetséges elrendezés, hogy minden implementációs részlet a forrásfájlban marad, a deklarációk és a tagváltozók pedig a header fájlban:
// 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; }
Amikor a header fájlt API-áttekintésként használjuk, az ember az ilyen információkat, például az alapértelmezett értékeket ott fogja keresni.
A konstruktor size paraméterének alapértelmezett értéke ezért a header fájlban szerepel, nem az implementációban.
A forrásfájlban a definíciók elé a robots névtér és a Flower osztálytípus kerül.
Egy másik elrendezési lehetőség a header only könyvtár, amelynek egyáltalán nincs .cpp fájlja:
// 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
A projektek ezeknek az elrendezéseknek a kombinációit használhatják, és sok vita folyik arról, hogy melyik illik legjobban az egyes felhasználási esetekhez.
Talán észrevetted a fenti példa header fájljában a #pragma once sort.
Ezt nevezik include guardnak: arról gondoskodik, hogy a fájl tartalma a fordításkor csak egyszer kerüljön bele, így elkerülve a hibákat.
Létezik az include guardnak egy másik, összetettebb változata is, amely #ifndef-től #endif-ig tart; ezt részletesen lentebb tárgyaljuk.
A C++ kód kiértékelése sorban, egymás után történik. Ha használni akarsz egy függvényt, annak a használat pillanatában már ismertnek kell lennie a fordító számára. Néha nem lehet olyan lineáris sorrendet találni, amelyben a definíciók elrendezhetők a forráskódban. Nézzük az alábbi példát:
int myFunction(int n) {
if (n < 10) {
return n;
} else {
return myOtherFunction(n / 10);
}
}
int myOtherFunction(int m) { return myFunction(m / 2); }
Mire a myFunction definiálásra kerül, a fordító még nem ismeri a myOtherFunction-t.
Sajnos a körkörös hivatkozás problémáját nem lehet a sorrend megcserélésével megoldani.
A C++ elődeklarációkkal teszi lehetővé, hogy a fordító már a definiálásuk előtt tudomást szerezzen a myFunction és a myOtherFunction függvényről.
A fordító feltételezi, hogy a definíció majd valamikor később, a deklaráció után következik.
A következő példa megmutatja, hogyan használható az elődeklaráció függvényeknél.
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); }
Az nem jelent problémát, ha ugyanazt a fájlt egy projekten belül többször is beillesztjük. A header fájloknak nem szabad definíciókat tartalmazniuk. A teljes projektben ugyanaz a definíció legfeljebb egyszer szerepelhet. Ezt nevezik „egyszeri definíció” szabályának. A fordító gondoskodik arról, hogy ez érvényesüljön.
Az include guardokkal könnyen elkerülhető ugyanannak a definíciónak a véletlen többszöri beillesztése.
Ezeket a fordítás egyes szakaszaiban egy speciális eljárás hozza létre.
A cél az, hogy egy fájlt csak akkor illesszünk be, ha egy bizonyos változó még nincs beállítva, majd a fájl beillesztése után beállítsuk azt.
A változó nevét gyakran a fájlnév valamilyen változatából képezik.
Egy másik módszer egy UUID generálása, hogy csökkenjen a név véletlen kétszeri használatának kockázata.
Az alábbi példában a szintaxis látható, ahol MY_HEADER_FILE_H a változó.
#ifndef MY_HEADER_FILE_H /* any name uniquely mapped to file name */
#define MY_HEADER_FILE_H
// file content
#endif
A #pragma once problémája, hogy a pragmák nem részei hivatalosan a C++ nyelvnek, és a megvalósításuk fordítóról fordítóra eltér.
Sok nagy projekt áttért az egyszerűbb pragma módszerre, de néhányan még mindig óvatosak.