Статические функции, объявленные в заголовочных файлах" C"

для меня это правило, чтобы определить и объявить статические функции внутри исходных файлов, я имею в виду .c файлов.

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

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

мои вопросы:

  • в чем проблема объявления статических функций в заголовочных файлах?
  • каковы риски?
  • какое влияние на время компиляции?
  • есть ли риск во время выполнения?

3 ответов


сначала я хотел бы уточнить свое понимание ситуации, которую вы описываете: заголовок содержит (только) объявление статической функции, а файл C содержит определение, т. е. исходный код функции. Например

некоторые.h:

static void f();
// potentially more declarations

некоторые.c:

#include "some.h"
static void f() { printf("Hello world\n"); }
// more code, some of it potentially using f()

Если это ситуация, которую вы описываете, я не согласен с вашим замечанием

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

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


Теперь давайте рассмотрим вопросы:
  • в чем проблема объявления статических функций в заголовочных файлах?
    это несколько необычно. Это имело бы смысл только в том случае, если большинство единиц перевода, которые включают заголовок с заданным объявлением функции, действительно используют эту функцию, потому что основным обоснованием и преимуществом статической функции является их ограниченная видимость. Они не загрязняют глобальное пространство имен (единственное, которое имеет C) и может использоваться как "частные" методы бедняка, которые не предназначены для использования широкой публикой и, следовательно, объявлены таковыми, что они доступны только там, где они необходимы.

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

  • каковы риск?
    я не вижу риска в вашей ситуации. (В отличие от включения функции определение в заголовке, который может нарушить принцип инкапсуляции.)

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

  • есть ли какой-либо риск во время выполнения?
    я не вижу.


это не ответ на указанные вопросы, но, надеюсь, показывает почему можно реализовать static (или static inline) функция в заголовочном файле.

