代码之家  ›  专栏  ›  技术社区  ›  Doohyeon Won

内联函数上的奇怪现象?[关闭]

  •  0
  • Doohyeon Won  · 技术社区  · 4 月前

    当我试图理解内联函数时,我发现自己陷入了一个非常深的兔子洞。我知道了 inline 是一个函数说明符,即 内联 这只是对编译器的一个提示,编译器是否执行内联扩展取决于编译器,编译器需要一个定义 内联 以及正常函数调用的另一个定义等。

    顺便说一句,考虑到这里的问题数量,似乎我并不是唯一一个很难理解这个话题的人。尽管我试着读这些答案,但我仍然觉得很神秘。

    然而,我确信有两种正确的方法来使用内联函数,它们是 static inline extern inline :

    /* Usage of static inline */
    /* This is a file named test.c */
    #include <stdio.h>
    
    static inline int fn(void);
    
    int main(void) {
       printf("%d\n", fn());
       return 0;
    }
    
    // Does this provide two definitions? I'm not sure.
    static inline int fn(void) {
       return 123;
    }
    
    /* Output */
    $ gcc -o test test.c
    $ ./test
    123
    
    /* Usage of extern inline */
    /* In a file named inline.h */
    inline int fn(void) {
       return 321;
    }
    
    /* In a file named inline.c */
    #include "inline.h"
    
    // So this "declaration" is treated as "definition"?
    extern int fn(void);
    
    /* In a file named test.c */
    #include <stdio.h>
    #include "inline.h"
    
    int main(void) {
       printf("%d\n", fn());
       return 0;
    }
    
    /* Output */
    $ gcc -o test test.c inline.c
    $ ./test
    321
    

    与此同时,我尝试了下面的代码,得到了 奇怪的结果 ,这就是本问题的主题。

    /* In a file named test_1.c */
    #include <stdio.h>
    
    inline int fn(int a, int b) {
       return a + b;
    }
    extern int fn(int a, int b);
    
    int wrapper(void);
    
    int main(void) {
       printf("%d\n", wrapper());
       return 0;
    }
    
    /* In a file named test_2.c */
    inline int fn(int a) {
       return a;
    }
    
    int wrapper(void) {
       return fn(123);
    }
    

    我写了两个版本的 fn 功能,我故意让它们不同。( fn 在test2.c中只有一个参数 fn 在test1.c中有两个参数。)

    在我编译并执行这个程序后,每次运行它时,它都会打印一个随机数。

    $ gcc -o test test_1.c test_2.c
    $ ./test
    -1466335670
    $ ./test
    81565994
    

    据我猜测 fn(123) test2.c中的调用使用了以下定义 fn 在test1.c中,因为编译器需要一个正常的定义 fn 因为它没有选择内联它,以及 fn 具有外部联系。(如果一个函数有外部链接,则该函数可以从其他文件中看到。)同时,编译器似乎可以识别 fn 由于 fn test_2.c中的定义。然而, fn(123) 缺少第二个参数。所以我认为事情出了问题,因为这个原因。或者,也许只是因为我写了一段代码,导致了未定义的行为或其他什么。

    很抱歉再加一个“我不理解在线功能”的问题,但请再慷慨地给我一个。如果您能解释一下为什么会发生这种情况,我们将不胜感激。非常感谢。

    编辑 :这是我构建可执行文件的环境。注意:我使用的是Windows Linux子系统(WSL)。

    > wsl -l -v
      NAME      STATE           VERSION
    * Ubuntu    Running         2
    
    $ lsb_release -a
    No LSB modules are available.
    Distributor ID: Ubuntu
    Description:    Ubuntu 22.04.3 LTS
    Release:        22.04
    Codename:       jammy
    
    $ gcc --version
    gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
    

    这是我用来构建可执行文件的命令。

    $ gcc -o test test_1.c test_2.c
    

    如果可能的话,我更喜欢C99,因为我学习C的那本书解释了C89&C99。非常感谢。

    1 回复  |  直到 4 月前
        1
  •  2
  •   Nate Eldredge    4 月前

    我认为你的代码有未定义的行为。C23草案N3220 6.2.7p2:

    所有引用同一对象或函数的声明应具有兼容的类型;否则 行为未定义。

    你有两个声明 fn 类型不兼容(参数数量不同)。

    A. 可能的 您观察到的实际行为的原因是,正如您所建议的,编译器选择不内联对 fn() 在里面 test_2.c (这是gcc在未优化时的正常行为: https://godbolt.org/z/dTTGbv9x9 ),因此发出了对外部定义的调用 test_1.c .自 test_1.c 版本预期有两个参数,但只传递了一个,另一个可能来自相关寄存器或内存位置中的任何值,其值是不可预测的,并且可能在运行过程中发生了变化。(如果它恰好是一个地址,而你的系统使用ASLR,那么它可能是更字面意义上的“随机”。)显然,为了确定这一点,你必须检查和/或跟踪编译器在你的系统上实际生成的机器代码。

    如果声明是兼容的(比如,两者都取一个 int 参数),并且调用已更新以匹配,那么应该没有UB,但仍然“未指定”(6.7.5p7)在每个实例中调用的是内联定义还是外部定义。因此,作为合理的编程实践,您通常应该确保所有定义都是相同的(例如,将定义放在一个头文件中 #include d在需要的地方输入每个翻译单元);或者至少它们具有功能上相同的行为。

    如果你想有多个实质上不同的定义(仍然具有兼容的声明),并在每次调用时“掷骰子”,标准允许这样做,但这似乎更可能是一个bug的来源,而不是一个有用的功能。

    (你提到对C99感兴趣:这篇文章中的所有内容对于C99和C23都是一样的。两个版本之间的相关规则没有变化。)