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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/42056160/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-02 10:36:28  来源:igfitidea点击:

Static functions declared in "C" header files

cfunctionstaticinternallinkage

提问by miguel azevedo

For me it's a rule to define and declare static functions inside source files, I mean .c files.

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

However in very rare situations I saw people declaring it in the header file. Since static functions have internal linkage we need to define it in every file we include the header file where the function is declared. This looks pretty odd and far from what we usually want when declaring something as static.

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

On the other hand if someone naive tries to use that function without defining it the compiler will complaint. So in some sense is not really unsafe to do this even sounding strange.

另一方面,如果有人天真地尝试使用该函数而不定义它,编译器会抱怨。所以从某种意义上说,这样做甚至听起来很奇怪也不是真的不安全。

My questions are:

我的问题是:

  • What is the problem of declaring static functions in header files?
  • What are the risks?
  • What the impact in compilation time?
  • Is there any risk in runtime?
  • 在头文件中声明静态函数有什么问题?
  • 有哪些风险?
  • 编译时间有什么影响?
  • 运行时有风险吗?

采纳答案by Peter - Reinstate Monica

First I'd like to clarify my understanding of the situation you describe: The header contains (only) a static function declaration while the C file contains the definition, i.e. the function's source code. For example

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

some.h:

一些.h:

static void f();
// potentially more declarations

some.c:

一些.c:

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

If this is the situation you describe, I take issue with your remark

如果这就是你所描述的情况,我对你的话有异议

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

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

If you declare the function but do not use it in a given translation unit, I don't think you have to define it. gcc accepts that with a warning; the standard does not seem to forbid it, unless I missed something. This may be important in your scenario because translation units which do not use the function but include the header with its declaration don't have to provide an unused definition.

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



现在让我们来研究这些问题:

  • What is the problem of declaring static functions in header files?
    It is somewhat unusual. Typically, static functions are functions needed in only one file. They are declared static to make that explicit by limiting their visibility. Declaring them in a header therefore is somewhat antithetical. If the function is indeed used in multiple files with identical definitions it should be made external, with a single definition. If only one translation unit actually uses it, the declaration does not belong in a header.

    One possible scenario therefore is to ensure a uniform function signature for different implementationsin the respective translation units. The common header leads to a compile time error for different return typesin C (and C++); different parameter typeswould cause a compile time error only in C (but not in C++' because of function overloading).
  • What are the risks?
    I do not see risks in your scenario. (As opposed to also including the function definitionin a header which may violate the encapsulation principle.)
  • What the impact in compilation time?
    A function declaration is small and its complexity is low, so the overhead of having additional function declarations in a header is likely negligible. But if you create and include an additional headerfor the declaration in many translation units the file handling overhead can be significant (i.e. the compiler idles a lot while it waits for the header I/O)
  • Is there any risk in runtime?
    I cannot see any.
  • 在头文件中声明静态函数有什么问题?
    这有点不寻常。通常,静态函数是仅在一个文件中需要的函数。它们被声明为静态以通过限制它们的可见性来使之明确。因此,在标题中声明它们有点对立。如果该函数确实在具有相同定义的多个文件中使用,则应将其设为外部,并具有单一定义。如果只有一个翻译单元实际使用它,则声明不属于标题。

    因此,一种可能的情况是确保各个翻译单元中不同实现的统一函数签名。对于C(和 C++)中的不同返回类型,公共头文件会导致编译时错误;不同的参数类型只会在 C 中导致编译时错误(但在 C++ 中不会,因为函数重载)。
  • 有哪些风险?
    我在你的场景中没有看到风险。(与在可能违反封装原则的标头中包含函数定义相反。)
  • 编译时间有什么影响?
    函数声明很小,复杂度也很低,因此在头文件中附加函数声明的开销可能可以忽略不计。但是,如果您在许多翻译单元中为声明创建并包含一个额外的头文件,文件处理开销可能会很大(即编译器在等待头文件 I/O 时会空闲很多)
  • 运行时有风险吗?
    我看不到任何。

回答by Nominal Animal

This is not an answer to the stated questions, but hopefully shows whyone might implement a static(or static inline) function in a header file.

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

I can personally only think of two good reasons to declare some functions staticin a header file:

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



  1. If the header file completely implements an interface that should only be visible in the current compilation unit

    This is extremely rare, but might be useful in e.g. an educational context, at some point during the development of some example library; or perhaps when interfacing to another programming language with minimal code.

    A developer might choose to do so if the library or interaface implementation is trivial and nearly so, and ease of use (to the developer using the header file) is more important than code size. In these cases, the declarations in the header file often use preprocessor macros, allowing the same header file to be included more than once, providing some sort of crude polymorphism in C.

    Here is a practical example: Shoot-yourself-in-the-foot playground for linear congruential pseudorandom number generators. Because the implementation is local to the compilation unit, each compilation unit will get their own copies of the PRNG. This example also shows how crude polymorphism can be implemented in 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
    

    An example using the above, 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;
    }
    

    The reason for marking both the _statevariable and the _next()function staticis that this way each compilation unit that includes the header file has their own copy of the variables and the functions -- here, their own copy of the PRNG. Each must be separately seeded, of course; and if seeded to the same value, will yield the same sequence.

    One should generally shy away from such polymorphism attempts in C, because it leads to complicated preprocessor macro shenanigans, making the implementation much harder to understand, maintain, and modify than necessary.

    However, when exploringthe parameter space of some algorithm -- like here, the types of 32-bit linear congruential generators, this lets us use a single implementation for each of the generators we examine, ensuring there are no implementation differences between them. Note that even this case is more like a development tool, and not something you ought to see in a implementation provided for others to use.

  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 位线性同余生成器的类型,这让我们对我们检查的每个生成器使用单一实现,确保它们之间没有实现差异。请注意,即使这种情况更像是一个开发工具,而不是您应该在提供给其他人使用的实现中看到的东西。



  1. If the header implements simple static inlineaccessor functions

    Preprocessor macros are commonly used to simplify code accessing complicated structure types. static inlinefunctions are similar, except that they also provide type checking at compile time, and can refer to their parameters several times (with macros, that is problematic).

    One practical use case is a simple interface for reading files using low-level POSIX.1 I/O (using <unistd.h>and <fcntl.h>instead of <stdio.h>). I've done this myself when reading very large (dozens of megabytes to gigabytes range) text files containing real numbers (with a custom float/double parser), as the GNU C standard I/O is not particularly fast.

    For example, 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 */
    

    Note that the above inbuffer_skip()and inbuffer_getc()do not check if ibis non-NULL; this is typical for such functions. These accessor functions are assumed to be "in the fast path", i.e. called very often. In such cases, even the function call overhead matters (and is avoided with static inlinefunctions, since they are duplicated in the code at the call site).

    Trivial accessor functions, like the above inbuffer_skip()and inbuffer_getc(), may also let the compiler avoid the register moves involved in function calls, because functions expect their parameters to be located in specific registers or on the stack, whereas inlined functions can be adapted (wrt. register use) to the code surrounding the inlined function.

    Personally, I do recommend writing a couple of test programs using the non-inlined functions first, and compare the performance and results to the inlined versions. Comparing the results ensure the inlined versions do not have bugs (off by one type is common here!), and comparing the performance and generated binaries (size, at least) tells you whether inlining is worth it in general.

  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. register use ) 到内联函数周围的代码。

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

回答by JHBonarius

Why would you want a both global and static function? In c, functions are global by default. You only use static functions if you want to limit the access to a function to the file they are declared. So you actively restrict access by declaring it static...

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

The only requirement for implementations in the header file, is for c++ template functions and template class member functions.

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