我应该在函数定义中还是在声明和定义中放置参数存储类说明符?

问题描述 投票:14回答:5

我正在努力将一些旧的K&R代码移植到ANSI C,所以我正在编写缺少的函数原型声明。很多函数定义都有寄存器存储类的参数,但是我不确定函数原型中是否可以省略寄存器存储类说明符?

有和没有寄存器存储类特定声明,代码编译正确(我尝试了GCC,VC ++和Watcom C)。我在ISO / ANSI C89标准中找不到任何关于正确方法的信息 - 如果我只是将register关键字放在函数定义中就可以了吗?

int add(register int x, register int y); 

int add(register int x, register int y)
{
  return x+y;
}

这也正确构建:

int add(int x, int y);

int add(register int x, register int y)
{
   return x+y;
}

我想确保根据标准真正考虑寄存器存储说明符(我的目标是使用非常旧的编译器进行编译,其中此存储类说明符很重要)。两者都好,这只是一个编码风格的问题,或不是?

c declaration definition c89
5个回答
15
投票

关键的规定是函数的每个声明都必须为它指定一个兼容的类型。这需要兼容的返回类型,并且对于诸如你的包含参数列表的声明,每对相应参数的兼容类型。

那么问题就在于存储类说明符是否区分了类型。虽然标准间接地通过省略类型派生的讨论中的存储类说明符,但它们没有这样做。因此,对象声明中的存储类说明符指定的属性与该对象的类型是分开的。

而且,C89 specifically says

除非声明的参数是函数定义的参数类型列表的成员之一,否则将忽略参数声明的声明说明符中的存储类说明符(如果存在)。

(重点补充)。函数定义是一个伴随函数体的声明,而不是前向声明,因此您的两个代码具有相同的语义。

有和没有寄存器存储类特定声明,代码编译正确(我尝试过gcc,VC ++和Watcom),我在ISO / ANSI C89标准中找不到任何关于正确方法的信息,或者它是否正常如果我只是将register关键字放在函数定义中?

就个人而言,我倾向于使每个前向声明与相应函数定义中的声明相同。如果函数定义本身正确,这绝不是错误的。

然而,

  1. register关键字是一个遗物。编译器没有义务做任何尝试将register变量实际分配给寄存器,现代编译器在决定如何将变量分配给寄存器以及无论如何生成快速代码时都比人类好得多。只要您转换旧代码,我就会借此机会删除register关键字的所有外观。
  2. C89已经过时了。该标准的最新版本是C 2018; C 2011被广泛部署;几乎在任何地方都可以使用C99(技术上已经过时了)。也许您有充分的理由将C89作为目标,但您应该强烈考虑使用C11或C18,或至少C99。

6
投票

根据gcc和clang的经验,函数参数上的register存储类的行为与params上的顶级限定符相同:只有定义中的那些(不是以前的原型)计数。

(对于顶级限定符,当考虑类型兼容性时,它们也被丢弃,即void f(int);void f(int const);是兼容的原型,但存储类不是类型的一部分,因此类型兼容性首先不是它们的问题)

从C程序员的角度来看,C中唯一可观察到的register结果是编译器不会让你获取声明对象的地址。

当我做:

void f(int A, int register B);

void f(int register A, int B) 
{
    /*&A;*/ //doesn't compile => A does have register storage here
    &B; //compiles => B doesn't have register storage here;
        //the register from the previous prototype wasn't considered
}

然后&B编译,但&A没有,因此只有定义中的限定符似乎计数。

我认为,如果你确实需要那些register,你最好的选择是在两个地方一致地使用它(原型中的register理论上可以修改调用的方式)。


6
投票

C89标准确实这样说(第3.5.4.3节外部定义):

唯一存在于参数声明中的存储类说明符是register

因此,虽然register作为函数参数存储类说明符是允许的,但我仍然认为这是否值得尊重,实际上取决于函数的体系结构和调用约定。

既然你提到了Watcom和C89,我假设你的目标是x86-16。 x86-16(pascalstdcallcdecl)的典型调用约定都要求将参数推送到堆栈而不是寄存器中,所以我怀疑关键字是否会实际修改参数在调用时传递给函数的方式现场。

考虑一下,您有以下函数定义:

int __stdcall add2(register int x, register int y);

根据stdcall的要求,该函数作为_add2@4进入目标文件。 @ 4表示在函数返回时从堆栈中删除的字节数。在这种情况下使用ret imm16(返回调用过程和来自堆栈的pop imm16字节)指令。

add2最后会有以下ret

ret 4

如果在调用站点没有在堆栈上推送4个字节(即因为参数实际上在寄存器中),则程序现在具有未对齐的堆栈和崩溃。


3
投票

由于您使用旧编译器来处理奇怪的平台,因此有时仅仅考虑编译器的作用比假设它完全符合C规范更重要。

这意味着您希望通过编译器运行示例的每个变体,并将编译器选项设置为生成程序集而不是可执行文件。查看程序集,看看你是否可以通过各种方式判断它是否正在使用寄存器。在gcc中,这是S选项;例如:

gcc myfile.c -S -o myfile.s

0
投票

从C17标准,6.7.1存储类说明符:

具有存储类说明符寄存器的对象的标识符声明表明对对象的访问尽可能快。这些建议有效的程度是实施定义的。

暗示编译器将尝试或不依赖于编译器来加速参数访问,但并不意味着对调用约定的任何修改(基本上没有对调用端进行修改)。

所以它应该存在于函数定义中,但在原型中是无关紧要的。

© www.soinside.com 2019 - 2024. All rights reserved.