在“C”头文件

时间:2017-02-05 19:17:46

标签: c function static internal linkage

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

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

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

我的问题是:

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

3 个答案:

答案 0 :(得分:10)

首先,我想澄清一下我对你描述的情况的理解:标题包含(仅)一个静态函数声明,而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中导致编译时错误,因为它没有函数重载。)从这种统一性角度来看如果函数在每个翻译单元中是相同的,则可能足以立即在头文件中提供适当的函数定义。这种方法的开销取决于所有包含标题的翻译单元是否也实际使用该函数。

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

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

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

答案 1 :(得分:8)

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

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

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

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

    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 */
      

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

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

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

      < / LI>

答案 2 :(得分:0)

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

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