Если проект содержит множество файлов, то может возникнуть ситуация, что один и тот же заголовочный файл будет подключен несколько раз. Однако на этапе компиляции это может привести к ошибкам. Например, пусть у нас есть заголовочный файл user.h со следующим кодом:
struct User { char* name; unsigned age; }; void printUser(struct User);
Здесь определена структура User и прототип функции printUser, которая должна выводить структуру User на консоль.
И также пусть будет файл user.c с определением функции printUser:
#include <stdio.h> #include "user.h" void printUser(struct User user) { printf("Name: %s Age: %d\n", user.name, user.age); }
Также пусть у нас будет заголовочный файл userlist.h со следующим кодом:
#include <stdio.h> // подключаем заголовочный файл, где определена структура User #include "user.h" // функция вывода массива структур User void printUserList(struct User*, size_t);
Здесь вначале подключается заголовочный файл "user.h", поскольку нам предстоит работать со структурами User, а потом определяется прототип функции printUserList для вывода массива структур User определенной длины на консоль.
В файле userlist.c реализуем функцию printUserList:
#include <stdio.h> #include "userlist.h" void printUserList(struct User* users, size_t count) { for(size_t i=0; i < count; i++) { printUser(users[i]); } }
Данная реализация функции проходит по массиву users и для каждого его элемента вызывает функцию printUser, которая объявлена в подключенном файле "user.h".
И определим файл app.c, который будет представлять главный файл программы и будет использовать функцию printUserList для вывода массива структур User на консоль:
#include "userlist.h" // подключаем API модуля userlist #include "user.h" // подключаем API модуля user #define USERS_COUNT 4 int main(void) { struct User users[USERS_COUNT] = { {"Tom", 39}, {"Bob", 42}, {"Sam", 29}, {"Alice", 34} }; printUserList(users, USERS_COUNT); }
Таким образом, у нас определено 5 файлов:
user.h
user.c
userlist.h
userlist.c
app.c
Хотя у нас казалось бы довольно простая программа, где сложно допустить ошибку, но тем не менее на стадии компиляции мы получим ошибку
c:\C>gcc -Wall -pedantic app.c userlist.c user.c -o app & app In file included from app.c:3: user.h:1:8: error: redefinition of 'struct User' 1 | struct User | ^~~~ In file included from userlist.h:1, from app.c:2: user.h:1:8: note: originally defined here 1 | struct User | ^~~~ user.h:7:6: error: conflicting types for 'printUser'; have 'void(struct User)' 7 | void printUser(struct User); | ^~~~~~~~~ user.h:7:6: note: previous declaration of 'printUser' with type 'void(struct User)' 7 | void printUser(struct User);
Так по консольному выводу мы видим, что идет переопределение структуры User, кроме того, проблемы с функцией printUser.
И проблема в нашем случае состоит в том, что в файле app.c два раза подключается заголовочный файл user.h
#include "userlist.h" // подключаем API модуля userlist #include "user.h" // подключаем API модуля user
Ведь в файле userlist.h уже подключен файл "user.h". Поэтому заголовочные файлы следует подключать в программу только один раз. И чтобы выйти из этой ситуации, мы можем убрать подключение файла user.h:
#include "userlist.h" // подключаем API модулей userlist и user #define USERS_COUNT 4 int main(void) { struct User users[USERS_COUNT] = { {"Tom", 39}, {"Bob", 42}, {"Sam", 29}, {"Alice", 34} }; printUserList(users, USERS_COUNT); }
Теперь ошибки не будет. Однако в большой кодовой базе, где подключается куча заголовочных файлов бывает сложно уследить за всеми подключениями. И в этом случае для проверки подключения используют два способа: выражение #ifdef и директиву #pragma once
Использование директивы #pragma once
может показаться наиболее простым способом, однако это выражение не определено в стандарте языка С, хотя и поддерживается
многими препроцессорами языка С. Эта директива указывается в подключаемом заголовочном файле:
#pragma once // однократное подключение файла struct User { char* name; unsigned age; }; void printUser(struct User);
Минусом данного подхода является то, что можно наткнуться на компилятор, препроцессор которого не поддерживает данную директиву.
Другой подход представляет проверка определения константы с помощью директивы #ifndef в следующем виде:
#ifndef SOMECODE_H // если НЕ определена константа SOMECODE_H #define SOMECODE_H // определяем константу SOMECODE_H // здесь идет определение заголовочного файла #endif // завершение конструкции #ifndef
Сначала мы проверяем, что НЕ определена некоторая константа. Если она НЕ определена, то это значит, что заголовочный файл НЕ подключен.
Если константа НЕ определена, определяем ее с помощью директивы #define и далее вплоть до выражения #ifdef помещаем содержимое заголовочного файла - определение структур, констант и прототипов функций.
Название проверяемой константы должно быть уникально на всем множестве файлов проекта. Поэтому обычно в качестве названия константы используется название подключаемого файла, где точка заменяется прочерком. Например, для файла user.h определяется константа user_h или USER_H. Бывают ситуации, особенно при подключении внешних заголовочных файлов, созданных другими разработчиками, что названия этих файлов дублируются. В этом случае можно в качестве названия констант можно генерировать уникальные значения UUID или использовать числовые метки времени.
Применение #ifndef в файле user.h:
#ifndef user_h // если user_h не определена #define user_h // определяем user_h struct User // и помещаем определение структуры User { char* name; unsigned age; }; void printUser(struct User); // и определение прототипа функиции #endif // конец конструкции ifndef
После этого мы можем многократно включать заголовочный файл в других файлах кода:
#include "userlist.h" // подключаем API модуля userlist #include "user.h" // подключаем API модуля user #include "user.h" // подключаем API модуля user #include "user.h" // подключаем API модуля user #define USERS_COUNT 4 int main(void) { struct User users[USERS_COUNT] = { {"Tom", 39}, {"Bob", 42}, {"Sam", 29}, {"Alice", 34} }; printUserList(users, USERS_COUNT); }
Однако поскольку идет проверка на определение константы user_h, то содержимое заголовочного файла будет вставляться только один раз.