лично я могу придумать только две веские причины для объявления некоторых функций static в заголовочный файл:


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

    это крайне редко, но может быть полезно, например, в образовательном контексте, в какой-то момент при разработке некоторой библиотеки примеров; или, возможно, при взаимодействии с другим языком программирования с минимальным кодом.

    разработчик может сделать это, если реализация библиотеки или interaface тривиальна и почти так, и простота использования (для разработчика, использующего файл заголовка) более важна, чем размер кода. В этих случаях объявления в заголовочном файле часто используют макросы препроцессора, что позволяет один и тот же файл заголовка должен быть включен более одного раза, обеспечивая какой-то грубый полиморфизм в C.

    вот практический пример: Shoot-yourself-in-the-foot playground для линейных конгруэнтных генераторов псевдослучайных чисел. Поскольку реализация является локальной для единицы компиляции, каждая единица компиляции получит свои собственные копии PRNG. Этот пример также показывает, как сырой полиморфизм может быть реализован в С.

    prng32.h:

    #if defined(PRNG_NAME) && defined(PRNG_MULTIPLIER) && defined(PRNG_CONSTANT) && defined(PRNG_MODULUS)
    #define MERGE3_(a,b,c) a ## b ## c
    #define MERGE3(a,b,c) MERGE3_(a,b,c)
    #define NAME(name) MERGE3(PRNG_NAME, _, name)
    
    static uint32_t NAME(state) = 0U;
    
    static uint32_t NAME(next)(void)
    {
        NAME(state) = ((uint64_t)PRNG_MULTIPLIER * (uint64_t)NAME(state) + (uint64_t)PRNG_CONSTANT) % (uint64_t)PRNG_MODULUS;
        return NAME(state);
    }
    
    #undef NAME
    #undef MERGE3
    #endif
    
    #undef PRNG_NAME
    #undef PRNG_MULTIPLIER
    #undef PRNG_CONSTANT
    #undef PRNG_MODULUS
    

    пример использования выше, пример-prng32.h:

    #include <stdlib.h>
    #include <stdint.h>
    #include <stdio.h>
    
    #define PRNG_NAME       glibc
    #define PRNG_MULTIPLIER 1103515245UL
    #define PRNG_CONSTANT   12345UL
    #define PRNG_MODULUS    2147483647UL
    #include "prng32.h"
    /* provides glibc_state and glibc_next() */
    
    #define PRNG_NAME       borland
    #define PRNG_MULTIPLIER 22695477UL
    #define PRNG_CONSTANT   1UL
    #define PRNG_MODULUS    2147483647UL
    #include "prng32.h"
    /* provides borland_state and borland_next() */
    
    int main(void)
    {
        int i;
    
        glibc_state = 1U;
        printf("glibc lcg: Seed %u\n", (unsigned int)glibc_state);
        for (i = 0; i < 10; i++)
            printf("%u, ", (unsigned int)glibc_next());
        printf("%u\n", (unsigned int)glibc_next());
    
        borland_state = 1U;
        printf("Borland lcg: Seed %u\n", (unsigned int)borland_state);
        for (i = 0; i < 10; i++)
            printf("%u, ", (unsigned int)borland_next());
        printf("%u\n", (unsigned int)borland_next());
    
        return EXIT_SUCCESS;
    }
    

    причина маркировки обоих _state переменной и _next() функции static таким образом, каждая единица компиляции, включающая файл заголовка, имеет свою собственную копию переменных и функций-здесь их собственная копия PRNG. Каждый должен быть посеян отдельно, конечно; и если посеян до того же значения, будет дайте ту же последовательность.

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

    однако, когда знакомства пространство параметров некоторого алгоритма - как здесь, типы 32-разрядные линейные конгруэнтные генераторы, это позволяет нам использовать одну реализацию для каждого из генераторов мы исследуем, гарантируя, что между ними нет различий в реализации. Обратите внимание, что даже этот случай больше похож на инструмент разработки, а не на то, что вы должны видеть в реализации, предоставленной для других.


  1. если заголовок реализует simple static inline функции-аксессоры

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

    один практический случай использования-это простой интерфейс для чтения файлов с помощью низкоуровневого POSIX.1 ввод / вывод (с помощью <unistd.h> и <fcntl.h> вместо <stdio.h>). Я сделал это сам при чтении очень больших (от десятков мегабайт до гигабайт) текстовых файлов, содержащих реальные числа( с пользовательским float / double parser), как стандарт GNU C I / O не особенно быстро.

    например, inbuffer.h:

    #ifndef   INBUFFER_H
    #define   INBUFFER_H
    
    typedef struct {
        unsigned char  *head;       /* Next buffered byte */
        unsigned char  *tail;       /* Next byte to be buffered */
        unsigned char  *ends;       /* data + size */
        unsigned char  *data;
        size_t          size;
        int             descriptor;
        unsigned int    status;     /* Bit mask */
    } inbuffer;
    #define INBUFFER_INIT { NULL, NULL, NULL, NULL, 0, -1, 0 }
    
    int inbuffer_open(inbuffer *, const char *);
    int inbuffer_close(inbuffer *);
    
    int inbuffer_skip_slow(inbuffer *, const size_t);
    int inbuffer_getc_slow(inbuffer *);
    
    static inline int inbuffer_skip(inbuffer *ib, const size_t n)
    {
        if (ib->head + n <= ib->tail) {
            ib->head += n;
            return 0;
        } else
            return inbuffer_skip_slow(ib, n);
    }
    
    static inline int inbuffer_getc(inbuffer *ib)
    {
        if (ib->head < ib->tail)
            return *(ib->head++);
        else
            return inbuffer_getc_slow(ib);
    }
    
    #endif /* INBUFFER_H */
    

    обратите внимание, что выше inbuffer_skip() и inbuffer_getc() не проверять, если ib не является нулевым; это типично для таких функций. Эти аксессоры функции предполагаются "в быстром пути", то есть звонил очень часто. В таких случаях даже накладные расходы на вызов функции имеют значение (и избегаются с помощью static inline функции, так как они дублируются в коде на вызов сайт.)

    тривиальные функции доступа, как указано выше inbuffer_skip() и inbuffer_getc(), может также позволить компилятору избежать перемещения регистра, участвующего в вызовах функций, поскольку функции ожидают, что их параметры будут расположены в определенных регистрах или в стеке, тогда как встроенные функции могут быть адаптированы (wrt. register use) для кода, окружающего встроенную функцию.

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


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

единственное требование для реализаций в заголовочном файле - это функции шаблона c++ и функции-члены класса шаблонов.