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

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

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

另一方面,如果有人天真地尝试使用该function而没有定义它,编译器将会投诉。 所以从某种意义上来说,即使听起来很奇怪也不是真的不安全。

我的问题是:

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

首先,我想澄清一下我对你描述的情况的理解:标题包含(仅)一个静态函数声明,而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() 

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

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

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


现在让我们来看看问题:

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

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

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

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

  • 运行时有任何风险吗?
    我看不到任何东西。

这不是对所述问题的回答,但希望能够说明为什么可以在头文件中实现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 

    使用上面的例子, 示例-prng32.h

     #include  #include  #include  #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. 如果头实现了简单的static inline访问器函数

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

    一个实际用例是使用低级POSIX.1 I / O(使用而不是 )读取文件的简单接口。 我在读取包含实数(包括自定义浮点/双解析器)的非常大(几十兆字节到几千兆字节范围)的文本文件时自己这样做,因为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是否为非NULL; 这是这种function的典型特征。 假设这些存取器函数是“在快速路径中” ,即经常调用。 在这种情况下,即使函数调用开销很重要(并且使用static inline函数也可以避免,因为它们在调用站点的代码中重复)。

    简单的访问器函数,如上面的inbuffer_skip()inbuffer_getc() ,也可以让编译器避免函数调用中涉及的寄存器移动,因为函数希望它们的参数位于特定的寄存器或堆栈中,而内联函数可以是适应(wrt。注册使用)到内联函数周围的代码。

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

为什么要同时具有全局和静态function? 在c中,默认情况下函数是全局的。 如果要将函数的访问权限限制为声明它们的文件,则只使用静态函数。 因此,您通过声明静态来主动限制访问…

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