Заголовочные файлы и файлы реализации

Все примеры, которые я использовал до этого, использовали только один файл, который компилировался через g++. Несмотря на то, что большие программы можно написать в одном файле и скомпилировать его, такие программы редко бывают удобны для последующей работы. Код, написанный в одном многометровом файле запутывает и усложняет т.н. «рефакторинг».

Существуют помимо файлов исходников (или реализации), стандартно имеющие расширение .cpp, файлы, называемые заголовочными; они имеют расширение .h. Это позволяет разбить программу на фрагменты, логически и функционально связанные; размещать их в разных папках, которые объединяют логически несколько файлов в один пакет и т.д.

С заголовными файлами мы уже встречались, когда подключали, например, библиотеки для работы со строками следующим образом: #include . Напомню, что препроцессорная директива #include имеет 2 способа указания на внешние заголовные файлы: с помощью треугольных скобок (<>) и с помощью кавычек («»). Принципиальное различие состоит в том, что при указании <> компилятор будет пытаться искать заголовочные файлы в библиотеках, которые указаны в ОС как источники библиотек C++. При разработке собственных файлов необходимо использовать кавычки; они указывают компилятору, что заголовочный файл нужно искать относительно той директории, в которой находится компилируем исходный файл.

Возьмём последний пример из статьи про классы.

main.cpp

#include <iostream>
#include <string.h>
 
using namespace std;
 
class Person {
    string lastname;
    string firstname;
    string middlename;
     
public:
     
    Person(string lastname, string firstname, string middlename) :
        lastname(lastname), firstname(firstname), middlename(middlename) {
        cout << "The new person has born!\n";
    }
     
    friend ostream& operator<< (ostream &out, Person &p);
};
 
ostream&amp; operator&lt;&lt;(ostream&amp; os, Person &amp;p) {
    os << p.lastname << " " << p.firstname << " " << p.middlename;
    return os;
}
 
int main(int argc, char *argv[]) {
    Person p = Person("Pelevin", "Maksim", "Sergeevich");
    cout << p;
}

Вынесем класс в заголвочный файл, у нас получится следующее:

person.h

#include <string>

using namespace std;

class Person {
    string lastname;
    string firstname;
    string middlename;
     
public:
     
    Person(string lastname, string firstname, string middlename) :
        lastname(lastname), firstname(firstname), middlename(middlename) {
        cout << "The new person has born!\n";
    }
     
    friend ostream&amp; operator&lt;&lt; (ostream &amp;out, Person &amp;p);
};
 
ostream&amp; operator&lt;&lt;(ostream&amp; os, Person &amp;p) {
    os << p.lastname << " " << p.firstname << " " << p.middlename;
    return os;
}

main.cpp

#include <iostream>
#include "person.h"

int main(int argc, char const *argv[])
{
	Person p = Person("Pelevin", "Maksim", "Sergeevich");
	std::cout << p << std::endl;
	return 0;
}

Собираем проект g++ main.cpp и получим точно такую же программу, как если бы сборка происходила из одного и того же файла. Это всё из-за того, что препроцессорная директива #include ищет нужные нам заголовочные файлы и включает их в тело программы. Таким образом нам удалось вынести целую сущность в отдельный файл! Теперь же необходимо избавиться от реализации в заголовчном файле. Это мотивировано тем, что некоторые программы могут быть исопльзованы сторонними людьми, поэтому они должны подключать заголовчный файл, наподобие того, как подключается файл со строками, а вот реализация для посторонних должна быть скрыта. Такие файлы комплириуются в статические или динамические библиотеки (последние знакомы всем жителями ОС Виндоус как .dll), но объяснение этих нюансов выходит за рамки этой статьи.

Выносим реализацию в отдельный файл и получаем проект из 3-х файлов (main.cpp не изменяется):

person.h

#include <string>
#include <iostream>

using namespace std;

#ifndef PERSON_H
#define PERSON_H

class Person {
    string lastname;
    string firstname;
    string middlename;
     
public:
     
    Person(string lastname, string firstname, string middlename);
     
    friend ostream& operator<< (ostream &amp;out, Person &amp;p);
};

#endif

person.cpp

#include <string>
#include <iostream>
#include "person.h"

using namespace std;

Person::Person(string lastname, string firstname, string middlename) :
    lastname(lastname), firstname(firstname), middlename(middlename) {
    cout << "The new person has born!\n";
}

ostream& operator<<(ostream&amp; os, Person &amp;p) {
    os << p.lastname << " " << p.firstname << " " << p.middlename;
    return os;
}

При реализации класса нам обязательно указывать название функции так: [Возвращаемое значеие] [Имя класса]::[Имя функции и аргументы].

#ifndef PERSON_H, #define PERSON_H и #endif нужны для того, чтобы заголовончый файл при компиляции был включён в тело программы только 1 раз. Эти директивы прочитываются следующим образом: если не определён [название], тогда определить [название] и закончить проверку. Без этих директив, компилятор попытается подключить один и тот же заголовочный файл столько раз, сколько файлов реализаций его подключают; в нашем случае, это будет 2 раза: из файла main.cpp и файла person.cpp.

Для того, чтобы программа скомпилировалась, g++ нужно передавать в качестве аргументов оба файла реализации: g++ main.cpp person.cpp. Кстати, на этом этапе вы можете попытаться создать библиотеку person, скомпилированную в объектный файл. Поступим следующим образом:

g++ -c person.cpp
g++ main.cpp person.o

Вы можете передать другому человеку только скомпилированный файл person.o и заголовочный файл person.h, и он сможет написать main.cpp и собрать программу (при условии, что у вас один и тот же компилятор)! Встаёт вопрос, как обеспечить сборку проекта из тысячи файлов? Не перечислять же их в аргументах, в самом деле. Для этих целей используются системы автоматической сборки, одной из которых является способ сборки с makefile. Прочитайте статью Makefile для самых маленьких, чтобы научится это делать.

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: