Подобного рода вопрос мне недавно задал один коллега, начинающий программировать на языке Си. И я подумал, что это хороший повод поделится своим пониманием данного вопроса. Потому, что даже опытные программисты не всегда имеют схожие точки зрения на этот счет.

Отчасти это дело вкуса, поэтому, кому интересно как это делаю я, добро пожаловать под кат.

Несмотря на то, что «вся правда» о h-файлах содержится в соответствующем разделе описания препроцессора gcc, позволю себе некоторые пояснения и иллюстрации.

Итак, если дословно, заголовочный файл (h-файл) — файл содержащий Си декларации и макро определения, предназначенные для использования в нескольких исходных файлах (с-файлах). Проиллюстрируем это.
image
Легко заметить, что функции 1 и 2, а так же макрос 2, упомянуты в обоих файлах. И, поскольку, включение заголовочных файлов приводит к таким же результатам, как и копирование содержимого в каждый си-файл, мы можем сделать следующее:
image
Таким образом мы просто выделили общую часть из двух файлов и поместили ее в заголовочный файл.
Но является ли заголовочный файл интерфейсом в данном случае?
  • Если нам нужно использовать функциональность, которую реализуют функции 1 и 2 где то еще, то Да
  • Если макрос 2, предназначен только для использования в файлах Unit1.c и Unit2.c, то ему не место в интерфейсном файле

Более того, действительно ли нам необходимо иметь два си-файла для реализации интерфейса, определенного в заголовочном файле? Или достаточно одного?
Ответ на этот вопрос зависит от деталей реализации интерфейсных функций и от их места реализации. Например, если сделать диаграммы более подробными, можно представить вариант, когда интерфейсные функции реализованы в разных файлах:
image
Такой вариант реализации приводит к высокой связности кода, низкой тестируемости и к сложности повторного использования таких модулей.
Для того, что бы не иметь таких трудностей, я всегда рассматриваю си-файл и заголовочный файл как один модуль. В котором,
  • заголовочный файл содержит только те декларации функций, типов, макросов, которые являются частью интерфейса данного модуля.
  • Си-файл, в свою очередь, должен содержать реализацию всех функций, декларированных в h- файле, а также приватные типы, макросы и функции, которые нужны для реализации интерфейса.

Таким образом, если бы мне довелось реализовывать код, которому соответствует диаграмма приведенная выше, я бы постарался, добиться следующего (окончания _с и _h в именах файлов добавлены по причине невозможности использовать точку в инструменте, которым я пользовался для создания диаграмм):
image
Из диаграммы видно, что на самом деле мы имеем дело с двумя независимыми модулями, у каждого из которых имеется свой интерфейс в виде заголовочного файла. Это дает возможность использовать только тот интерфейс, который действительно необходим в данном конкретном случае.Более того, эти модули могут быть протестированы независимо друг от друга.
Читатель, наверное, заметил, что макрос 2 из заголовочного файла снова вернулся в виде копии в оба си-файла. Конечно, это не очень удобно поддерживать. Но и делать данный макрос частью интерфейса не правильно.
В таких случаях, я предпочитаю делать отдельный заголовочный файл содержащий типы и макросы, необходимые нескольким си-файлам.
image

Надеюсь, мне удалось обозначить те сущности, которые нуждаются в том, что бы быть помещенными в заголовочные файлы. А так же, показать разницу между интерфейсам и файлами, содержащими декларации и макросы, необходимые нескольким си-файлам.

Спасибо за внимание к материалу.

Комментарии (25)


  1. Wilk
    04.04.2016 02:04
    +8

    Здравствуйте.
    В ряде случаев вопрос с необходимостью использования макроса в нескольких независимых файлах я решаю созданием отдельного заголовочного файла, содержащего только этот макрос. Решение не самое элегантное, однако избавляет от решения сложнейшей задачи — придумывания имения для файла, в котором хранятся различные утилитарные макросы.
    В случае, если необходимо объявить какой-либо тип, который будет входить в интерфейс модуля, я стараюсь сделать так, чтобы пользователю описание типа было не видно. Конечно, это маразм и попахивает плюсами головного мозга, однако у меня есть оправдание — я хочу максимально сохранить совместимость между версиями библиотек и избавить себя от необходимости изменять зависимое ПО в случае изменения структуры данных в новой версии библиотеки. Поясню идею: я вижу пользователя библиотеки неплохим парнем, но желающим решить какую-либо задачу попроще либо хитро. Это означает, что пользователь может использовать для работы с объектами типов данных, объявленных в библиотеке, не библиотечные функции. Например, выделять память для объекта самостоятельно, либо использовать напрямую какое-либо поле. Всё бы ничего, но в случае, если структура данных изменится, весь код надо будет исправлять для поддержки работы с новой версией библиотеки.
    Кроме того, при использовании такого подхода появляется возможность экспериментировать с типами данных, подменяя их на различные варианты при сборке. Хотя до полноценного решения, аналогичного применяемому в том же ядре Linux и других проектах, использующих объектно-ориентированные техники в C, всё ещё далеко.


    1. 0xFE
      05.04.2016 01:05

      Доброго времени суток.

      я стараюсь сделать так, чтобы пользователю описание типа было не видно

      Я не совсем понял как Вы это делаете.
      Если интерфейсная функция принимает указатель на структуру в качестве параметра, Вы просто декларируете это как foo( void * )? Я просто не сталкивался с такой потребностью.
      Спасибо.


      1. Wilk
        05.04.2016 01:49
        +3

        Здравствуйте.
        Я использую предварительное объявление:
        AliceInWonderland.h:

        typedef struct Alice Alice_t;
        
        Alice_t* aliceNew ();
        
        void aliceDestroy (Alice_t* o_alice);
        
        size_t aliceSize ();
        
        void rabbitHole (Alice_t i_alice);

        private/AliceInWonderland.h:
        #include "../AliceInWonderland.h"
        
        struct Alice {
            // ...
        }

        AliceInWonderland.c:
        #include "AliceInWonderland.h"
        #include "private/AliceInWonderland.h"
        
        Alice_t* aliceNew () {
            return (Alice_t*) malloc (aliceSize ());
        }
        
        void aliceDestroy (Alice_t* o_alice) {
            free (o_alice);
        }
        
        size_t aliceSize () {
            return sizeof (Alice_t);
        }
        
        void rabbitHole (Alice_t i_alice) {
            // ...
        }

        Таким образом, определение типа видно только там, где это необходимо. Пользователям же библиотеки известно только о том, что существует такой тип, как Alice_t. Более ничего. Естественно, в случае необходимости доступа к полям возникает необходимость в создании соответствующих функций доступа.
        Конечно, решение не идеальное и может создавать лишнюю головную боль, но на данный момент код, в котором я использую такой подход, имеет малый объём, поддерживается только мной и работает.
        P.S. Да, я ошибся термином: правильно было сказать не "описание типа" (declaraion), а "определение типа" (definition).


        1. 0xFE
          05.04.2016 23:31

          Да, это интересно. Буду иметь ввиду такой способ.
          Большое спасибо.


  1. Cromathaar
    04.04.2016 10:43
    +1

    Если думать о заголовочных файлах как об интерфейсах, то интерфейс в первую очередь должен представлять из себя согласованную абстракцию, а не быть местом, куда попросту выносятся пересекающиеся декларации.


    1. MacIn
      04.04.2016 17:08

      Пересекающиеся декларации — это тоже вид абстракции, правда, самый примитивный.


  1. ffs
    04.04.2016 10:50
    -3

    Стыдно в таком признаться в 2016, но сколько раз я не начинал «изучать с++ за N дней», всегда запинался на подобной мелочи и потом меня «отпускало».
    Самое первое — беру книгу, читаю, хочу начать писать код. Скачиваю вижуал студию, создаю проект, компилю-запускаю, бдыщ — не хватает всяких std**** итд. Гуглю, подключаю, их не находит. Позже оказывается что они есть только в про- или какой-то ещё крутой версии, а чего стоит структура пустого проекта из десятка файлов, в назначении которых без бутылки не разобраться, а толком описания всего хозяйства я нигде так и не нашел.
    В результате самого большого прогресса достиг пользуясь убунтой и gcc, но там далеко продолжить не смог, т.к. по непонятным для меня причинам видеокарта перестала что-либо показывать после загрузки иксов в любом линуксе с драйвером nvidia.
    Вот прочитал вашу статью и опять загорелся, тем более что друг зазывает делать игру на unreal 4. Можете (не только автор, все) посоветовать какую-то книгу/цикл_статей/еще_что-то, что поможет в моём случае?


    1. Alexx999
      04.04.2016 12:24
      +3

      По поводу студии — нынешний Community Edition является прошкой без полутора фич, так что в этом плане стало попроще.
      GCC можно поднять и на винде, вместе со всеми прочими никсовыми утилитами (MinGW либо Cygwin).


    1. Shifty_Fox
      04.04.2016 12:24
      +2

      Можно скачать mingw, использовать с Eclipse CDT или %YOUR_FAVOR_IDE%. Eclipse умеет сам составлять makefile для сборки. Сам по себе C\C++ достаточно прост, в нем просто много возможностей, которые можно использовать, а можно и не использовать, самый минимум же языка требует только auto main() -> int, пары системных заголовков и того что вы сами в него вставите.
      По поводу литературы, возможно книга Герберта Шилдта вам поможет. Но помните, под каждый движок, включая unreal 4, архитектура и паттерны с код стайлом сильно разнятся, язык очень универсальный, общий знаменатель там фактически функции и классы, а способов выразить один и тот же паттерн по 3-4 варианта. Изучайте основы, и углубляйтесь в программирование конкретно под Unreal 4, сэкономите время.


      1. Gryphon88
        04.04.2016 13:40
        +3

        Добавлю, что помимо справочников Шилдта, которые можно использовать для быстрого старта, хорошо бы почитать Страуструпа. Вообще, хорошо взять книгу автора языка, просто чтобы понять, зачем и почему сделано именно так, а потом прочесть уважаемую книгу со словом «effective» в названии, чтобы понять, во что оно выродилось.
        Плюс надо помнить, что многие хорошие практики в С являются нежелательными в плюсах, например (осторожно, умеренный холивар!) сишные перечисления и макросы рекомендуется в плюсах не использовать, а упирать на инлайновые функции и шаблоны.


        1. akalend
          04.04.2016 14:32
          +3

          Книги с «effective» в названии явно не для новичков, пусть сперва Шилдта освоит.


        1. MacIn
          04.04.2016 17:10
          +2

          Справочное руководство по STL тоже в помощь.


      1. Foreglance
        07.04.2016 00:22

        Из вступления к курсу «DEV210x Introduction to C++» на edx.org:

        So if you have a book that, I don't know, one of your
        parents used in college to learn C++ from, please do not look
        at that book. Like don't even open it. Because it's going to
        be full of old-school, we don't do it like that anymore, that's
        harder than it needs to be ways of coming at C++. And what James
        and I want to show you today is that C++ is not a scary language.
        It's a very powerful and expressive language with elegance and expressivity.


        1. Gryphon88
          07.04.2016 14:45
          +1

          Очень грамотное вступление. Я примерно так же открыл для себя «21st Century C» by Klemens. Книга начинается с настройки окружения, в т.ч. гита, рассматривается жизненный цикл приложения, есть ряд оговорок типа «так сделали в 70е и это было разумно, а потом лепили костыли. Теперь все пользуются этой штукой и нам остаётся только изучать её и лепить свои костыли» (про automake и поведение линковщика вроде) и «Если Ваш компьютер имеет больше 512 Мб оперативки. Вы можете игнорировать эту „best practice“»


        1. MacIn
          07.04.2016 16:08

          Это самый начальный курс? У них в аннотации написано, что освоенное пригодится при прохождении следующих курсов, но я не нашел курса для более продвинутых. Вы не смотрели следующие?


          1. Foreglance
            08.04.2016 08:54

            Этот курс для начинающих — базовый синтаксис с интересными деталями. Другие пока не встречал и не искал.


    1. moveax3
      05.04.2016 00:24
      +2

      вот поэтому изучать надо начинать с запусков gcc в консольке, простого текстового редактора и понимания сути того, что ты делаешь и что происходит, а не с создания проектов в визуалстудиях


    1. 0xFE
      05.04.2016 00:56

      Ничего зазорного в незнании нет. Я тоже "не умею С++", если так можно выразиться. Но у меня и нет необходимости в этом. Может у Вас тоже нет?


      1. ffs
        05.04.2016 09:46

        Видимо да, не было. Сейчас как занятие на свободное время понадобилось, вот и ищу.


  1. Xitsa
    04.04.2016 11:55

    А я, наверное, не устану рекомендовать классическую книгу, посвящённую в том числе и планированию размещения кода по заголовочным, исходным файлам и модулям: John S. Lakos — Large-Scale C++ Software Design.
    Она, конечно, скучная и уже старая, но общие идеи не устарели до сих пор.


  1. olegy
    04.04.2016 12:05
    +3

    Нужно разделять интерфейсные и внутренние описания, аля в C++ public vs private & protected описания. В больших С проектах, мы пользуемся такими соглашениями в именовании файлов:
    <module_name>-<submodule1>.c
    <module_name>-<submodule2>.c
    <module_name>.h — интерфейс модуля (public)
    <module_name>_int.h — внутренние определения модуля (protected)
    Это в общем то предмет соглашения разработчиков (стиль программирование, так же сюда входят именование переменных и т.п.)


  1. orcy
    04.04.2016 13:21
    +1

    Инструмент для рисования диаграмм выглядит очень интересным, спасибо за ссылку!


  1. r44083
    04.04.2016 13:47
    +4

    Также стоило бы добавить что "приватные" функции в Си-файлах лучше объявлять как

    static
    Это ограничит их видимость.


    1. 0xFE
      04.04.2016 14:32
      +2

      Да, Вы правы, но "это уже совсем другая история" ;)


  1. a-ilin
    05.04.2016 00:51
    +1

    В некоторых случаях используемые фреймворки накладывают свои условия. Например, в Qt, если класс использует сигналы/слоты или другую метаинформацию, такой класс должен располагаться в заголовочном файле. Так как moc-компилятор (один из инструментов Qt) по умолчанию обрабатывает только заголовочные файлы. Для сокрытия «приватных» классов от пользователя используются «приватные» заголовочные файлы, имеющие формат имени {classname}_p.h