Header (.h) и source (.cpp) файлове в C++


Header файловете в C++ служат за деклариране на функции и класове, без да се дава тяхната реализация. Source файловете описват реализацията на функциите и класовете, описани в съответните header файлове. Един проект може да се състои от няколко header и няколко source файла.

Header файловете по правило не могат да се компилират самостоятелно, те се включват в source файловете с помощта на директивата #include. Всеки source файл се компилира отделно, като се вземат предвид всички header файлове, които са изброение #include. След като всички source файлове се компилират, те се свързват (link) в една обща програма. Тогава се проверява дали всички "обещани" декларации в header файловете наистина са изпълнени в някой от source файловете.

Това позволява разделянето на програмата на отделни модули, върху които може да се работи отделно.

Например:

book.h:
class Book
{
 private:
 char* title;
 char* author;
 ...
 public:
 Book();
 Book(Book const&);
 ...
};

book.cpp
#include "book.h"

Book::Book()
{
 title = author = NULL;
 ...
}
...

Така например ако искаме да напишем клас за библиотека, можем само да включим header файла book.h, за да "обещаем" че в същия проект имаме сорс файл book.cpp, който реализира функциите, описани в book.h:

library.h
#include "book.h"

class Library
{
 private:
 Book* books;
 int nBooks;
 ...
 public:
 void addBook(Book const&);
 ...
};

library.cpp

#include "library.h"

void Library::addBook(Book const& b)
{
 books[nBooks++] = b;
 ...
}

Предимството на този подход е, че при промяна в реализацията на някоя от функциите в book.cpp (напр. оправяме някой бъг), се прекомпилира само book.cpp, а library.cpp не е нужно да се прекомпилира. Ако обаче се наложи да направим промяна по интерфейса на класа Book, т.е. променим book.h, тогава и library.cpp ще трябва да бъде прекомпилиран, понеже например може да сме изтрили някоя функция в Book, на която класът Library разчита.

Какво представлява #include?

#include е предпроцесорна директива. Това означава, че преди source файла да се подаде на компилатора, една програма, наречена предпроцесор, прави предварителна обработка, така че копира дословно съдържанието на включения header файл вътре във source файла. Това става в паметта, така че вие не го виждате.

Избягване на дублиране на header файлове

Ако използвате header файлове, може да се сблъскате с подобна грешки:

error: redefinition of ‘class A’
error: previous definition of ‘class A’

Предпроцесорът не "разбира" C++, а само предпроцесорните директиви - за него всичко останало е просто някакъв текст. Затова ако напишете погрешка #include два пъти един след друг, тогава съдържанието на header файла ще се копира два пъти в source файла, което ще доведе до грешка на компилатора, че се опитвате да дефинирате един и същи клас или функция два пъти.

За съжаление повторение може да се получи и косвено. Например, ако в примера по-горе, в library.cpp напишете

#include "library.h"
#include "book.h"

тогава отново ще получите грешка, понеже book.h ще е включен два пъти - един път директно и един път като част от library.h.

За да избегнете това, може да инструктирате предпроцесора да "помни", че вече е включил даден файл. Това става като добавите в началото и края на вашия header файл следните редове:

#ifndef __LIBRARY_H
#define __LIBRARY_H
// тук е съдържанието на файла library.h
...
#endif

__LIBRARY_H е "маркер", чието име измисляте вие. Добра идея е маркерът да се казва по същия начин като файла, за да сте сигурни, че всеки файл ще си има собствен маркер. Директивите по-горе се четат като: "ако не си срещал вече маркера __LIBRARY_H, отбележи си, че сега го срещаш и копирай по-долните редове, в противен случай не копирай нищо".

Така осигуряваме, че library.h може да се включи най-много един път.

Циклични зависимости

Понякога може да се случи, че пишем два класа A и B, като всеки от тях трябва да знае за другия. Това може да се случи например, ако A съдържа в член-данните си указател към B и B съдържа указател към A:

classa.h
#include "classb.h"
class A
{
 private:
 B* pb;
 ...
};

classb.h
#include "classa.h"
class B
{
 private:
 A* pa;
 ...
};

Тази ситуация се нарича циклична зависимост и причинява трудности, понеже ако във вашия source файл напишете #include "classa.h", това ще доведе до включването на classa.h, в който ще се копира classb.h, в който ще се копира classa.h, ... и така до безкрайност - ще получите съобщение за грешка.

Принципно е добър стил на програмиране да се избягват цикличните зависимости, дори някои езици го забраняват изцяло. C++ не го забранява и този проблем се решава в две стъпки. Първата е да включите маркери, както е описано в предната точка:

classa.h
#ifndef __CLASSA_H
#define __CLASSA_H

#include "classb.h"
class A
{
 private:
 B* pb;
 ...
};

#endif

classb.h
#ifndef __CLASSB_H
#define __CLASSB_H

#include "classa.h"
class B
{
 private:
 A* pa;
 ...
};

#endif

Така файловете няма да се включват безкрайно много пъти, но все още има проблем. Ако в главната програма напишете #include "classa.h", това ще накара предпроцесора да включи дефинициите на класовете един след друг в следния ред:

class B
{
 private:
 A* pa;
 ...
}; class A
{
 private:
 B* pb;
 ...
};


Това отново ще доведе до грешка, понеже в момента на компилиране на class B, не се знае, че има class A. Ясно е, че ако разменим двата класа ще имаме същия проблем.

Това води до нужда от втората стъпка, която е да включим forward декларация на класа, т.е. да обявим, че даден клас ще бъде описан по-долу в кода. Това се прави просто като се напише class A; или class B;

Така окончателният вид на файловете ще бъде:

classa.h

#ifndef __CLASSA_H
#define __CLASSA_H

#include "classb.h"

// обещаваме, че ще има клас B
class B;

// описваме клас A
class A
{
 private:
 B* pb;
 ...
};

#endif

classb.h
#ifndef __CLASSB_H
#define __CLASSB_H

#include "classa.h"

// обещаваме, че ще има клас A
class A;

// описваме клас B
class B
{
 private:
 A* pa;
 ...
};

#endif

Общото правило за цикличните зависимости е: използвайте ги само в случай, че сте наистина убедени, че ви трябват. В повечето случаи можете лесно да промените класовете си, така че да нямат циклични зависимости помежду си.

Шаблони и header файлове

Принципно е добра практика да разделяме класовете на header и source файлове, понеже всеки source файл може да се компилира отделно и независимо от другите, например на отделно процесорно ядро.

Шаблоните на класове обаче са изкючение от това правило. Причината е, че шаблонът на клас не генерира никакъв код до момента в който не се инстанцира (специализира) за конкретен тип. Така ако се опитаме да компилираме шаблон на клас (като LList<T>) самостоятелно в отделен source (.cpp) файл и после се опитаме да го свържем с главната програма, която използва конкретна инстанция на шаблона (например LList<int>), ще получим грешка на свързващата програма (linker error), че не може да намери функциите на съответната инстанция на шаблона (LList<int>). Причината е, че отделно компилирания файл съдържащ шаблона няма как да знае точно как ще инстанцираме шаблона в друг файл.

Решението на този проблем е шаблоните да не се компилират отделно като source файлове, а да се включват директно в header или source файла, който ги използва, например така:

#include "LList.cpp"

Last modified: Saturday, 12 November 2011, 5:38 PM