这是一个非常特别的问题,我以前从来没有意识到过.显然,我可以用C语言创建一个在传递参数方面与其函数原型不同的函数.唯一的要求是函数在编译时不能知道函数原型.

在我的最小示例中,我创建了以下头文件Test.h:

int Test(int);

我还创建了以下main.c:

#include <stdio.h>
#include "Test.h"

int main()
{
    int a = Test(5);
    int b = Test(5);
    printf("%d", a);
}

当我使用此Test.c时,如预期的那样,编译时出错:

#include <stdio.h>
#include "Test.h"

void Test(void)
{
    printf("HW\n");
}

如果我像这样编写Test.c,程序将被编译、链接并且是可执行的.当然,变量a有a个看似任意的值.

#include <stdio.h>

void Test(void)
{
    printf("HW\n");
}

例如,为什么可以链接这个程序,返回参数从何而来? 如果类似的事情在较大的代码中意外发生,这似乎也非常危险.

快速补充信息,当我在C++项目中使用相同的代码时,出现链接器错误.因此,如果我使用main.cpp和Test.cpp而不是main.c和Test.c.在这方面,左翼的处理方式似乎有所不同.但我也会感到惊讶,因为C++有函数重载.

推荐答案

正如您所观察到的,行为是未定义的.

没有收到错误的原因是链接器没有关于Test.cmain.c中使用的调用约定的信息,唯一的信息是符号名称Test和引用的类型(函数调用).

以下是可能发生的一系列事件:

  • main函数调用Test函数,该函数的行为可能与预期一致:它调用printf,后者应该打印HW和一个换行符,然后返回,不返回任何值.
  • 然后,main检索它期望已经存储在常规位置的返回值,通常,对于int返回类型,这是一个寄存器,并且main调用printf 以打印此值.
  • 打印的值很有可能是3,因为printf返回3作为int,因此它将3存储在正确的寄存器中,并且Test可能没有在printf调用和它自己的return语句之间更改寄存器(不能保证).

您可以通过让编译器生成-s-S的汇编源代码,或者使用Godbolt Compiler Explorer来进行更多的研究

将代码编译为C++确实会产生错误,因为与C不同,C++将参数类型和类信息与处理重载的函数名一起存储(它在一个名为mangling的进程中构造合成名称),因此当您将main.o对象文件与不正确的Test.o链接时,函数名不匹配,并且会出现缺少符号的错误.

您的示例是链接多个模块时潜在问题的一个很好的示例.程序员(S)应遵循缓解此问题的良好做法:

  • 从源文件导出的所有函数必须在相应的头文件中声明,并且
  • 该头文件必须包含在使用函数的源文件和定义函数的源文件中.

现代编译器(如gccclang)支持警告以try 和实施此方法.您应该始终使用-Wall -Wextra -Werror编译定义以下内容的所有源文件:

  • -Wmissing-prototypes(仅限C和OBJECT-C)

    如果定义了一个没有原型声明的全局函数,则发出警告.即使定义本身提供了原型,也会发出此警告.使用此选项检测头文件中没有匹配原型声明的全局函数.此选项对C++无效,因为所有函数声明都提供原型,并且不匹配的声明声明重载,而不是与前面的声明冲突.使用-Wmissing-declarations个来检测C++中缺少的声明.

  • -Wmissing-variable-declarations(仅限C和OBJECT-C)

    如果在没有先前声明的情况下定义了全局变量,则发出警告.使用此选项可以检测在头文件中没有匹配的外部声明的全局变量.

  • -Wmissing-declarations

    如果在没有先前声明的情况下定义了全局函数,则发出警告.即使定义本身提供了原型,也要这样做.使用此选项可检测未在头文件中声明的全局函数.在C中,不会对具有以前的非原型声明的函数发出警告;使用-Wmissing-prototypes来检测丢失的原型.在C++中,不会对函数模板、内联函数或匿名命名空间中的函数发出警告.

C++相关问答推荐

try 使用sigqueue函数将指向 struct 体的指针数据传递到信号处理程序,使用siginfo_t struct 体从一个进程传递到另一个进程

Apple Libm的罪恶功能

intellisense不工作,甚至已经下载了c/c++扩展

以c格式打印时间戳

如何一次获取一个字符

C:二进制搜索和二进制插入

为什么GCC C23中的关键字FALSE不是整数常量表达式?

C lang:当我try 将3个或更多元素写入数组时,出现总线错误

字符是否必须转换为无符号字符,然后才能与getc家族的返回值进行比较?

GDB输出ARM助记符

插座打开MacOS组件

1处的解析器错误:yacc语法的语法错误

使用nmake for程序比Hello World稍微复杂一些

用C++构建和使用DLL的困惑

生产者消费者计数器意外输出的C代码

为什么我的二叉树删除删除整个左部分的树?

在下面的C程序中,.Ap0是如何解释的?

强制GCC始终加载常量(即只读),即使启用了优化

C Makefile - 如何避免重复提及文件名

当循环变量在溢出时未定义时,可以进行哪些优化?