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

mig*_*edo 19 c static function internal linkage

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

然而,在非常罕见的情况下,我看到人们在头文件中声明它.由于静态函数具有内部链接,我们需要在包含声明函数的头文件的每个文件中定义它.这看起来很奇怪,远非我们通常在将某些东西声明为静态时所需要的.

另一方面,如果有人天真地尝试使用该功能而没有定义它,编译器会投诉.所以在某种意义上说这甚至听起来很奇怪并不是不安全的.

我的问题是:

  • 在头文件中声明静态函数有什么问题?
  • 有什么风险?
  • 编译时间有什么影响?
  • 运行时有任何风险吗?

Pet*_*ica 13

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

some.h:

static void f();
// potentially more declarations
Run Code Online (Sandbox Code Playgroud)

some.c:

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

如果这是您描述的情况,我会对您的评论提出异议

由于静态函数具有内部链接,我们需要在包含声明函数的头文件的每个文件中定义它.

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


现在让我们来看看问题:

  • 在头文件中声明静态函数有什么问题?
    这有点不寻常.只有包含带有给定函数声明的标题的大多数翻译单元确实使用该函数才有意义,因为静态函数的主要原理和好处是它们的可见性有限.它们不会污染全局命名空间(C中唯一的一个)并且可以用作穷人的"私人"方法,这些方法并不是一般公众可以使用的,因此声明它们只能在它们可以访问的地方使用是必要的.

    另一方面,在标题中包含声明实际上可能是有益的,因为它确保所有本地定义至少在函数签名中一致.(具有相同名称但返回类型不同的两个函数将导致C(和C++)中的编译时错误;不同的参数类型将仅在C中导致编译时错误,因为它没有函数重载.)从这种统一性角度来看如果函数在每个翻译单元中是相同的,则可能足以立即在头文件中提供适当的函数定义.这种方法的开销取决于包含头部的所有翻译单元是否也实际使用该功能.

  • 有什么风险?
    我不认为您的方案存在风险.(而不是在标题中包含可能违反封装原则的函数定义.)

  • 编译时间有什么影响?
    函数声明很小并且其复杂性很低,因此在头文件中具有附加函数声明的开销可能是微不足道的.但是,如果您在许多翻译单元中为声明创建并包含其他标头,则文件处理开销可能很大(即编译器在等待标头I/O时闲置很多).

  • 运行时有任何风险吗?
    我什么也看不见.


Nom*_*mal 12

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

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


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

    这是非常罕见的,但在某些示例库的开发过程中的某些时候,可能在例如教育环境中有用; 或者,当用最少的代码连接到另一种编程语言时.

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

    这是一个实际的例子:用于线性同余伪随机数生成器的射击自己的游乐场.因为实现是编译单元的本地实现,所以每个编译单元都将获得自己的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
    
    Run Code Online (Sandbox Code Playgroud)

    使用上面的例子,示例-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;
    }
    
    Run Code Online (Sandbox Code Playgroud)

    标记_state变量和_next()函数的原因static是这样,包含头文件的每个编译单元都有自己的变量和函数副本 - 这里是他们自己的PRNG副本.当然,每个都必须单独播种; 如果播种到相同的值,将产生相同的序列.

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

    但是,在探索某些算法的参数空间时 - 就像这里的32位线性同余生成器的类型一样,这使我们可以为我们检查的每个生成器使用单个实现,确保它们之间没有实现差异.请注意,即使这种情况更像是一个开发工具,而不是您应该在为其他人提供的实现中看到的东西.


  1. 如果标头实现了简单的static inline访问器功能

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

    一个实际用例是使用低级POSIX.1 I/O(使用<unistd.h><fcntl.h>代替<stdio.h>)读取文件的简单接口.我在读取包含实数(包括自定义浮点/双解析器)的非常大(几十兆字节到几千兆字节)的文本文件时自己这样做,因为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 */
    
    Run Code Online (Sandbox Code Playgroud)

    注意上面inbuffer_skip()inbuffer_getc()没有检查是否ib为非NULL; 这是这种功能的典型特征.假设这些存取器函数是"在快速路径中",即经常调用.在这种情况下,即使函数调用开销很重要(并且static inline由于它们在调用站点的代码中重复,因此可以避免使用函数).

    琐碎存取器函数,如上述inbuffer_skip()inbuffer_getc(),也可能让编译器避免参与函数调用寄存器移动时,因为函数期望其参数为位于特定寄存器或堆栈上,而内联函数可以适于(WRT.寄存器使用)到内联函数周围的代码.

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