0

我正在尝试在 C(不是 C++)的仅标头库中实现一个全局单例变量。所以在这个论坛和其他地方搜索之后,我发现了我在这里适应 C 的 Meyer 单例的一个变体:

    /* File: sing.h */
    #ifndef SING_H
    #define SING_H
    inline int * singleton()
    {
        static int foo = 0;
        return &foo;
    }
    #endif

请注意,我正在返回一个指针,因为 C 在 C++ 中缺少 & 引用,所以我必须解决它。

好的,现在我要测试它,所以这里是一个简单的测试代码:

    /* File: side.h */
    #ifndef SIDE_H
    #define SIDE_H
    void side();
    #endif
    /*File: side.c*/
    #include "sing.h"
    #include <stdio.h>
    void side()
    {
        printf("%d\n",*(singleton()));
    }
    /*File: main.c*/
    #include "sing.h"
    #include "side.h"
    #include <stdio.h>
    int main(int argc, char * argv[])
    {
        /* Output default value - expected output: 0 */
        printf("%d\n",*(singleton()));
        *(singleton()) = 5;
        /* Output modified value - expected output: 5 */
        printf("%d\n",*(singleton()));
        /* Output the same value from another module - expected output: 5*/
        side();
        return 0;
    }

在 C 模式下的 MSVC 中编译和运行良好(也在 C++ 模式下,但这不是主题)。但是,在 gcc 中,它输出两个警告(警告:'foo' 是静态的,但在非静态的内联函数 'singleton' 中声明),并生成一个可执行文件,然后当我尝试运行它时会出现段错误。警告本身对我来说有点道理(事实上,我很惊讶我在 MSVC 中没有得到它)​​,但是 segfault 暗示了 gcc 永远不会将 foo 编译为静态变量的可能性,使其成为局部变量stack 然后返回该变量的过期堆栈地址。

我尝试将单例声明为extern inline,它在 MSVC 中编译并运行良好,导致 gcc 中的链接器错误(同样,我不抱怨链接器错误,这是合乎逻辑的)。我也尝试过static inline(在 MSVC 和 gcc 中都编译得很好,但可以预见的是在第三行运行时输出错误,因为 side.c 翻译单元现在有自己的单例副本。

那么,我在 gcc 中做错了什么?我在 C++ 中没有这些问题,但在这种情况下我不能使用 C++,它必须是直接的 C 解决方案。

我还可以接受任何其他形式的单例实现,这些单例实现在 gcc 和 MSVC 中的纯 C 中的纯标头库中工作。

4

1 回答 1

1

我正在尝试在 C(不是 C++)的仅标头库中实现一个全局单例变量。

“全局”是指“具有静态存储持续时间和外部链接”。至少,这是 C 所能达到的最接近的程度。这也与 C 可以接近内置类型的“单例”一样接近,因此从这个意义上说,术语“全局单例”是多余的。

请注意,我正在返回一个指针,因为 C 在 C++ 中缺少 & 引用,所以我必须解决它。

C 没有引用是正确的,但如果您不使用函数来包装对对象的访问,则不需要指针或引用。我并没有真正看到你想从中获得什么。如果没有,您可能会发现更容易获得您想要的东西。例如,当面对相同变量标识符的重复外部定义时,除了最新版本的 GCC 之外,所有默认行为都是将它们合并为一个变量。尽管当前的 GCC 将此情况报告为错误,但通过打开命令行开关仍然可以使用旧的行为。

另一方面,您的内联函数方法不太可能在许多 C 实现中工作。请特别注意,C 中inline的语义与 C++ 中的语义完全不同,尤其是外部内联函数在 C 中很少有用。请考虑 C 标准的这些规定:

  • 第 6.7.4/3 段(语言限制):

    具有外部链接的函数的内联定义不应包含具有静态或线程存储持续时间的可修改对象的定义,并且不应包含对具有内部链接的标识符的引用。

    因此,您的示例代码不符合标准,需要符合标准的编译器对其进行诊断。尽管如此,他们可能会接受您的代码,但他们可以用它做任何他们选择的事情。期望您可以依靠随机的符合 C 实现来接受您的函数代码并编译它,以便不同翻译单元中的调用者可以通过调用该函数获得指向同一对象的指针,这似乎是不合理的希望。

  • 第6.9/5段:

    外部定义是一个外部声明,它也是函数(内联定义除外)或对象的定义。如果在表达式 [...] 中使用了通过外部链接声明的标识符,则在整个程序的某处,该标识符[...] 应该有一个确切的外部定义。

    请注意,尽管具有外部链接的函数标识符的内联定义(例如您的)提供了该标识符的外部声明,但它不提供该标识符的外部定义。这意味着程序中的某处需要单独的外部定义(除非该函数完全未使用)。此外,该外部定义不能位于包含内联定义的翻译单元中。这是外部内联函数在 C 中很少有用的原因之一。

  • 第 6.7.4/7 段:

    对于具有外部链接的函数,以下限制适用: [...] 如果翻译单元中函数的所有文件范围声明都包含不带 的内联函数说明符extern,则该翻译单元中的定义是内联定义。内联定义不提供函数的外部定义,也不禁止在另一个翻译单元中进行外部定义。内联定义提供了外部定义的替代方案,翻译器可以使用它来实现对同一翻译单元中函数的任何调用。未指定对函数的调用是使用内联定义还是外部定义。

    除了呼应 6.9/5 的一部分之外,它还警告您,如果您确实提供了函数的外部定义以与内联定义一起使用,您无法确定哪个将用于服务任何特定调用。

  • 此外,您不能通过声明具有内部链接的函数来解决这些问题,因为尽管这将允许您在其中声明一个静态变量,但函数的每个定义都是不同的函数。以免有任何疑问,脚注 140 澄清了在这种情况下,

    由于内联定义不同于相应的外部定义以及其他翻译单元中的任何其他相应的内联定义,因此具有静态存储持续时间的所有相应对象在每个定义中也是不同的

(强调补充。)

同样,您的示例中提出的方法不能依赖于在 C 中工作,尽管您可能会发现在实践中它确实适用于某些编译器。


如果您需要这是一个仅包含标头的库,那么您可以通过对用户提出额外要求以可移植的方式实现它:使用您的标头库的任何程序中的一个翻译单元必须在包含标头之前定义一个特殊的宏. 例如:

/* File: sing.h */
#ifndef SING_H
#define SING_H

#ifdef SING_MASTER

int singleton = 0;

#else

extern int singleton;

#endif
#endif

SING_MASTER这样,在包含之前定义的一个翻译单元sing.h(第一次)将提供所需的定义singleton,而所有其他翻译单元将只有一个声明。此外,该变量将可直接访问,无需调用函数或取消引用指针。

于 2021-06-30T02:45:07.853 回答