Подключение заголовочных файлов

Последнее обновление: 18.09.2023

Если проект содержит множество файлов, то может возникнуть ситуация, что один и тот же заголовочный файл будет подключен несколько раз. Однако на этапе компиляции это может привести к ошибкам. Например, пусть у нас есть заголовочный файл 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, то содержимое заголовочного файла будет вставляться только один раз.

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850