在 "C" 个头文件中声明的静态函数

Static functions declared in "C" header files

对我来说,在源文件中定义和声明静态函数是一条规则,我指的是 .c 文件。

然而,在极少数情况下,我看到人们在头文件中声明它。 由于静态函数有内部链接,我们需要在每个文件中定义它,我们在声明函数的地方包含头文件。这看起来很奇怪,与我们通常在将某些东西声明为静态时想要的相去甚远。

另一方面,如果天真的人试图在未定义的情况下使用该函数,编译器将会抱怨。所以从某种意义上说,这样做并不是真的不安全,即使听起来很奇怪。

我的问题是:

为什么您需要全局函数和静态函数?在 c 中,函数默认是全局的。如果要将对函数的访问限制为声明它们的文件,则只能使用静态函数。因此,您通过将其声明为静态来主动限制访问...

头文件中实现的唯一要求是 C++ 模板函数和模板 class 成员函数。

首先我想澄清我对你描述的情况的理解:header包含(仅)一个静态函数声明,而C文件包含定义,即函数的源代码。例如

some.h:

static void f();
// potentially more declarations

some.c:

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

如果这是你描述的情况,我对你的评论有异议

Since static functions have internal linkage we need to define it in every file we include the header file where the function is declared.

如果您声明了函数但不在给定的翻译单元中使用它,我认为您不必定义它。 gcc 接受并发出警告;标准似乎并没有禁止它,除非我错过了什么。这在您的场景中可能很重要,因为不使用该函数但在其声明中包含 header 的翻译单元不必提供未使用的定义。


现在让我们检查问题:

  • 在header个文件中声明静态函数有什么问题?
    这有点不寻常。通常,静态函数是仅在一个文件中需要的函数。它们被声明为静态的,以通过限制它们的可见性来使其明确。因此,在 header 中声明它们有些矛盾。如果该函数确实在具有相同定义的多个文件中使用,则它应该是外部的,具有单一定义。如果只有一个翻译单元实际使用它,则该声明不属于 header。

    因此,一种可能的情况是确保各个翻译单元中不同实现的函数签名统一。对于 C(和 C++)中的 不同 return 类型 ,常见的 header 会导致编译时错误; 不同的参数类型只会在 C 中导致编译时错误(但在 C++ 中不会,因为函数重载)。
  • 有什么风险?
    我看不到您的情况存在风险。 (与在 header 中还包含函数 definition 相反,这可能违反封装原则。)
  • 对编译时间有什么影响?
    函数声明很小且复杂度低,因此在 header 中进行额外函数声明的开销可能可以忽略不计。但是,如果您为许多翻译单元中的声明创建并包含 一个额外的 header,文件处理开销可能会很大(即编译器在等待 header I/O)
  • 运行时有风险吗?
    我看不到。

这不是对所述问题的回答,但希望表明为什么 可以在 static(或 static inline)函数中实现 header 文件.

我个人只能想到在 header 文件中声明一些函数 static 的两个很好的理由:


  1. 如果header文件完全实现了一个应该只在当前编译单元可见的接口

    这种情况极为罕见,但可能在某些方面很有用,例如教育背景,在某些示例库开发过程中的某个时刻;或者可能在使用最少的代码连接到另一种编程语言时。

    如果库或界面实现非常简单且几乎如此,并且易用性(对于使用 header 文件的开发人员而言)比代码大小更重要,开发人员可能会选择这样做。在这些情况下,header 文件中的声明通常使用预处理器宏,允许多次包含相同的 header 文件,从而在 C 中提供某种粗略的多态性。

    这是一个实际示例:Shoot-yourself-in-the-foot 线性同余伪随机数生成器的游乐场。因为实现是编译单元本地的,所以每个编译单元都会得到自己的 PRNG 副本。此示例还展示了如何在 C 中实现粗略的多态性。

    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
    

    使用上面的例子,example-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的原因是这样每个包含header文件的编译单元都有自己的副本变量和函数——这里是它们自己的 PRNG 副本。当然,每个都必须单独播种;如果播种到相同的值,将产生相同的序列。

    人们通常应该回避 C 中的这种多态性尝试,因为它会导致复杂的预处理器宏恶作剧,使实现比必要的更难理解、维护和修改。

    然而,当探索某些算法的参数space时——像这里,32-bit linear congruential generators的类型,这让我们可以使用单一的实现对于我们检查的每个生成器,确保它们之间没有实现差异。请注意,即使这种情况更像是一种开发工具,而不是您应该在提供给其他人使用的实现中看到的东西。


  1. 如果 header 实现了简单的 static inline 访问器函数

    预处理器宏通常用于简化访问复杂结构类型的代码。 static inline 函数类似,除了它们还提供编译时的类型检查,并且可以多次引用它们的参数(使用宏,这是有问题的)。

    一个实际用例是使用 low-level POSIX.1 I/O(使用 <unistd.h><fcntl.h> 而不是 <stdio.h>).我自己在读取包含实数的非常大(数十兆字节到千兆字节范围)的文本文件(使用自定义 float/double 解析器)时自己完成了此操作,因为 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是否为non-NULL;这是此类功能的典型特征。这些访问器函数被假定为 "in the fast path",即经常调用。在这种情况下,即使是函数调用开销也很重要(并且 static inline 函数可以避免,因为它们在调用站点的代码中是重复的)。

    像上面的 inbuffer_skip()inbuffer_getc() 这样的普通访问函数,也可以让编译器避免函数调用中涉及的寄存器移动,因为函数期望它们的参数位于特定的寄存器或在堆栈上,而内联函数可以适应(wrt。寄存器使用)内联函数周围的代码。

    就我个人而言,我确实建议首先使用 non-inlined 函数编写几个测试程序,然后将性能和结果与内联版本进行比较。比较结果确保内联版本没有错误(这里常见的是一种类型!),并且比较性能和生成的二进制文件(至少大小)告诉您内联通常是否值得。