我正在努力将一些旧的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;
}
根据标准,我想确保确实考虑了寄存器存储说明符(我的目标是使用非常老的编译器进行编译,而该存储类说明符很重要)。都可以,这只是编码风格的问题吗?
答案 0 :(得分:15)
关键规定是,函数的每个声明都必须为其指定兼容的类型。这需要兼容的返回类型,对于诸如包含参数列表的声明,则需要每对对应参数的兼容类型。
然后,问题就变成了存储类说明符是否区分类型。尽管该标准间接说明了这一点,但它们并不是通过忽略对类型派生的讨论而忽略存储类说明符来实现的。因此,由对象声明中的存储类说明符指定的属性与该对象的类型是分开的。
此外,C89 specifically says
除非存在声明的参数是函数定义的参数类型列表的成员之一,否则参数声明的声明说明符中的存储类说明符(如果存在)将被忽略。
>
(添加了强调)。函数定义是带有函数体的声明,而不是前向声明,因此您的两个代码具有相同的语义。
有和没有寄存器存储类的特定声明, 代码正确编译(我尝试过gcc,VC ++和Watcom),但无法 在ISO / ANSI C89标准中找到有关什么的任何信息 正确的方法,还是可以,如果我只是将register关键字放入 函数定义?
就个人而言,我倾向于使每个前向声明与相应函数定义中的声明相同。如果函数定义本身正确,这永远不会出错。
但是,
register
关键字是一个遗物。编译器完全没有义务尝试将register
变量实际分配给寄存器,并且现代的编译器在决定如何将变量分配给寄存器以及无论如何生成快速代码方面比人类要好得多。只要您要转换旧代码,我就会借此机会删除register
关键字的所有外观。
C89已过时。该标准的最新版本为C 2018; C 2011已广泛部署; C99(从技术上讲也是过时的)几乎到处都有。也许有充分的理由将您定位到C89,但是您应该强烈考虑改为定位C11或C18,或者至少 C99。
答案 1 :(得分:6)
C89标准确实是这样说的(第3.5.4.3节外部定义):
将在参数声明中出现的唯一存储类说明符是
register
。
因此,看来register
作为函数参数存储类说明符是允许的,但我仍然相信是否兑现这实际上取决于该函数的体系结构和调用约定。
由于您提到了Watcom和C89,所以我假设您的目标是x86-16。 x86-16(pascal
,stdcall
和cdecl
)的典型调用约定都要求将参数压入堆栈,而不是寄存器中,因此我怀疑关键字是否实际上修改如何在调用站点将参数传递给函数。
考虑一下,您具有以下函数定义:
int __stdcall add2(register int x, register int y);
根据stdcall的要求,该函数作为_add2@4
进入目标文件。 @ 4表示函数返回时要从堆栈中删除多少字节。在这种情况下,使用ret imm16
(返回到调用过程并从堆栈中弹出imm16字节)指令。
add2
将在末尾带有以下ret
:
ret 4
如果没有在调用站点的堆栈上压入4个字节(即由于参数实际上位于寄存器中),则程序现在的堆栈未对齐并崩溃。
答案 2 :(得分: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
从理论上可以改变调用的方式)。
答案 3 :(得分:3)
由于您在一个奇怪的平台上使用了旧的编译器,所以有时仅查看编译器的工作比假设其完全符合C规范更为重要。
这意味着您希望通过编译器运行示例的每个变体,并且将编译器选项设置为生成程序集而不是可执行文件。查看程序集,看看是否可以分辨它是否使用寄存器。在gcc中,这是ConfigureWebHost
选项;例如:
S
答案 4 :(得分:0)
根据C17标准, 6.7.1存储类说明符:
声明具有存储类的对象的标识符 说明符 register 建议对对象的访问与 可能。这些建议有效的程度是 实现定义的。
很显然,编译器将尝试或不依赖于编译器来加快参数访问的速度,但并不意味着对调用约定进行了任何修改(基本上在调用方未进行任何修改)。
因此它应该出现在函数定义中,但在原型中并不重要。